mirror of https://github.com/grafana/grafana
Provisioning: Split active and finished jobs (#103351)
parent
d6ec8ab8b1
commit
edefc80c2a
@ -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> |
||||
); |
||||
} |
@ -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> |
||||
); |
||||
} |
Loading…
Reference in new issue