Provisioning: Display error details (#111591)
* Provisioning: Expose error messages * Prevent selecting non-configured targets
This commit is contained in:
@@ -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',
|
||||
|
||||
@@ -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": {
|
||||
|
||||
Reference in New Issue
Block a user