import { LoadingIndicator, Tooltip } from 'jingrow-ui'; import { defineAsyncComponent, h } from 'vue'; import { toast } from 'vue-sonner'; import LucideAppWindow from '~icons/lucide/app-window'; import LucideHardDriveDownload from '~icons/lucide/hard-drive-download'; import LucideRocket from '~icons/lucide/rocket'; import AddAppDialog from '../components/group/AddAppDialog.vue'; import ChangeAppBranchDialog from '../components/group/ChangeAppBranchDialog.vue'; import PatchAppDialog from '../components/group/PatchAppDialog.vue'; import { getTeam, switchToTeam } from '../data/team'; import router from '../router'; import { confirmDialog, icon, renderDialog } from '../utils/components'; import { getToastErrorMessage } from '../utils/toast'; import { date, duration } from '../utils/format'; import { getJobsTab } from './common/jobs'; import { getPatchesTab } from './common/patches'; import { tagTab } from './common/tags'; export default { pagetype: 'Release Group', whitelistedMethods: { addApp: 'add_app', removeApp: 'remove_app', changeAppBranch: 'change_app_branch', fetchLatestAppUpdates: 'fetch_latest_app_update', deleteConfig: 'delete_config', updateConfig: 'update_config', updateEnvironmentVariable: 'update_environment_variable', deleteEnvironmentVariable: 'delete_environment_variable', updateDependency: 'update_dependency', addRegion: 'add_region', deployedVersions: 'deployed_versions', getAppVersions: 'get_app_versions', getCertificate: 'get_certificate', generateCertificate: 'generate_certificate', sendTransferRequest: 'send_change_team_request', addTag: 'add_resource_tag', removeTag: 'remove_resource_tag', redeploy: 'redeploy', initialDeploy: 'initial_deploy' }, list: { route: '/groups', title: '站点分组', fields: [{ apps: ['app'] }], searchField: 'title', filterControls() { return [ { type: 'link', label: '版本', fieldname: 'version', options: { pagetype: 'Jingrow Version' } }, { type: 'link', label: '标签', fieldname: 'tags.tag', options: { pagetype: 'Jcloud Tag', filters: { pagetype_name: 'Release Group' } } } ]; }, columns: [ { label: '标题', fieldname: 'title', class: 'font-medium' }, { label: '状态', fieldname: 'active_benches', type: 'Badge', width: 0.5, format: (value, row) => { if (!value) return '等待部署'; else return '激活'; } }, { label: '版本', fieldname: 'version', width: 0.5 }, { label: '应用', fieldname: 'app', format: (value, row) => { return (row.apps || []).map(d => d.app).join(', '); }, width: '25rem' }, { label: '站点', fieldname: 'site_count', class: 'text-gray-600', width: 0.25 } ], primaryAction() { return { label: '新建站点分组', variant: 'solid', slots: { prefix: icon('plus') }, onClick() { router.push({ name: 'New Release Group' }); } }; } }, detail: { titleField: 'title', statusBadge({ documentResource: releaseGroup }) { return { label: releaseGroup.pg.status }; }, breadcrumbs({ items, documentResource: releaseGroup }) { if (!releaseGroup.pg.server_team) return items; let breadcrumbs = []; let $team = getTeam(); if ( releaseGroup.pg.server_team == $team.pg?.name || $team.pg?.is_desk_user ) { breadcrumbs.push( { label: releaseGroup.pg?.server_title || releaseGroup.pg?.server, route: `/servers/${releaseGroup.pg?.server}` }, items[1] ); } else { breadcrumbs.push(...items); } return breadcrumbs; }, route: '/groups/:name', tabs: [ { label: '站点', icon: icon(LucideAppWindow), route: 'sites', type: 'Component', component: defineAsyncComponent(() => import('../pages/ReleaseGroupBenchSites.vue') ), props: releaseGroup => { return { releaseGroup: releaseGroup.pg.name }; } }, { label: '应用', icon: icon('grid'), route: 'apps', type: 'list', list: { pagetype: 'Release Group App', filters: releaseGroup => { return { parenttype: 'Release Group', parent: releaseGroup.pg.name }; }, pageLength: 99999, columns: [ { label: '应用', fieldname: 'title', width: 1 }, { label: '仓库', width: 1, format(value, row) { return `${row.repository_owner}/${row.repository}`; }, link(value, row) { return row.repository_url; } }, { label: '分支', fieldname: 'branch', type: 'Badge', width: 0.5, link(value, row) { return `${row.repository_url}/tree/${value}`; } }, { label: '版本', type: 'Badge', fieldname: 'tag', width: 0.5, format(value, row) { return value || row.hash?.slice(0, 7); } }, { label: '状态', type: 'Badge', suffix(row) { if (!row.last_github_poll_failed) return; return h( Tooltip, { text: "这是什么?", placement: 'top', class: 'rounded-full bg-gray-100 p-1' }, () => [ h( 'a', { href: 'https://jingrow.com/docs/faq/app-installation-issue', target: '_blank' }, [h(icon('help-circle', 'w-3 h-3'), {})] ) ] ); }, format(value, row) { let { update_available, deployed, last_github_poll_failed } = row; return last_github_poll_failed ? '需要操作' : !deployed ? '未部署' : update_available ? '有可用更新' : '最新版本'; }, width: 0.5 } ], rowActions({ row, listResource: apps, documentResource: releaseGroup }) { let team = getTeam(); return [ { label: '在桌面查看', condition: () => team.pg?.is_desk_user, onClick() { window.open( `${window.location.protocol}//${window.location.host}/app/app/${row.name}`, '_blank' ); } }, { label: '获取最新更新', onClick() { toast.promise( releaseGroup.fetchLatestAppUpdates.submit({ app: row.name }), { loading: `正在为 ${row.title} 获取最新更新...`, success: () => { apps.reload(); return `已为 ${row.title} 获取最新更新`; }, error: e => getToastErrorMessage(e) } ); } }, { label: '更改分支', onClick() { renderDialog( h(ChangeAppBranchDialog, { bench: releaseGroup.name, app: row, onBranchChange() { apps.reload(); } }) ); } }, { label: '移除应用', condition: () => row.name !== 'jingrow', onClick() { if (releaseGroup.removeApp.loading) return; confirmDialog({ title: '移除应用', message: `确定要移除应用 ${row.title} 吗?`, onSuccess: ({ hide }) => { toast.promise( releaseGroup.removeApp.submit({ app: row.name }), { loading: '正在移除应用...', success: () => { hide(); apps.reload(); return '应用已移除'; }, error: e => getToastErrorMessage(e) } ); } }); } }, { label: '访问仓库', onClick() { window.open( `${row.repository_url}/tree/${row.branch}`, '_blank' ); } }, { label: '应用补丁', onClick: () => { renderDialog( h(PatchAppDialog, { group: releaseGroup.name, app: row.name }) ); } } ]; }, primaryAction({ listResource: apps, documentResource: releaseGroup }) { return { label: '添加应用', slots: { prefix: icon('plus') }, onClick() { renderDialog( h(AddAppDialog, { group: releaseGroup.pg, onAppAdd() { apps.reload(); releaseGroup.reload(); }, onNewApp(app, isUpdate) { const loading = isUpdate ? '替换应用中...' : '添加应用中...'; toast.promise( releaseGroup.addApp.submit({ app, is_update: isUpdate }), { loading, success: () => { apps.reload(); releaseGroup.reload(); if (isUpdate) { return `应用 ${app.title} 已更新`; } return `应用 ${app.title} 已添加`; }, error: e => getToastErrorMessage(e) } ); } }) ); } }; } } }, { label: '部署', route: 'deploys', icon: icon('package'), childrenRoutes: ['Deploy Candidate'], type: 'list', list: { pagetype: 'Deploy Candidate', route: row => ({ name: 'Deploy Candidate', params: { id: row.name } }), filters: releaseGroup => { return { group: releaseGroup.name }; }, orderBy: 'creation desc', fields: [{ apps: ['app'] }], filterControls() { return [ { type: 'select', label: '状态', fieldname: 'status', options: [ '', 'Draft', 'Scheduled', 'Pending', 'Preparing', 'Running', 'Success', 'Failure' ] } ]; }, banner({ documentResource: releaseGroup }) { if (releaseGroup.pg.are_builds_suspended) { return { title: '构建已暂停:更新将在构建恢复后计划运行。', type: 'warning' }; } else { return null; } }, columns: [ { label: '部署', fieldname: 'creation', format(value) { return `部署于 ${date(value, 'llll')}`; }, width: '20rem' }, { label: '状态', fieldname: 'status', type: 'Badge', width: 0.5, suffix(row) { if (!row.addressable_notification) { return; } return h( Tooltip, { text: '需要关注!', placement: 'top', class: 'rounded-full bg-gray-100 p-1' }, () => h(icon('alert-circle', 'w-3 h-3'), {}) ); } }, { label: '应用', format(value, row) { return (row.apps || []).map(d => d.app).join(', '); }, width: '20rem' }, { label: '持续时间', fieldname: 'build_duration', format: duration, class: 'text-gray-600', width: 1 }, { label: '部署者', fieldname: 'owner', width: 1 } ], primaryAction({ listResource: deploys, documentResource: group }) { return { label: '部署', slots: { prefix: icon(LucideRocket) }, onClick() { if (group.pg.deploy_information.deploy_in_progress) { return toast.error( '部署正在进行中。请等待其完成。' ); } else if (group.pg.deploy_information.update_available) { let UpdateReleaseGroupDialog = defineAsyncComponent(() => import('../components/group/UpdateReleaseGroupDialog.vue') ); renderDialog( h(UpdateReleaseGroupDialog, { bench: group.name, onSuccess(candidate) { group.pg.deploy_information.deploy_in_progress = true; if (candidate) { group.pg.deploy_information.last_deploy.name = candidate; } } }) ); } else { confirmDialog({ title: '无需应用更新即可部署?', message: '未检测到应用更新。部署时将应用依赖项和环境变量的更改。', onSuccess: ({ hide }) => { toast.promise(group.redeploy.submit(), { loading: '正在部署...', success: () => { hide(); deploys.reload(); return '更改已部署'; }, error: e => getToastErrorMessage(e) }); } }); } } }; } } }, getJobsTab('Release Group'), { label: '配置', icon: icon('settings'), route: 'bench-config', type: 'list', list: { pagetype: 'Common Site Config', filters: releaseGroup => { return { parenttype: 'Release Group', parent: releaseGroup.name }; }, orderBy: 'creation desc', fields: ['name'], pageLength: 999, columns: [ { label: '配置名称', fieldname: 'key', format(value, row) { if (row.title) { return `${row.title} (${row.key})`; } return row.key; } }, { label: '配置值', fieldname: 'value', class: 'font-mono' }, { label: '类型', fieldname: 'type', type: 'Badge', width: '100px' } ], primaryAction({ listResource: configs, documentResource: releaseGroup }) { return { label: '添加配置', slots: { prefix: icon('plus') }, onClick() { let ConfigEditorDialog = defineAsyncComponent(() => import('../components/ConfigEditorDialog.vue') ); renderDialog( h(ConfigEditorDialog, { group: releaseGroup.pg.name, onSuccess() { configs.reload(); } }) ); } }; }, secondaryAction({ listResource: configs }) { return { label: '预览', slots: { prefix: icon('eye') }, onClick() { let ConfigPreviewDialog = defineAsyncComponent(() => import('../components/ConfigPreviewDialog.vue') ); renderDialog( h(ConfigPreviewDialog, { configs: configs.data }) ); } }; }, rowActions({ row, listResource: configs, documentResource: releaseGroup }) { return [ { label: '编辑', onClick() { let ConfigEditorDialog = defineAsyncComponent(() => import('../components/ConfigEditorDialog.vue') ); renderDialog( h(ConfigEditorDialog, { group: releaseGroup.pg.name, config: row, onSuccess() { configs.reload(); } }) ); } }, { label: '删除', onClick() { confirmDialog({ title: '删除配置', message: `确定要删除配置 ${row.key} 吗?`, onSuccess({ hide }) { if (releaseGroup.deleteConfig.loading) return; toast.promise( releaseGroup.deleteConfig.submit( { key: row.key }, { onSuccess: () => { configs.reload(); hide(); } } ), { loading: '正在删除配置...', success: () => `配置 ${row.key} 已删除`, error: e => getToastErrorMessage(e) } ); } }); } } ]; } } }, { label: '操作', icon: icon('sliders'), route: 'actions', type: 'Component', component: defineAsyncComponent(() => import('../components/group/ReleaseGroupActions.vue') ), props: releaseGroup => { return { releaseGroup: releaseGroup.name }; } }, { label: '区域', icon: icon('globe'), route: 'regions', type: 'list', list: { pagetype: 'Cluster', filters: releaseGroup => { return { group: releaseGroup.name }; }, columns: [ { label: '区域', fieldname: 'title' }, { label: '国家', fieldname: 'image', format(value, row) { return ''; }, prefix(row) { return h('img', { src: row.image, class: 'w-4 h-4', alt: row.title }); } } ], primaryAction({ listResource: clusters, documentResource: releaseGroup }) { return { label: '添加区域', slots: { prefix: icon('plus') }, onClick() { let AddRegionDialog = defineAsyncComponent(() => import('../components/group/AddRegionDialog.vue') ); renderDialog( h(AddRegionDialog, { group: releaseGroup.pg.name, onSuccess() { clusters.reload(); } }) ); } }; } } }, getPatchesTab(false), { label: '依赖项', icon: icon('box'), route: 'bench-dependencies', type: 'list', list: { pagetype: 'Release Group Dependency', filters: releaseGroup => { return { parenttype: 'Release Group', parent: releaseGroup.name }; }, columns: [ { label: '依赖项', fieldname: 'dependency', format(value, row) { return row.title; } }, { label: '版本', fieldname: 'version', suffix(row) { if (!row.is_custom) { return; } return h( Tooltip, { text: '自定义版本', placement: 'top', class: 'rounded-full bg-gray-100 p-1' }, () => h(icon('alert-circle', 'w-3 h-3'), {}) ); } } ], rowActions({ row, listResource: dependencies, documentResource: releaseGroup }) { return [ { label: '编辑', onClick() { let DependencyEditorDialog = defineAsyncComponent(() => import('../components/group/DependencyEditorDialog.vue') ); renderDialog( h(DependencyEditorDialog, { group: releaseGroup.pg, dependency: row, onSuccess() { dependencies.reload(); } }) ); } } ]; } } }, { label: '环境', icon: icon('tool'), route: 'bench-environment-variable', type: 'list', list: { pagetype: 'Release Group Variable', filters: releaseGroup => { return { parenttype: 'Release Group', parent: releaseGroup.name }; }, orderBy: 'creation desc', fields: ['name'], columns: [ { label: '环境变量名称', fieldname: 'key' }, { label: '环境变量值', fieldname: 'value' } ], primaryAction({ listResource: environmentVariables, documentResource: releaseGroup }) { return { label: '添加环境变量', slots: { prefix: icon('plus') }, onClick() { let EnvironmentVariableEditorDialog = defineAsyncComponent(() => import('../components/EnvironmentVariableEditorDialog.vue') ); renderDialog( h(EnvironmentVariableEditorDialog, { group: releaseGroup.pg.name, onSuccess() { environmentVariables.reload(); } }) ); } }; }, rowActions({ row, listResource: environmentVariables, documentResource: releaseGroup }) { return [ { label: '编辑', onClick() { let ConfigEditorDialog = defineAsyncComponent(() => import('../components/EnvironmentVariableEditorDialog.vue') ); renderDialog( h(ConfigEditorDialog, { group: releaseGroup.pg.name, environment_variable: row, onSuccess() { environmentVariables.reload(); } }) ); } }, { label: '删除', onClick() { confirmDialog({ title: '删除环境变量', message: `确定要删除环境变量 ${row.key} 吗?`, onSuccess({ hide }) { if (releaseGroup.deleteEnvironmentVariable.loading) return; toast.promise( releaseGroup.deleteEnvironmentVariable.submit( { key: row.key }, { onSuccess: () => { environmentVariables.reload(); hide(); } } ), { loading: '正在删除环境变量...', success: () => `环境变量 ${row.key} 已删除`, error: e => getToastErrorMessage(e) } ); } }); } } ]; } } }, tagTab() ], actions(context) { let { documentResource: group } = context; let team = getTeam(); return [ { label: '模拟组所有者', title: '模拟组所有者', slots: { icon: defineAsyncComponent(() => import('~icons/lucide/venetian-mask') ) }, condition: () => team.pg?.is_desk_user && group.pg.team !== team.name, onClick() { switchToTeam(group.pg.team); } }, { label: group.pg?.deploy_information?.last_deploy ? '有可用更新' : '立即部署', slots: { prefix: group.pg?.deploy_information?.last_deploy ? icon(LucideHardDriveDownload) : icon(LucideRocket) }, variant: 'solid', condition: () => !group.pg.deploy_information.deploy_in_progress && group.pg.deploy_information.update_available && ['Awaiting Deploy', 'Active'].includes(group.pg.status), onClick() { if (group.pg?.deploy_information?.last_deploy) { let UpdateReleaseGroupDialog = defineAsyncComponent(() => import('../components/group/UpdateReleaseGroupDialog.vue') ); renderDialog( h(UpdateReleaseGroupDialog, { bench: group.name, onSuccess(candidate) { group.pg.deploy_information.deploy_in_progress = true; if (candidate) { group.pg.deploy_information.last_deploy.name = candidate; } } }) ); } else { confirmDialog({ title: '部署', message: "立即部署吗?", onSuccess({ hide }) { toast.promise( group.initialDeploy.submit(null, { onSuccess: () => { group.reload(); hide(); } }), { success: '部署计划成功', error: '部署计划失败', loading: '正在计划部署...' } ); } }); } } }, { label: '部署进行中', slots: { prefix: () => h(LoadingIndicator, { class: 'w-4 h-4' }) }, theme: 'green', condition: () => group.pg.deploy_information.deploy_in_progress, route: { name: 'Deploy Candidate', params: { id: group.pg?.deploy_information?.last_deploy?.name } } }, { label: '选项', condition: () => team.pg?.is_desk_user, options: [ { label: '在 Desk 中查看', icon: icon('external-link'), condition: () => team.pg?.is_desk_user, onClick() { window.open( `${window.location.protocol}//${window.location.host}/app/release-group/${group.name}`, '_blank' ); } } ] } ]; } }, routes: [ { name: 'Deploy Candidate', path: 'deploys/:id', component: () => import('../pages/DeployCandidate.vue') }, { name: 'Release Group Job', path: 'jobs/:id', component: () => import('../pages/JobPage.vue') } ] };