632 lines
16 KiB
Vue
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> |