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 { Button } from '@evobgp/ui/components/button'
import { Badge } from '@/components/reui/badge'
import { DataGridMutedCell, DataGridNameCell } from '@/components/data-grid-cell'
import { StatusBadge } from '@/components/status-badge'
import { DataGridColumnHeader } from '@/components/reui/data-grid/data-grid-column-header'
@@ -14,7 +15,7 @@ import {
type DataGridColumnDef,
} from '@/components/reui-kit'
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 { QueryClient } from '@tanstack/react-query'
@@ -71,18 +72,25 @@ export function OperationsJobsGrid({
{
accessorKey: 'kind',
header: ({ column }) => <DataGridColumnHeader column={column} title="Вид" />,
cell: ({ row }) => (
<DataGridNameCell
icon={ListTodo}
title={jobKindRu(row.original.kind)}
subtitle={
row.original.meta?.module_id
? (nameById.get(String(row.original.meta.module_id)) ??
String(row.original.meta.module_id))
: undefined
}
/>
),
cell: ({ row }) => {
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: 'Вид' },
},
{
@@ -156,10 +164,9 @@ export function OperationsJobsGrid({
onClearFilters={() => setFilterQuery(createTextFilterQuery('search'))}
getFilterFieldValue={(item, field) => {
if (field !== 'search') return undefined
const moduleName = item.meta?.module_id
? (nameById.get(String(item.meta.module_id)) ?? String(item.meta.module_id))
: ''
return `${jobKindRu(item.kind)} ${item.status} ${item.job_id} ${moduleName}`
const subtitle = jobSubtitleRu(item.kind, item.meta, nameById)
const trigger = jobTriggerRu(item.meta?.trigger)
return `${jobKindRu(item.kind)} ${item.status} ${item.job_id} ${subtitle ?? ''} ${trigger ?? ''}`
}}
columns={columns}
data={items}
@@ -6,12 +6,14 @@ import { ScheduleJobsGrid } from './schedule-jobs-grid'
/** Jobs ResourcePage with status tabs (data-grid-filtering-2). */
export function ScheduleJobsCard({
jobs,
nameById,
isLoading,
isError,
error,
onRetry,
}: {
jobs: JobRow[]
nameById?: Map<string, string>
isLoading: boolean
isError: boolean
error: unknown
@@ -27,7 +29,11 @@ export function ScheduleJobsCard({
onRetry={onRetry}
>
{(items) => (
<ScheduleJobsGrid items={items} isLoading={isLoading && items.length > 0} />
<ScheduleJobsGrid
items={items}
nameById={nameById}
isLoading={isLoading && items.length > 0}
/>
)}
</QueryState>
)
@@ -10,7 +10,8 @@ import {
createTextFilterQuery,
type DataGridColumnDef,
} 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'
const FAILED_STATUSES = ['failed', 'error', 'cancelled']
@@ -33,9 +34,11 @@ function tabFilter(item: JobRow, tabId: string): boolean {
export function ScheduleJobsGrid({
items,
nameById = new Map<string, string>(),
isLoading = false,
}: {
items: JobRow[]
nameById?: Map<string, string>
isLoading?: boolean
}) {
const [filterQuery, setFilterQuery] = useState<FilterQuery>(() =>
@@ -64,9 +67,25 @@ export function ScheduleJobsGrid({
{
accessorKey: 'kind',
header: ({ column }) => <DataGridColumnHeader column={column} title="Вид" />,
cell: ({ row }) => (
<DataGridNameCell icon={ListTodo} title={jobKindRu(row.original.kind)} />
),
cell: ({ row }) => {
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: 'Вид' },
},
{
@@ -119,7 +138,7 @@ export function ScheduleJobsGrid({
meta: { headerTitle: 'Ошибка' },
},
],
[],
[nameById],
)
return (
@@ -134,7 +153,9 @@ export function ScheduleJobsGrid({
onClearFilters={() => setFilterQuery(createTextFilterQuery('search'))}
getFilterFieldValue={(item, field) => {
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}
data={items}
+48
View File
@@ -58,6 +58,54 @@ export function jobKindRu(kind: string): string {
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> = {
queued: 'В очереди',
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 { ScheduleModulesGrid } from '@/components/schedule/schedule-modules-grid'
import { apiMutate } from '@/lib/api-client'
import { moduleNameById } from '@/queries/overview'
import { modulesListQueryOptions } from '@/queries/modules'
import { operationsJobsQueryOptions } from '@/queries/operations'
@@ -117,6 +118,7 @@ function ScheduleComponent() {
<ScheduleJobsCard
jobs={jobs}
nameById={moduleNameById(modules)}
isLoading={jobsQ.isLoading}
isError={jobsQ.isError}
error={jobsQ.error}
+6 -1
View File
@@ -448,7 +448,10 @@ func (s *Server) handleModuleRefresh(w http.ResponseWriter, r *http.Request) {
idemPtr = &idem
}
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 {
writeInternalError(w, "internal", err)
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{
"revision_id": revID,
"strategy": body.Strategy,
"trigger": "api",
})
if err != nil {
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{
"revision_id": revID,
"speaker_id": spkID,
"trigger": "api",
})
if err != nil {
writeInternalError(w, "internal", err)
+5 -3
View File
@@ -436,9 +436,11 @@ func (w *Worker) enqueueDeployAllSpeakers(j *Job, tenantID, revID string) {
if revID == "" {
return
}
applyJob, _, err := w.Registry.Enqueue(tenantID, KindDeployApply, nil, nil, map[string]any{
"revision_id": revID,
})
meta := map[string]any{"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 {
j.mergeMeta(map[string]any{"deploy_apply_enqueue_error": err.Error()})
return