1774 lines
46 KiB
JavaScript

import {
createListResource,
createResource,
LoadingIndicator,
} from 'frappe-ui';
import LucideVenetianMask from '~icons/lucide/venetian-mask';
import { defineAsyncComponent, h } from 'vue';
import { unparse } from 'papaparse';
import { toast } from 'vue-sonner';
import AddDomainDialog from '../components/AddDomainDialog.vue';
import GenericDialog from '../components/GenericDialog.vue';
import ObjectList from '../components/ObjectList.vue';
import SiteActions from '../components/SiteActions.vue';
import { getTeam, switchToTeam } from '../data/team';
import router from '../router';
import { getRunningJobs } from '../utils/agentJob';
import { confirmDialog, icon, renderDialog } from '../utils/components';
import dayjs from '../utils/dayjs';
import { bytes, date, userCurrency } from '../utils/format';
import { getToastErrorMessage } from '../utils/toast';
import { getDocResource } from '../utils/resource';
import { trialDays } from '../utils/site';
import { clusterOptions, getUpsellBanner } from './common';
import { getAppsTab } from './common/apps';
import { isMobile } from '../utils/device';
import { getQueryParam, setQueryParam } from '../utils/index';
export default {
doctype: 'Site',
whitelistedMethods: {
activate: 'activate',
addDomain: 'add_domain',
archive: 'archive',
backup: 'schedule_backup',
clearSiteCache: 'clear_site_cache',
deactivate: 'deactivate',
disableReadWrite: 'disable_read_write',
enableReadWrite: 'enable_read_write',
installApp: 'install_app',
uninstallApp: 'uninstall_app',
migrate: 'migrate',
moveToBench: 'move_to_bench',
moveToGroup: 'move_to_group',
loginAsAdmin: 'login_as_admin',
loginAsTeam: 'login_as_team',
isSetupWizardComplete: 'is_setup_wizard_complete',
reinstall: 'reinstall',
removeDomain: 'remove_domain',
redirectToPrimary: 'set_redirect',
removeRedirect: 'unset_redirect',
setPrimaryDomain: 'set_host_name',
fetchCertificate: 'fetch_certificate',
restoreSite: 'restore_site',
restoreSiteFromFiles: 'restore_site_from_files',
restoreSiteFromPhysicalBackup: 'restore_site_from_physical_backup',
scheduleUpdate: 'schedule_update',
editScheduledUpdate: 'edit_scheduled_update',
cancelUpdate: 'cancel_scheduled_update',
setPlan: 'set_plan',
updateConfig: 'update_config',
deleteConfig: 'delete_config',
sendTransferRequest: 'send_change_team_request',
addTag: 'add_resource_tag',
removeTag: 'remove_resource_tag',
getBackupDownloadLink: 'get_backup_download_link',
fetchDatabaseTableSchemas: 'fetch_database_table_schemas',
fetchSitesDataForExport: 'fetch_sites_data_for_export',
},
list: {
route: '/sites',
title: 'Sites',
fields: [
'plan.plan_title as plan_title',
'plan.price_usd as price_usd',
'plan.price_inr as price_inr',
'group.title as group_title',
'group.public as group_public',
'group.team as group_team',
'group.version as version',
'cluster.image as cluster_image',
'cluster.title as cluster_title',
'trial_end_date',
'creation',
'is_monitoring_disabled',
],
orderBy: 'creation desc',
searchField: 'host_name',
filterControls() {
return [
{
type: 'select',
label: 'Status',
fieldname: 'status',
options: [
'',
'Active',
'Inactive',
'Suspended',
'Broken',
'Archived',
],
},
{
type: 'link',
label: 'Version',
fieldname: 'group.version',
options: {
doctype: 'Frappe Version',
},
},
{
type: 'link',
label: 'Bench Group',
fieldname: 'group',
options: {
doctype: 'Release Group',
},
},
{
type: 'select',
label: 'Region',
fieldname: 'cluster',
options: clusterOptions,
},
{
type: 'link',
label: 'Tag',
fieldname: 'tags.tag',
options: {
doctype: 'Press Tag',
filters: {
doctype_name: 'Site',
},
},
},
];
},
columns: [
{
label: 'Site',
fieldname: 'host_name',
width: 1.5,
class: 'font-medium',
format(value, row) {
return value || row.name;
},
},
{ label: 'Status', fieldname: 'status', type: 'Badge', width: '140px' },
{
label: 'Plan',
fieldname: 'plan',
width: 0.85,
format(value, row) {
if (row.trial_end_date) {
return trialDays(row.trial_end_date);
}
const $team = getTeam();
if (row.price_usd > 0) {
const india = $team.pg?.currency === 'INR';
const formattedValue = userCurrency(
india ? row.price_inr : row.price_usd,
0,
);
return `${formattedValue}/mo`;
}
return row.plan_title;
},
},
{
label: 'Region',
fieldname: 'cluster',
width: 1,
format(value, row) {
return row.cluster_title || value;
},
prefix(row) {
return h('img', {
src: row.cluster_image,
class: 'w-4 h-4',
alt: row.cluster_title,
});
},
},
{
label: 'Bench Group',
fieldname: 'group',
width: '15rem',
format(value, row) {
return row.group_public ? 'Shared' : row.group_title || value;
},
},
{
label: 'Version',
fieldname: 'version',
width: 0.5,
},
],
primaryAction({ listResource: sites }) {
return {
label: 'New Site',
variant: 'solid',
slots: {
prefix: icon('plus'),
},
onClick() {
router.push({ name: 'New Site' });
},
};
},
moreActions({ listResource: sites }) {
return [
{
label: 'Export as CSV',
icon: 'download',
onClick() {
const fields = [
'host_name',
'plan_title',
'cluster_title',
'group_title',
'tags',
'version',
'creation',
];
createListResource({
doctype: 'Site',
url: 'press.api.site.fetch_sites_data_for_export',
auto: true,
onSuccess(data) {
let csv = unparse({
fields,
data,
});
csv = '\uFEFF' + csv; // for utf-8
// create a blob and trigger a download
const blob = new Blob([csv], {
type: 'text/csv;charset=utf-8',
});
const today = new Date().toISOString().split('T')[0];
const filename = `sites-${today}.csv`;
const link = document.createElement('a');
link.href = URL.createObjectURL(blob);
link.download = filename;
link.click();
URL.revokeObjectURL(link.href);
},
});
},
},
];
},
},
detail: {
titleField: 'name',
route: '/sites/:name',
statusBadge({ documentResource: site }) {
return { label: site.pg.status };
},
breadcrumbs({ items, documentResource: site }) {
let breadcrumbs = [];
let $team = getTeam();
let siteCrumb = {
label: site.pg.host_name || site.pg?.name,
route: `/sites/${site.pg?.name}`,
};
if (
(site.pg.server_team == $team.pg?.name &&
site.pg.group_team == $team.pg?.name) ||
$team.pg?.is_desk_user ||
$team.pg?.is_support_agent
) {
breadcrumbs.push({
label: site.pg?.server_title || site.pg?.server,
route: `/servers/${site.pg?.server}`,
});
}
if (
site.pg.group_team == $team.pg?.name ||
$team.pg?.is_desk_user ||
$team.pg?.is_support_agent
) {
breadcrumbs.push(
{
label: site.pg?.group_title,
route: `/groups/${site.pg?.group}`,
},
siteCrumb,
);
} else {
breadcrumbs.push(...items.slice(0, -1), siteCrumb);
}
return breadcrumbs;
},
tabs: [
{
label: 'Overview',
icon: icon('home'),
route: 'overview',
type: 'Component',
condition: (site) => site.pg?.status !== 'Archived',
component: defineAsyncComponent(
() => import('../components/SiteOverview.vue'),
),
props: (site) => {
return { site: site.pg?.name };
},
},
{
label: 'Insights',
icon: icon('bar-chart-2'),
route: 'insights',
type: 'Component',
condition: (site) => site.pg?.status !== 'Archived',
redirectTo: 'Site Analytics',
childrenRoutes: [
'Site Jobs',
'Site Job',
'Site Logs',
'Site Log',
'Site Analytics',
'Site Performance Reports',
'Site Performance Request Logs',
'Site Performance Slow Queries',
'Site Performance Process List',
'Site Performance Request Log',
'Site Performance Deadlock Report',
],
nestedChildrenRoutes: [
{
name: 'Site Analytics',
path: 'analytics',
component: () => import('../components/site/SiteAnalytics.vue'),
},
{
name: 'Site Jobs',
path: 'jobs',
component: () => import('../components/site/SiteJobs.vue'),
},
{
name: 'Site Job',
path: 'jobs/:id',
component: () => import('../pages/JobPage.vue'),
},
{
name: 'Site Logs',
path: 'logs/:type?',
component: () => import('../components/site/SiteLogs.vue'),
},
{
name: 'Site Log',
path: 'logs/view/:logName',
component: () => import('../pages/LogPage.vue'),
},
{
name: 'Site Performance Reports',
path: 'performance',
component: () =>
import('../components/site/performance/SitePerformance.vue'),
},
{
name: 'Site Performance Slow Queries',
path: 'performance/slow-queries',
component: () =>
import('../components/site/performance/SiteSlowQueries.vue'),
},
{
name: 'Site Performance Process List',
path: 'performance/process-list',
component: () =>
import('../components/site/performance/SiteProcessList.vue'),
},
{
name: 'Site Performance Request Logs',
path: 'performance/request-log',
component: () =>
import('../components/site/performance/SiteRequestLogs.vue'),
},
{
name: 'Site Performance Deadlock Report',
path: 'performance/deadlock-report',
component: () =>
import('../components/site/performance/SiteDeadlockReport.vue'),
},
],
component: defineAsyncComponent(
() => import('../components/site/SiteInsights.vue'),
),
props: (site) => {
return { site: site.pg?.name };
},
},
getAppsTab(true),
{
label: 'Domains',
icon: icon('external-link'),
route: 'domains',
type: 'list',
condition: (site) => {
return site.pg?.status !== 'Archived';
},
list: {
doctype: 'Site Domain',
fields: ['redirect_to_primary'],
filters: (site) => {
return { site: site.pg?.name };
},
columns: [
{
label: 'Domain',
fieldname: 'domain',
},
{
label: 'Status',
fieldname: 'status',
type: 'Badge',
},
{
label: 'Primary',
fieldname: 'primary',
type: 'Icon',
Icon(value) {
return value ? 'check' : '';
},
},
{
label: 'DNS Type',
fieldname: 'dns_type',
type: 'Badge',
},
],
banner({ documentResource: site }) {
if (site.pg.broken_domain_error) {
return {
title:
'There was an error fetching an https certificate for your domain.',
type: 'error',
button: {
label: 'View Error',
variant: 'outline',
onClick() {
renderDialog(
h(
GenericDialog,
{
options: {
title: 'Error fetching certificate',
size: 'xl',
},
},
{
default: () => {
return h('pre', {
class:
'whitespace-pre-wrap text-sm rounded border-2 p-3 border-gray-200 bg-gray-100',
innerHTML: site.pg.broken_domain_error,
});
},
},
),
);
},
},
};
} else {
return null;
}
},
primaryAction({ listResource: domains, documentResource: site }) {
return {
label: 'Add Domain',
slots: {
prefix: icon('plus'),
},
onClick() {
renderDialog(
h(AddDomainDialog, {
site: site.pg,
onDomainAdded() {
domains.reload();
},
}),
);
},
};
},
rowActions({ row, listResource: domains, documentResource: site }) {
return [
{
label: 'Remove',
condition: () => row.domain !== site.pg?.name,
onClick() {
confirmDialog({
title: `Remove Domain`,
message: `Are you sure you want to remove the domain <b>${row.domain}</b> from the site <b>${site.pg?.name}</b>?`,
onSuccess({ hide }) {
if (site.removeDomain.loading) return;
toast.promise(
site.removeDomain.submit({
domain: row.domain,
}),
{
loading: 'Removing domain...',
success: () => {
hide();
return 'Domain removed';
},
error: (e) => getToastErrorMessage(e),
},
);
},
});
},
},
{
label: 'Set Primary',
condition: () => !row.primary && row.status === 'Active',
onClick() {
confirmDialog({
title: `Set Primary Domain`,
message: `Are you sure you want to set the domain <b>${row.domain}</b> as the primary domain for the site <b>${site.pg?.name}</b>?`,
onSuccess({ hide }) {
if (site.setPrimaryDomain.loading) return;
toast.promise(
site.setPrimaryDomain.submit({
domain: row.domain,
}),
{
loading: 'Setting primary domain...',
success: () => {
hide();
return 'Primary domain set';
},
error: (e) => getToastErrorMessage(e),
},
);
},
});
},
},
{
label: 'Redirect to Primary',
condition: () =>
!row.primary &&
!row.redirect_to_primary &&
row.status === 'Active',
onClick() {
confirmDialog({
title: `Redirect Domain`,
message: `Are you sure you want to redirect the domain <b>${row.domain}</b> to the primary domain of the site <b>${site.pg?.host_name}</b>?`,
onSuccess({ hide }) {
if (site.redirectToPrimary.loading) return;
toast.promise(
site.redirectToPrimary.submit({
domain: row.domain,
}),
{
loading: 'Redirecting domain...',
success: () => {
hide();
return 'Domain redirected';
},
error: (e) => getToastErrorMessage(e),
},
);
},
});
},
},
{
label: 'Remove Redirect',
condition: () =>
!row.primary &&
row.redirect_to_primary &&
row.status === 'Active',
onClick() {
confirmDialog({
title: `Remove Redirect`,
message: `Are you sure you want to remove the redirect from the domain <b>${row.domain}</b> to the primary domain of the site <b>${site.pg?.host_name}</b>?`,
onSuccess({ hide }) {
if (site.removeRedirect.loading) return;
toast.promise(
site.removeRedirect.submit({
domain: row.domain,
}),
{
loading: 'Removing redirect...',
success: () => {
hide();
return 'Redirect removed';
},
error: (e) => getToastErrorMessage(e),
},
);
},
});
},
},
{
label: 'Fetch Certificate',
condition: () =>
row.status === 'Broken' &&
site.pg.broken_domain_error &&
site.pg.tls_cert_retry_count < 8,
onClick() {
confirmDialog({
title: `Fetch Certificate`,
message: `Are you sure you want to retry fetching the certificate for the domain <b>${row.domain}</b>?<br><br>
<b>Note:</b> This action is rate limited. Please allow some time for dns changes (if any) to propagate before retrying.`,
onSuccess({ hide }) {
if (site.fetchCertificate.loading) return;
toast.promise(
site.fetchCertificate.submit({
domain: row.domain,
}),
{
loading: 'Fetching certificate...',
success: () => {
hide();
return 'Certificate fetch scheduled. Please wait a few minutes.';
},
error: (e) => getToastErrorMessage(e),
},
);
},
});
},
},
];
},
},
},
{
label: 'Backups',
icon: icon('archive'),
route: 'backups',
type: 'list',
list: {
doctype: 'Site Backup',
filters: (site) => {
let filters = {
site: site.pg?.name,
};
const backup_name = getQueryParam('name');
if (backup_name) {
filters.name = backup_name;
}
return filters;
},
orderBy: 'creation desc',
fields: [
'name',
'job',
'status',
'database_url',
'public_url',
'private_url',
'config_file_url',
'site',
'remote_database_file',
'remote_public_file',
'remote_private_file',
'remote_config_file',
'physical',
],
columns: [
{
label: 'Timestamp',
fieldname: 'creation',
width: 1,
format(value) {
return `Backup on ${date(value, 'llll')}`;
},
},
{
label: 'Status',
fieldname: 'status',
width: '150px',
align: 'center',
type: 'Badge',
},
{
label: 'Database',
fieldname: 'database_size',
width: 0.5,
format(value) {
return value ? bytes(value) : '';
},
},
{
label: 'Public Files',
fieldname: 'public_size',
width: 0.5,
format(value) {
return value ? bytes(value) : '';
},
},
{
label: 'Private Files',
fieldname: 'private_size',
width: 0.5,
format(value) {
return value ? bytes(value) : '';
},
},
{
label: 'Files',
fieldname: 'with_files',
type: 'Icon',
width: 0.25,
Icon(value) {
return value ? 'check' : '';
},
},
{
label: 'Offsite',
fieldname: 'offsite',
width: 0.25,
type: 'Icon',
Icon(value) {
return value ? 'check' : '';
},
},
{
label: 'Physical',
fieldname: 'physical',
width: 0.25,
type: 'Icon',
Icon(value) {
return value ? 'check' : '';
},
},
],
searchField: getQueryParam('name') ? null : 'name',
updateFilters({ name }) {
setQueryParam('name', name);
},
autoReloadAfterUpdateFilterCallback: true,
filterControls() {
const backup_name = getQueryParam('name');
let filters = backup_name
? [
{
type: 'text',
label: 'Backup Record',
fieldname: 'name',
},
]
: [];
filters = filters.concat([
{
type: 'checkbox',
label: 'Physical Backups',
fieldname: 'physical',
},
{
type: 'checkbox',
label: 'Offsite Backups',
fieldname: 'offsite',
},
]);
return filters;
},
rowActions({ row, documentResource: site }) {
if (row.status != 'Success') return;
function getFileName(file) {
if (file == 'database') return 'database';
if (file == 'public') return 'public files';
if (file == 'private') return 'private files';
if (file == 'config') return 'config file';
}
function confirmDownload(backup, file) {
confirmDialog({
title: 'Download Backup',
message: `You will be downloading the ${getFileName(
file,
)} backup of the site <b>${
site.pg?.host_name || site.pg?.name
}</b> that was created on ${date(backup.creation, 'llll')}.${
!backup.offsite
? '<br><br><div class="p-2 bg-gray-100 rounded border-gray-200">You have to be logged in as a <b>System Manager</b> <em>in your site</em> to download the backup.<div>'
: ''
}`,
onSuccess() {
downloadBackup(backup, file);
},
});
}
async function downloadBackup(backup, file) {
// file: database, public, or private
if (backup.offsite) {
site.getBackupDownloadLink.submit(
{ backup: backup.name, file },
{
onSuccess(r) {
// TODO: fix this in documentResource, it should return message directly
if (r.message) {
window.open(r.message);
}
},
},
);
} else {
const url =
file == 'config'
? backup.config_file_url
: backup[file + '_url'];
const domainRegex = /^(https?:\/\/)?([^/]+)\/?/;
const newUrl = url.replace(
domainRegex,
`$1${site.pg.host_name}/`,
);
window.open(newUrl);
}
}
return [
{
group: 'Details',
items: [
{
label: 'View Job',
onClick() {
router.push({
name: 'Site Job',
params: { name: site.name, id: row.job },
});
},
},
],
},
{
group: 'Download',
condition: () => !row.physical,
items: [
{
label: 'Download Database',
onClick() {
return confirmDownload(row, 'database');
},
},
{
label: 'Download Public',
onClick() {
return confirmDownload(row, 'public');
},
condition: () => row.public_url,
},
{
label: 'Download Private',
onClick() {
return confirmDownload(row, 'private');
},
condition: () => row.private_url,
},
{
label: 'Download Config',
onClick() {
return confirmDownload(row, 'config');
},
condition: () => row.config_file_url,
},
],
},
{
group: 'Restore',
condition: () => row.offsite || row.physical,
items: [
{
label: 'Restore Backup',
condition: () => site.pg.status !== 'Archived',
onClick() {
if (row.physical && row.ready_to_restore) {
toast.error(
'Physical Snapshot is not ready to restore. Try again after 10 minutes.',
);
return;
}
if (row.physical) {
confirmDialog({
title: 'Restore Physical Backup',
message: `Are you sure you want to restore your site's database from physical backup taken on <b>${dayjs(
row.creation,
).format('lll')}</b> ?`,
onSuccess({ hide }) {
toast.promise(
site.restoreSiteFromPhysicalBackup.submit({
backup: row.name,
}),
{
loading:
'Scheduling physical backup restore...',
success: () => {
hide();
router.push({
name: 'Site Jobs',
params: {
name: site.name,
},
});
return 'Backup restore scheduled successfully.';
},
error: (e) => getToastErrorMessage(e),
},
);
},
});
} else {
confirmDialog({
title: 'Restore Backup',
message: `Are you sure you want to restore your site to this offsite backup from <b>${dayjs(
row.creation,
).format('lll')}</b> ?`,
onSuccess({ hide }) {
toast.promise(
site.restoreSiteFromFiles.submit({
files: {
database: row.remote_database_file,
public: row.remote_public_file,
private: row.remote_private_file,
config: row.remote_config_file,
},
}),
{
loading: 'Scheduling backup restore...',
success: (jobId) => {
hide();
router.push({
name: 'Site Job',
params: {
name: site.name,
id: jobId,
},
});
return 'Backup restore scheduled successfully.';
},
error: (e) => getToastErrorMessage(e),
},
);
},
});
}
},
},
{
label: 'Restore Backup on another Site',
condition: () => !row.physical,
onClick() {
let SelectSiteForRestore = defineAsyncComponent(
() =>
import('../components/site/SelectSiteForRestore.vue'),
);
renderDialog(
h(SelectSiteForRestore, {
site: site.name,
database_backup_exists: Boolean(
row.remote_database_file,
),
public_backup_exists: Boolean(row.remote_public_file),
private_backup_exists: Boolean(
row.remote_private_file,
),
config_backup_exists: Boolean(row.remote_config_file),
onRestore({
selectedSite,
restoreDatabase,
restorePublic,
restorePrivate,
restoreConfig,
}) {
const restoreSite = createResource({
url: 'press.api.site.restore',
});
let payload = {
name: selectedSite,
files: {},
};
if (restoreDatabase) {
payload.files.database = row.remote_database_file;
}
if (restorePublic) {
payload.files.public = row.remote_public_file;
}
if (restorePrivate) {
payload.files.private = row.remote_private_file;
}
if (restoreConfig) {
payload.files.config = row.remote_config_file;
}
// check if any file is selected
if (Object.keys(payload.files).length === 0) {
toast.error(
'Please select at least one file to restore.',
);
return;
}
return toast.promise(restoreSite.submit(payload), {
loading: 'Scheduling backup restore...',
success: (jobId) => {
router.push({
name: 'Site Job',
params: { name: selectedSite, id: jobId },
});
return 'Backup restore scheduled successfully.';
},
error: (e) => getToastErrorMessage(e),
});
},
}),
);
},
},
],
},
].filter((d) => (d.condition ? d.condition() : true));
},
primaryAction({ listResource: backups, documentResource: site }) {
return {
label: 'Schedule Backup',
slots: {
prefix: icon('upload-cloud'),
},
loading: site.backup.loading,
onClick() {
renderDialog(
h(
defineAsyncComponent(
() => import('../components/site/SiteScheduleBackup.vue'),
),
{
site: site.name,
onScheduleBackupSuccess: () => backups.reload(),
},
),
);
},
};
},
banner({ documentResource: site, listResource: backups }) {
if (site.pg?.status === 'Archived') {
if (backups?.data && backups.data.length > 0) {
return {
title: 'Need help with restoring your archived site.',
dismissable: true,
id: site.pg.name,
type: 'gray',
button: {
label: 'Contact Support',
variant: 'outline',
onClick() {
window.open('https://jcloud.jingrow.com/support', '_blank');
},
},
};
}
return;
}
return getUpsellBanner(
site,
'Your site is currently on a shared bench group. Upgrade plan for offsite backups and <a href="https://jcloud.jingrow.com/shared-hosting#benches" class="underline" target="_blank">more</a>.',
);
},
},
},
{
label: 'Site Config',
icon: icon('settings'),
route: 'site-config',
type: 'list',
condition: (site) => {
return site.pg?.status !== 'Archived';
},
list: {
doctype: 'Site Config',
filters: (site) => {
return { parent: site.pg?.name, parenttype: 'Site' };
},
fields: ['name'],
pageLength: 999,
orderBy: 'creation desc',
columns: [
{
label: 'Config Name',
fieldname: 'key',
width: 1,
format(value, row) {
if (row.title) {
return `${row.title} (${row.key})`;
}
return row.key;
},
},
{
label: 'Config Value',
fieldname: 'value',
class: 'font-mono',
width: 2,
},
{
label: 'Type',
fieldname: 'type',
type: 'Badge',
width: '100px',
},
],
primaryAction({ listResource: configs, documentResource: site }) {
return {
label: 'Add Config',
slots: {
prefix: icon('plus'),
},
onClick() {
let ConfigEditorDialog = defineAsyncComponent(
() => import('../components/ConfigEditorDialog.vue'),
);
renderDialog(
h(ConfigEditorDialog, {
site: site.pg?.name,
onSuccess() {
configs.reload();
},
}),
);
},
};
},
secondaryAction({ listResource: configs }) {
return {
label: 'Preview',
slots: {
prefix: icon('eye'),
},
onClick() {
let ConfigPreviewDialog = defineAsyncComponent(
() => import('../components/ConfigPreviewDialog.vue'),
);
renderDialog(
h(ConfigPreviewDialog, {
configs: configs.data,
}),
);
},
};
},
rowActions({ row, listResource: configs, documentResource: site }) {
return [
{
label: 'Edit',
onClick() {
let ConfigEditorDialog = defineAsyncComponent(
() => import('../components/ConfigEditorDialog.vue'),
);
renderDialog(
h(ConfigEditorDialog, {
site: site.pg?.name,
config: row,
onSuccess() {
configs.reload();
},
}),
);
},
},
{
label: 'Delete',
onClick() {
confirmDialog({
title: 'Delete Config',
message: `Are you sure you want to delete the config <b>${row.key}</b>?`,
onSuccess({ hide }) {
if (site.deleteConfig.loading) return;
toast.promise(
site.deleteConfig.submit(
{ key: row.key },
{
onSuccess: () => {
configs.reload();
hide();
},
},
),
{
loading: 'Deleting config...',
success: () => `Config ${row.key} removed`,
error: (e) => getToastErrorMessage(e),
},
);
},
});
},
},
];
},
},
},
{
label: 'Actions',
icon: icon('sliders'),
route: 'actions',
type: 'Component',
condition: (site) => {
return site.pg?.status !== 'Archived';
},
component: SiteActions,
props: (site) => {
return { site: site.pg?.name };
},
},
{
label: 'Updates',
icon: icon('arrow-up-circle'),
route: 'updates',
type: 'list',
condition: (site) => {
return site.pg?.status !== 'Archived';
},
childrenRoutes: ['Site Update'],
list: {
doctype: 'Site Update',
filters: (site) => {
return { site: site.pg?.name };
},
orderBy: 'creation',
fields: [
'difference',
'update_job.end as updated_on',
'update_job',
'backup_type',
'recover_job',
],
columns: [
{
label: 'Type',
fieldname: 'deploy_type',
width: 0.3,
},
{
label: 'Status',
fieldname: 'status',
type: 'Badge',
width: 0.5,
},
// {
// label: 'Backup',
// width: 0.4,
// type: 'Component',
// component({ row }) {
// return h(
// 'div',
// {
// class: 'truncate text-base',
// },
// row.skipped_backups
// ? 'Skipped'
// : row.backup_type || 'Logical',
// );
// },
// },
{
label: 'Created By',
fieldname: 'owner',
},
{
label: 'Scheduled At',
fieldname: 'scheduled_time',
format(value) {
return date(value, 'lll');
},
},
{
label: 'Updated On',
fieldname: 'updated_on',
format(value) {
return date(value, 'lll');
},
},
],
rowActions({ row, documentResource: site }) {
return [
{
label: 'Edit',
condition: () => row.status === 'Scheduled',
onClick() {
let SiteUpdateDialog = defineAsyncComponent(
() => import('../components/SiteUpdateDialog.vue'),
);
renderDialog(
h(SiteUpdateDialog, {
site: site.pg?.name,
existingUpdate: row.name,
}),
);
},
},
{
label: 'Cancel',
condition: () => row.status === 'Scheduled',
onClick() {
confirmDialog({
title: 'Cancel Update',
message: `Are you sure you want to cancel the scheduled update?`,
onSuccess({ hide }) {
if (site.cancelUpdate.loading) return;
toast.promise(
site.cancelUpdate.submit({ site_update: row.name }),
{
loading: 'Cancelling update...',
success: () => {
hide();
site.reload();
return 'Update cancelled';
},
error: (e) => getToastErrorMessage(e),
},
);
},
});
},
},
{
label: 'View Job',
condition: () =>
!['Scheduled', 'Cancelled'].includes(row.status),
onClick() {
router.push({
name: 'Site Update',
params: { id: row.name },
});
},
},
{
label: 'Update Now',
condition: () => row.status === 'Scheduled',
onClick() {
let siteUpdate = getDocResource({
doctype: 'Site Update',
name: row.name,
whitelistedMethods: {
updateNow: 'start',
},
});
toast.promise(siteUpdate.updateNow.submit(), {
loading: 'Updating site...',
success: () => {
router.push({
name: 'Site Update',
params: { id: row.name },
});
return 'Site update started';
},
error: 'Failed to update site',
});
},
},
{
label: 'View App Changes',
onClick() {
createListResource({
doctype: 'Deploy Candidate Difference App',
fields: [
'difference.github_diff_url as diff_url',
'difference.source_hash as source_hash',
'difference.destination_hash as destination_hash',
'app.title as app',
],
filters: {
parenttype: 'Deploy Candidate Difference',
parent: row.difference,
},
auto: true,
pageLength: 99,
onSuccess(data) {
if (data?.length) {
renderDialog(
h(
GenericDialog,
{
options: {
title: 'App Changes',
size: '2xl',
},
},
{
default: () =>
h(ObjectList, {
options: {
data: () => data,
columns: [
{
label: 'App',
fieldname: 'app',
},
{
label: 'From',
fieldname: 'source_hash',
type: 'Button',
Button({ row }) {
return {
label:
row.source_tag ||
row.source_hash.slice(0, 7),
variant: 'ghost',
class: 'font-mono',
link: `${
row.diff_url.split('/compare')[0]
}/commit/${row.source_hash}`,
};
},
},
{
label: 'To',
fieldname: 'destination_hash',
type: 'Button',
Button({ row }) {
return {
label:
row.destination_tag ||
row.destination_hash.slice(0, 7),
variant: 'ghost',
class: 'font-mono',
link: `${
row.diff_url.split('/compare')[0]
}/commit/${row.destination_hash}`,
};
},
},
{
label: 'App Changes',
fieldname: 'diff_url',
align: 'right',
type: 'Button',
Button({ row }) {
return {
label: 'View',
variant: 'ghost',
slots: {
prefix: icon('external-link'),
},
link: row.diff_url,
};
},
},
],
},
}),
},
),
);
} else toast.error('No app changes found');
},
});
},
},
];
},
actions({ documentResource: site }) {
if (site.pg.group_public) return [];
return [
{
label: 'Configure',
slots: {
prefix: icon('settings'),
},
onClick() {
let ConfigureAutoUpdateDialog = defineAsyncComponent(
() =>
import(
'../components/site/ConfigureAutoUpdateDialog.vue'
),
);
renderDialog(
h(ConfigureAutoUpdateDialog, {
site: site.pg?.name,
}),
);
},
},
];
},
banner({ documentResource: site }) {
const bannerTitle =
'Your site is currently on a shared bench group. Upgrade to a private bench group to configure auto updates and <a href="https://jcloud.jingrow.com/shared-hosting#benches" class="underline" target="_blank">more</a>.';
return getUpsellBanner(site, bannerTitle);
},
},
},
{
label: 'Activity',
icon: icon('activity'),
route: 'activity',
type: 'list',
condition: (site) => site.pg?.status !== 'Archived',
list: {
doctype: 'Site Activity',
filters: (site) => {
return { site: site.pg?.name };
},
fields: ['owner', 'job'],
orderBy: 'creation desc',
route(row) {
if (!row.job) return {};
return {
name: 'Site Job',
params: { id: row.job },
};
},
columns: [
{
label: 'Action',
fieldname: 'action',
format(value, row) {
let action = row.action;
if (action == 'Create') {
action = 'Site created';
}
return `${action} by ${row.owner}`;
},
},
{
label: 'Description',
fieldname: 'reason',
class: 'text-gray-600',
},
{
label: '',
fieldname: 'creation',
type: 'Timestamp',
align: 'right',
},
],
filterControls() {
return [
{
type: 'select',
label: 'Action',
fieldname: 'action',
class: !isMobile() ? 'w-52' : '',
options: [
'',
'Activate Site',
'Add Domain',
'Archive',
'Backup',
'Create',
'Clear Cache',
'Deactivate Site',
'Disable Database Access',
'Drop Offsite Backups',
'Enable Database Access',
'Install App',
'Login as Administrator',
'Migrate',
'Reinstall',
'Restore',
'Suspend Site',
'Uninstall App',
'Unsuspend Site',
'Update',
'Update Configuration',
],
},
];
},
},
},
],
actions(context) {
let { documentResource: site } = context;
let $team = getTeam();
let runningJobs = getRunningJobs({ site: site.pg?.name });
return [
{
label: 'Jobs in progress',
slots: {
prefix: () => h(LoadingIndicator, { class: 'w-4 h-4' }),
},
condition() {
return (
runningJobs.filter((job) =>
['Pending', 'Running'].includes(job.status),
).length > 0
);
},
onClick() {
router.push({
name: 'Site Jobs',
params: { name: site.name },
});
},
},
{
label: 'Update Available',
variant: site.pg?.setup_wizard_complete ? 'solid' : 'subtle',
slots: {
prefix: icon('alert-circle'),
},
condition() {
return (
!site.pg?.has_scheduled_updates &&
site.pg.update_information?.update_available &&
['Active', 'Inactive', 'Suspended', 'Broken'].includes(
site.pg.status,
)
);
},
onClick() {
let SiteUpdateDialog = defineAsyncComponent(
() => import('../components/SiteUpdateDialog.vue'),
);
renderDialog(h(SiteUpdateDialog, { site: site.pg?.name }));
},
},
{
label: 'Update Scheduled',
slots: {
prefix: icon('calendar'),
},
condition: () => site.pg?.has_scheduled_updates,
onClick() {
router.push({
name: 'Site Detail Updates',
params: { name: site.name },
});
},
},
{
label: 'Enable Monitoring',
slots: {
prefix: icon('activity'),
},
condition: () => site.pg?.is_monitoring_disabled,
onClick() {
let SiteEnableMonitoringDialog = defineAsyncComponent(
() => import('../components/site/SiteEnableMonitoringDialog.vue'),
);
renderDialog(
h(SiteEnableMonitoringDialog, { site: site.pg?.name }),
);
},
},
{
label: 'Impersonate Site Owner',
title: 'Impersonate Site Owner', // for label to pop-up on hover
slots: {
icon: icon(LucideVenetianMask),
},
condition: () =>
$team.pg?.is_desk_user && site.pg.team !== $team.name,
onClick() {
switchToTeam(site.pg.team);
},
},
{
label: 'Visit Site',
slots: {
prefix: icon('external-link'),
},
condition: () =>
site.pg.status !== 'Archived' && site.pg?.setup_wizard_complete,
onClick() {
let siteURL = `https://${site.name}`;
if (
site.pg.version === 'Nightly' ||
Number(site.pg.version.split(' ')[1]) >= 15
)
siteURL += '/apps';
window.open(siteURL, '_blank');
},
},
{
label: 'Setup Site',
slots: {
prefix: icon('external-link'),
},
variant: 'solid',
loading: site.loginAsAdmin.loading || site.loginAsTeam.loading,
condition: () =>
site.pg.status === 'Active' && !site.pg?.setup_wizard_complete,
onClick() {
if (site.pg.additional_system_user_created) {
site.loginAsTeam
.submit({ reason: '' })
.then((url) => window.open(url, '_blank'));
} else {
site.loginAsAdmin
.submit({ reason: '' })
.then((url) => window.open(url, '_blank'));
}
},
},
{
label: 'Options',
context,
options: [
{
label: 'View in Desk',
icon: 'external-link',
condition: () => $team.pg?.is_desk_user,
onClick: () => {
window.open(
`${window.location.protocol}//${window.location.host}/app/site/${site.name}`,
'_blank',
);
},
},
{
label: 'Login As Administrator',
icon: 'external-link',
condition: () => ['Active', 'Broken'].includes(site.pg.status),
onClick: () => {
confirmDialog({
title: 'Login as Administrator',
message: `Are you sure you want to login as administrator on the site <b>${site.pg?.name}</b>?`,
fields:
$team.name !== site.pg.team || $team.pg.is_desk_user
? [
{
label: 'Reason',
type: 'textarea',
fieldname: 'reason',
},
]
: [],
onSuccess: ({ hide, values }) => {
if (
!values.reason &&
($team.name !== site.pg.team || $team.pg.is_desk_user)
) {
throw new Error('Reason is required');
}
return site.loginAsAdmin
.submit({ reason: values.reason })
.then((result) => {
let url = result;
window.open(url, '_blank');
hide();
});
},
});
},
},
],
},
];
},
},
routes: [
{
name: 'Site Update',
path: 'updates/:id',
component: () => import('../pages/SiteUpdate.vue'),
},
],
};