Provisioning: Display error details (#111591)

* Provisioning: Expose error messages

* Prevent selecting non-configured targets
This commit is contained in:
Alex Khomenko
2025-09-25 16:48:38 +03:00
committed by GitHub
parent 25d1a03187
commit 45d5336c9f
11 changed files with 96 additions and 38 deletions
@@ -62,7 +62,12 @@ export function ConfigForm({ data }: ConfigFormProps) {
setError,
watch,
getValues,
} = useForm<RepositoryFormData>({ defaultValues: getDefaultValues(data?.spec) });
} = useForm<RepositoryFormData>({
defaultValues: getDefaultValues({
repository: data?.spec,
allowedTargets: settings.data?.allowedTargets,
}),
});
const isEdit = Boolean(repositoryName);
const [tokenConfigured, setTokenConfigured] = useState(isEdit);
@@ -1,11 +1,21 @@
import { t } from '@grafana/i18n';
import { RepositorySpec } from '../../../api/clients/provisioning/v0alpha1';
import { RepositorySpec, RepositoryViewList } from '../../../api/clients/provisioning/v0alpha1';
import { RepositoryFormData } from '../types';
import { specToData } from '../utils/data';
export function getDefaultValues(repository?: RepositorySpec): RepositoryFormData {
export interface GetDefaultValuesOptions {
repository?: RepositorySpec;
allowedTargets?: RepositoryViewList['allowedTargets'];
}
export function getDefaultValues({
repository,
allowedTargets = ['instance', 'folder'],
}: GetDefaultValuesOptions = {}): RepositoryFormData {
if (!repository) {
const defaultTarget = allowedTargets.includes('folder') ? 'folder' : 'instance';
return {
type: 'github',
title: t('provisioning.get-default-values.title.repository', 'Repository'),
@@ -18,7 +28,7 @@ export function getDefaultValues(repository?: RepositorySpec): RepositoryFormDat
path: 'grafana/',
sync: {
enabled: false,
target: 'folder', // start with folder so we can shift to instance later (without an error)
target: defaultTarget,
intervalSeconds: 60,
},
};
@@ -6,12 +6,12 @@ import AutoSizer from 'react-virtualized-auto-sizer';
import { urlUtil } from '@grafana/data';
import { Trans, t } from '@grafana/i18n';
import { isFetchError } from '@grafana/runtime';
import { Alert, CodeEditor, LinkButton, Button, Stack, Tab, TabContent, TabsBar, DeleteButton } from '@grafana/ui';
import { Alert, Button, CodeEditor, DeleteButton, LinkButton, Stack, Tab, TabContent, TabsBar } from '@grafana/ui';
import {
useGetRepositoryFilesWithPathQuery,
ResourceWrapper,
useReplaceRepositoryFilesWithPathMutation,
useDeleteRepositoryFilesWithPathMutation,
useGetRepositoryFilesWithPathQuery,
useReplaceRepositoryFilesWithPathMutation,
} from 'app/api/clients/provisioning/v0alpha1';
import { Page } from 'app/core/components/Page/Page';
import { useQueryParams } from 'app/core/hooks/useQueryParams';
@@ -28,7 +28,6 @@ export default function FileStatusPage() {
const path = params['*'] ?? '';
const file = useGetRepositoryFilesWithPathQuery({ name, path, ref });
const { isReadOnlyRepo } = useGetResourceRepositoryView({ name });
console.log('isReadOnlyRepo', isReadOnlyRepo);
return (
<Page
@@ -1,5 +1,5 @@
import { css } from '@emotion/css';
import { useEffect } from 'react';
import { memo, useEffect } from 'react';
import { Controller, useFormContext } from 'react-hook-form';
import { GrafanaTheme2 } from '@grafana/data';
@@ -20,7 +20,7 @@ export interface Props {
repoName: string;
}
export function BootstrapStep({ settingsData, repoName }: Props) {
export const BootstrapStep = memo(function BootstrapStep({ settingsData, repoName }: Props) {
const { setStepStatusInfo } = useStepStatus();
const {
register,
@@ -139,7 +139,7 @@ export function BootstrapStep({ settingsData, repoName }: Props) {
</Stack>
</Stack>
);
}
});
const getStyles = (theme: GrafanaTheme2) => ({
divider: css({
@@ -1,6 +1,7 @@
import { useParams } from 'react-router-dom-v5-compat';
import { t } from '@grafana/i18n';
import { useGetFrontendSettingsQuery } from 'app/api/clients/provisioning/v0alpha1';
import { Page } from 'app/core/components/Page/Page';
import { isGitProvider } from '../utils/repositoryTypes';
@@ -11,6 +12,7 @@ import { RepoType } from './types';
export default function ConnectPage() {
const { type } = useParams<{ type: RepoType }>();
const { data: settingsData } = useGetFrontendSettingsQuery();
if (!type) {
return null;
@@ -29,7 +31,7 @@ export default function ConnectPage() {
>
<Page.Contents>
<StepStatusProvider>
<ProvisioningWizard type={type} />
<ProvisioningWizard type={type} settingsData={settingsData} />
</StepStatusProvider>
</Page.Contents>
</Page>
@@ -1,4 +1,4 @@
import { useState } from 'react';
import { memo, useState } from 'react';
import { Controller, useFormContext } from 'react-hook-form';
import { Combobox, Field, Input, SecretInput, Stack } from '@grafana/ui';
@@ -11,7 +11,7 @@ import { isGitProvider } from '../utils/repositoryTypes';
import { getGitProviderFields, getLocalProviderFields } from './fields';
import { WizardFormData } from './types';
export function ConnectStep() {
export const ConnectStep = memo(function ConnectStep() {
const {
register,
control,
@@ -174,4 +174,4 @@ export function ConnectStep() {
)}
</Stack>
);
}
});
@@ -1,4 +1,4 @@
import { useEffect } from 'react';
import { memo, useEffect } from 'react';
import { useFormContext } from 'react-hook-form';
import { Trans, t } from '@grafana/i18n';
@@ -11,7 +11,7 @@ import { isGitProvider } from '../utils/repositoryTypes';
import { getGitProviderFields } from './fields';
import { WizardFormData } from './types';
export function FinishStep() {
export const FinishStep = memo(function FinishStep() {
const { register, watch, setValue } = useFormContext<WizardFormData>();
const settings = useGetFrontendSettingsQuery();
@@ -115,4 +115,4 @@ export function FinishStep() {
)}
</Stack>
);
}
});
@@ -1,5 +1,5 @@
import { css } from '@emotion/css';
import { useCallback, useEffect, useState } from 'react';
import { memo, useCallback, useEffect, useState } from 'react';
import { FormProvider, useForm } from 'react-hook-form';
import { useNavigate } from 'react-router-dom-v5-compat';
@@ -7,7 +7,7 @@ import { AppEvents, GrafanaTheme2 } from '@grafana/data';
import { t } from '@grafana/i18n';
import { getAppEvents, isFetchError } from '@grafana/runtime';
import { Box, Button, ConfirmModal, Stack, Text, useStyles2 } from '@grafana/ui';
import { useDeleteRepositoryMutation, useGetFrontendSettingsQuery } from 'app/api/clients/provisioning/v0alpha1';
import { RepositoryViewList, useDeleteRepositoryMutation } from 'app/api/clients/provisioning/v0alpha1';
import { FormPrompt } from 'app/core/components/FormPrompt/FormPrompt';
import { getDefaultValues } from '../Config/defaults';
@@ -58,7 +58,13 @@ const getSteps = (): Array<Step<WizardStep>> => {
];
};
export function ProvisioningWizard({ type }: { type: RepoType }) {
export const ProvisioningWizard = memo(function ProvisioningWizard({
type,
settingsData,
}: {
type: RepoType;
settingsData?: RepositoryViewList;
}) {
const [activeStep, setActiveStep] = useState<WizardStep>('connection');
const [completedSteps, setCompletedSteps] = useState<WizardStep[]>([]);
@@ -66,6 +72,15 @@ export function ProvisioningWizard({ type }: { type: RepoType }) {
const [isCancelling, setIsCancelling] = useState(false);
const [showCancelConfirmation, setShowCancelConfirmation] = useState(false);
const repositoryRequestFailed = t(
'provisioning.provisioning-wizard.on-submit.title.repository-request-failed',
'Repository request failed'
);
const repositoryConnectionFailed = t(
'provisioning.provisioning-wizard.on-submit.title.repository-connection-failed',
'Repository connection failed'
);
const { stepStatusInfo, setStepStatusInfo, isStepSuccess, isStepRunning, hasStepError, hasStepWarning } =
useStepStatus();
@@ -74,14 +89,13 @@ export function ProvisioningWizard({ type }: { type: RepoType }) {
activeStep === 'finish' && (isStepSuccess || completedSteps.includes('synchronize'));
const shouldUseCancelBehavior = activeStep === 'connection' || isSyncCompleted || isFinishWithSyncCompleted;
const { data } = useGetFrontendSettingsQuery();
const isLegacyStorage = Boolean(data?.legacyStorage);
const isLegacyStorage = Boolean(settingsData?.legacyStorage);
const navigate = useNavigate();
const steps = getSteps();
const styles = useStyles2(getStyles);
const values = getDefaultValues();
const values = getDefaultValues({ allowedTargets: settingsData?.allowedTargets });
const methods = useForm<WizardFormData>({
defaultValues: {
repository: { ...values, type },
@@ -124,7 +138,7 @@ export function ProvisioningWizard({ type }: { type: RepoType }) {
// A different repository is marked with instance target -- nothing will succeed
useEffect(() => {
if (data?.items.some((item) => item.target === 'instance' && item.name !== repoName)) {
if (settingsData?.items.some((item) => item.target === 'instance' && item.name !== repoName)) {
appEvents.publish({
type: AppEvents.alertError.name,
payload: [
@@ -134,7 +148,7 @@ export function ProvisioningWizard({ type }: { type: RepoType }) {
navigate(PROVISIONING_URL);
}
}, [navigate, repoName, data?.items]);
}, [navigate, repoName, settingsData?.items]);
const handleRepositoryDeletion = async (name: string) => {
setIsCancelling(true);
@@ -283,10 +297,20 @@ export function ProvisioningWizard({ type }: { type: RepoType }) {
const spec = dataToSpec(formData.repository);
const rsp = await submitData(spec, formData.repository.token);
if (rsp.error) {
setStepStatusInfo({
status: 'error',
error: 'Repository request failed',
});
if (isFetchError(rsp.error)) {
setStepStatusInfo({
status: 'error',
error: {
title: repositoryRequestFailed,
message: rsp.error.data.message,
},
});
} else {
setStepStatusInfo({
status: 'error',
error: repositoryRequestFailed,
});
}
return;
}
@@ -304,11 +328,19 @@ export function ProvisioningWizard({ type }: { type: RepoType }) {
const [field, errorMessage] = getFormErrors(error.data.errors);
if (field && errorMessage) {
setError(field, errorMessage);
} else {
setStepStatusInfo({
status: 'error',
error: {
title: repositoryConnectionFailed,
message: error.data.message,
},
});
}
} else {
setStepStatusInfo({
status: 'error',
error: 'Repository connection failed',
error: repositoryConnectionFailed,
});
}
} finally {
@@ -357,7 +389,7 @@ export function ProvisioningWizard({ type }: { type: RepoType }) {
<div className={styles.content}>
{activeStep === 'connection' && <ConnectStep />}
{activeStep === 'bootstrap' && <BootstrapStep settingsData={data} repoName={repoName} />}
{activeStep === 'bootstrap' && <BootstrapStep settingsData={settingsData} repoName={repoName} />}
{activeStep === 'synchronize' && (
<SynchronizeStep
isLegacyStorage={isLegacyStorage}
@@ -399,7 +431,7 @@ export function ProvisioningWizard({ type }: { type: RepoType }) {
/>
</FormProvider>
);
}
});
const getStyles = (theme: GrafanaTheme2) => ({
form: css({
@@ -1,5 +1,5 @@
import { skipToken } from '@reduxjs/toolkit/query';
import { useState } from 'react';
import { memo, useState } from 'react';
import { useFormContext } from 'react-hook-form';
import { Trans, t } from '@grafana/i18n';
@@ -20,7 +20,11 @@ export interface SynchronizeStepProps {
isCancelling?: boolean;
}
export function SynchronizeStep({ isLegacyStorage, onCancel, isCancelling }: SynchronizeStepProps) {
export const SynchronizeStep = memo(function SynchronizeStep({
isLegacyStorage,
onCancel,
isCancelling,
}: SynchronizeStepProps) {
const { getValues, register, watch } = useFormContext<WizardFormData>();
const { setStepStatusInfo } = useStepStatus();
const [repoName = '', repoType] = watch(['repositoryName', 'repository.type']);
@@ -163,4 +167,4 @@ export function SynchronizeStep({ isLegacyStorage, onCancel, isCancelling }: Syn
</Field>
</Stack>
);
}
});
@@ -26,8 +26,6 @@ export interface ModeOption {
subtitle: string;
}
export type StepStatus = 'idle' | 'running' | 'error' | 'success';
export const RepoTypeDisplay: { [key in RepoType]: string } = {
github: 'GitHub',
gitlab: 'GitLab',
+8
View File
@@ -11522,6 +11522,14 @@
"subtitle": "Use this option if you want to sync and manage your entire Grafana instance through external storage."
}
},
"provisioning-wizard": {
"on-submit": {
"title": {
"repository-connection-failed": "Repository connection failed",
"repository-request-failed": "Repository request failed"
}
}
},
"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": {