import {
createListResource,
createResource,
LoadingIndicator,
} from 'jingrow-ui';
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, getStatusLabel, getDeployTypeLabel } 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 { getJobsTab } from './common/jobs';
export default {
pagetype: 'Site',
whitelistedMethods: {
activate: 'activate',
addDomain: 'add_domain',
archive: 'archive',
backup: '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',
restoreSite: 'restore_site',
restoreSiteFromFiles: 'restore_site_from_files',
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',
},
list: {
route: '/sites',
title: '站点',
fields: [
'plan.plan_title as plan_title',
'plan.price_usd as price_usd',
'plan.price_cny as price_cny',
'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',
'site_end_date',
],
orderBy: 'creation desc',
searchField: 'host_name',
filterControls() {
return [
{
type: 'select',
label: '状态',
fieldname: 'status',
options: [
{ label: '', value: '' },
{ label: '激活', value: 'Active' },
{ label: '未激活', value: 'Inactive' },
{ label: '已暂停', value: 'Suspended' },
{ label: '损坏', value: 'Broken' },
{ label: '已归档', value: 'Archived' },
],
},
{
type: 'link',
label: '版本',
fieldname: 'group.version',
options: {
pagetype: 'Jingrow Version',
},
},
{
type: 'link',
label: '站点分组',
fieldname: 'group',
options: {
pagetype: 'Release Group',
},
},
{
type: 'select',
label: '区域',
fieldname: 'cluster',
options: clusterOptions,
},
{
type: 'link',
label: '标签',
fieldname: 'tags.tag',
options: {
pagetype: 'Jcloud Tag',
filters: {
pagetype_name: 'Site',
},
},
},
];
},
columns: [
{
label: '站点',
fieldname: 'host_name',
width: 1.5,
class: 'font-medium',
format(value, row) {
return value || row.name;
},
},
{
label: '状态',
fieldname: 'status',
type: 'Badge',
width: '140px',
format(value) {
const statusMap = {
'Active': '激活',
'Inactive': '未激活',
'Suspended': '已暂停',
'Broken': '损坏',
'Archived': '已归档',
'Pending': '待处理',
'Installing': '安装中',
'Update Available': '可更新',
'Running': '运行中',
'Success': '成功',
'Failure': '失败'
};
return statusMap[value] || value;
}
},
{
label: '计划',
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 china = $team.pg?.currency === 'CNY';
const formattedValue = userCurrency(
china ? row.price_cny : row.price_usd,
0,
);
return `${formattedValue}/月`;
}
return row.plan_title;
},
},
{
label: '区域',
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: '站点分组',
fieldname: 'group',
width: '15rem',
format(value, row) {
return row.group_public ? '公域' : row.group_title || value;
},
},
{
label: '版本',
fieldname: 'version',
width: 0.5,
},
{
label: '到期时间',
fieldname: 'site_end_date',
width: 1,
format(value) {
return value ? date(value, 'YYYY-MM-DD') : '';
},
},
],
primaryAction({ listResource: sites }) {
return {
label: '新建站点',
variant: 'solid',
slots: {
prefix: icon('plus'),
},
onClick() {
router.push({ name: 'New Site' });
},
};
},
moreActions({ listResource: sites }) {
return [
{
label: '导出为CSV',
icon: 'download',
onClick() {
const fields = [
'host_name',
'plan_title',
'cluster_title',
'group_title',
'version',
];
const data = sites.data.map((site) => {
const row = {};
fields.forEach((field) => {
row[field] = site[field];
});
return row;
});
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 }) {
const statusMap = {
'Active': '激活',
'Inactive': '未激活',
'Suspended': '已暂停',
'Broken': '损坏',
'Archived': '已归档',
'Pending': '待处理',
'Installing': '安装中',
'Running': '运行中',
'Success': '成功',
'Failure': '失败'
};
return { label: statusMap[site.pg.status] || 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
) {
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: '概览',
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 };
},
},
getAppsTab(true),
{
label: '域名',
icon: icon('external-link'),
route: 'domains',
type: 'list',
condition: (site) => site.pg?.status !== 'Archived',
list: {
pagetype: 'Site Domain',
fields: ['redirect_to_primary'],
filters: (site) => {
return { site: site.pg?.name };
},
columns: [
{
label: '域名',
fieldname: 'domain',
},
{
label: '状态',
fieldname: 'status',
type: 'Badge',
format(value) {
const statusMap = {
'Active': '激活',
'Inactive': '未激活',
'Suspended': '已暂停',
'Broken': '损坏',
'Archived': '已归档',
'Pending': '待处理',
'Installing': '安装中',
'Running': '运行中',
'Success': '成功',
'Failure': '失败'
};
return statusMap[value] || value;
}
},
{
label: '主域名',
fieldname: 'primary',
type: 'Icon',
Icon(value) {
return value ? 'check' : '';
},
},
{
label: 'DNS 类型',
fieldname: 'dns_type',
type: 'Badge',
},
],
banner({ documentResource: site }) {
if (site.pg.broken_domain_error) {
return {
title:
'获取您的域名的 HTTPS 证书时出错。',
type: 'error',
button: {
label: '查看错误',
variant: 'outline',
onClick() {
renderDialog(
h(
GenericDialog,
{
options: {
title: '获取证书时出错',
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: '添加域名',
slots: {
prefix: icon('plus'),
},
onClick() {
renderDialog(
h(AddDomainDialog, {
site: site.pg,
onDomainAdded() {
domains.reload();
},
}),
);
},
};
},
rowActions({ row, listResource: domains, documentResource: site }) {
return [
{
label: '移除',
condition: () => row.domain !== site.pg?.name,
onClick() {
confirmDialog({
title: `移除域名`,
message: `您确定要从站点 ${site.pg?.name} 中移除域名 ${row.domain} 吗?`,
onSuccess({ hide }) {
if (site.removeDomain.loading) return;
toast.promise(
site.removeDomain.submit({
domain: row.domain,
}),
{
loading: '正在移除域名...',
success: () => {
hide();
return '域名已移除';
},
error: (e) => getToastErrorMessage(e),
},
);
},
});
},
},
{
label: '设为主域名',
condition: () => !row.primary && row.status === 'Active',
onClick() {
confirmDialog({
title: `设为主域名`,
message: `您确定要将域名 ${row.domain} 设为站点 ${site.pg?.name} 的主域名吗?`,
onSuccess({ hide }) {
if (site.setPrimaryDomain.loading) return;
toast.promise(
site.setPrimaryDomain.submit({
domain: row.domain,
}),
{
loading: '正在设置主域名...',
success: () => {
hide();
return '主域名已设置';
},
error: (e) => getToastErrorMessage(e),
},
);
},
});
},
},
{
label: '重定向到主域名',
condition: () =>
!row.primary &&
!row.redirect_to_primary &&
row.status === 'Active',
onClick() {
confirmDialog({
title: `重定向域名`,
message: `您确定要将域名 ${row.domain} 重定向到站点 ${site.pg?.name} 的主域名吗?`,
onSuccess({ hide }) {
if (site.redirectToPrimary.loading) return;
toast.promise(
site.redirectToPrimary.submit({
domain: row.domain,
}),
{
loading: '正在重定向域名...',
success: () => {
hide();
return '域名已重定向';
},
error: (e) => getToastErrorMessage(e),
},
);
},
});
},
},
{
label: '移除重定向',
condition: () =>
!row.primary &&
row.redirect_to_primary &&
row.status === 'Active',
onClick() {
confirmDialog({
title: `移除重定向`,
message: `您确定要移除从域名 ${row.domain} 到站点 ${site.pg?.name} 主域名的重定向吗?`,
onSuccess({ hide }) {
if (site.removeRedirect.loading) return;
toast.promise(
site.removeRedirect.submit({
domain: row.domain,
}),
{
loading: '正在移除重定向...',
success: () => {
hide();
return '重定向已移除';
},
error: (e) => getToastErrorMessage(e),
},
);
},
});
},
},
];
},
},
},
{
label: '备份',
icon: icon('archive'),
route: 'backups',
type: 'list',
list: {
pagetype: 'Site Backup',
filters: (site) => {
return {
site: site.pg?.name,
files_availability: 'Available',
status: ['in', ['Pending', 'Running', 'Success']],
};
},
orderBy: 'creation desc',
fields: [
'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: '时间戳',
fieldname: 'creation',
width: 1,
format(value) {
return `备份于 ${date(value, 'llll')}`;
},
},
{
label: '数据库',
fieldname: 'database_size',
width: 0.5,
format(value) {
return value ? bytes(value) : '';
},
},
{
label: '公共文件',
fieldname: 'public_size',
width: 0.5,
format(value) {
return value ? bytes(value) : '';
},
},
{
label: '私有文件',
fieldname: 'private_size',
width: 0.5,
format(value) {
return value ? bytes(value) : '';
},
},
{
label: '包含文件的备份',
fieldname: 'with_files',
type: 'Icon',
width: 0.5,
Icon(value) {
return value ? 'check' : '';
},
},
{
label: '异地备份',
fieldname: 'offsite',
width: 0.5,
type: 'Icon',
Icon(value) {
return value ? 'check' : '';
},
},
],
filterControls() {
return [
{
type: 'checkbox',
label: '异地备份',
fieldname: 'offsite',
},
];
},
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: '下载备份',
message: `您将下载站点 ${
site.pg?.host_name || site.pg?.name
} 的 ${getFileName(file)} 备份,该备份创建于 ${date(backup.creation, 'llll')}。${
!backup.offsite
? '