Provisioning: Split active and finished jobs (#103351)
This commit is contained in:
@@ -176,7 +176,7 @@ func (d *jobDriver) drive(ctx context.Context) error {
|
||||
}
|
||||
|
||||
// Save the finished job
|
||||
err = d.historicJobs.WriteJob(ctx, job)
|
||||
err = d.historicJobs.WriteJob(ctx, job.DeepCopy())
|
||||
if err != nil {
|
||||
// We're not going to return this as it is not critical. Not ideal, but not critical.
|
||||
logger.Warn("failed to create historic job", "historic_job", *job, "error", err)
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import { Job } from 'app/api/clients/provisioning';
|
||||
|
||||
import { JobContent } from './JobContent';
|
||||
import { useJobStatusEffect } from './hooks';
|
||||
|
||||
export interface ActiveJobProps {
|
||||
job: Job;
|
||||
onStatusChange?: (success: boolean) => void;
|
||||
onRunningChange?: (isRunning: boolean) => void;
|
||||
onErrorChange?: (error: string | null) => void;
|
||||
}
|
||||
|
||||
export function ActiveJobStatus({ job, onStatusChange, onRunningChange, onErrorChange }: ActiveJobProps) {
|
||||
useJobStatusEffect(job, onStatusChange, onRunningChange, onErrorChange);
|
||||
return <JobContent job={job} isFinishedJob={false} />;
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
import { useEffect, useRef } from 'react';
|
||||
|
||||
import { Alert, Spinner, Stack, Text } from '@grafana/ui';
|
||||
import { useGetRepositoryJobsWithPathQuery } from 'app/api/clients/provisioning';
|
||||
import { Trans, t } from 'app/core/internationalization';
|
||||
|
||||
import { JobContent } from './JobContent';
|
||||
import { useJobStatusEffect } from './hooks';
|
||||
|
||||
export interface FinishedJobProps {
|
||||
jobUid: string;
|
||||
repositoryName: string;
|
||||
onStatusChange?: (success: boolean) => void;
|
||||
onRunningChange?: (isRunning: boolean) => void;
|
||||
onErrorChange?: (error: string | null) => void;
|
||||
}
|
||||
|
||||
export function FinishedJobStatus({
|
||||
jobUid,
|
||||
repositoryName,
|
||||
onStatusChange,
|
||||
onRunningChange,
|
||||
onErrorChange,
|
||||
}: FinishedJobProps) {
|
||||
const hasRetried = useRef(false);
|
||||
const finishedQuery = useGetRepositoryJobsWithPathQuery({
|
||||
name: repositoryName,
|
||||
uid: jobUid,
|
||||
});
|
||||
const retryFailed = hasRetried.current && finishedQuery.isError;
|
||||
|
||||
const job = finishedQuery.data;
|
||||
|
||||
useJobStatusEffect(job, onStatusChange, onRunningChange, onErrorChange);
|
||||
|
||||
useEffect(() => {
|
||||
const shouldRetry = !job && !hasRetried.current && !finishedQuery.isFetching;
|
||||
let timeoutId: ReturnType<typeof setTimeout>;
|
||||
|
||||
if (shouldRetry) {
|
||||
hasRetried.current = true;
|
||||
timeoutId = setTimeout(() => {
|
||||
finishedQuery.refetch();
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (timeoutId) {
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
};
|
||||
}, [finishedQuery, job]);
|
||||
|
||||
if (retryFailed) {
|
||||
return (
|
||||
<Alert severity="error" title={t('provisioning.job-status.no-job-found', 'No job found')}>
|
||||
<Trans i18nKey="provisioning.job-status.no-job-found-message">
|
||||
The job may have been deleted or could not be retrieved. Cancel the current process and start again.
|
||||
</Trans>
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
if (!job || finishedQuery.isLoading || finishedQuery.isFetching) {
|
||||
return (
|
||||
<Stack direction="row" alignItems="center" justifyContent="center" gap={2}>
|
||||
<Spinner size={24} />
|
||||
<Text element="h4" color="secondary">
|
||||
<Trans i18nKey="provisioning.job-status.loading-finished-job">Loading finished job...</Trans>
|
||||
</Text>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
return <JobContent job={job} isFinishedJob={true} />;
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
import { Alert, ControlledCollapse, Spinner, Stack, Text } from '@grafana/ui';
|
||||
import { Job } from 'app/api/clients/provisioning';
|
||||
import { Trans, t } from 'app/core/internationalization';
|
||||
|
||||
import { RepositoryLink } from '../Repository/RepositoryLink';
|
||||
import ProgressBar from '../Shared/ProgressBar';
|
||||
|
||||
import { JobSummary } from './JobSummary';
|
||||
|
||||
export interface JobContentProps {
|
||||
job?: Job;
|
||||
isFinishedJob?: boolean;
|
||||
}
|
||||
|
||||
export function JobContent({ job, isFinishedJob = false }: JobContentProps) {
|
||||
if (!job?.status) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { state, message, progress, summary } = job.status;
|
||||
|
||||
const getStatusDisplay = () => {
|
||||
switch (state) {
|
||||
case 'success':
|
||||
return (
|
||||
<Alert
|
||||
severity="success"
|
||||
title={t('provisioning.job-status.status.title-job-completed-successfully', 'Job completed successfully')}
|
||||
/>
|
||||
);
|
||||
case 'error':
|
||||
return (
|
||||
<Alert
|
||||
severity="error"
|
||||
title={t('provisioning.job-status.status.title-error-running-job', 'Error running job')}
|
||||
>
|
||||
{message}
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Stack direction="row" alignItems="center" justifyContent="center" gap={2}>
|
||||
{['working', 'pending'].includes(state ?? '') && <Spinner size={24} />}
|
||||
<Text element="h4" color="secondary">
|
||||
{message ?? state ?? ''}
|
||||
</Text>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Stack direction="column" gap={2}>
|
||||
<Stack direction="column" gap={2}>
|
||||
{getStatusDisplay()}
|
||||
|
||||
<Stack direction="row" alignItems="center" justifyContent="center" gap={2}>
|
||||
<ProgressBar progress={progress} />
|
||||
</Stack>
|
||||
|
||||
{isFinishedJob && summary && (
|
||||
<Stack direction="column" gap={2}>
|
||||
<Text variant="h3">
|
||||
<Trans i18nKey="provisioning.job-status.summary">Summary</Trans>
|
||||
</Text>
|
||||
<JobSummary summary={summary} />
|
||||
</Stack>
|
||||
)}
|
||||
{state === 'success' ? (
|
||||
<RepositoryLink name={job.metadata?.labels?.repository} />
|
||||
) : (
|
||||
<ControlledCollapse label={t('provisioning.job-status.label-view-details', 'View details')} isOpen={false}>
|
||||
<pre>{JSON.stringify(job, null, 2)}</pre>
|
||||
</ControlledCollapse>
|
||||
)}
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
@@ -1,19 +1,9 @@
|
||||
import { skipToken } from '@reduxjs/toolkit/query';
|
||||
import { useEffect } from 'react';
|
||||
import { Spinner, Stack, Text } from '@grafana/ui';
|
||||
import { Job, useListJobQuery } from 'app/api/clients/provisioning';
|
||||
import { Trans } from 'app/core/internationalization';
|
||||
|
||||
import { Alert, ControlledCollapse, LinkButton, Spinner, Stack, Text } from '@grafana/ui';
|
||||
import {
|
||||
Job,
|
||||
useGetRepositoryJobsWithPathQuery,
|
||||
useGetRepositoryQuery,
|
||||
useListJobQuery,
|
||||
} from 'app/api/clients/provisioning';
|
||||
import { Trans, t } from 'app/core/internationalization';
|
||||
|
||||
import ProgressBar from '../Shared/ProgressBar';
|
||||
import { getRepoHref } from '../utils/git';
|
||||
|
||||
import { JobSummary } from './JobSummary';
|
||||
import { ActiveJobStatus } from './ActiveJobStatus';
|
||||
import { FinishedJobStatus } from './FinishedJobStatus';
|
||||
|
||||
export interface JobStatusProps {
|
||||
watch: Job;
|
||||
@@ -29,143 +19,51 @@ export function JobStatus({ watch, onStatusChange, onRunningChange, onErrorChang
|
||||
});
|
||||
const activeJob = activeQuery?.data?.items?.[0];
|
||||
const repoLabel = watch.metadata?.labels?.['provisioning.grafana.app/repository'];
|
||||
const finishedQuery = useGetRepositoryJobsWithPathQuery(
|
||||
activeJob || activeQuery.isUninitialized || activeQuery.isLoading || !repoLabel
|
||||
? skipToken
|
||||
: {
|
||||
name: repoLabel,
|
||||
uid: watch.metadata?.uid!,
|
||||
}
|
||||
);
|
||||
|
||||
const job = activeJob || finishedQuery.data;
|
||||
// Only initialize finished query if we've checked active jobs and found none
|
||||
const activeQueryCompleted = !activeQuery.isUninitialized && !activeQuery.isLoading;
|
||||
const shouldCheckFinishedJobs = activeQueryCompleted && !activeJob && !!repoLabel;
|
||||
|
||||
useEffect(() => {
|
||||
if (!job && !finishedQuery.isUninitialized) {
|
||||
finishedQuery.refetch();
|
||||
}
|
||||
}, [finishedQuery, job]);
|
||||
|
||||
useEffect(() => {
|
||||
if (onStatusChange && job?.status?.state === 'success') {
|
||||
onStatusChange(true);
|
||||
if (onRunningChange) {
|
||||
onRunningChange(false);
|
||||
}
|
||||
}
|
||||
if (onErrorChange && job?.status?.state === 'error') {
|
||||
onErrorChange(job.status.message ?? t('provisioning.job-status.error-unknown', 'An unknown error occurred'));
|
||||
if (onRunningChange) {
|
||||
onRunningChange(false);
|
||||
}
|
||||
}
|
||||
}, [job, onStatusChange, onErrorChange, onRunningChange]);
|
||||
|
||||
if (!job || activeQuery.isLoading) {
|
||||
if (activeQuery.isLoading) {
|
||||
return (
|
||||
<Stack direction="row" alignItems="center" justifyContent="center" gap={2}>
|
||||
<Spinner size={24} />
|
||||
<Text element="h4" weight="bold">
|
||||
<Text element="h4" color="secondary">
|
||||
<Trans i18nKey="provisioning.job-status.starting">Starting...</Trans>
|
||||
</Text>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
const status = () => {
|
||||
switch (job.status?.state) {
|
||||
case 'success':
|
||||
return (
|
||||
<Alert
|
||||
severity="success"
|
||||
title={t('provisioning.job-status.status.title-job-completed-successfully', 'Job completed successfully')}
|
||||
/>
|
||||
);
|
||||
case 'error':
|
||||
return (
|
||||
<Alert
|
||||
severity="error"
|
||||
title={t('provisioning.job-status.status.title-error-running-job', 'error running job')}
|
||||
>
|
||||
{job.status.message}
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
if (activeJob) {
|
||||
return (
|
||||
<Stack direction="row" alignItems="center" justifyContent="center" gap={2}>
|
||||
{!job.status?.progress && <Spinner size={24} />}
|
||||
<Text element="h4" color="secondary">
|
||||
{job.status?.message ?? job.status?.state ?? ''}
|
||||
</Text>
|
||||
</Stack>
|
||||
<ActiveJobStatus
|
||||
job={activeJob}
|
||||
onStatusChange={onStatusChange}
|
||||
onRunningChange={onRunningChange}
|
||||
onErrorChange={onErrorChange}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Stack direction="column" gap={2}>
|
||||
{job.status && (
|
||||
<Stack direction="column" gap={2}>
|
||||
{status()}
|
||||
|
||||
<Stack direction="row" alignItems="center" justifyContent="center" gap={2}>
|
||||
<ProgressBar progress={job.status.progress} />
|
||||
</Stack>
|
||||
|
||||
{job.status.summary && (
|
||||
<Stack direction="column" gap={2}>
|
||||
<Text variant="h3">
|
||||
<Trans i18nKey="provisioning.job-status.summary">Summary</Trans>
|
||||
</Text>
|
||||
<JobSummary summary={job.status.summary} />
|
||||
</Stack>
|
||||
)}
|
||||
{job.status.state === 'success' ? (
|
||||
<RepositoryLink name={job.metadata?.labels?.repository} />
|
||||
) : (
|
||||
<ControlledCollapse label={t('provisioning.job-status.label-view-details', 'View details')} isOpen={false}>
|
||||
<pre>{JSON.stringify(job, null, 2)}</pre>
|
||||
</ControlledCollapse>
|
||||
)}
|
||||
</Stack>
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
type RepositoryLinkProps = {
|
||||
name?: string;
|
||||
};
|
||||
|
||||
function RepositoryLink({ name }: RepositoryLinkProps) {
|
||||
const repoQuery = useGetRepositoryQuery(name ? { name } : skipToken);
|
||||
const repo = repoQuery.data;
|
||||
|
||||
if (!repo || repoQuery.isLoading || repo.spec?.type !== 'github' || !repo.spec?.github?.url) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const repoHref = getRepoHref(repo.spec?.github);
|
||||
const folderHref = repo.spec?.sync.target === 'folder' ? `/dashboards/f/${repo.metadata?.name}` : '/dashboards';
|
||||
|
||||
if (!repoHref) {
|
||||
return null;
|
||||
if (shouldCheckFinishedJobs) {
|
||||
return (
|
||||
<FinishedJobStatus
|
||||
jobUid={watch.metadata?.uid!}
|
||||
repositoryName={repoLabel}
|
||||
onStatusChange={onStatusChange}
|
||||
onRunningChange={onRunningChange}
|
||||
onErrorChange={onErrorChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Stack direction="column" gap={1}>
|
||||
<Text>
|
||||
<Trans i18nKey="provisioning.repository-link.grafana-repository">
|
||||
Grafana and your repository are now in sync.
|
||||
</Trans>
|
||||
<Stack direction="row" alignItems="center" justifyContent="center" gap={2}>
|
||||
<Spinner size={24} />
|
||||
<Text element="h4" weight="bold">
|
||||
<Trans i18nKey="provisioning.job-status.starting">Starting...</Trans>
|
||||
</Text>
|
||||
<Stack direction="row" gap={2}>
|
||||
<LinkButton fill="outline" href={repoHref} icon="external-link-alt" target="_blank" rel="noopener noreferrer">
|
||||
<Trans i18nKey="provisioning.repository-link.view-repository">View repository</Trans>
|
||||
</LinkButton>
|
||||
<LinkButton fill="outline" href={folderHref} icon="folder-open">
|
||||
<Trans i18nKey="provisioning.repository-link.view-folder">View folder</Trans>
|
||||
</LinkButton>
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import { useEffect } from 'react';
|
||||
|
||||
import { Job } from 'app/api/clients/provisioning';
|
||||
import { t } from 'app/core/internationalization';
|
||||
|
||||
// Shared hook for status change effects
|
||||
export function useJobStatusEffect(
|
||||
job?: Job,
|
||||
onStatusChange?: (success: boolean) => void,
|
||||
onRunningChange?: (isRunning: boolean) => void,
|
||||
onErrorChange?: (error: string | null) => void
|
||||
) {
|
||||
useEffect(() => {
|
||||
if (!job) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (onStatusChange && job.status?.state === 'success') {
|
||||
onStatusChange(true);
|
||||
if (onRunningChange) {
|
||||
onRunningChange(false);
|
||||
}
|
||||
}
|
||||
if (onErrorChange && job.status?.state === 'error') {
|
||||
onErrorChange(job.status.message ?? t('provisioning.job-status.error-unknown', 'An unknown error occurred'));
|
||||
if (onRunningChange) {
|
||||
onRunningChange(false);
|
||||
}
|
||||
}
|
||||
}, [job, onStatusChange, onErrorChange, onRunningChange]);
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import { skipToken } from '@reduxjs/toolkit/query';
|
||||
|
||||
import { LinkButton, Stack, Text } from '@grafana/ui';
|
||||
import { useGetRepositoryQuery } from 'app/api/clients/provisioning';
|
||||
import { Trans } from 'app/core/internationalization';
|
||||
|
||||
import { getRepoHref } from '../utils/git';
|
||||
|
||||
type RepositoryLinkProps = {
|
||||
name?: string;
|
||||
};
|
||||
|
||||
export function RepositoryLink({ name }: RepositoryLinkProps) {
|
||||
const repoQuery = useGetRepositoryQuery(name ? { name } : skipToken);
|
||||
const repo = repoQuery.data;
|
||||
|
||||
if (!repo || repoQuery.isLoading || repo.spec?.type !== 'github' || !repo.spec?.github?.url) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const repoHref = getRepoHref(repo.spec?.github);
|
||||
const folderHref = repo.spec?.sync.target === 'folder' ? `/dashboards/f/${repo.metadata?.name}` : '/dashboards';
|
||||
|
||||
if (!repoHref) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Stack direction="column" gap={1}>
|
||||
<Text>
|
||||
<Trans i18nKey="provisioning.repository-link.grafana-repository">
|
||||
Grafana and your repository are now in sync.
|
||||
</Trans>
|
||||
</Text>
|
||||
<Stack direction="row" gap={2}>
|
||||
<LinkButton fill="outline" href={repoHref} icon="external-link-alt" target="_blank" rel="noopener noreferrer">
|
||||
<Trans i18nKey="provisioning.repository-link.view-repository">View repository</Trans>
|
||||
</LinkButton>
|
||||
<LinkButton fill="outline" href={folderHref} icon="folder-open">
|
||||
<Trans i18nKey="provisioning.repository-link.view-folder">View folder</Trans>
|
||||
</LinkButton>
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
@@ -5323,9 +5323,12 @@
|
||||
"job-status": {
|
||||
"error-unknown": "An unknown error occurred",
|
||||
"label-view-details": "View details",
|
||||
"loading-finished-job": "Loading finished job...",
|
||||
"no-job-found": "No job found",
|
||||
"no-job-found-message": "The job may have been deleted or could not be retrieved. Cancel the current process and start again.",
|
||||
"starting": "Starting...",
|
||||
"status": {
|
||||
"title-error-running-job": "error running job",
|
||||
"title-error-running-job": "Error running job",
|
||||
"title-job-completed-successfully": "Job completed successfully"
|
||||
},
|
||||
"summary": "Summary"
|
||||
|
||||
Reference in New Issue
Block a user