已安装app后增加卸载按钮
This commit is contained in:
parent
07133169da
commit
d14334fe03
@ -1,230 +1,284 @@
|
|||||||
import { defineAsyncComponent, h } from 'vue';
|
import { defineAsyncComponent, h } from 'vue';
|
||||||
import { toast } from 'vue-sonner';
|
import { toast } from 'vue-sonner';
|
||||||
import { getTeam } from '../../data/team';
|
import { getTeam } from '../../data/team';
|
||||||
import router from '../../router';
|
import router from '../../router';
|
||||||
import { confirmDialog, icon, renderDialog } from '../../utils/components';
|
import { confirmDialog, icon, renderDialog } from '../../utils/components';
|
||||||
import { planTitle } from '../../utils/format';
|
import { planTitle } from '../../utils/format';
|
||||||
import type {
|
import type {
|
||||||
ColumnField,
|
ColumnField,
|
||||||
DialogConfig,
|
DialogConfig,
|
||||||
FilterField,
|
FilterField,
|
||||||
Tab,
|
Tab,
|
||||||
TabList
|
TabList
|
||||||
} from './types';
|
} from './types';
|
||||||
import { getUpsellBanner } from '.';
|
import { getUpsellBanner } from '.';
|
||||||
import { isMobile } from '../../utils/device';
|
import { isMobile } from '../../utils/device';
|
||||||
import { getToastErrorMessage } from '../../utils/toast';
|
import { getToastErrorMessage } from '../../utils/toast';
|
||||||
|
|
||||||
export function getAppsTab(forSite: boolean) {
|
export function getAppsTab(forSite: boolean) {
|
||||||
return {
|
return {
|
||||||
label: '应用',
|
label: '应用',
|
||||||
icon: icon('grid'),
|
icon: icon('grid'),
|
||||||
route: 'apps',
|
route: 'apps',
|
||||||
type: 'list',
|
type: 'list',
|
||||||
condition: docResource => forSite && docResource.pg?.status !== 'Archived',
|
condition: docResource => forSite && docResource.pg?.status !== 'Archived',
|
||||||
list: getAppsTabList(forSite)
|
list: getAppsTabList(forSite)
|
||||||
} satisfies Tab as Tab;
|
} satisfies Tab as Tab;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getAppsTabList(forSite: boolean) {
|
function getAppsTabList(forSite: boolean) {
|
||||||
const options = forSite ? siteAppListOptions : benchAppListOptions;
|
const options = forSite ? siteAppListOptions : benchAppListOptions;
|
||||||
const list: TabList = {
|
const list: TabList = {
|
||||||
pagetype: '',
|
pagetype: '',
|
||||||
filters: () => ({}),
|
filters: () => ({}),
|
||||||
...options,
|
...options,
|
||||||
columns: getAppsTabColumns(forSite),
|
columns: getAppsTabColumns(forSite),
|
||||||
searchField: !forSite ? 'title' : undefined,
|
searchField: !forSite ? 'title' : undefined,
|
||||||
filterControls: r => {
|
filterControls: r => {
|
||||||
if (forSite) return [];
|
if (forSite) return [];
|
||||||
else
|
else
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
type: 'select',
|
type: 'select',
|
||||||
label: '分支',
|
label: '分支',
|
||||||
class: !isMobile() ? 'w-24' : '',
|
class: !isMobile() ? 'w-24' : '',
|
||||||
fieldname: 'branch',
|
fieldname: 'branch',
|
||||||
options: [
|
options: [
|
||||||
'',
|
'',
|
||||||
...new Set(r.listResource.data?.map(i => String(i.branch)) || [])
|
...new Set(r.listResource.data?.map(i => String(i.branch)) || [])
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'select',
|
type: 'select',
|
||||||
label: '所有者',
|
label: '所有者',
|
||||||
class: !isMobile() ? 'w-24' : '',
|
class: !isMobile() ? 'w-24' : '',
|
||||||
fieldname: 'repository_owner',
|
fieldname: 'repository_owner',
|
||||||
options: [
|
options: [
|
||||||
'',
|
'',
|
||||||
...new Set(
|
...new Set(
|
||||||
r.listResource.data?.map(
|
r.listResource.data?.map(
|
||||||
i => String(i.repository_url).split('/').at(-2) || ''
|
i => String(i.repository_url).split('/').at(-2) || ''
|
||||||
) || []
|
) || []
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
] satisfies FilterField[];
|
] satisfies FilterField[];
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return list;
|
return list;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getAppsTabColumns(forSite: boolean) {
|
function getAppsTabColumns(forSite: boolean) {
|
||||||
const appTabColumns: ColumnField[] = [
|
const appTabColumns: ColumnField[] = [
|
||||||
{
|
{
|
||||||
label: '应用',
|
label: '应用',
|
||||||
fieldname: 'title',
|
fieldname: 'title',
|
||||||
width: 1,
|
width: 1,
|
||||||
suffix(row) {
|
suffix(row) {
|
||||||
if (!row.is_app_patched) {
|
if (!row.is_app_patched) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
return h(
|
return h(
|
||||||
'div',
|
'div',
|
||||||
{
|
{
|
||||||
title: '应用已打补丁',
|
title: '应用已打补丁',
|
||||||
class: 'rounded-full bg-gray-100 p-1'
|
class: 'rounded-full bg-gray-100 p-1'
|
||||||
},
|
},
|
||||||
h(icon('hash', 'w-3 h-3'))
|
h(icon('hash', 'w-3 h-3'))
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
format: (value, row) => value || row.app_title
|
format: (value, row) => value || row.app_title
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '计划',
|
label: '计划',
|
||||||
width: 0.75,
|
width: 0.75,
|
||||||
class: 'text-gray-600 text-sm',
|
class: 'text-gray-600 text-sm',
|
||||||
format(_, row) {
|
format(_, row) {
|
||||||
const planText = planTitle(row.plan_info);
|
const planText = planTitle(row.plan_info);
|
||||||
if (planText) return `${planText}/月`;
|
if (planText) return `${planText}/月`;
|
||||||
else return '免费';
|
else return '免费';
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '版本',
|
label: '版本',
|
||||||
fieldname: 'branch',
|
fieldname: 'branch',
|
||||||
type: 'Badge',
|
type: 'Badge',
|
||||||
width: 1,
|
width: 1,
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
if (forSite) return appTabColumns;
|
// 为站点应用添加操作列,包含卸载按钮
|
||||||
return appTabColumns.filter(c => c.label !== '计划');
|
if (forSite) {
|
||||||
}
|
appTabColumns.push({
|
||||||
|
label: '操作',
|
||||||
const siteAppListOptions: Partial<TabList> = {
|
width: 0.75,
|
||||||
pagetype: 'Site App',
|
align: 'right',
|
||||||
filters: res => {
|
type: 'Button',
|
||||||
return { parenttype: 'Site', parent: res.pg?.name };
|
Button: ({ row, listResource, documentResource }) => {
|
||||||
},
|
// 如果是 jingrow 应用,不显示卸载按钮
|
||||||
primaryAction({ listResource: apps, documentResource: site }) {
|
if (row.app === 'jingrow') {
|
||||||
return {
|
return null;
|
||||||
label: '安装应用',
|
}
|
||||||
slots: {
|
|
||||||
prefix: icon('plus')
|
return {
|
||||||
},
|
label: '卸载',
|
||||||
onClick() {
|
variant: 'ghost',
|
||||||
const InstallAppDialog = defineAsyncComponent(
|
class: 'text-red-600 hover:text-red-700 hover:bg-red-50',
|
||||||
() => import('../../components/site/InstallAppDialog.vue')
|
slots: {
|
||||||
);
|
prefix: icon('trash-2')
|
||||||
|
},
|
||||||
renderDialog(
|
onClick: () => {
|
||||||
h(InstallAppDialog, {
|
const appName = row.title || row.app_title;
|
||||||
site: site.name,
|
const dialogConfig: DialogConfig = {
|
||||||
onInstalled() {
|
title: `卸载应用`,
|
||||||
apps.reload();
|
message: `您确定要从站点 <b>${documentResource.pg?.name}</b> 卸载应用 <b>${appName}</b> 吗?<br>
|
||||||
}
|
所有与此应用相关的页面类型和模块将被移除。`,
|
||||||
})
|
onSuccess({ hide }) {
|
||||||
);
|
// 立即从列表中移除该应用(乐观更新)
|
||||||
}
|
const currentData = listResource.data || [];
|
||||||
};
|
const updatedData = currentData.filter(item => item.name !== row.name);
|
||||||
},
|
listResource.data = updatedData;
|
||||||
rowActions({ row, listResource: apps, documentResource: site }) {
|
|
||||||
let $team = getTeam();
|
const promise = documentResource.uninstallApp.submit({
|
||||||
|
app: row.app
|
||||||
return [
|
});
|
||||||
{
|
|
||||||
label: '在 Desk 中查看',
|
toast.promise(promise, {
|
||||||
condition: () => $team.pg?.is_desk_user,
|
loading: '正在安排应用卸载...',
|
||||||
onClick() {
|
success: (jobId: string) => {
|
||||||
window.open(`/app/app-source/${row.name}`, '_blank');
|
hide();
|
||||||
}
|
return '应用卸载已安排';
|
||||||
},
|
},
|
||||||
{
|
error: (e: Error) => {
|
||||||
label: '更改计划',
|
// 如果失败,重新加载列表恢复状态
|
||||||
condition: () => row.plan_info && row.plans.length > 1,
|
listResource.reload();
|
||||||
onClick() {
|
return getToastErrorMessage(e);
|
||||||
let SiteAppPlanChangeDialog = defineAsyncComponent(
|
}
|
||||||
() => import('../../components/site/SiteAppPlanSelectDialog.vue')
|
});
|
||||||
);
|
}
|
||||||
renderDialog(
|
};
|
||||||
h(SiteAppPlanChangeDialog, {
|
confirmDialog(dialogConfig);
|
||||||
app: row,
|
}
|
||||||
currentPlan: row.plans.find(
|
};
|
||||||
(plan: Record<string, any>) => plan.name === row.plan_info.name
|
}
|
||||||
),
|
});
|
||||||
onPlanChanged() {
|
|
||||||
apps.reload();
|
return appTabColumns;
|
||||||
}
|
}
|
||||||
})
|
|
||||||
);
|
return appTabColumns.filter(c => c.label !== '计划');
|
||||||
}
|
}
|
||||||
},
|
|
||||||
{
|
const siteAppListOptions: Partial<TabList> = {
|
||||||
label: '卸载',
|
pagetype: 'Site App',
|
||||||
condition: () => row.app !== 'jingrow',
|
filters: res => {
|
||||||
onClick() {
|
return { parenttype: 'Site', parent: res.pg?.name };
|
||||||
const dialogConfig: DialogConfig = {
|
},
|
||||||
title: `卸载应用`,
|
primaryAction({ listResource: apps, documentResource: site }) {
|
||||||
message: `您确定要从站点 <b>${site.pg?.name}</b> 卸载应用 <b>${row.title}</b> 吗?<br>
|
return {
|
||||||
所有与此应用相关的页面类型和模块将被移除。`,
|
label: '安装应用',
|
||||||
onSuccess({ hide }) {
|
slots: {
|
||||||
if (site.uninstallApp.loading) return;
|
prefix: icon('plus')
|
||||||
toast.promise(
|
},
|
||||||
site.uninstallApp.submit({
|
onClick() {
|
||||||
app: row.app
|
const InstallAppDialog = defineAsyncComponent(
|
||||||
}),
|
() => import('../../components/site/InstallAppDialog.vue')
|
||||||
{
|
);
|
||||||
loading: '正在安排应用卸载...',
|
|
||||||
success: (jobId: string) => {
|
renderDialog(
|
||||||
hide();
|
h(InstallAppDialog, {
|
||||||
router.push({
|
site: site.name,
|
||||||
name: 'Site Job',
|
onInstalled() {
|
||||||
params: {
|
apps.reload();
|
||||||
name: site.name,
|
}
|
||||||
id: jobId
|
})
|
||||||
}
|
);
|
||||||
});
|
}
|
||||||
return '应用卸载已安排';
|
};
|
||||||
},
|
},
|
||||||
error: (e: Error) => getToastErrorMessage(e)
|
rowActions({ row, listResource: apps, documentResource: site }) {
|
||||||
}
|
let $team = getTeam();
|
||||||
);
|
|
||||||
}
|
return [
|
||||||
};
|
{
|
||||||
confirmDialog(dialogConfig);
|
label: '在 Desk 中查看',
|
||||||
}
|
condition: () => $team.pg?.is_desk_user,
|
||||||
}
|
onClick() {
|
||||||
];
|
window.open(`/app/app-source/${row.name}`, '_blank');
|
||||||
}
|
}
|
||||||
};
|
},
|
||||||
|
{
|
||||||
const benchAppListOptions: Partial<TabList> = {
|
label: '更改计划',
|
||||||
pagetype: 'Bench App',
|
condition: () => row.plan_info && row.plans.length > 1,
|
||||||
filters: res => {
|
onClick() {
|
||||||
return { parenttype: 'Bench', parent: res.pg?.name };
|
let SiteAppPlanChangeDialog = defineAsyncComponent(
|
||||||
},
|
() => import('../../components/site/SiteAppPlanSelectDialog.vue')
|
||||||
rowActions({ row }) {
|
);
|
||||||
let $team = getTeam();
|
renderDialog(
|
||||||
return [
|
h(SiteAppPlanChangeDialog, {
|
||||||
{
|
app: row,
|
||||||
label: '在 Desk 中查看',
|
currentPlan: row.plans.find(
|
||||||
condition: () => $team.pg?.is_desk_user,
|
(plan: Record<string, any>) => plan.name === row.plan_info.name
|
||||||
onClick() {
|
),
|
||||||
window.open(`/app/app-release/${row.release}`, '_blank');
|
onPlanChanged() {
|
||||||
}
|
apps.reload();
|
||||||
}
|
}
|
||||||
];
|
})
|
||||||
}
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '卸载',
|
||||||
|
condition: () => row.app !== 'jingrow',
|
||||||
|
onClick() {
|
||||||
|
const appName = row.title || row.app_title;
|
||||||
|
const dialogConfig: DialogConfig = {
|
||||||
|
title: `卸载应用`,
|
||||||
|
message: `您确定要从站点 <b>${site.pg?.name}</b> 卸载应用 <b>${appName}</b> 吗?<br>
|
||||||
|
所有与此应用相关的页面类型和模块将被移除。`,
|
||||||
|
onSuccess({ hide }) {
|
||||||
|
toast.promise(
|
||||||
|
site.uninstallApp.submit({
|
||||||
|
app: row.app
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
loading: '正在安排应用卸载...',
|
||||||
|
success: (jobId: string) => {
|
||||||
|
hide();
|
||||||
|
apps.reload();
|
||||||
|
return '应用卸载已安排';
|
||||||
|
},
|
||||||
|
error: (e: Error) => {
|
||||||
|
return getToastErrorMessage(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
confirmDialog(dialogConfig);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const benchAppListOptions: Partial<TabList> = {
|
||||||
|
pagetype: 'Bench App',
|
||||||
|
filters: res => {
|
||||||
|
return { parenttype: 'Bench', parent: res.pg?.name };
|
||||||
|
},
|
||||||
|
rowActions({ row }) {
|
||||||
|
let $team = getTeam();
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
label: '在 Desk 中查看',
|
||||||
|
condition: () => $team.pg?.is_desk_user,
|
||||||
|
onClick() {
|
||||||
|
window.open(`/app/app-release/${row.release}`, '_blank');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
};
|
};
|
||||||
@ -1,216 +1,228 @@
|
|||||||
import type { defineAsyncComponent, h, Component } from 'vue';
|
import type { defineAsyncComponent, h, Component } from 'vue';
|
||||||
import type { icon } from '../../utils/components';
|
import type { icon } from '../../utils/components';
|
||||||
|
|
||||||
type ListResource = {
|
type ListResource = {
|
||||||
data: Record<string, unknown>[];
|
data: Record<string, unknown>[];
|
||||||
reload: () => void;
|
reload: () => void;
|
||||||
runDocMethod: {
|
runDocMethod: {
|
||||||
submit: (r: { method: string; [key: string]: any }) => Promise<unknown>;
|
submit: (r: { method: string; [key: string]: any }) => Promise<unknown>;
|
||||||
};
|
};
|
||||||
delete: {
|
delete: {
|
||||||
submit: (name: string, cb: { onSuccess: () => void }) => Promise<unknown>;
|
submit: (name: string, cb: { onSuccess: () => void }) => Promise<unknown>;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
export interface ResourceBase {
|
export interface ResourceBase {
|
||||||
url: string;
|
url: string;
|
||||||
auto: boolean;
|
auto: boolean;
|
||||||
cache: string[];
|
cache: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ResourceWithParams extends ResourceBase {
|
export interface ResourceWithParams extends ResourceBase {
|
||||||
params: Record<string, unknown>;
|
params: Record<string, unknown>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ResourceWithMakeParams extends ResourceBase {
|
export interface ResourceWithMakeParams extends ResourceBase {
|
||||||
makeParams: () => Record<string, unknown>;
|
makeParams: () => Record<string, unknown>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Resource = ResourceWithParams | ResourceWithMakeParams;
|
export type Resource = ResourceWithParams | ResourceWithMakeParams;
|
||||||
|
|
||||||
export interface DocumentResource {
|
export interface DocumentResource {
|
||||||
name: string;
|
name: string;
|
||||||
pg: Record<string, any>;
|
pg: Record<string, any>;
|
||||||
[key: string]: any;
|
[key: string]: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
type Icon = ReturnType<typeof icon>;
|
type Icon = ReturnType<typeof icon>;
|
||||||
type AsyncComponent = ReturnType<typeof defineAsyncComponent>;
|
type AsyncComponent = ReturnType<typeof defineAsyncComponent>;
|
||||||
|
|
||||||
export interface DashboardObject {
|
export interface DashboardObject {
|
||||||
pagetype: string;
|
pagetype: string;
|
||||||
whitelistedMethods: Record<string, string>;
|
whitelistedMethods: Record<string, string>;
|
||||||
list: List;
|
list: List;
|
||||||
detail: Detail;
|
detail: Detail;
|
||||||
routes: RouteDetail[];
|
routes: RouteDetail[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Detail {
|
export interface Detail {
|
||||||
titleField: string;
|
titleField: string;
|
||||||
statusBadge: StatusBadge;
|
statusBadge: StatusBadge;
|
||||||
breadcrumbs?: Breadcrumbs;
|
breadcrumbs?: Breadcrumbs;
|
||||||
route: string;
|
route: string;
|
||||||
tabs: Tab[];
|
tabs: Tab[];
|
||||||
actions: (r: { documentResource: DocumentResource }) => Action[];
|
actions: (r: { documentResource: DocumentResource }) => Action[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface List {
|
export interface List {
|
||||||
route: string;
|
route: string;
|
||||||
title: string;
|
title: string;
|
||||||
fields: string[]; // TODO: Incomplete
|
fields: string[]; // TODO: Incomplete
|
||||||
searchField: string;
|
searchField: string;
|
||||||
columns: ColumnField[];
|
columns: ColumnField[];
|
||||||
orderBy: string;
|
orderBy: string;
|
||||||
filterControls: FilterControls;
|
filterControls: FilterControls;
|
||||||
primaryAction?: PrimaryAction;
|
primaryAction?: PrimaryAction;
|
||||||
}
|
}
|
||||||
type R = {
|
type R = {
|
||||||
listResource: ListResource;
|
listResource: ListResource;
|
||||||
documentResource: DocumentResource;
|
documentResource: DocumentResource;
|
||||||
};
|
};
|
||||||
type FilterControls = (r: R) => FilterField[];
|
type FilterControls = (r: R) => FilterField[];
|
||||||
type PrimaryAction = (r: R) => {
|
type PrimaryAction = (r: R) => {
|
||||||
label: string;
|
label: string;
|
||||||
variant?: string;
|
variant?: string;
|
||||||
slots: {
|
slots: {
|
||||||
prefix: Icon;
|
prefix: Icon;
|
||||||
};
|
};
|
||||||
onClick?: () => void;
|
onClick?: () => void;
|
||||||
};
|
};
|
||||||
type StatusBadge = (r: { documentResource: DocumentResource }) => {
|
type StatusBadge = (r: { documentResource: DocumentResource }) => {
|
||||||
label: string;
|
label: string;
|
||||||
};
|
};
|
||||||
export type Breadcrumb = { label: string; route: string };
|
export type Breadcrumb = { label: string; route: string };
|
||||||
export type BreadcrumbArgs = {
|
export type BreadcrumbArgs = {
|
||||||
documentResource: DocumentResource;
|
documentResource: DocumentResource;
|
||||||
items: Breadcrumb[];
|
items: Breadcrumb[];
|
||||||
};
|
};
|
||||||
export type Breadcrumbs = (r: BreadcrumbArgs) => Breadcrumb[];
|
export type Breadcrumbs = (r: BreadcrumbArgs) => Breadcrumb[];
|
||||||
|
|
||||||
export interface FilterField {
|
export interface FilterField {
|
||||||
label: string;
|
label: string;
|
||||||
fieldname: string;
|
fieldname: string;
|
||||||
type: string;
|
type: string;
|
||||||
class?: string;
|
class?: string;
|
||||||
options?:
|
options?:
|
||||||
| {
|
| {
|
||||||
pagetype: string;
|
pagetype: string;
|
||||||
filters?: {
|
filters?: {
|
||||||
pagetype_name?: string;
|
pagetype_name?: string;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
| string[];
|
| string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ColumnField {
|
export interface ColumnField {
|
||||||
label: string;
|
label: string;
|
||||||
fieldname?: string;
|
fieldname?: string;
|
||||||
class?: string;
|
class?: string;
|
||||||
width?: string | number;
|
width?: string | number;
|
||||||
type?: string;
|
type?: string;
|
||||||
format?: (value: any, row: Row) => string | undefined;
|
format?: (value: any, row: Row) => string | undefined;
|
||||||
link?: (value: unknown, row: Row) => string;
|
link?: (value: unknown, row: Row) => string;
|
||||||
prefix?: (row: Row) => Component | undefined;
|
prefix?: (row: Row) => Component | undefined;
|
||||||
suffix?: (row: Row) => Component | undefined;
|
suffix?: (row: Row) => Component | undefined;
|
||||||
theme?: (value: unknown) => string;
|
theme?: (value: unknown) => string;
|
||||||
align?: 'left' | 'right';
|
align?: 'left' | 'right';
|
||||||
}
|
Button?: (r: {
|
||||||
|
row: Row;
|
||||||
export type Row = Record<string, any>;
|
listResource: ListResource;
|
||||||
|
documentResource: DocumentResource;
|
||||||
export interface Tab {
|
}) => {
|
||||||
label: string;
|
label: string;
|
||||||
icon: Icon;
|
variant?: string;
|
||||||
route: string;
|
class?: string;
|
||||||
type: string;
|
slots?: {
|
||||||
condition?: (r: DocumentResource) => boolean;
|
prefix?: Icon;
|
||||||
childrenRoutes?: string[];
|
};
|
||||||
component?: AsyncComponent;
|
onClick?: () => void;
|
||||||
props?: (r: DocumentResource) => Record<string, unknown>;
|
} | null;
|
||||||
list?: TabList;
|
}
|
||||||
}
|
|
||||||
|
export type Row = Record<string, any>;
|
||||||
export interface TabList {
|
|
||||||
pagetype?: string;
|
export interface Tab {
|
||||||
orderBy?: string;
|
label: string;
|
||||||
filters?: (r: DocumentResource) => Record<string, unknown>;
|
icon: Icon;
|
||||||
route?: (row: Row) => Route;
|
route: string;
|
||||||
pageLength?: number;
|
type: string;
|
||||||
columns: ColumnField[];
|
condition?: (r: DocumentResource) => boolean;
|
||||||
fields?: Record<string, string[]>[] | string[];
|
childrenRoutes?: string[];
|
||||||
rowActions?: (r: {
|
component?: AsyncComponent;
|
||||||
row: Row;
|
props?: (r: DocumentResource) => Record<string, unknown>;
|
||||||
listResource: ListResource;
|
list?: TabList;
|
||||||
documentResource: DocumentResource;
|
}
|
||||||
}) => Action[];
|
|
||||||
primaryAction?: PrimaryAction;
|
export interface TabList {
|
||||||
filterControls?: FilterControls;
|
pagetype?: string;
|
||||||
banner?: (r: {
|
orderBy?: string;
|
||||||
documentResource: DocumentResource;
|
filters?: (r: DocumentResource) => Record<string, unknown>;
|
||||||
}) => BannerConfig | undefined;
|
route?: (row: Row) => Route;
|
||||||
searchField?: string;
|
pageLength?: number;
|
||||||
experimental?: boolean;
|
columns: ColumnField[];
|
||||||
documentation?: string;
|
fields?: Record<string, string[]>[] | string[];
|
||||||
resource?: (r: { documentResource: DocumentResource }) => Resource;
|
rowActions?: (r: {
|
||||||
}
|
row: Row;
|
||||||
|
listResource: ListResource;
|
||||||
interface Action {
|
documentResource: DocumentResource;
|
||||||
label: string;
|
}) => Action[];
|
||||||
slots?: {
|
primaryAction?: PrimaryAction;
|
||||||
prefix?: Icon;
|
filterControls?: FilterControls;
|
||||||
};
|
banner?: (r: {
|
||||||
theme?: string;
|
documentResource: DocumentResource;
|
||||||
variant?: string;
|
}) => BannerConfig | undefined;
|
||||||
onClick?: () => void;
|
searchField?: string;
|
||||||
condition?: () => boolean;
|
experimental?: boolean;
|
||||||
route?: Route;
|
documentation?: string;
|
||||||
options?: Option[];
|
resource?: (r: { documentResource: DocumentResource }) => Resource;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Route {
|
interface Action {
|
||||||
name: string;
|
label: string;
|
||||||
params: Record<string, unknown>;
|
slots?: {
|
||||||
}
|
prefix?: Icon;
|
||||||
|
};
|
||||||
export interface RouteDetail {
|
theme?: string;
|
||||||
name: string;
|
variant?: string;
|
||||||
path: string;
|
onClick?: () => void;
|
||||||
component: Component;
|
condition?: () => boolean;
|
||||||
}
|
route?: Route;
|
||||||
|
options?: Option[];
|
||||||
interface Option {
|
}
|
||||||
label: string;
|
|
||||||
icon: Icon | AsyncComponent;
|
export interface Route {
|
||||||
condition: () => boolean;
|
name: string;
|
||||||
onClick: () => void;
|
params: Record<string, unknown>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface BannerConfig {
|
export interface RouteDetail {
|
||||||
title: string;
|
name: string;
|
||||||
}
|
path: string;
|
||||||
dismissable: boolean;
|
component: Component;
|
||||||
id: string;
|
}
|
||||||
type?: string;
|
|
||||||
button?: {
|
interface Option {
|
||||||
label: string;
|
label: string;
|
||||||
variant: string;
|
icon: Icon | AsyncComponent;
|
||||||
onClick?: () => void;
|
condition: () => boolean;
|
||||||
};
|
onClick: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DialogConfig {
|
export interface BannerConfig {
|
||||||
title: string;
|
title: string;
|
||||||
message: string;
|
dismissable: boolean;
|
||||||
primaryAction?: { onClick: () => void };
|
id: string;
|
||||||
onSuccess?: (o: { hide: () => void }) => void;
|
type?: string;
|
||||||
}
|
button?: {
|
||||||
|
label: string;
|
||||||
export interface Process {
|
variant: string;
|
||||||
program: string;
|
onClick?: () => void;
|
||||||
name: string;
|
};
|
||||||
status: string;
|
}
|
||||||
uptime?: number;
|
|
||||||
uptime_string?: string;
|
export interface DialogConfig {
|
||||||
message?: string;
|
title: string;
|
||||||
group?: string;
|
message: string;
|
||||||
pid?: number;
|
primaryAction?: { onClick: () => void };
|
||||||
|
onSuccess?: (o: { hide: () => void }) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Process {
|
||||||
|
program: string;
|
||||||
|
name: string;
|
||||||
|
status: string;
|
||||||
|
uptime?: number;
|
||||||
|
uptime_string?: string;
|
||||||
|
message?: string;
|
||||||
|
group?: string;
|
||||||
|
pid?: number;
|
||||||
}
|
}
|
||||||
@ -22,6 +22,7 @@ import { trialDays } from '../utils/site';
|
|||||||
import { clusterOptions, getUpsellBanner } from './common';
|
import { clusterOptions, getUpsellBanner } from './common';
|
||||||
import { getAppsTab } from './common/apps';
|
import { getAppsTab } from './common/apps';
|
||||||
import { isMobile } from '../utils/device';
|
import { isMobile } from '../utils/device';
|
||||||
|
import { getJobsTab } from './common/jobs';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
pagetype: 'Site',
|
pagetype: 'Site',
|
||||||
@ -881,6 +882,7 @@ export default {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
getJobsTab('Site'),
|
||||||
{
|
{
|
||||||
label: '操作',
|
label: '操作',
|
||||||
icon: icon('sliders'),
|
icon: icon('sliders'),
|
||||||
@ -1340,5 +1342,10 @@ export default {
|
|||||||
path: 'updates/:id',
|
path: 'updates/:id',
|
||||||
component: () => import('../pages/SiteUpdate.vue'),
|
component: () => import('../pages/SiteUpdate.vue'),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'Site Job',
|
||||||
|
path: 'jobs/:id',
|
||||||
|
component: () => import('../pages/JobPage.vue')
|
||||||
|
}
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
3887
dashboard/yarn.lock
3887
dashboard/yarn.lock
File diff suppressed because it is too large
Load Diff
@ -299,7 +299,7 @@ class Site(Document, TagHelpers):
|
|||||||
status = jingrow.get_value(inst.pagetype, inst.name, "status", for_update=True)
|
status = jingrow.get_value(inst.pagetype, inst.name, "status", for_update=True)
|
||||||
if status not in allowed_status:
|
if status not in allowed_status:
|
||||||
jingrow.throw(
|
jingrow.throw(
|
||||||
f"Site action not allowed for site with status: {jingrow.bold(status)}.\nAllowed status are: {jingrow.bold(comma_and(allowed_status))}."
|
f"不允许对状态为 {jingrow.bold(status)} 的站点执行此操作。\n允许的状态为:{jingrow.bold(comma_and(allowed_status))}。"
|
||||||
)
|
)
|
||||||
return func(inst, *args, **kwargs)
|
return func(inst, *args, **kwargs)
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user