390 lines
8.4 KiB
TypeScript
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[];
|
|
} |