jcloud/dashboard/src2/pages/DeployCandidate.vue
2025-04-12 17:39:38 +08:00

263 lines
7.0 KiB
Vue

<template>
<div class="p-5" v-if="deploy">
<AlertAddressableError
v-if="error"
class="mb-5"
:name="error.name"
:title="error.title"
@done="$resources.errors.reload()"
/>
<AlertBanner
v-if="alertMessage && !error"
:title="alertMessage"
type="warning"
class="mb-5"
/>
<Button :route="{ name: `${object.pagetype} Detail Deploys` }">
<template #prefix>
<i-lucide-arrow-left class="inline-block h-4 w-4" />
</template>
所有部署
</Button>
<div class="mt-3">
<div class="flex w-full items-center">
<h2 class="text-lg font-medium text-gray-900">{{ deploy.name }}</h2>
<Badge class="ml-2" :label="deploy.status" />
<div class="ml-auto flex items-center space-x-2">
<Button
@click="$resources.deploy.reload()"
:loading="$resources.deploy.get.loading"
>
<template #icon>
<i-lucide-refresh-ccw class="h-4 w-4" />
</template>
</Button>
<Dropdown v-if="dropdownOptions?.length" :options="dropdownOptions">
<template v-slot="{ open }">
<Button>
<template #icon>
<i-lucide-more-horizontal class="h-4 w-4" />
</template>
</Button>
</template>
</Dropdown>
</div>
</div>
<div>
<div class="mt-4 grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-5">
<div>
<div class="text-sm font-medium text-gray-500">创建时间</div>
<div class="mt-2 text-sm text-gray-900">
{{ $format.date(deploy.creation, 'lll') }}
</div>
</div>
<div>
<div class="text-sm font-medium text-gray-500">创建者</div>
<div class="mt-2 text-sm text-gray-900">
{{ deploy.owner }}
</div>
</div>
<div>
<div class="text-sm font-medium text-gray-500">持续时间</div>
<div class="mt-2 text-sm text-gray-900">
{{
deploy.build_end ? $format.duration(deploy.build_duration) : '-'
}}
</div>
</div>
<div>
<div class="text-sm font-medium text-gray-500">开始时间</div>
<div class="mt-2 text-sm text-gray-900">
{{ $format.date(deploy.build_start, 'lll') }}
</div>
</div>
<div>
<div class="text-sm font-medium text-gray-500">结束时间</div>
<div class="mt-2 text-sm text-gray-900">
{{
deploy.build_end ? $format.date(deploy.build_end, 'lll') : '-'
}}
</div>
</div>
</div>
</div>
</div>
<!-- 构建步骤 -->
<div :class="deploy.build_error ? 'mt-4' : 'mt-8'" class="space-y-4">
<JobStep
v-for="step in deploy.build_steps"
:step="step"
:key="step.name"
/>
<JobStep v-for="job in deploy.jobs" :step="job" :key="job.name" />
</div>
</div>
</template>
<script>
import { createResource, getCachedDocumentResource } from 'jingrow-ui';
import { getObject } from '../objects';
import JobStep from '../components/JobStep.vue';
import AlertAddressableError from '../components/AlertAddressableError.vue';
import AlertBanner from '../components/AlertBanner.vue';
import dayjs from 'dayjs';
import { toast } from 'vue-sonner';
export default {
name: 'DeployCandidate',
props: ['id', 'objectType'],
components: {
JobStep,
AlertBanner,
AlertAddressableError,
},
resources: {
deploy() {
return {
type: 'document',
pagetype: 'Deploy Candidate',
name: this.id,
transform: this.transformDeploy,
};
},
errors() {
return {
type: 'list',
cache: ['Jcloud Notification', 'Error', 'Deploy Candidate', this.id],
pagetype: 'Jcloud Notification',
auto: true,
fields: ['title', 'name'],
filters: {
document_type: 'Deploy Candidate',
document_name: this.id,
is_actionable: true,
class: 'Error',
},
limit: 1,
};
},
},
mounted() {
this.$socket.emit('pg_subscribe', 'Deploy Candidate', this.id);
this.$socket.on(`bench_deploy:${this.id}:steps`, (data) => {
if (data.name === this.id && this.$resources.deploy.pg) {
this.$resources.deploy.pg.build_steps = this.transformDeploy({
build_steps: data.steps,
})?.build_steps;
}
});
this.$socket.on(`bench_deploy:${this.id}:finished`, () => {
const rgDoc = getCachedDocumentResource(
'Release Group',
this.$resources.deploy.pg?.group,
);
if (rgDoc) rgDoc.reload();
this.$resources.deploy.reload();
this.$resources.errors.reload();
});
},
beforeUnmount() {
this.$socket.emit('pg_unsubscribe', 'Deploy Candidate', this.id);
this.$socket.off(`bench_deploy:${this.id}:steps`);
},
computed: {
deploy() {
return this.$resources.deploy.pg;
},
object() {
return getObject(this.objectType);
},
error() {
return this.$resources.errors?.data?.[0] ?? null;
},
alertMessage() {
if (!this.deploy) {
return null;
}
if (this.deploy.retry_count > 0 && this.deploy.status === 'Scheduled') {
return '上一次部署失败,将很快尝试重新部署';
}
return null;
},
dropdownOptions() {
return [
{
label: '在桌面查看',
icon: 'external-link',
condition: () => this.$team?.pg?.is_desk_user,
onClick: () => {
window.open(
`${window.location.protocol}//${window.location.host}/app/deploy-candidate/${this.id}`,
'_blank',
);
},
},
{
label: '失败并重新部署',
icon: 'repeat',
condition: () => this.showFailAndRedeploy,
onClick: () => this.failAndRedeploy(),
},
].filter((option) => option.condition?.() ?? true);
},
showFailAndRedeploy() {
if (!this.deploy || this.deploy.status !== 'Running') {
return false;
}
const start = dayjs(this.deploy.build_start);
const now = dayjs(new Date());
return now.diff(start, 'hours') > 2;
},
},
methods: {
transformDeploy(deploy) {
for (let step of deploy.build_steps) {
if (step.status === 'Running') {
step.isOpen = true;
} else {
step.isOpen = this.$resources.deploy?.pg?.build_steps?.find(
(s) => s.name === step.name,
)?.isOpen;
}
step.title = `${step.stage} - ${step.step}`;
step.output =
step.command || step.output
? `${step.command || ''}\n${step.output || ''}`.trim()
: '';
step.duration = ['Success', 'Failure'].includes(step.status)
? step.cached
? 'Cached'
: `${step.duration}s`
: null;
}
return deploy;
},
failAndRedeploy() {
if (!this.deploy) {
return;
}
const group = this.deploy.group;
const onError = () => toast.error('无法失败并重新部署');
const router = this.$router;
createResource({
url: 'jcloud.api.bench.fail_and_redeploy',
params: { name: group, dc_name: this.deploy.name },
onSuccess(name) {
if (!name) {
onError();
} else {
router.push(`/groups/${group}/deploys/${name}`);
}
},
onError,
}).fetch();
},
},
};
</script>