diff --git a/.betterer.results b/.betterer.results index f4cd2232eef..4b110d17c26 100644 --- a/.betterer.results +++ b/.betterer.results @@ -2653,18 +2653,6 @@ exports[`better eslint`] = { "public/app/features/provisioning/GettingStarted/SidebarItem.tsx:5381": [ [0, 0, 0, "Add noMargin prop to Card components to remove built-in margins. Use layout components like Stack or Grid with the gap prop instead for consistent spacing.", "0"] ], - "public/app/features/provisioning/Job/RecentJobs.tsx:5381": [ - [0, 0, 0, "Add noMargin prop to Card components to remove built-in margins. Use layout components like Stack or Grid with the gap prop instead for consistent spacing.", "0"] - ], - "public/app/features/provisioning/Repository/RepositoryCard.tsx:5381": [ - [0, 0, 0, "Add noMargin prop to Card components to remove built-in margins. Use layout components like Stack or Grid with the gap prop instead for consistent spacing.", "0"] - ], - "public/app/features/provisioning/Repository/RepositoryOverview.tsx:5381": [ - [0, 0, 0, "Add noMargin prop to Card components to remove built-in margins. Use layout components like Stack or Grid with the gap prop instead for consistent spacing.", "0"], - [0, 0, 0, "Add noMargin prop to Card components to remove built-in margins. Use layout components like Stack or Grid with the gap prop instead for consistent spacing.", "1"], - [0, 0, 0, "Add noMargin prop to Card components to remove built-in margins. Use layout components like Stack or Grid with the gap prop instead for consistent spacing.", "2"], - [0, 0, 0, "Add noMargin prop to Card components to remove built-in margins. Use layout components like Stack or Grid with the gap prop instead for consistent spacing.", "3"] - ], "public/app/features/query/components/QueryEditorRow.tsx:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"], [0, 0, 0, "Do not use any type assertions.", "1"], diff --git a/public/app/features/provisioning/Job/FinishedJobStatus.tsx b/public/app/features/provisioning/Job/FinishedJobStatus.tsx index b5fd32c4353..9f633447433 100644 --- a/public/app/features/provisioning/Job/FinishedJobStatus.tsx +++ b/public/app/features/provisioning/Job/FinishedJobStatus.tsx @@ -1,7 +1,7 @@ import { useEffect, useRef } from 'react'; import { Trans, t } from '@grafana/i18n'; -import { Alert, Spinner, Stack, Text } from '@grafana/ui'; +import { Spinner, Stack, Text } from '@grafana/ui'; import { useGetRepositoryJobsWithPathQuery } from 'app/api/clients/provisioning/v0alpha1'; import { useStepStatus } from '../Wizard/StepStatusContext'; @@ -36,15 +36,26 @@ export function FinishedJobStatus({ jobUid, repositoryName }: FinishedJobProps) } if (finishedQuery.isSuccess && job?.status) { - if (job.status.state === 'error') { + const { state, message, errors } = job.status; + + if (state === 'error') { setStepStatusInfo({ status: 'error', + error: { + title: t('provisioning.job-status.status.title-error-running-job', 'Error running job'), + message: errors?.length ? errors : message, + }, }); - } else if (job.status.state === 'success') { - setStepStatusInfo({ status: 'success' }); - } else if (job.status.state === 'warning') { - // We treat warnings as success for now, but this could be changed later + } else if (state === 'success') { setStepStatusInfo({ status: 'success' }); + } else if (state === 'warning') { + setStepStatusInfo({ + status: 'warning', + warning: { + title: t('provisioning.job-status.status.title-warning-running-job', 'Job completed with warnings'), + message: errors?.length ? errors : message, + }, + }); } } @@ -56,14 +67,17 @@ export function FinishedJobStatus({ jobUid, repositoryName }: FinishedJobProps) }, [finishedQuery, job, setStepStatusInfo]); if (retryFailed) { - setStepStatusInfo({ status: 'error' }); - return ( - - - The job may have been deleted or could not be retrieved. Cancel the current process and start again. - - - ); + setStepStatusInfo({ + status: 'error', + error: { + title: t('provisioning.job-status.no-job-found', 'No job found'), + message: t( + '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.' + ), + }, + }); + return null; } if (!job || finishedQuery.isLoading || finishedQuery.isFetching) { diff --git a/public/app/features/provisioning/Job/JobContent.tsx b/public/app/features/provisioning/Job/JobContent.tsx index 841800f2f7f..867f7844fda 100644 --- a/public/app/features/provisioning/Job/JobContent.tsx +++ b/public/app/features/provisioning/Job/JobContent.tsx @@ -1,9 +1,12 @@ +import { useEffect, useRef } from 'react'; + import { Trans, t } from '@grafana/i18n'; -import { Alert, ControlledCollapse, Spinner, Stack, Text } from '@grafana/ui'; +import { ControlledCollapse, Spinner, Stack, Text } from '@grafana/ui'; import { Job } from 'app/api/clients/provisioning/v0alpha1'; import { RepositoryLink } from '../Repository/RepositoryLink'; import ProgressBar from '../Shared/ProgressBar'; +import { useStepStatus } from '../Wizard/StepStatusContext'; import { JobSummary } from './JobSummary'; @@ -13,6 +16,9 @@ export interface JobContentProps { } export function JobContent({ job, isFinishedJob = false }: JobContentProps) { + const { setStepStatusInfo } = useStepStatus(); + const errorSetRef = useRef(false); + if (!job?.status) { return null; } @@ -20,48 +26,60 @@ export function JobContent({ job, isFinishedJob = false }: JobContentProps) { const { state, message, progress, summary, errors } = job.status; const repoName = job.metadata?.labels?.['provisioning.grafana.app/repository']; - const getStatusDisplay = () => { + // Update step status based on job state + useEffect(() => { + if (!state) { + return; + } + switch (state) { case 'success': - return ( - - ); + setStepStatusInfo({ status: 'success' }); + break; case 'warning': - return ( - - {errors?.length ? errors?.join('\n') : message} - - ); + if (!errorSetRef.current) { + setStepStatusInfo({ + status: 'warning', + warning: { + title: t('provisioning.job-status.status.title-warning-running-job', 'Job completed with warnings'), + message: errors?.length ? errors : message, + }, + }); + errorSetRef.current = true; + } + break; case 'error': - return ( - - {errors?.length ? errors?.join('\n') : message} - - ); + if (!errorSetRef.current) { + setStepStatusInfo({ + status: 'error', + error: { + title: t('provisioning.job-status.status.title-error-running-job', 'Error running job'), + message: errors?.length ? errors : message, + }, + }); + errorSetRef.current = true; + } + break; + case 'working': + case 'pending': + setStepStatusInfo({ status: 'running' }); + break; + default: + break; } - return ( - - {['working', 'pending'].includes(state ?? '') && } - - {message ?? state ?? ''} - - - ); - }; + }, [state, message, errors, setStepStatusInfo]); return ( - {getStatusDisplay()} + {['working', 'pending'].includes(state ?? '') && ( + + + + {message ?? state ?? t('provisioning.job-status.starting', 'Starting...')} + + + )} {state && !['success', 'error'].includes(state) && ( diff --git a/public/app/features/provisioning/Job/JobStatus.tsx b/public/app/features/provisioning/Job/JobStatus.tsx index f003245e263..ee05d56c051 100644 --- a/public/app/features/provisioning/Job/JobStatus.tsx +++ b/public/app/features/provisioning/Job/JobStatus.tsx @@ -1,4 +1,4 @@ -import { Trans } from '@grafana/i18n'; +import { Trans, t } from '@grafana/i18n'; import { Spinner, Stack, Text } from '@grafana/ui'; import { Job, useListJobQuery } from 'app/api/clients/provisioning/v0alpha1'; @@ -36,7 +36,12 @@ export function JobStatus({ watch }: JobStatusProps) { } if (activeQuery.isError) { - setStepStatusInfo({ status: 'error', error: 'Error fetching active job' }); + setStepStatusInfo({ + status: 'error', + error: { + title: t('provisioning.job-status.title.error-fetching-active-job', 'Error fetching active job'), + }, + }); return null; } diff --git a/public/app/features/provisioning/Job/RecentJobs.tsx b/public/app/features/provisioning/Job/RecentJobs.tsx index cf1291bcb2f..550511b5069 100644 --- a/public/app/features/provisioning/Job/RecentJobs.tsx +++ b/public/app/features/provisioning/Job/RecentJobs.tsx @@ -2,10 +2,11 @@ import { useMemo } from 'react'; import { intervalToAbbreviatedDurationString, TraceKeyValuePair } from '@grafana/data'; import { Trans, t } from '@grafana/i18n'; -import { Alert, Badge, Box, Card, Icon, InteractiveTable, Spinner, Stack, Text } from '@grafana/ui'; +import { Alert, Badge, Box, Card, InteractiveTable, Spinner, Stack, Text } from '@grafana/ui'; import { Job, Repository, SyncStatus } 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 { formatTimestamp } from '../utils/time'; @@ -126,21 +127,7 @@ function ExpandedRow({ row }: ExpandedRowProps) { )} - {hasErrors && ( - - {row.status?.errors?.map( - (error, index) => - error.trim() && ( - - - - {error} - - - ) - )} - - )} + {hasErrors && } {hasSummary && ( @@ -212,7 +199,7 @@ export function RecentJobs({ repo }: Props) { } return ( - + Jobs diff --git a/public/app/features/provisioning/Repository/RepositoryCard.tsx b/public/app/features/provisioning/Repository/RepositoryCard.tsx index e2a022cebb5..e56f8655eb9 100644 --- a/public/app/features/provisioning/Repository/RepositoryCard.tsx +++ b/public/app/features/provisioning/Repository/RepositoryCard.tsx @@ -58,7 +58,7 @@ export function RepositoryCard({ repository }: Props) { }; return ( - + diff --git a/public/app/features/provisioning/Repository/RepositoryOverview.tsx b/public/app/features/provisioning/Repository/RepositoryOverview.tsx index ba18bf32fcf..b4f7bfd019a 100644 --- a/public/app/features/provisioning/Repository/RepositoryOverview.tsx +++ b/public/app/features/provisioning/Repository/RepositoryOverview.tsx @@ -6,6 +6,7 @@ import { Box, Card, CellProps, Grid, InteractiveTable, LinkButton, Stack, Text, import { Repository, ResourceCount } from 'app/api/clients/provisioning/v0alpha1'; import { RecentJobs } from '../Job/RecentJobs'; +import { MessageList } from '../Shared/MessageList'; import { formatTimestamp } from '../utils/time'; import { CheckRepository } from './CheckRepository'; @@ -51,7 +52,7 @@ export function RepositoryOverview({ repo }: { repo: Repository }) {
- + Resources @@ -73,7 +74,7 @@ export function RepositoryOverview({ repo }: { repo: Repository }) {
{repo.status?.health && (
- + Health @@ -129,7 +130,7 @@ export function RepositoryOverview({ repo }: { repo: Repository }) {
)}
- + Pull status @@ -192,13 +193,7 @@ export function RepositoryOverview({ repo }: { repo: Repository }) {
- - {status.sync.message.map((msg, idx) => ( - - {msg} - - ))} - +
)} @@ -211,7 +206,7 @@ export function RepositoryOverview({ repo }: { repo: Repository }) { {repo.status?.webhook && (
- + Webhook diff --git a/public/app/features/provisioning/Shared/MessageList.tsx b/public/app/features/provisioning/Shared/MessageList.tsx new file mode 100644 index 00000000000..2d7f0f15f2f --- /dev/null +++ b/public/app/features/provisioning/Shared/MessageList.tsx @@ -0,0 +1,28 @@ +import { css } from '@emotion/css'; + +import { GrafanaTheme2 } from '@grafana/data'; +import { Text, useStyles2 } from '@grafana/ui'; + +interface MessageListProps { + messages: string[]; + variant?: 'body' | 'bodySmall'; +} + +export function MessageList({ messages, variant }: MessageListProps) { + const styles = useStyles2(getStyles); + + return ( +
    + {messages.map((msg, index) => ( +
  • {variant ? {msg} : msg}
  • + ))} +
+ ); +} + +const getStyles = (theme: GrafanaTheme2) => ({ + messageList: css({ + margin: 0, + paddingLeft: theme.spacing(3), + }), +}); diff --git a/public/app/features/provisioning/Shared/ProvisioningAlert.tsx b/public/app/features/provisioning/Shared/ProvisioningAlert.tsx new file mode 100644 index 00000000000..94fb45423cc --- /dev/null +++ b/public/app/features/provisioning/Shared/ProvisioningAlert.tsx @@ -0,0 +1,51 @@ +import { t } from '@grafana/i18n'; +import { Alert } from '@grafana/ui'; + +import { ProvisioningErrorInfo } from '../types'; + +import { MessageList } from './MessageList'; + +interface ProvisioningAlertProps { + error?: string | ProvisioningErrorInfo; + warning?: string | ProvisioningErrorInfo; +} + +const getTitle = (alert: string | ProvisioningErrorInfo, isWarning = false) => { + if (typeof alert === 'string') { + return alert; + } + + if (isWarning) { + return alert.title || t('provisioning.warning-title-default', 'Warning'); + } else { + return alert.title || t('provisioning.error-title-default', 'Error'); + } +}; + +const getMessage = (alert: string | ProvisioningErrorInfo) => { + if (typeof alert === 'string' || !alert.message) { + return null; + } + + if (Array.isArray(alert.message)) { + return ; + } + + return alert.message; +}; + +export function ProvisioningAlert({ error, warning }: ProvisioningAlertProps) { + const alertData = error || warning; + const isWarning = Boolean(warning); + const severity = isWarning ? 'warning' : 'error'; + + if (!alertData) { + return null; + } + + return ( + + {getMessage(alertData)} + + ); +} diff --git a/public/app/features/provisioning/Shared/RepositoryList.tsx b/public/app/features/provisioning/Shared/RepositoryList.tsx index 95d42d28d10..368ac5b644d 100644 --- a/public/app/features/provisioning/Shared/RepositoryList.tsx +++ b/public/app/features/provisioning/Shared/RepositoryList.tsx @@ -30,7 +30,7 @@ export function RepositoryList({ items }: Props) { )} - + {filteredItems.length ? ( filteredItems.map((item) => ) ) : ( diff --git a/public/app/features/provisioning/Wizard/ProvisioningWizard.tsx b/public/app/features/provisioning/Wizard/ProvisioningWizard.tsx index edf8b5aea3e..6c9ef49e917 100644 --- a/public/app/features/provisioning/Wizard/ProvisioningWizard.tsx +++ b/public/app/features/provisioning/Wizard/ProvisioningWizard.tsx @@ -6,11 +6,12 @@ import { useNavigate } from 'react-router-dom-v5-compat'; import { AppEvents, GrafanaTheme2 } from '@grafana/data'; import { t } from '@grafana/i18n'; import { getAppEvents, isFetchError } from '@grafana/runtime'; -import { Alert, Box, Button, Stack, Text, useStyles2 } from '@grafana/ui'; +import { Box, Button, Stack, Text, useStyles2 } from '@grafana/ui'; import { useDeleteRepositoryMutation, useGetFrontendSettingsQuery } from 'app/api/clients/provisioning/v0alpha1'; import { FormPrompt } from 'app/core/components/FormPrompt/FormPrompt'; import { getDefaultValues } from '../Config/defaults'; +import { ProvisioningAlert } from '../Shared/ProvisioningAlert'; import { PROVISIONING_URL } from '../constants'; import { useCreateOrUpdateRepository } from '../hooks/useCreateOrUpdateRepository'; import { dataToSpec } from '../utils/data'; @@ -64,7 +65,8 @@ export function ProvisioningWizard({ type }: { type: RepoType }) { const [isSubmitting, setIsSubmitting] = useState(false); const [isCancelling, setIsCancelling] = useState(false); - const { stepStatusInfo, setStepStatusInfo, isStepSuccess, isStepRunning, hasStepError } = useStepStatus(); + const { stepStatusInfo, setStepStatusInfo, isStepSuccess, isStepRunning, hasStepError, hasStepWarning } = + useStepStatus(); const { data } = useGetFrontendSettingsQuery(); const isLegacyStorage = Boolean(data?.legacyStorage); @@ -250,8 +252,8 @@ export function ProvisioningWizard({ type }: { type: RepoType }) { setIsSubmitting(false); } } else { - // only proceed if the job was successful - if (isStepSuccess) { + // proceed if the job was successful or had warnings + if (isStepSuccess || hasStepWarning) { handleNext(); } } @@ -262,9 +264,9 @@ export function ProvisioningWizard({ type }: { type: RepoType }) { if (activeStep !== 'connection' && hasStepError) { return true; } - // Synchronize step requires success to proceed + // Synchronize step requires success or warning to proceed if (activeStep === 'synchronize') { - return !isStepSuccess; // Disable next button if the step is not successful + return !(isStepSuccess || hasStepWarning); // Disable next button if the step is not successful or has warnings } return isSubmitting || isCancelling || isStepRunning || isCreatingSkipJob; }; @@ -283,9 +285,8 @@ export function ProvisioningWizard({ type }: { type: RepoType }) { - {hasStepError && 'error' in stepStatusInfo && stepStatusInfo.error && ( - - )} + {hasStepError && 'error' in stepStatusInfo && } + {hasStepWarning && 'warning' in stepStatusInfo && }
{activeStep === 'connection' && } diff --git a/public/app/features/provisioning/Wizard/StepStatusContext.tsx b/public/app/features/provisioning/Wizard/StepStatusContext.tsx index e3081bfba02..efd86869979 100644 --- a/public/app/features/provisioning/Wizard/StepStatusContext.tsx +++ b/public/app/features/provisioning/Wizard/StepStatusContext.tsx @@ -11,6 +11,7 @@ interface StepStatusContextData { // Computed status checks hasStepError: boolean; + hasStepWarning: boolean; isStepRunning: boolean; isStepSuccess: boolean; isStepIdle: boolean; @@ -29,6 +30,7 @@ export const StepStatusProvider = ({ children }: PropsWithChildren) => { stepStatusInfo, setStepStatusInfo, hasStepError: stepStatusInfo.status === 'error', + hasStepWarning: stepStatusInfo.status === 'warning', isStepRunning: stepStatusInfo.status === 'running', isStepSuccess: stepStatusInfo.status === 'success', isStepIdle: stepStatusInfo.status === 'idle', diff --git a/public/app/features/provisioning/Wizard/types.ts b/public/app/features/provisioning/Wizard/types.ts index 7b77e277ffa..fd9fcd00f1c 100644 --- a/public/app/features/provisioning/Wizard/types.ts +++ b/public/app/features/provisioning/Wizard/types.ts @@ -1,6 +1,6 @@ import { RepositorySpec, SyncOptions } from 'app/api/clients/provisioning/v0alpha1'; -import { RepositoryFormData } from '../types'; +import { ProvisioningErrorInfo, RepositoryFormData } from '../types'; export type WizardStep = 'connection' | 'bootstrap' | 'finish' | 'synchronize'; @@ -26,5 +26,7 @@ export interface ModeOption { subtitle: string; } -export type StepStatus = 'idle' | 'running' | 'error' | 'success'; -export type StepStatusInfo = { status: StepStatus } | { status: 'error'; error: string }; +export type StepStatusInfo = + | { status: 'idle' | 'running' | 'success' } + | { status: 'error'; error: string | ProvisioningErrorInfo } + | { status: 'warning'; warning: string | ProvisioningErrorInfo }; diff --git a/public/app/features/provisioning/types.ts b/public/app/features/provisioning/types.ts index 516beb0dc67..8f8ee2df385 100644 --- a/public/app/features/provisioning/types.ts +++ b/public/app/features/provisioning/types.ts @@ -89,3 +89,8 @@ export type HistoryListResponse = { metadata?: Record; items?: HistoryItem[]; }; + +export interface ProvisioningErrorInfo { + title?: string; + message?: string | string[]; +} diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index 8a70d10f52d..4de18dad330 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -11070,10 +11070,10 @@ "title-instant-updates-requests-webhooks": "Instant updates and pull requests with webhooks.", "title-visual-previews-in-pull-requests": "Visual previews in pull requests with image rendering" }, + "error-title-default": "Error", "expanded-row": { "job-specification": "Job Specification", - "summary": "Summary", - "title-error": "Error" + "summary": "Summary" }, "features-list": { "actions": { @@ -11218,10 +11218,12 @@ "starting": "Starting...", "status": { "title-error-running-job": "Error running job", - "title-job-completed-successfully": "Job completed successfully", "title-warning-running-job": "Job completed with warnings" }, - "summary": "Summary" + "summary": "Summary", + "title": { + "error-fetching-active-job": "Error fetching active job" + } }, "local": { "path-description": "Local file system path to the repository", @@ -11387,6 +11389,7 @@ "go-to": "Go to", "make-sure": "Make sure to include these permissions" }, + "warning-title-default": "Warning", "wizard": { "alert-point-1": "Resources won't be able to be created, edited, or deleted during this process. In the last step, they will disappear.", "alert-point-2": "Once provisioning is complete, resources will reappear and be managed through external storage.",