Compare commits

..
1 Commits
Author SHA1 Message Date
Denozordec 95d4305241 feat(web): show job trigger and module details in jobs list
quality / commitlint (push) Skipped
quality / changes (push) Successful in 11s
quality / docker-check (push) Skipped
quality / openapi (push) Successful in 1m3s
quality / web (push) Successful in 1m58s
quality / go (push) Successful in 3m21s
quality / bird2 (push) Successful in 19s
CD / quality (push) Successful in 7m6s
CD / publish (push) Successful in 9m18s
Колонка «Вид» в журнале задач (Операции → Задачи и Расписание) стала информативнее: подзаголовок с именами обновляемых модулей для tenant_refresh (из meta.module_ids), сообщение apply_summary для deploy_apply и бейдж триггера («по расписанию», «вручную», «после правки данных»). Поиск по журналу учитывает новые поля.

Бэкенд: meta.trigger теперь записывается для module_refresh и deploy_apply при ручных запусках (POST /v1/modules/{id}/refresh, /v1/apply, /v1/speakers/{id}/apply), а deploy_apply из цепочки обновления наследует триггер родительской задачи.
2026-09-25 16:16:53 +07:00
7 changed files with 119 additions and 28 deletions
@@ -4,6 +4,7 @@ import { ListTodo, SearchIcon } from 'lucide-react'
import { toast } from 'sonner' import { toast } from 'sonner'
import { Button } from '@evobgp/ui/components/button' import { Button } from '@evobgp/ui/components/button'
import { Badge } from '@/components/reui/badge'
import { DataGridMutedCell, DataGridNameCell } from '@/components/data-grid-cell' import { DataGridMutedCell, DataGridNameCell } from '@/components/data-grid-cell'
import { StatusBadge } from '@/components/status-badge' import { StatusBadge } from '@/components/status-badge'
import { DataGridColumnHeader } from '@/components/reui/data-grid/data-grid-column-header' import { DataGridColumnHeader } from '@/components/reui/data-grid/data-grid-column-header'
@@ -14,7 +15,7 @@ import {
type DataGridColumnDef, type DataGridColumnDef,
} from '@/components/reui-kit' } from '@/components/reui-kit'
import { apiMutate } from '@/lib/api-client' import { apiMutate } from '@/lib/api-client'
import { jobKindRu } from '@/lib/ui-labels' import { jobKindRu, jobSubtitleRu, jobTriggerRu } from '@/lib/ui-labels'
import type { JobRow } from '@/types/api' import type { JobRow } from '@/types/api'
import type { QueryClient } from '@tanstack/react-query' import type { QueryClient } from '@tanstack/react-query'
@@ -71,18 +72,25 @@ export function OperationsJobsGrid({
{ {
accessorKey: 'kind', accessorKey: 'kind',
header: ({ column }) => <DataGridColumnHeader column={column} title="Вид" />, header: ({ column }) => <DataGridColumnHeader column={column} title="Вид" />,
cell: ({ row }) => ( cell: ({ row }) => {
<DataGridNameCell const trigger = jobTriggerRu(row.original.meta?.trigger)
icon={ListTodo} return (
title={jobKindRu(row.original.kind)} <DataGridNameCell
subtitle={ icon={ListTodo}
row.original.meta?.module_id title={
? (nameById.get(String(row.original.meta.module_id)) ?? <span className="flex min-w-0 items-center gap-1.5">
String(row.original.meta.module_id)) <span className="truncate">{jobKindRu(row.original.kind)}</span>
: undefined {trigger ? (
} <Badge variant="outline" size="sm" className="shrink-0 font-normal">
/> {trigger}
), </Badge>
) : null}
</span>
}
subtitle={jobSubtitleRu(row.original.kind, row.original.meta, nameById)}
/>
)
},
meta: { headerTitle: 'Вид' }, meta: { headerTitle: 'Вид' },
}, },
{ {
@@ -156,10 +164,9 @@ export function OperationsJobsGrid({
onClearFilters={() => setFilterQuery(createTextFilterQuery('search'))} onClearFilters={() => setFilterQuery(createTextFilterQuery('search'))}
getFilterFieldValue={(item, field) => { getFilterFieldValue={(item, field) => {
if (field !== 'search') return undefined if (field !== 'search') return undefined
const moduleName = item.meta?.module_id const subtitle = jobSubtitleRu(item.kind, item.meta, nameById)
? (nameById.get(String(item.meta.module_id)) ?? String(item.meta.module_id)) const trigger = jobTriggerRu(item.meta?.trigger)
: '' return `${jobKindRu(item.kind)} ${item.status} ${item.job_id} ${subtitle ?? ''} ${trigger ?? ''}`
return `${jobKindRu(item.kind)} ${item.status} ${item.job_id} ${moduleName}`
}} }}
columns={columns} columns={columns}
data={items} data={items}
@@ -6,12 +6,14 @@ import { ScheduleJobsGrid } from './schedule-jobs-grid'
/** Jobs ResourcePage with status tabs (data-grid-filtering-2). */ /** Jobs ResourcePage with status tabs (data-grid-filtering-2). */
export function ScheduleJobsCard({ export function ScheduleJobsCard({
jobs, jobs,
nameById,
isLoading, isLoading,
isError, isError,
error, error,
onRetry, onRetry,
}: { }: {
jobs: JobRow[] jobs: JobRow[]
nameById?: Map<string, string>
isLoading: boolean isLoading: boolean
isError: boolean isError: boolean
error: unknown error: unknown
@@ -27,7 +29,11 @@ export function ScheduleJobsCard({
onRetry={onRetry} onRetry={onRetry}
> >
{(items) => ( {(items) => (
<ScheduleJobsGrid items={items} isLoading={isLoading && items.length > 0} /> <ScheduleJobsGrid
items={items}
nameById={nameById}
isLoading={isLoading && items.length > 0}
/>
)} )}
</QueryState> </QueryState>
) )
@@ -10,7 +10,8 @@ import {
createTextFilterQuery, createTextFilterQuery,
type DataGridColumnDef, type DataGridColumnDef,
} from '@/components/reui-kit' } from '@/components/reui-kit'
import { isRefreshJobKind, jobKindRu } from '@/lib/ui-labels' import { Badge } from '@/components/reui/badge'
import { isRefreshJobKind, jobKindRu, jobSubtitleRu, jobTriggerRu } from '@/lib/ui-labels'
import type { JobRow } from '@/types/api' import type { JobRow } from '@/types/api'
const FAILED_STATUSES = ['failed', 'error', 'cancelled'] const FAILED_STATUSES = ['failed', 'error', 'cancelled']
@@ -33,9 +34,11 @@ function tabFilter(item: JobRow, tabId: string): boolean {
export function ScheduleJobsGrid({ export function ScheduleJobsGrid({
items, items,
nameById = new Map<string, string>(),
isLoading = false, isLoading = false,
}: { }: {
items: JobRow[] items: JobRow[]
nameById?: Map<string, string>
isLoading?: boolean isLoading?: boolean
}) { }) {
const [filterQuery, setFilterQuery] = useState<FilterQuery>(() => const [filterQuery, setFilterQuery] = useState<FilterQuery>(() =>
@@ -64,9 +67,25 @@ export function ScheduleJobsGrid({
{ {
accessorKey: 'kind', accessorKey: 'kind',
header: ({ column }) => <DataGridColumnHeader column={column} title="Вид" />, header: ({ column }) => <DataGridColumnHeader column={column} title="Вид" />,
cell: ({ row }) => ( cell: ({ row }) => {
<DataGridNameCell icon={ListTodo} title={jobKindRu(row.original.kind)} /> const trigger = jobTriggerRu(row.original.meta?.trigger)
), return (
<DataGridNameCell
icon={ListTodo}
title={
<span className="flex min-w-0 items-center gap-1.5">
<span className="truncate">{jobKindRu(row.original.kind)}</span>
{trigger ? (
<Badge variant="outline" size="sm" className="shrink-0 font-normal">
{trigger}
</Badge>
) : null}
</span>
}
subtitle={jobSubtitleRu(row.original.kind, row.original.meta, nameById)}
/>
)
},
meta: { headerTitle: 'Вид' }, meta: { headerTitle: 'Вид' },
}, },
{ {
@@ -119,7 +138,7 @@ export function ScheduleJobsGrid({
meta: { headerTitle: 'Ошибка' }, meta: { headerTitle: 'Ошибка' },
}, },
], ],
[], [nameById],
) )
return ( return (
@@ -134,7 +153,9 @@ export function ScheduleJobsGrid({
onClearFilters={() => setFilterQuery(createTextFilterQuery('search'))} onClearFilters={() => setFilterQuery(createTextFilterQuery('search'))}
getFilterFieldValue={(item, field) => { getFilterFieldValue={(item, field) => {
if (field !== 'search') return undefined if (field !== 'search') return undefined
return `${jobKindRu(item.kind)} ${item.status} ${item.error ?? ''} ${item.job_id}` const subtitle = jobSubtitleRu(item.kind, item.meta, nameById)
const trigger = jobTriggerRu(item.meta?.trigger)
return `${jobKindRu(item.kind)} ${item.status} ${item.error ?? ''} ${item.job_id} ${subtitle ?? ''} ${trigger ?? ''}`
}} }}
columns={columns} columns={columns}
data={items} data={items}
+48
View File
@@ -58,6 +58,54 @@ export function jobKindRu(kind: string): string {
return JOB_KIND_RU[kind] ?? kind return JOB_KIND_RU[kind] ?? kind
} }
const JOB_TRIGGER_RU: Record<string, string> = {
scheduler: 'по расписанию',
api: 'вручную',
peer_patch: 'правка пиров',
}
export function jobTriggerRu(trigger: unknown): string | undefined {
if (typeof trigger !== 'string' || trigger === '') return undefined
const known = JOB_TRIGGER_RU[trigger]
if (known) return known
if (/_(create|patch|delete|import_csv)$/.test(trigger)) return 'после правки данных'
return undefined
}
export function jobSubtitleRu(
kind: string,
meta: Record<string, unknown> | undefined,
nameById: Map<string, string>,
): string | undefined {
if (!meta) return undefined
switch (kind) {
case 'tenant_refresh': {
const ids = Array.isArray(meta.module_ids)
? meta.module_ids.map(String).filter((id) => id !== '')
: []
if (ids.length === 0) return undefined
const names = ids.map((id) => nameById.get(id) ?? id)
if (names.length <= 3) return names.join(', ')
return `${names.slice(0, 2).join(', ')} +${names.length - 2} ещё`
}
case 'module_refresh': {
const id = typeof meta.module_id === 'string' ? meta.module_id : ''
if (id === '') return undefined
return nameById.get(id) ?? id
}
case 'deploy_apply': {
const summary = meta.apply_summary
if (summary && typeof summary === 'object') {
const message = (summary as Record<string, unknown>).message
if (typeof message === 'string' && message !== '') return message
}
return undefined
}
default:
return undefined
}
}
const JOB_STATUS_RU: Record<string, string> = { const JOB_STATUS_RU: Record<string, string> = {
queued: 'В очереди', queued: 'В очереди',
running: 'Выполняется', running: 'Выполняется',
+2
View File
@@ -11,6 +11,7 @@ import { KpiStatGrid, type KpiStatItem } from '@/components/reui-kit'
import { ScheduleJobsCard } from '@/components/schedule/schedule-jobs-card' import { ScheduleJobsCard } from '@/components/schedule/schedule-jobs-card'
import { ScheduleModulesGrid } from '@/components/schedule/schedule-modules-grid' import { ScheduleModulesGrid } from '@/components/schedule/schedule-modules-grid'
import { apiMutate } from '@/lib/api-client' import { apiMutate } from '@/lib/api-client'
import { moduleNameById } from '@/queries/overview'
import { modulesListQueryOptions } from '@/queries/modules' import { modulesListQueryOptions } from '@/queries/modules'
import { operationsJobsQueryOptions } from '@/queries/operations' import { operationsJobsQueryOptions } from '@/queries/operations'
@@ -117,6 +118,7 @@ function ScheduleComponent() {
<ScheduleJobsCard <ScheduleJobsCard
jobs={jobs} jobs={jobs}
nameById={moduleNameById(modules)}
isLoading={jobsQ.isLoading} isLoading={jobsQ.isLoading}
isError={jobsQ.isError} isError={jobsQ.isError}
error={jobsQ.error} error={jobsQ.error}
+6 -1
View File
@@ -448,7 +448,10 @@ func (s *Server) handleModuleRefresh(w http.ResponseWriter, r *http.Request) {
idemPtr = &idem idemPtr = &idem
} }
mid := mod.ID mid := mod.ID
j, _, err := s.jobs.Enqueue(a.TenantID, jobs.KindModuleRefresh, idemPtr, &mid, map[string]any{"module_id": moduleID}) j, _, err := s.jobs.Enqueue(a.TenantID, jobs.KindModuleRefresh, idemPtr, &mid, map[string]any{
"module_id": moduleID,
"trigger": "api",
})
if err != nil { if err != nil {
writeInternalError(w, "internal", err) writeInternalError(w, "internal", err)
return return
@@ -751,6 +754,7 @@ func (s *Server) handleApply(w http.ResponseWriter, r *http.Request) {
j, _, err := s.jobs.Enqueue(a.TenantID, jobs.KindDeployApply, idemPtr, nil, map[string]any{ j, _, err := s.jobs.Enqueue(a.TenantID, jobs.KindDeployApply, idemPtr, nil, map[string]any{
"revision_id": revID, "revision_id": revID,
"strategy": body.Strategy, "strategy": body.Strategy,
"trigger": "api",
}) })
if err != nil { if err != nil {
writeInternalError(w, "internal", err) writeInternalError(w, "internal", err)
@@ -801,6 +805,7 @@ func (s *Server) handleSpeakerApply(w http.ResponseWriter, r *http.Request) {
j, _, err := s.jobs.Enqueue(a.TenantID, jobs.KindDeployApply, idemPtr, nil, map[string]any{ j, _, err := s.jobs.Enqueue(a.TenantID, jobs.KindDeployApply, idemPtr, nil, map[string]any{
"revision_id": revID, "revision_id": revID,
"speaker_id": spkID, "speaker_id": spkID,
"trigger": "api",
}) })
if err != nil { if err != nil {
writeInternalError(w, "internal", err) writeInternalError(w, "internal", err)
+5 -3
View File
@@ -436,9 +436,11 @@ func (w *Worker) enqueueDeployAllSpeakers(j *Job, tenantID, revID string) {
if revID == "" { if revID == "" {
return return
} }
applyJob, _, err := w.Registry.Enqueue(tenantID, KindDeployApply, nil, nil, map[string]any{ meta := map[string]any{"revision_id": revID}
"revision_id": revID, if tr := strings.TrimSpace(j.metaString("trigger")); tr != "" {
}) meta["trigger"] = tr
}
applyJob, _, err := w.Registry.Enqueue(tenantID, KindDeployApply, nil, nil, meta)
if err != nil { if err != nil {
j.mergeMeta(map[string]any{"deploy_apply_enqueue_error": err.Error()}) j.mergeMeta(map[string]any{"deploy_apply_enqueue_error": err.Error()})
return return