1337 lines
35 KiB
JavaScript
1337 lines
35 KiB
JavaScript
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 } 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';
|
|
|
|
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': '安装中',
|
|
'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: `您确定要从站点 <b>${site.pg?.name}</b> 中移除域名 <b>${row.domain}</b> 吗?`,
|
|
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: `您确定要将域名 <b>${row.domain}</b> 设为站点 <b>${site.pg?.name}</b> 的主域名吗?`,
|
|
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: `您确定要将域名 <b>${row.domain}</b> 重定向到站点 <b>${site.pg?.name}</b> 的主域名吗?`,
|
|
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: `您确定要移除从域名 <b>${row.domain}</b> 到站点 <b>${site.pg?.name}</b> 主域名的重定向吗?`,
|
|
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: `您将下载站点 <b>${
|
|
site.pg?.host_name || site.pg?.name
|
|
}</b> 的 ${getFileName(file)} 备份,该备份创建于 ${date(backup.creation, 'llll')}。${
|
|
!backup.offsite
|
|
? '<br><br><div class="p-2 bg-gray-100 border-gray-200 rounded">您需要以 <b>系统管理员</b> 身份登录 <em>您的站点</em> 才能下载备份。<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: '详情',
|
|
items: [
|
|
{
|
|
label: '查看任务',
|
|
onClick() {
|
|
router.push({
|
|
name: 'Site Job',
|
|
params: { name: site.name, id: row.job },
|
|
});
|
|
},
|
|
},
|
|
],
|
|
},
|
|
{
|
|
group: '下载',
|
|
items: [
|
|
{
|
|
label: '下载数据库',
|
|
onClick() {
|
|
return confirmDownload(row, 'database');
|
|
},
|
|
},
|
|
{
|
|
label: '下载公共文件',
|
|
onClick() {
|
|
return confirmDownload(row, 'public');
|
|
},
|
|
condition: () => row.public_url,
|
|
},
|
|
{
|
|
label: '下载私有文件',
|
|
onClick() {
|
|
return confirmDownload(row, 'private');
|
|
},
|
|
condition: () => row.private_url,
|
|
},
|
|
{
|
|
label: '下载配置文件',
|
|
onClick() {
|
|
return confirmDownload(row, 'config');
|
|
},
|
|
condition: () => row.config_file_url,
|
|
},
|
|
],
|
|
},
|
|
{
|
|
group: '恢复',
|
|
condition: () => row.offsite,
|
|
items: [
|
|
{
|
|
label: '恢复备份',
|
|
condition: () => site.pg.status !== 'Archived',
|
|
onClick() {
|
|
confirmDialog({
|
|
title: '恢复备份',
|
|
message: `您确定要将您的站点恢复到<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: '正在安排备份恢复...',
|
|
success: (jobId) => {
|
|
hide();
|
|
router.push({
|
|
name: 'Site Job',
|
|
params: {
|
|
name: site.name,
|
|
id: jobId,
|
|
},
|
|
});
|
|
return '备份恢复已成功安排。';
|
|
},
|
|
error: (e) => getToastErrorMessage(e),
|
|
},
|
|
);
|
|
},
|
|
});
|
|
},
|
|
},
|
|
{
|
|
label: '在另一个站点上恢复备份',
|
|
onClick() {
|
|
let SelectSiteForRestore = defineAsyncComponent(
|
|
() =>
|
|
import('../components/site/SelectSiteForRestore.vue'),
|
|
);
|
|
renderDialog(
|
|
h(SelectSiteForRestore, {
|
|
site: site.name,
|
|
onRestore(siteName) {
|
|
const restoreSite = createResource({
|
|
url: 'jcloud.api.site.restore',
|
|
});
|
|
|
|
return toast.promise(
|
|
restoreSite.submit({
|
|
name: siteName,
|
|
files: {
|
|
database: row.remote_database_file,
|
|
public: row.remote_public_file,
|
|
private: row.remote_private_file,
|
|
config: row.remote_config_file,
|
|
},
|
|
}),
|
|
{
|
|
loading: '正在安排备份恢复...',
|
|
success: (jobId) => {
|
|
router.push({
|
|
name: 'Site Job',
|
|
params: { name: siteName, id: jobId },
|
|
});
|
|
return '备份恢复已成功安排。';
|
|
},
|
|
error: (e) => getToastErrorMessage(e),
|
|
},
|
|
);
|
|
},
|
|
}),
|
|
);
|
|
},
|
|
},
|
|
],
|
|
},
|
|
].filter((d) => (d.condition ? d.condition() : true));
|
|
},
|
|
primaryAction({ listResource: backups, documentResource: site }) {
|
|
return {
|
|
label: '安排备份',
|
|
slots: {
|
|
prefix: icon('upload-cloud'),
|
|
},
|
|
loading: site.backup.loading,
|
|
onClick() {
|
|
confirmDialog({
|
|
title: '安排备份',
|
|
message:
|
|
'您确定要安排备份吗?这将创建一个本地备份。',
|
|
onSuccess({ hide }) {
|
|
toast.promise(
|
|
site.backup.submit({
|
|
with_files: true,
|
|
}),
|
|
{
|
|
loading: '正在安排备份...',
|
|
success: () => {
|
|
hide();
|
|
router.push({
|
|
name: 'Site Jobs',
|
|
params: { name: site.name },
|
|
});
|
|
return '备份已成功安排。';
|
|
},
|
|
error: (e) => getToastErrorMessage(e),
|
|
},
|
|
);
|
|
},
|
|
});
|
|
},
|
|
};
|
|
},
|
|
},
|
|
},
|
|
{
|
|
label: '操作',
|
|
icon: icon('sliders'),
|
|
route: 'actions',
|
|
type: 'Component',
|
|
condition: (site) => site.pg?.status !== 'Archived',
|
|
component: SiteActions,
|
|
props: (site) => {
|
|
return { site: site.pg?.name };
|
|
},
|
|
},
|
|
{
|
|
label: '更新',
|
|
icon: icon('arrow-up-circle'),
|
|
route: 'updates',
|
|
type: 'list',
|
|
condition: (site) => site.pg?.status !== 'Archived',
|
|
childrenRoutes: ['Site Update'],
|
|
list: {
|
|
pagetype: '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: '类型',
|
|
fieldname: 'deploy_type',
|
|
width: 0.3,
|
|
},
|
|
{
|
|
label: '状态',
|
|
fieldname: 'status',
|
|
type: 'Badge',
|
|
width: 0.5,
|
|
},
|
|
// {
|
|
// label: '备份',
|
|
// width: 0.4,
|
|
// type: 'Component',
|
|
// component({ row }) {
|
|
// return h(
|
|
// 'div',
|
|
// {
|
|
// class: 'truncate text-base',
|
|
// },
|
|
// row.skipped_backups
|
|
// ? '跳过'
|
|
// : row.backup_type || '逻辑',
|
|
// );
|
|
// },
|
|
// },
|
|
{
|
|
label: '创建者',
|
|
fieldname: 'owner',
|
|
},
|
|
{
|
|
label: '计划时间',
|
|
fieldname: 'scheduled_time',
|
|
format(value) {
|
|
return date(value, 'lll');
|
|
},
|
|
},
|
|
{
|
|
label: '更新时间',
|
|
fieldname: 'updated_on',
|
|
format(value) {
|
|
return date(value, 'lll');
|
|
},
|
|
},
|
|
],
|
|
rowActions({ row, documentResource: site }) {
|
|
return [
|
|
{
|
|
label: '编辑',
|
|
condition: () => row.status === 'Scheduled',
|
|
onClick() {
|
|
let SiteUpdateDialog = defineAsyncComponent(
|
|
() => import('../components/SiteUpdateDialog.vue'),
|
|
);
|
|
renderDialog(
|
|
h(SiteUpdateDialog, {
|
|
site: site.pg?.name,
|
|
existingUpdate: row.name,
|
|
}),
|
|
);
|
|
},
|
|
},
|
|
{
|
|
label: '取消',
|
|
condition: () => row.status === 'Scheduled',
|
|
onClick() {
|
|
confirmDialog({
|
|
title: '取消更新',
|
|
message: `您确定要取消计划的更新吗?`,
|
|
onSuccess({ hide }) {
|
|
if (site.cancelUpdate.loading) return;
|
|
toast.promise(
|
|
site.cancelUpdate.submit({ site_update: row.name }),
|
|
{
|
|
loading: '正在取消更新...',
|
|
success: () => {
|
|
hide();
|
|
site.reload();
|
|
return '更新已取消';
|
|
},
|
|
error: (e) => getToastErrorMessage(e),
|
|
},
|
|
);
|
|
},
|
|
});
|
|
},
|
|
},
|
|
{
|
|
label: '查看任务',
|
|
condition: () => row.status !== 'Scheduled',
|
|
onClick() {
|
|
router.push({
|
|
name: 'Site Update',
|
|
params: { id: row.name },
|
|
});
|
|
},
|
|
},
|
|
{
|
|
label: '立即更新',
|
|
condition: () => row.status === 'Scheduled',
|
|
onClick() {
|
|
let siteUpdate = getDocResource({
|
|
pagetype: 'Site Update',
|
|
name: row.name,
|
|
whitelistedMethods: {
|
|
updateNow: 'start',
|
|
},
|
|
});
|
|
|
|
toast.promise(siteUpdate.updateNow.submit(), {
|
|
loading: '正在更新站点...',
|
|
success: () => {
|
|
router.push({
|
|
name: 'Site Update',
|
|
params: { id: row.name },
|
|
});
|
|
|
|
return '站点更新已启动';
|
|
},
|
|
error: '更新站点失败',
|
|
});
|
|
},
|
|
},
|
|
{
|
|
label: '查看应用更改',
|
|
onClick() {
|
|
createListResource({
|
|
pagetype: '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: '应用更改',
|
|
size: '2xl',
|
|
},
|
|
},
|
|
{
|
|
default: () =>
|
|
h(ObjectList, {
|
|
options: {
|
|
data: () => data,
|
|
columns: [
|
|
{
|
|
label: '应用',
|
|
fieldname: 'app',
|
|
},
|
|
{
|
|
label: '从',
|
|
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: '到',
|
|
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: '应用变更',
|
|
fieldname: 'diff_url',
|
|
align: 'right',
|
|
type: 'Button',
|
|
Button({ row }) {
|
|
return {
|
|
label: '查看',
|
|
variant: 'ghost',
|
|
slots: {
|
|
prefix: icon('external-link'),
|
|
},
|
|
link: row.diff_url,
|
|
};
|
|
},
|
|
},
|
|
],
|
|
},
|
|
}),
|
|
},
|
|
),
|
|
);
|
|
} else toast.error('未找到应用变更');
|
|
},
|
|
});
|
|
},
|
|
},
|
|
];
|
|
},
|
|
actions({ documentResource: site }) {
|
|
if (site.pg.group_public) return [];
|
|
|
|
return [
|
|
{
|
|
label: '配置',
|
|
slots: {
|
|
prefix: icon('settings'),
|
|
},
|
|
onClick() {
|
|
let ConfigureAutoUpdateDialog = defineAsyncComponent(
|
|
() =>
|
|
import(
|
|
'../components/site/ConfigureAutoUpdateDialog.vue'
|
|
),
|
|
);
|
|
|
|
renderDialog(
|
|
h(ConfigureAutoUpdateDialog, {
|
|
site: site.pg?.name,
|
|
}),
|
|
);
|
|
},
|
|
},
|
|
];
|
|
},
|
|
},
|
|
},
|
|
|
|
],
|
|
actions(context) {
|
|
let { documentResource: site } = context;
|
|
let $team = getTeam();
|
|
let runningJobs = getRunningJobs({ site: site.pg?.name });
|
|
|
|
return [
|
|
{
|
|
label: '进行中的任务',
|
|
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: '有可用更新',
|
|
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: '已安排更新',
|
|
slots: {
|
|
prefix: icon('calendar'),
|
|
},
|
|
condition: () => site.pg?.has_scheduled_updates,
|
|
onClick() {
|
|
router.push({
|
|
name: 'Site Detail Updates',
|
|
params: { name: site.name },
|
|
});
|
|
},
|
|
},
|
|
{
|
|
label: '模拟站点所有者',
|
|
title: '模拟站点所有者', // for label to pop-up on hover
|
|
slots: {
|
|
icon: defineAsyncComponent(
|
|
() => import('~icons/lucide/venetian-mask'),
|
|
),
|
|
},
|
|
condition: () =>
|
|
$team.pg?.is_desk_user && site.pg.team !== $team.name,
|
|
onClick() {
|
|
switchToTeam(site.pg.team);
|
|
},
|
|
},
|
|
{
|
|
label: '访问站点',
|
|
slots: {
|
|
prefix: icon('external-link'),
|
|
},
|
|
condition: () =>
|
|
site.pg.status !== 'Archived' && site.pg?.setup_wizard_complete,
|
|
onClick() {
|
|
window.open(`https://${site.name}`, '_blank');
|
|
},
|
|
},
|
|
{
|
|
label: '设置站点',
|
|
slots: {
|
|
prefix: icon('external-link'),
|
|
},
|
|
variant: 'solid',
|
|
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: '选项',
|
|
context,
|
|
options: [
|
|
{
|
|
label: '在 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: '以管理员身份登录',
|
|
icon: 'external-link',
|
|
condition: () => ['Active', 'Broken'].includes(site.pg.status),
|
|
onClick: () => {
|
|
confirmDialog({
|
|
title: '以管理员身份登录',
|
|
message: `您确定要以管理员身份登录站点 <b>${site.pg?.name}</b> 吗?`,
|
|
fields:
|
|
$team.name !== site.pg.team
|
|
? [
|
|
{
|
|
label: '原因',
|
|
type: 'textarea',
|
|
fieldname: 'reason',
|
|
},
|
|
]
|
|
: [],
|
|
onSuccess: ({ hide, values }) => {
|
|
if (!values.reason && $team.name !== site.pg.team) {
|
|
throw new Error('原因必填');
|
|
}
|
|
return site.loginAsAdmin
|
|
.submit({ reason: values.reason })
|
|
.then((result) => {
|
|
let url = result;
|
|
window.open(url, '_blank');
|
|
hide();
|
|
});
|
|
},
|
|
});
|
|
},
|
|
},
|
|
],
|
|
},
|
|
];
|
|
},
|
|
},
|
|
|
|
routes: [
|
|
{
|
|
name: '站点更新',
|
|
path: 'updates/:id',
|
|
component: () => import('../pages/SiteUpdate.vue'),
|
|
},
|
|
],
|
|
}; |