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 ${row.domain} from the site ${site.pg?.name}?`,
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 ${row.domain} as the primary domain for the site ${site.pg?.name}?`,
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 ${row.domain} to the primary domain of the site ${site.pg?.host_name}?`,
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 ${row.domain} to the primary domain of the site ${site.pg?.host_name}?`,
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 ${row.domain}?
Note: 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 ${
site.pg?.host_name || site.pg?.name
} that was created on ${date(backup.creation, 'llll')}.${
!backup.offsite
? '