import Tooltip from 'jingrow-ui/src/components/Tooltip/Tooltip.vue'; import LucideAppWindow from '~icons/lucide/app-window'; import type { VNode } from 'vue'; import { defineAsyncComponent, h } from 'vue'; import { getTeam, switchToTeam } from '../data/team'; import { icon } from '../utils/components'; import { clusterOptions, getSitesTabColumns, sitesTabRoute, siteTabFilterControls } from './common'; import { getAppsTab } from './common/apps'; import { getJobsTab } from './common/jobs'; import type { Breadcrumb, BreadcrumbArgs, ColumnField, DashboardObject, Detail, FilterField, List, RouteDetail, Row, Tab } from './common/types'; import { getLogsTab } from './tabs/site/logs'; import { getPatchesTab } from './common/patches'; export default { pagetype: 'Bench', whitelistedMethods: {}, detail: getDetail(), list: getList(), routes: getRoutes() } satisfies DashboardObject as DashboardObject; function getDetail() { return { titleField: 'name', statusBadge: ({ documentResource: bench }) => ({ label: bench.pg.status }), route: '/benches/:name', tabs: getTabs(), actions: ({ documentResource: res }) => { const team = getTeam(); return [ { label: '选项', condition: () => team.pg?.is_desk_user ?? false, options: [ { label: '在 Desk 中查看', icon: icon('external-link'), condition: () => team.pg?.is_desk_user, onClick() { window.open( `${window.location.protocol}//${window.location.host}/app/bench/${res.name}`, '_blank' ); } }, { label: '模拟团队', icon: defineAsyncComponent( () => import('~icons/lucide/venetian-mask') ), condition: () => window.is_system_user ?? false, onClick() { switchToTeam(res.pg.team); } } ] } ]; } // breadcrumbs // use default breadcrumbs } satisfies Detail as Detail; } function getTabs() { return [ getSitesTab(), getAppsTab(false), getJobsTab('Bench'), getProcessesTab(), getLogsTab(false), getPatchesTab(true) ] satisfies Tab[] as Tab[]; } function getRoutes() { return [ { name: 'Bench Job', path: 'jobs/:id', component: () => import('../pages/JobPage.vue') }, { name: 'Bench Log', path: 'logs/:logName', component: () => import('../pages/LogPage.vue') } ] satisfies RouteDetail[] as RouteDetail[]; } function getList() { return { route: '/benches', title: '工作台', fields: [ 'group.title as group_title', 'cluster.name as cluster_name', 'cluster.image as cluster_image', 'cluster.title as cluster_title' ], orderBy: 'creation desc', searchField: 'name', columns: [ { label: '工作台', fieldname: 'name', class: 'font-medium', suffix: getBenchTitleSuffix }, { label: '状态', fieldname: 'status', type: 'Badge', width: '100px' }, { label: '站点', fieldname: 'site_count', type: 'Number', width: '100px', align: 'right' }, { label: '区域', fieldname: 'cluster', width: 0.75, format: (value, row) => String(row.cluster_title || value || ''), prefix: getClusterImagePrefix }, { label: '站点分组', fieldname: 'group_title', width: '350px' } ], filterControls } satisfies List as List; } function getBenchTitleSuffix(row: Row) { const ch: VNode[] = []; if (row.inplace_update_docker_image) ch.push(getInPlaceUpdatesSuffix(row)); if (row.has_app_patch_applied) ch.push(getAppPatchSuffix(row)); if (!ch.length) return; return h( 'div', { class: 'flex flex-row gap-2' }, ch ); } function getInPlaceUpdatesSuffix(row: Row) { const count = Number( String(row.inplace_update_docker_image).split('-').at(-1) ); let title = '工作台已就地更新'; if (!Number.isNaN(count) && count > 1) { title += ` ${count} 次`; } return h( 'div', { title, class: 'rounded-full bg-gray-100 p-1' }, h(icon('star', 'w-3 h-3')) ); } function getAppPatchSuffix(row: Row) { return h( 'div', { title: '此工作台中的应用可能已打补丁', class: 'rounded-full bg-gray-100 p-1' }, h(icon('hash', 'w-3 h-3')) ); } function getClusterImagePrefix(row: Row) { if (!row.cluster_image) return; return h('img', { src: row.cluster_image, class: 'w-4 h-4', alt: row.cluster_title }); } function filterControls() { return [ { type: 'select', label: '状态', fieldname: 'status', options: [ { label: '', value: '' }, { label: '激活', value: 'Active' }, { label: '待定', value: 'Pending' }, { label: '安装中', value: 'Installing' }, { label: '更新中', value: 'Updating' }, { label: '损坏', value: 'Broken' }, { label: '已归档', value: 'Archived' } ] }, { type: 'link', label: '站点分组', fieldname: 'group', options: { pagetype: 'Release Group' } }, { type: 'select', label: '区域', fieldname: 'cluster', options: clusterOptions } ] satisfies FilterField[] as FilterField[]; } export function getSitesTab() { return { label: '站点', icon: icon(LucideAppWindow), route: 'sites', type: 'list', list: { pagetype: 'Site', filters: r => ({ group: r.pg.group, bench: r.name, skip_team_filter_for_system_user_and_support_agent: true }), fields: [ 'name', 'status', 'host_name', 'plan.plan_title as plan_title', 'plan.price_usd as price_usd', 'plan.price_cny as price_cny', 'cluster.image as cluster_image', 'cluster.title as cluster_title' ], orderBy: 'creation desc, bench desc', pageLength: 99999, columns: getSitesTabColumns(true), filterControls: siteTabFilterControls, route: sitesTabRoute, primaryAction: r => { return { label: '新建站点', slots: { prefix: icon('plus', 'w-4 h-4') }, route: { name: 'Release Group New Site', params: { bench: r.documentResource.pg.group } } }; }, rowActions: ({ row }) => [ { label: '在桌面查看', condition: () => getTeam()?.pg?.is_desk_user, onClick() { window.open( `${window.location.protocol}//${window.location.host}/app/site/${row.name}`, '_blank' ); } } ] } } satisfies Tab; } export function getProcessesTab() { const url = 'jcloud.api.bench.get_processes'; return { label: '进程', icon: icon('cpu'), route: 'processes', type: 'list', list: { resource({ documentResource: res }) { return { params: { name: res.name }, url, auto: true, cache: ['ObjectList', url, res.name] }; }, columns: getProcessesColumns(), rowActions: () => [] // TODO: 允许发出 supectl 命令 } } satisfies Tab as Tab; } export function getProcessesColumns() { const processStatusColorMap = { Starting: 'blue', Backoff: 'yellow', Running: 'green', Stopping: 'yellow', Stopped: 'gray', Exited: 'gray', Unknown: 'gray', Fatal: 'red' }; type Status = keyof typeof processStatusColorMap; return [ { label: '名称', width: 2, fieldname: 'name' }, { label: '组', width: 1.5, fieldname: 'group', format: v => String(v ?? '') }, { label: '状态', type: 'Badge', width: 0.7, fieldname: 'status', theme: value => processStatusColorMap[value as Status] ?? 'gray', suffix: ({ message }) => { if (!message) { return; } return h( Tooltip, { text: message, placement: 'top' }, () => h(icon('alert-circle', 'w-3 h-3')) ); } }, { label: '运行时间', fieldname: 'uptime_string' } ] satisfies ColumnField[] as ColumnField[]; } function breadcrumbs({ items, documentResource: bench }: BreadcrumbArgs) { const $team = getTeam(); const benchCrumb = { label: bench.pg?.name, route: `/benches/${bench.pg?.name}` }; if (bench.pg.group_team == $team.pg?.name || $team.pg?.is_desk_user) { return [ { label: bench.pg?.group_title, route: `/groups/${bench.pg?.group}` }, benchCrumb ] satisfies Breadcrumb[]; } return [...items.slice(0, -1), benchCrumb] satisfies Breadcrumb[]; }