diff --git a/public/app/features/provisioning/Job/RecentJobs.tsx b/public/app/features/provisioning/Job/RecentJobs.tsx index 660851f7003..b1491ea224a 100644 --- a/public/app/features/provisioning/Job/RecentJobs.tsx +++ b/public/app/features/provisioning/Job/RecentJobs.tsx @@ -1,13 +1,14 @@ -import { useMemo } from 'react'; +import { useMemo, useRef } from 'react'; import { intervalToAbbreviatedDurationString, TraceKeyValuePair } from '@grafana/data'; import { t, Trans } from '@grafana/i18n'; -import { Alert, Badge, Box, Card, InteractiveTable, Spinner, Stack, Text } from '@grafana/ui'; +import { Badge, Box, Card, InteractiveTable, Spinner, Stack, Text } from '@grafana/ui'; import { Job, Repository } from 'app/api/clients/provisioning/v0alpha1'; import KeyValuesTable from 'app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/KeyValuesTable'; import { ProvisioningAlert } from '../Shared/ProvisioningAlert'; import { useRepositoryAllJobs } from '../hooks/useRepositoryAllJobs'; +import { getErrorMessage } from '../utils/httpUtils'; import { getStatusColor } from '../utils/repositoryStatus'; import { formatTimestamp } from '../utils/time'; @@ -23,6 +24,21 @@ type JobCell = { }; }; +function formatJobDuration(job: Job): string | null { + const interval = { + start: job.status?.started ?? 0, + end: job.status?.finished ?? Date.now(), + }; + if (!interval.start) { + return null; + } + const elapsed = interval.end - interval.start; + if (elapsed < 1000) { + return `${elapsed}ms`; + } + return intervalToAbbreviatedDurationString(interval, true); +} + const getJobColumns = () => [ { id: 'jobId', @@ -53,20 +69,7 @@ const getJobColumns = () => [ { id: 'duration', header: t('provisioning.recent-jobs.column-duration', 'Duration'), - cell: ({ row: { original: job } }: JobCell) => { - const interval = { - start: job.status?.started ?? 0, - end: job.status?.finished ?? Date.now(), - }; - if (!interval.start) { - return null; - } - const elapsed = interval.end - interval.start; - if (elapsed < 1000) { - return `${elapsed}ms`; - } - return intervalToAbbreviatedDurationString(interval, true); - }, + cell: ({ row: { original: job } }: JobCell) => formatJobDuration(job), }, { id: 'message', @@ -105,6 +108,14 @@ function ExpandedRow({ row }: ExpandedRowProps) { return null; } + const state = row.status?.state; + const isValidState = state && ['success', 'warning', 'error'].includes(state); + const alertProps = isValidState + ? { + [state]: { message: row.status?.errors }, + } + : null; + return ( @@ -116,13 +127,13 @@ function ExpandedRow({ row }: ExpandedRowProps) { )} - {hasErrors && } - {hasSummary && ( + {alertProps && } + {hasSummary && row.status?.summary && ( Summary - + )} @@ -140,43 +151,55 @@ function EmptyState() { ); } -function ErrorLoading(typ: string, error: string) { - return ( - -
{JSON.stringify(error)}
-
- ); -} - -function Loading() { - return ( - - - - ); -} - export function RecentJobs({ repo }: Props) { - // TODO: Decide on whether we want to wait on historic jobs to show the current ones. - // Gut feeling is that current jobs are far more important to show than historic ones. const [jobs, activeQuery, historicQuery] = useRepositoryAllJobs({ repositoryName: repo.metadata?.name ?? 'x', }); const jobColumns = useMemo(() => getJobColumns(), []); + const hasLoadedDataRef = useRef(false); - let description: JSX.Element; - if (activeQuery.isLoading || historicQuery.isLoading) { - description = Loading(); - } else if (activeQuery.isError) { - description = ErrorLoading(t('provisioning.recent-jobs.active-jobs', 'active jobs'), activeQuery.error); - // TODO: Figure out what to do if historic fails. Maybe a separate card? - } else if (!jobs?.length) { - description = ; - } else { - description = ( + if (activeQuery.data || historicQuery.data) { + hasLoadedDataRef.current = true; + } + + const renderContent = () => { + const isInitialLoading = !hasLoadedDataRef.current && (activeQuery.isLoading || historicQuery.isLoading); + + if (isInitialLoading) { + return ( + + + + ); + } + + if (activeQuery.isError) { + return ( + + ); + } + + if (historicQuery.isError) { + return ( + + ); + } + + if (!jobs?.length) { + return ; + } + + return ( ); - } + }; return ( Jobs - {description} + {renderContent()} ); } diff --git a/public/app/features/provisioning/utils/repositoryStatus.ts b/public/app/features/provisioning/utils/repositoryStatus.ts index 9d899588506..f06ca35f182 100644 --- a/public/app/features/provisioning/utils/repositoryStatus.ts +++ b/public/app/features/provisioning/utils/repositoryStatus.ts @@ -29,9 +29,9 @@ export const getStatusIcon = (state?: SyncStatus['state']): IconName => { switch (state) { case 'success': return 'check'; - case 'working': case 'warning': return 'exclamation-triangle'; + case 'working': case 'pending': return 'spinner'; case 'error': diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index 140f63f511e..554150ef77d 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -11729,14 +11729,14 @@ "read-only-local-tooltip": "This resource is read-only and provisioned through file provisioning. To make any changes, update the connected repository. To modify the settings go to Administration > Provisioning > Repositories.", "read-only-remote-tooltip": "This resource is read-only and provisioned through Git. To make any changes, update the connected repository. To modify the settings go to Administration > Provisioning > Repositories.", "recent-jobs": { - "active-jobs": "active jobs", "column-action": "Action", "column-duration": "Duration", "column-job-id": "Job ID", "column-message": "Message", "column-started": "Started", "column-status": "Status", - "error-loading": "Error loading {{type}}", + "error-loading-active-jobs": "Error loading active jobs", + "error-loading-historic-jobs": "Error loading historic jobs", "jobs": "Jobs" }, "repository-actions": {