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')
}
]
};