2025-04-12 17:39:38 +08:00

390 lines
8.4 KiB
TypeScript

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[];
}