deploy fix bug
This commit is contained in:
parent
53629519f5
commit
4cd2d4e31c
@ -136,14 +136,14 @@ type ReleaseRunStep struct {
|
|||||||
ServiceName string `gorm:"size:32;index;not null" json:"service_name"`
|
ServiceName string `gorm:"size:32;index;not null" json:"service_name"`
|
||||||
PreviousReleaseID string `gorm:"size:128;index" json:"previous_release_id"`
|
PreviousReleaseID string `gorm:"size:128;index" json:"previous_release_id"`
|
||||||
Status string `gorm:"size:32;index;not null" json:"status"`
|
Status string `gorm:"size:32;index;not null" json:"status"`
|
||||||
DeploymentID uint `gorm:"index" json:"deployment_id"`
|
DeploymentID *uint `gorm:"index" json:"deployment_id"`
|
||||||
Deployment *Deployment `json:"deployment,omitempty"`
|
Deployment *Deployment `json:"deployment,omitempty"`
|
||||||
ErrorMessage string `gorm:"type:text" json:"error_message"`
|
ErrorMessage string `gorm:"type:text" json:"error_message"`
|
||||||
LogExcerpt string `gorm:"type:text" json:"log_excerpt"`
|
LogExcerpt string `gorm:"type:text" json:"log_excerpt"`
|
||||||
StartedAt *time.Time `json:"started_at"`
|
StartedAt *time.Time `json:"started_at"`
|
||||||
FinishedAt *time.Time `json:"finished_at"`
|
FinishedAt *time.Time `json:"finished_at"`
|
||||||
RollbackStatus string `gorm:"size:32;index" json:"rollback_status"`
|
RollbackStatus string `gorm:"size:32;index" json:"rollback_status"`
|
||||||
RollbackDeploymentID uint `gorm:"index" json:"rollback_deployment_id"`
|
RollbackDeploymentID *uint `gorm:"index" json:"rollback_deployment_id"`
|
||||||
RollbackDeployment *Deployment `json:"rollback_deployment,omitempty"`
|
RollbackDeployment *Deployment `json:"rollback_deployment,omitempty"`
|
||||||
RollbackErrorMessage string `gorm:"type:text" json:"rollback_error_message"`
|
RollbackErrorMessage string `gorm:"type:text" json:"rollback_error_message"`
|
||||||
RollbackLogExcerpt string `gorm:"type:text" json:"rollback_log_excerpt"`
|
RollbackLogExcerpt string `gorm:"type:text" json:"rollback_log_excerpt"`
|
||||||
|
|||||||
@ -455,7 +455,7 @@ func (m *Manager) ProcessReleaseRun(ctx context.Context, runID uint) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
step.Status = model.DeploymentSuccess
|
step.Status = model.DeploymentSuccess
|
||||||
step.DeploymentID = deployment.ID
|
step.DeploymentID = &deployment.ID
|
||||||
completedSteps = append(completedSteps, step)
|
completedSteps = append(completedSteps, step)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -642,6 +642,7 @@ func (m *Manager) ProcessDeployment(ctx context.Context, deploymentID uint) erro
|
|||||||
|
|
||||||
targetFinishedAt := time.Now()
|
targetFinishedAt := time.Now()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
errorMessage := summarizeDeploymentError(err, output)
|
||||||
_ = m.db.WithContext(ctx).Model(&model.DeploymentTarget{}).
|
_ = m.db.WithContext(ctx).Model(&model.DeploymentTarget{}).
|
||||||
Where("id = ?", target.ID).
|
Where("id = ?", target.ID).
|
||||||
Updates(map[string]any{
|
Updates(map[string]any{
|
||||||
@ -654,10 +655,10 @@ func (m *Manager) ProcessDeployment(ctx context.Context, deploymentID uint) erro
|
|||||||
Where("id = ?", deploymentID).
|
Where("id = ?", deploymentID).
|
||||||
Updates(map[string]any{
|
Updates(map[string]any{
|
||||||
"status": model.DeploymentFailed,
|
"status": model.DeploymentFailed,
|
||||||
"error_message": err.Error(),
|
"error_message": errorMessage,
|
||||||
"finished_at": &targetFinishedAt,
|
"finished_at": &targetFinishedAt,
|
||||||
}).Error
|
}).Error
|
||||||
return err
|
return errors.New(errorMessage)
|
||||||
}
|
}
|
||||||
|
|
||||||
updates := map[string]any{
|
updates := map[string]any{
|
||||||
@ -911,6 +912,26 @@ func summarizeBuildError(err error, output string) string {
|
|||||||
return err.Error()
|
return err.Error()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func summarizeDeploymentError(err error, output string) string {
|
||||||
|
lines := strings.Split(output, "\n")
|
||||||
|
for i := len(lines) - 1; i >= 0; i-- {
|
||||||
|
line := strings.TrimSpace(lines[i])
|
||||||
|
if line == "" || line == err.Error() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if line == "Traceback (most recent call last):" || strings.HasPrefix(line, "File ") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(line, "RuntimeError:") {
|
||||||
|
line = strings.TrimSpace(strings.TrimPrefix(line, "RuntimeError:"))
|
||||||
|
}
|
||||||
|
if line != "" {
|
||||||
|
return line
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return err.Error()
|
||||||
|
}
|
||||||
|
|
||||||
func deploymentLogExcerpt(deployment *model.Deployment) string {
|
func deploymentLogExcerpt(deployment *model.Deployment) string {
|
||||||
if deployment == nil {
|
if deployment == nil {
|
||||||
return ""
|
return ""
|
||||||
|
|||||||
3
frontend/src/constants/operator.ts
Normal file
3
frontend/src/constants/operator.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
// TODO: Replace this fallback with the authenticated user once identity is wired end-to-end,
|
||||||
|
// then restore operator-related fields in the UI.
|
||||||
|
export const defaultOperator = 'platform-admin'
|
||||||
@ -26,16 +26,15 @@ import { useEffect, useMemo, useState } from 'react'
|
|||||||
|
|
||||||
import { api } from '@/api/client'
|
import { api } from '@/api/client'
|
||||||
import StatusChip from '@/components/StatusChip'
|
import StatusChip from '@/components/StatusChip'
|
||||||
|
import { defaultOperator } from '@/constants/operator'
|
||||||
import type { BuildJob, CatalogService } from '@/types'
|
import type { BuildJob, CatalogService } from '@/types'
|
||||||
|
|
||||||
type FormState = {
|
type FormState = {
|
||||||
service_name: string
|
service_name: string
|
||||||
release_id: string
|
release_id: string
|
||||||
branch: string
|
branch: string
|
||||||
operator: string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultOperator = 'platform-admin'
|
|
||||||
const activeBuildStatuses = new Set(['queued', 'running'])
|
const activeBuildStatuses = new Set(['queued', 'running'])
|
||||||
|
|
||||||
function formatDateTime(value?: string) {
|
function formatDateTime(value?: string) {
|
||||||
@ -56,8 +55,7 @@ export default function BuildsPage() {
|
|||||||
const [form, setForm] = useState<FormState>({
|
const [form, setForm] = useState<FormState>({
|
||||||
service_name: '',
|
service_name: '',
|
||||||
release_id: '',
|
release_id: '',
|
||||||
branch: 'main',
|
branch: 'main'
|
||||||
operator: defaultOperator
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const selectedService = useMemo(
|
const selectedService = useMemo(
|
||||||
@ -77,8 +75,7 @@ export default function BuildsPage() {
|
|||||||
setForm({
|
setForm({
|
||||||
service_name: first?.name ?? '',
|
service_name: first?.name ?? '',
|
||||||
release_id: '',
|
release_id: '',
|
||||||
branch: first?.default_branch ?? 'main',
|
branch: first?.default_branch ?? 'main'
|
||||||
operator: defaultOperator
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -141,7 +138,7 @@ export default function BuildsPage() {
|
|||||||
|
|
||||||
const createBuild = async () => {
|
const createBuild = async () => {
|
||||||
try {
|
try {
|
||||||
const created = await api.createBuild(form)
|
const created = await api.createBuild({ ...form, operator: defaultOperator })
|
||||||
setOpen(false)
|
setOpen(false)
|
||||||
resetForm(services)
|
resetForm(services)
|
||||||
setMessage('构建任务已创建,已打开实时构建日志')
|
setMessage('构建任务已创建,已打开实时构建日志')
|
||||||
@ -267,11 +264,6 @@ export default function BuildsPage() {
|
|||||||
value={form.branch}
|
value={form.branch}
|
||||||
onChange={(event) => setForm((prev) => ({ ...prev, branch: event.target.value }))}
|
onChange={(event) => setForm((prev) => ({ ...prev, branch: event.target.value }))}
|
||||||
/>
|
/>
|
||||||
<TextField
|
|
||||||
label="操作人"
|
|
||||||
value={form.operator}
|
|
||||||
onChange={(event) => setForm((prev) => ({ ...prev, operator: event.target.value }))}
|
|
||||||
/>
|
|
||||||
<TextField label="仓库" value={selectedService?.repo ?? ''} slotProps={{ input: { readOnly: true } }} />
|
<TextField label="仓库" value={selectedService?.repo ?? ''} slotProps={{ input: { readOnly: true } }} />
|
||||||
<Typography variant="body2" color="text.secondary">
|
<Typography variant="body2" color="text.secondary">
|
||||||
平台会先在构建机本地源码目录中自动查找匹配仓库;找到后执行 `git fetch / checkout / pull`。如果本地没有对应仓库,再自动克隆到构建工作区。
|
平台会先在构建机本地源码目录中自动查找匹配仓库;找到后执行 `git fetch / checkout / pull`。如果本地没有对应仓库,再自动克隆到构建工作区。
|
||||||
|
|||||||
@ -1,8 +1,11 @@
|
|||||||
import {
|
import {
|
||||||
Alert,
|
Alert,
|
||||||
|
Box,
|
||||||
Button,
|
Button,
|
||||||
Card,
|
Card,
|
||||||
CardContent,
|
CardContent,
|
||||||
|
Chip,
|
||||||
|
CircularProgress,
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogActions,
|
DialogActions,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
@ -24,32 +27,31 @@ import { useNavigate } from 'react-router-dom'
|
|||||||
|
|
||||||
import { api } from '@/api/client'
|
import { api } from '@/api/client'
|
||||||
import StatusChip from '@/components/StatusChip'
|
import StatusChip from '@/components/StatusChip'
|
||||||
import type { Deployment, ReleaseRun } from '@/types'
|
import { defaultOperator } from '@/constants/operator'
|
||||||
|
import type { Deployment, DeploymentTarget, ReleaseRun } from '@/types'
|
||||||
|
|
||||||
type DeploymentFormState = {
|
type DeploymentFormState = {
|
||||||
service_name: string
|
service_name: string
|
||||||
release_id: string
|
release_id: string
|
||||||
operation: string
|
operation: string
|
||||||
operator: string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type ReleaseRunFormState = {
|
type ReleaseRunFormState = {
|
||||||
release_id: string
|
release_id: string
|
||||||
operator: string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const emptyDeploymentForm: DeploymentFormState = {
|
const emptyDeploymentForm: DeploymentFormState = {
|
||||||
service_name: 'user',
|
service_name: 'user',
|
||||||
release_id: '',
|
release_id: '',
|
||||||
operation: 'deploy',
|
operation: 'deploy'
|
||||||
operator: 'platform-admin'
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const emptyReleaseRunForm: ReleaseRunFormState = {
|
const emptyReleaseRunForm: ReleaseRunFormState = {
|
||||||
release_id: '',
|
release_id: ''
|
||||||
operator: 'platform-admin'
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const activeDeploymentStatuses = new Set(['queued', 'running'])
|
||||||
|
|
||||||
function stepSummary(run: ReleaseRun) {
|
function stepSummary(run: ReleaseRun) {
|
||||||
return (run.steps ?? []).map((step) => ({
|
return (run.steps ?? []).map((step) => ({
|
||||||
key: `${run.id}-${step.sequence}`,
|
key: `${run.id}-${step.sequence}`,
|
||||||
@ -60,14 +62,29 @@ function stepSummary(run: ReleaseRun) {
|
|||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatDateTime(value?: string) {
|
||||||
|
if (!value) return '-'
|
||||||
|
return new Date(value).toLocaleString()
|
||||||
|
}
|
||||||
|
|
||||||
|
function targetLabel(target: DeploymentTarget) {
|
||||||
|
const hostName = target.host?.name || `host-${target.host_id}`
|
||||||
|
const privateIP = target.host?.private_ip
|
||||||
|
return privateIP ? `${hostName} · ${privateIP}` : hostName
|
||||||
|
}
|
||||||
|
|
||||||
export default function DeploymentsPage() {
|
export default function DeploymentsPage() {
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const [deployments, setDeployments] = useState<Deployment[]>([])
|
const [deployments, setDeployments] = useState<Deployment[]>([])
|
||||||
const [releaseRuns, setReleaseRuns] = useState<ReleaseRun[]>([])
|
const [releaseRuns, setReleaseRuns] = useState<ReleaseRun[]>([])
|
||||||
const [deploymentOpen, setDeploymentOpen] = useState(false)
|
const [deploymentOpen, setDeploymentOpen] = useState(false)
|
||||||
const [releaseRunOpen, setReleaseRunOpen] = useState(false)
|
const [releaseRunOpen, setReleaseRunOpen] = useState(false)
|
||||||
|
const [detailOpen, setDetailOpen] = useState(false)
|
||||||
const [deploymentForm, setDeploymentForm] = useState<DeploymentFormState>(emptyDeploymentForm)
|
const [deploymentForm, setDeploymentForm] = useState<DeploymentFormState>(emptyDeploymentForm)
|
||||||
const [releaseRunForm, setReleaseRunForm] = useState<ReleaseRunFormState>(emptyReleaseRunForm)
|
const [releaseRunForm, setReleaseRunForm] = useState<ReleaseRunFormState>(emptyReleaseRunForm)
|
||||||
|
const [selectedDeploymentID, setSelectedDeploymentID] = useState<number | null>(null)
|
||||||
|
const [selectedDeployment, setSelectedDeployment] = useState<Deployment | null>(null)
|
||||||
|
const [detailLoading, setDetailLoading] = useState(false)
|
||||||
const [message, setMessage] = useState('')
|
const [message, setMessage] = useState('')
|
||||||
const [error, setError] = useState('')
|
const [error, setError] = useState('')
|
||||||
|
|
||||||
@ -86,7 +103,7 @@ export default function DeploymentsPage() {
|
|||||||
|
|
||||||
const createDeployment = async () => {
|
const createDeployment = async () => {
|
||||||
try {
|
try {
|
||||||
await api.createDeployment(deploymentForm)
|
await api.createDeployment({ ...deploymentForm, operator: defaultOperator })
|
||||||
setDeploymentOpen(false)
|
setDeploymentOpen(false)
|
||||||
setDeploymentForm(emptyDeploymentForm)
|
setDeploymentForm(emptyDeploymentForm)
|
||||||
setMessage('单服务部署任务已创建')
|
setMessage('单服务部署任务已创建')
|
||||||
@ -98,7 +115,7 @@ export default function DeploymentsPage() {
|
|||||||
|
|
||||||
const createReleaseRun = async () => {
|
const createReleaseRun = async () => {
|
||||||
try {
|
try {
|
||||||
await api.createReleaseRun(releaseRunForm)
|
await api.createReleaseRun({ ...releaseRunForm, operator: defaultOperator })
|
||||||
setReleaseRunOpen(false)
|
setReleaseRunOpen(false)
|
||||||
setReleaseRunForm(emptyReleaseRunForm)
|
setReleaseRunForm(emptyReleaseRunForm)
|
||||||
setMessage('总发布任务已创建,将按 user -> pay -> gateway 执行')
|
setMessage('总发布任务已创建,将按 user -> pay -> gateway 执行')
|
||||||
@ -110,7 +127,7 @@ export default function DeploymentsPage() {
|
|||||||
|
|
||||||
const rollback = async (row: Deployment) => {
|
const rollback = async (row: Deployment) => {
|
||||||
try {
|
try {
|
||||||
await api.rollbackDeployment(row.id, { release_id: row.release_id, operator: 'platform-admin' })
|
await api.rollbackDeployment(row.id, { release_id: row.release_id, operator: defaultOperator })
|
||||||
setMessage('回滚任务已创建')
|
setMessage('回滚任务已创建')
|
||||||
await load()
|
await load()
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@ -118,6 +135,27 @@ export default function DeploymentsPage() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const loadDeploymentDetail = async (id: number, silent = false) => {
|
||||||
|
if (!silent) {
|
||||||
|
setDetailLoading(true)
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const data = await api.getDeployment(id)
|
||||||
|
setSelectedDeployment(data)
|
||||||
|
} catch (err) {
|
||||||
|
setError(err instanceof Error ? err.message : '加载部署详情失败')
|
||||||
|
} finally {
|
||||||
|
setDetailLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const openDeploymentDetail = (row: Deployment) => {
|
||||||
|
setSelectedDeploymentID(row.id)
|
||||||
|
setSelectedDeployment(row)
|
||||||
|
setDetailOpen(true)
|
||||||
|
void loadDeploymentDetail(row.id)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack spacing={3}>
|
<Stack spacing={3}>
|
||||||
<Card>
|
<Card>
|
||||||
@ -161,7 +199,6 @@ export default function DeploymentsPage() {
|
|||||||
<TableCell>版本</TableCell>
|
<TableCell>版本</TableCell>
|
||||||
<TableCell>状态</TableCell>
|
<TableCell>状态</TableCell>
|
||||||
<TableCell>阶段</TableCell>
|
<TableCell>阶段</TableCell>
|
||||||
<TableCell>操作人</TableCell>
|
|
||||||
<TableCell>错误</TableCell>
|
<TableCell>错误</TableCell>
|
||||||
<TableCell align="right">动作</TableCell>
|
<TableCell align="right">动作</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
@ -195,7 +232,6 @@ export default function DeploymentsPage() {
|
|||||||
))}
|
))}
|
||||||
</Stack>
|
</Stack>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>{run.operator}</TableCell>
|
|
||||||
<TableCell sx={{ maxWidth: 320 }}>
|
<TableCell sx={{ maxWidth: 320 }}>
|
||||||
<Typography variant="body2" color="text.secondary">
|
<Typography variant="body2" color="text.secondary">
|
||||||
{run.error_message || '-'}
|
{run.error_message || '-'}
|
||||||
@ -228,7 +264,7 @@ export default function DeploymentsPage() {
|
|||||||
<TableCell>版本</TableCell>
|
<TableCell>版本</TableCell>
|
||||||
<TableCell>操作</TableCell>
|
<TableCell>操作</TableCell>
|
||||||
<TableCell>状态</TableCell>
|
<TableCell>状态</TableCell>
|
||||||
<TableCell>操作人</TableCell>
|
<TableCell>错误</TableCell>
|
||||||
<TableCell align="right">动作</TableCell>
|
<TableCell align="right">动作</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHead>
|
</TableHead>
|
||||||
@ -242,11 +278,20 @@ export default function DeploymentsPage() {
|
|||||||
<TableCell>
|
<TableCell>
|
||||||
<StatusChip status={row.status} />
|
<StatusChip status={row.status} />
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>{row.operator}</TableCell>
|
<TableCell sx={{ maxWidth: 360 }}>
|
||||||
|
<Typography variant="body2" color="text.secondary">
|
||||||
|
{row.error_message || '-'}
|
||||||
|
</Typography>
|
||||||
|
</TableCell>
|
||||||
<TableCell align="right">
|
<TableCell align="right">
|
||||||
<Button size="small" disabled={!row.release_id} onClick={() => void rollback(row)}>
|
<Stack direction="row" spacing={1} justifyContent="flex-end">
|
||||||
回滚到此版本
|
<Button size="small" onClick={() => openDeploymentDetail(row)}>
|
||||||
</Button>
|
查看详情
|
||||||
|
</Button>
|
||||||
|
<Button size="small" disabled={!row.release_id} onClick={() => void rollback(row)}>
|
||||||
|
回滚到此版本
|
||||||
|
</Button>
|
||||||
|
</Stack>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
))}
|
))}
|
||||||
@ -266,11 +311,6 @@ export default function DeploymentsPage() {
|
|||||||
value={releaseRunForm.release_id}
|
value={releaseRunForm.release_id}
|
||||||
onChange={(e) => setReleaseRunForm((prev) => ({ ...prev, release_id: e.target.value }))}
|
onChange={(e) => setReleaseRunForm((prev) => ({ ...prev, release_id: e.target.value }))}
|
||||||
/>
|
/>
|
||||||
<TextField
|
|
||||||
label="操作人"
|
|
||||||
value={releaseRunForm.operator}
|
|
||||||
onChange={(e) => setReleaseRunForm((prev) => ({ ...prev, operator: e.target.value }))}
|
|
||||||
/>
|
|
||||||
</Stack>
|
</Stack>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
<DialogActions>
|
<DialogActions>
|
||||||
@ -310,11 +350,6 @@ export default function DeploymentsPage() {
|
|||||||
value={deploymentForm.release_id}
|
value={deploymentForm.release_id}
|
||||||
onChange={(e) => setDeploymentForm((prev) => ({ ...prev, release_id: e.target.value }))}
|
onChange={(e) => setDeploymentForm((prev) => ({ ...prev, release_id: e.target.value }))}
|
||||||
/>
|
/>
|
||||||
<TextField
|
|
||||||
label="操作人"
|
|
||||||
value={deploymentForm.operator}
|
|
||||||
onChange={(e) => setDeploymentForm((prev) => ({ ...prev, operator: e.target.value }))}
|
|
||||||
/>
|
|
||||||
</Stack>
|
</Stack>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
<DialogActions>
|
<DialogActions>
|
||||||
@ -325,6 +360,121 @@ export default function DeploymentsPage() {
|
|||||||
</DialogActions>
|
</DialogActions>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
|
<Dialog open={detailOpen} onClose={() => setDetailOpen(false)} fullWidth maxWidth="lg">
|
||||||
|
<DialogTitle>部署详情</DialogTitle>
|
||||||
|
<DialogContent dividers>
|
||||||
|
{detailLoading && !selectedDeployment ? (
|
||||||
|
<Stack alignItems="center" justifyContent="center" sx={{ py: 8 }}>
|
||||||
|
<CircularProgress size={28} />
|
||||||
|
</Stack>
|
||||||
|
) : selectedDeployment ? (
|
||||||
|
<Stack spacing={2}>
|
||||||
|
<Stack direction={{ xs: 'column', lg: 'row' }} spacing={1.5} justifyContent="space-between">
|
||||||
|
<Stack spacing={1}>
|
||||||
|
<Typography variant="h6">{`${selectedDeployment.service_name} · ${selectedDeployment.release_id || '-'}`}</Typography>
|
||||||
|
<Stack direction="row" spacing={1} useFlexGap flexWrap="wrap">
|
||||||
|
<Chip size="small" variant="outlined" label={`任务 #${selectedDeployment.id}`} />
|
||||||
|
<Chip size="small" variant="outlined" label={`操作 ${selectedDeployment.operation}`} />
|
||||||
|
<Chip
|
||||||
|
size="small"
|
||||||
|
variant="outlined"
|
||||||
|
label={`开始 ${formatDateTime(selectedDeployment.started_at || selectedDeployment.created_at)}`}
|
||||||
|
/>
|
||||||
|
<Chip size="small" variant="outlined" label={`结束 ${formatDateTime(selectedDeployment.finished_at)}`} />
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
<Stack direction="row" spacing={1} alignItems="center">
|
||||||
|
<StatusChip status={selectedDeployment.status} />
|
||||||
|
<Button
|
||||||
|
variant="outlined"
|
||||||
|
onClick={() => selectedDeploymentID && void loadDeploymentDetail(selectedDeploymentID, true)}
|
||||||
|
>
|
||||||
|
立即刷新
|
||||||
|
</Button>
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
{activeDeploymentStatuses.has(selectedDeployment.status) ? (
|
||||||
|
<Alert severity="info" variant="outlined">
|
||||||
|
当前任务仍在执行,目标机状态和日志需要手动刷新查看最新结果。
|
||||||
|
</Alert>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{selectedDeployment.error_message ? (
|
||||||
|
<Alert severity="error" variant="filled">
|
||||||
|
{selectedDeployment.error_message}
|
||||||
|
</Alert>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{(selectedDeployment.targets ?? []).length === 0 ? (
|
||||||
|
<Alert severity="info" variant="outlined">
|
||||||
|
当前任务还没有目标机执行记录。
|
||||||
|
</Alert>
|
||||||
|
) : (
|
||||||
|
<Stack spacing={1.5}>
|
||||||
|
{(selectedDeployment.targets ?? []).map((target) => (
|
||||||
|
<Box
|
||||||
|
key={target.id}
|
||||||
|
sx={{
|
||||||
|
border: '1px solid rgba(53,80,161,0.1)',
|
||||||
|
borderRadius: 3,
|
||||||
|
overflow: 'hidden',
|
||||||
|
backgroundColor: 'rgba(53,80,161,0.02)'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Stack
|
||||||
|
direction={{ xs: 'column', sm: 'row' }}
|
||||||
|
spacing={1}
|
||||||
|
justifyContent="space-between"
|
||||||
|
alignItems={{ sm: 'center' }}
|
||||||
|
sx={{ px: 2, py: 1.5, borderBottom: '1px solid rgba(53,80,161,0.08)' }}
|
||||||
|
>
|
||||||
|
<Stack spacing={0.5}>
|
||||||
|
<Typography variant="subtitle2">{targetLabel(target)}</Typography>
|
||||||
|
<Typography variant="body2" color="text.secondary">
|
||||||
|
{`开始 ${formatDateTime(target.started_at)} · 结束 ${formatDateTime(target.finished_at)}`}
|
||||||
|
</Typography>
|
||||||
|
</Stack>
|
||||||
|
<Stack direction="row" spacing={1} alignItems="center">
|
||||||
|
<Chip size="small" variant="outlined" label={`步骤 ${target.step || '-'}`} />
|
||||||
|
<StatusChip status={target.status} />
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
<Box
|
||||||
|
component="pre"
|
||||||
|
sx={{
|
||||||
|
m: 0,
|
||||||
|
px: 2,
|
||||||
|
py: 1.5,
|
||||||
|
maxHeight: 320,
|
||||||
|
overflow: 'auto',
|
||||||
|
whiteSpace: 'pre-wrap',
|
||||||
|
wordBreak: 'break-word',
|
||||||
|
fontSize: 13,
|
||||||
|
lineHeight: 1.7,
|
||||||
|
fontFamily: '"SFMono-Regular", "Roboto Mono", "Menlo", monospace',
|
||||||
|
backgroundColor: 'rgba(16,24,40,0.98)',
|
||||||
|
color: 'rgb(231, 239, 255)'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{target.log_excerpt || '无日志输出'}
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
))}
|
||||||
|
</Stack>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
) : (
|
||||||
|
<Alert severity="warning" variant="outlined">
|
||||||
|
未找到该部署任务。
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<Button onClick={() => setDetailOpen(false)}>关闭</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
<Snackbar open={Boolean(message)} autoHideDuration={3000} onClose={() => setMessage('')}>
|
<Snackbar open={Boolean(message)} autoHideDuration={3000} onClose={() => setMessage('')}>
|
||||||
<Alert severity="success" variant="filled">
|
<Alert severity="success" variant="filled">
|
||||||
{message}
|
{message}
|
||||||
|
|||||||
@ -637,7 +637,6 @@ export default function ReleaseRunDetailPage() {
|
|||||||
</Box>
|
</Box>
|
||||||
<Stack direction="row" spacing={1} useFlexGap flexWrap="wrap" alignItems="flex-start">
|
<Stack direction="row" spacing={1} useFlexGap flexWrap="wrap" alignItems="flex-start">
|
||||||
<StatusChip status={run.status} />
|
<StatusChip status={run.status} />
|
||||||
<Chip variant="outlined" label={`操作人 ${run.operator || '-'}`} />
|
|
||||||
<Chip variant="outlined" label={`成功阶段 ${summary.succeeded}`} />
|
<Chip variant="outlined" label={`成功阶段 ${summary.succeeded}`} />
|
||||||
<Chip variant="outlined" label={`失败阶段 ${summary.failed}`} />
|
<Chip variant="outlined" label={`失败阶段 ${summary.failed}`} />
|
||||||
<Chip variant="outlined" label={`已回滚 ${summary.rollbacked}`} />
|
<Chip variant="outlined" label={`已回滚 ${summary.rollbacked}`} />
|
||||||
|
|||||||
@ -73,6 +73,8 @@ export interface DeploymentTarget {
|
|||||||
status: string
|
status: string
|
||||||
step: string
|
step: string
|
||||||
log_excerpt: string
|
log_excerpt: string
|
||||||
|
started_at?: string
|
||||||
|
finished_at?: string
|
||||||
host?: Host
|
host?: Host
|
||||||
service_instance?: ServiceInstance
|
service_instance?: ServiceInstance
|
||||||
}
|
}
|
||||||
@ -87,6 +89,7 @@ export interface Deployment {
|
|||||||
trigger_source: string
|
trigger_source: string
|
||||||
error_message: string
|
error_message: string
|
||||||
created_at?: string
|
created_at?: string
|
||||||
|
started_at?: string
|
||||||
finished_at?: string
|
finished_at?: string
|
||||||
targets?: DeploymentTarget[]
|
targets?: DeploymentTarget[]
|
||||||
}
|
}
|
||||||
@ -98,11 +101,11 @@ export interface ReleaseRunStep {
|
|||||||
service_name: string
|
service_name: string
|
||||||
previous_release_id: string
|
previous_release_id: string
|
||||||
status: string
|
status: string
|
||||||
deployment_id: number
|
deployment_id: number | null
|
||||||
error_message: string
|
error_message: string
|
||||||
log_excerpt: string
|
log_excerpt: string
|
||||||
rollback_status: string
|
rollback_status: string
|
||||||
rollback_deployment_id: number
|
rollback_deployment_id: number | null
|
||||||
rollback_error_message: string
|
rollback_error_message: string
|
||||||
rollback_log_excerpt: string
|
rollback_log_excerpt: string
|
||||||
created_at?: string
|
created_at?: string
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user