jcloud/dashboard/src2/components/group/UpdateReleaseGroupDialog.vue
2025-04-12 17:39:38 +08:00

632 lines
16 KiB
Vue

<template>
<Dialog
v-model="show"
:options="{
size: '4xl',
title: '更新站点分组',
}"
>
<template #body-content>
<AlertBanner
v-if="benchDocResource.pg.are_builds_suspended"
class="mb-4"
title="<b>构建已暂停:</b>更新将在构建恢复后安排运行。"
type="warning"
/>
<!-- 更新步骤 -->
<div class="space-y-4">
<!-- 选择应用步骤 -->
<div v-if="step === 'select-apps'">
<h2 class="mb-4 text-lg font-medium">选择要更新的应用</h2>
<GenericList
class="max-h-[500px]"
v-if="benchDocResource.pg.deploy_information.update_available"
:options="updatableAppOptions"
@update:selections="handleAppSelection"
/>
<p v-else class="text-center text-base text-gray-600">
没有可更新的应用
</p>
</div>
<!-- 移除应用步骤 -->
<div v-else-if="step === 'removed-apps'">
<h2 class="mb-4 text-lg font-medium">这些应用将被移除</h2>
<GenericList class="max-h-[500px]" :options="removedAppOptions" />
</div>
<!-- 选择站点步骤 -->
<div v-else-if="step === 'select-sites'">
<h2 class="mb-4 text-lg font-medium">选择要更新的站点</h2>
<GenericList
class="max-h-[500px]"
v-if="benchDocResource.pg.deploy_information.sites.length"
:options="siteOptions"
@update:selections="handleSiteSelection"
/>
<p
class="text-center text-base font-medium text-gray-600"
v-else-if="!benchDocResource.pg.deploy_information.sites.length"
>
没有可更新的活跃站点
</p>
</div>
<!-- 限制构建步骤 -->
<div
v-else-if="step === 'restrict-build' && restrictMessage"
class="flex flex-col gap-4"
>
<div class="flex items-center gap-2">
<h2 class="text-lg font-medium">构建可能会失败</h2>
<a
href="https://jingrow.com/docs/common-issues/build-might-fail"
target="_blank"
class="cursor-pointer rounded-full border border-gray-200 bg-gray-100 p-0.5 text-base text-gray-700"
>
<i-lucide-help-circle :class="`h-4 w-4 text-red-600`" />
</a>
</div>
<p
class="text-base font-medium text-gray-800"
v-html="restrictMessage"
></p>
<div class="mt-4">
<FormControl
label="我明白,仍然运行部署"
type="checkbox"
v-model="ignoreWillFailCheck"
/>
</div>
</div>
<div v-if="canUpdateInPlace" class="flex gap-2">
<FormControl
label="使用原地更新"
type="checkbox"
v-model="useInPlaceUpdate"
/>
<Tooltip text="查看文档">
<a
href="https://jingrow.com/docs/in-place-updates"
target="_blank"
>
<i-lucide-help-circle :class="`h-4 w-4 text-gray-600`" />
</a>
</Tooltip>
</div>
<ErrorMessage :message="errorMessage" />
</div>
</template>
<template #actions>
<div class="flex items-center justify-between space-y-2">
<div v-if="!canShowBack"><!-- 占位div --></div>
<Button v-if="canShowBack" label="返回" @click="back" />
<Button v-if="canShowNext" variant="solid" label="下一步" @click="next" />
<Button
v-if="canShowDeploy"
variant="solid"
:label="deployLabel"
:loading="
$resources.deployAndUpdate.loading ||
$resources.updateInPlace.loading
"
@click="updateBench"
/>
</div>
</template>
</Dialog>
</template>
<script>
import { h } from 'vue';
import { Checkbox, getCachedDocumentResource } from 'jingrow-ui';
import CommitChooser from '@/components/utils/CommitChooser.vue';
import CommitTag from '@/components/utils/CommitTag.vue';
import GenericList from '../../components/GenericList.vue';
import { getTeam } from '../../data/team';
import { DashboardError } from '../../utils/error';
import AlertBanner from '../AlertBanner.vue';
export default {
name: 'UpdateReleaseGroupDialog',
props: ['bench'],
components: {
GenericList,
CommitChooser,
CommitTag,
AlertBanner,
},
data() {
return {
show: true,
step: '',
errorMessage: '',
ignoreWillFailCheck: false,
useInPlaceUpdate: false,
restrictMessage: '',
selectedApps: [],
selectedSites: [],
};
},
mounted() {
if (this.hasUpdateAvailable) {
this.step = '选择应用';
} else if (this.hasRemovedApps) {
this.step = '已移除应用';
} else {
this.step = '选择站点';
}
},
computed: {
updatableAppOptions() {
let deployInformation = this.benchDocResource.pg.deploy_information;
let appData = deployInformation.apps.filter(
(app) => app.update_available === true,
);
return {
data: appData,
selectable: true,
columns: [
{
label: '应用',
fieldname: 'title',
width: 1.75,
},
{
label: '从',
fieldname: 'current_hash',
type: 'Component',
component({ row: app }) {
if (!app.current_hash) return null;
let tag = app.will_branch_change
? app.current_branch
: app.current_hash.slice(0, 7);
return h(CommitTag, {
tag: tag,
link: `${app.repository_url}/commit/${tag}`,
});
},
},
{
label: '到',
fieldname: 'next_release',
type: 'Component',
component({ row: app }) {
if (app.will_branch_change) {
return h(CommitTag, {
tag: app.branch,
link: `${app.repository_url}/commit/${app.branch}`,
});
}
function commitChooserOptions(app) {
return app.releases.map((release) => {
const messageMaxLength = 75;
let message = release.message.split('\n')[0];
message =
message.length > messageMaxLength
? message.slice(0, messageMaxLength) + '...'
: message;
return {
label: release.tag
? release.tag
: `${message} (${release.hash.slice(0, 7)})`,
value: release.name,
};
});
}
function initialDeployTo(app) {
const next_release = app.releases.filter(
(release) => release.name === app.next_release,
)[0];
if (app.will_branch_change) {
return app.branch;
} else if (next_release) {
return next_release.tag || next_release.hash.slice(0, 7);
}
}
if (!app.releases.length) return undefined;
let initialValue = {
label: initialDeployTo(app),
value: app.next_release,
};
return h(CommitChooser, {
options: commitChooserOptions(app),
modelValue: initialValue,
'onUpdate:modelValue': (value) => {
appData.find((a) => a.name === app.name).next_release =
value.value;
},
});
},
},
{
label: '状态',
fieldname: 'title',
type: 'Badge',
format(value, row) {
if (
deployInformation.removed_apps.find(
(app) => app.name === row.name,
)
) {
return '将被卸载';
} else if (!row.will_branch_change && !row.current_hash) {
return '首次部署';
}
return '有更新可用';
},
},
{
label: '变更',
type: '按钮',
width: 0.5,
align: 'right',
Button({ row }) {
let url;
if (row.current_hash && row.next_release) {
let hash = row.releases.find(
(release) => release.name === row.next_release,
)?.hash;
if (hash)
url = `${row.repository_url}/compare/${row.current_hash}...${hash}`;
} else if (row.next_release) {
url = `${row.repository_url}/commit/${
row.releases.find(
(release) => release.name === row.next_release,
).hash
}`;
}
if (!url) return null;
return {
label: '查看',
variant: 'ghost',
onClick() {
window.open(url, '_blank');
},
};
},
},
],
};
},
removedAppOptions() {
let deployInformation = this.benchDocResource.pg.deploy_information;
let appData = deployInformation.removed_apps;
return {
data: appData,
columns: [
{
label: '应用',
fieldname: 'title',
},
{
label: '状态',
fieldname: 'name',
type: 'Badge',
format() {
return '将被卸载';
},
},
],
};
},
siteOptions() {
let deployInformation = this.benchDocResource.pg.deploy_information;
let siteData = deployInformation.sites;
let team = getTeam();
/**
* 如果使用就地更新,失败的补丁将被跳过
* 并且默认情况下不会进行站点备份。
*/
const disabled = this.useInPlaceUpdate;
return {
data: siteData,
selectable: true,
columns: [
{
label: '站点',
fieldname: 'name',
},
{
label: '跳过失败的补丁',
fieldname: 'skip_failing_patches',
width: 0.5,
type: 'Component',
component({ row }) {
return h(Checkbox, {
modelValue: row.skip_failing_patches,
disabled,
});
},
},
{
label: '跳过备份',
fieldname: 'skip_backups',
width: 0.3,
type: 'Component',
condition() {
return !!team.pg.skip_backups;
},
component({ row }) {
return h(Checkbox, {
modelValue: row.skip_backups,
disabled,
});
},
},
],
};
},
benchDocResource() {
return getCachedDocumentResource('Release Group', this.bench);
},
hasUpdateAvailable() {
return this.benchDocResource.pg.deploy_information.apps.some(
(app) => app.update_available === true,
);
},
hasRemovedApps() {
return !!this.benchDocResource.pg.deploy_information.removed_apps.length;
},
deployInformation() {
return this.benchDocResource?.pg.deploy_information;
},
canShowBack() {
if (this.step === 'select-apps') {
return false;
}
return this.hasUpdateAvailable || this.step === 'restrict-build';
},
canShowNext() {
if (this.step === 'restrict-build') {
return false;
}
if (this.step === 'select-sites' && !this.restrictMessage) {
return false;
}
return true;
},
canShowDeploy() {
return !this.canShowNext;
},
deployLabel() {
if (this.selectedSites.length === 0) {
return '跳过并部署';
}
let site = '站点';
if (this.selectedSites.length > 1) {
site = `${this.selectedSites.length} 个站点`;
}
if (this.useInPlaceUpdate) {
return `就地更新 ${site}`;
}
return `部署并更新 ${site}`;
},
canUpdateInPlace() {
if (!this.benchDocResource?.pg?.enable_inplace_updates) {
return false;
}
// 就地更新不能与已移除的应用一起进行。
if (this.hasRemovedApps) {
return false;
}
// 所有要更新的站点必须属于同一个工作台。
const benches = new Set(this.selectedSites.map((s) => s.bench));
if (benches.size !== 1) {
return false;
}
// 就地更新失败的工作台必须进行常规更新。
const inPlaceUpdateFailedBenches =
this.benchDocResource?.pg?.inplace_update_failed_benches ?? [];
const allSites = this.siteOptions.data
.filter(
(s) =>
benches.has(s.bench) ||
inPlaceUpdateFailedBenches.includes(s.bench),
)
.map((s) => s.name);
// 工作台下的所有站点都应更新
if (allSites.length !== this.selectedSites.length) {
return false;
}
return true;
},
},
resources: {
deployAndUpdate() {
return {
url: 'jcloud.api.bench.deploy_and_update',
params: {
name: this.bench,
apps: this.selectedApps,
sites: this.selectedSites,
run_will_fail_check: !this.ignoreWillFailCheck,
},
validate() {
if (
this.hasUpdateAvailable &&
this.selectedApps.length === 0 &&
this.deployInformation.removed_apps.length === 0
) {
throw new DashboardError('请选择一个应用以继续');
}
},
onSuccess(candidate) {
this.$router.push({
name: 'Deploy Candidate',
params: {
id: candidate,
name: this.bench,
},
});
this.restrictMessage = '';
this.show = false;
this.$emit('success', candidate);
},
onError: this.setErrorMessage.bind(this),
};
},
updateInPlace() {
return {
url: 'jcloud.api.bench.update_inplace',
params: {
name: this.bench,
apps: this.selectedApps,
sites: this.selectedSites,
},
onSuccess(id) {
this.$router.push({
name: 'Release Group Job',
params: { id },
});
this.restrictMessage = '';
this.show = false;
this.$emit('success', null);
},
onError: this.setErrorMessage.bind(this),
};
},
},
methods: {
back() {
if (this.step === 'select-apps') {
return;
} else if (this.step === 'removed-apps') {
this.step = 'select-apps';
} else if (this.step === 'select-sites' && this.hasRemovedApps) {
this.step = 'removed-apps';
} else if (this.step === 'select-sites' && !this.hasRemovedApps) {
this.step = 'select-apps';
} else if (this.step === 'restrict-build') {
this.step = 'select-sites';
}
if (this.step === 'select-apps') {
this.selectedApps = [];
}
},
next() {
if (this.errorMessage) {
this.errorMessage = '';
}
if (this.step === 'select-apps' && this.selectedApps.length === 0) {
this.errorMessage = '请选择一个应用以继续';
return;
} else if (this.step === 'select-apps' && this.hasRemovedApps) {
this.step = 'removed-apps';
} else if (this.step === 'select-apps' && !this.hasRemovedApps) {
this.step = 'select-sites';
} else if (this.step === 'removed-apps') {
this.step = 'select-sites';
} else if (this.step === 'select-sites' && this.restrictMessage) {
this.step = 'restrict-build';
}
},
handleAppSelection(apps) {
apps = Array.from(apps);
let appData = this.benchDocResource.pg.deploy_information.apps;
this.selectedApps = appData
.filter((app) => apps.includes(app.name))
.map((app) => {
return {
app: app.name,
source: app.source,
release: app.next_release,
hash: app.releases.find(
(release) => release.name === app.next_release,
).hash,
};
});
},
handleSiteSelection(sites) {
sites = Array.from(sites);
let siteData = this.benchDocResource.pg.deploy_information.sites;
this.selectedSites = siteData.filter((site) => sites.includes(site.name));
},
deployFrom(app) {
if (app.will_branch_change) {
return app.current_branch;
}
return app.current_hash
? app.current_tag || app.current_hash.slice(0, 7)
: null;
},
initialDeployTo(app) {
return this.benchDocResource.pg.deploy_information.apps.find(
(a) => a.app === app.app,
).next_release;
},
updateBench() {
if (this.restrictMessage && !this.ignoreWillFailCheck) {
this.errorMessage = '请勾选<b>我理解</b>复选框以继续';
return;
}
this.errorMessage = '';
if (this.canUpdateInPlace && this.useInPlaceUpdate) {
this.setSkipBackupsAndFailingPatches();
this.$resources.updateInPlace.submit();
} else {
this.$resources.deployAndUpdate.submit();
}
},
setSkipBackupsAndFailingPatches() {
for (const site of this.selectedSites) {
site.skip_failing_patches = true;
site.skip_backups = true;
}
},
setErrorMessage(error) {
this.ignoreWillFailCheck = false;
if (error?.exc_type === 'BuildValidationError') {
this.restrictMessage = error?.messages?.[0] ?? '';
}
if (error?.exc_type === 'PermissionError') {
this.errorMessage = error?.messages?.[0] ?? '';
return;
}
if (this.restrictMessage) {
this.step = 'restrict-build';
return;
}
this.errorMessage =
'内部服务器错误:无法启动部署';
},
},
};
</script>