diff --git a/public/app/features/provisioning/Config/ConfigForm.tsx b/public/app/features/provisioning/Config/ConfigForm.tsx index f7139f3a1ff..d206d6bc2fd 100644 --- a/public/app/features/provisioning/Config/ConfigForm.tsx +++ b/public/app/features/provisioning/Config/ConfigForm.tsx @@ -62,7 +62,12 @@ export function ConfigForm({ data }: ConfigFormProps) { setError, watch, getValues, - } = useForm({ defaultValues: getDefaultValues(data?.spec) }); + } = useForm({ + defaultValues: getDefaultValues({ + repository: data?.spec, + allowedTargets: settings.data?.allowedTargets, + }), + }); const isEdit = Boolean(repositoryName); const [tokenConfigured, setTokenConfigured] = useState(isEdit); diff --git a/public/app/features/provisioning/Config/defaults.ts b/public/app/features/provisioning/Config/defaults.ts index 32c96fffc53..65ab1bd84fa 100644 --- a/public/app/features/provisioning/Config/defaults.ts +++ b/public/app/features/provisioning/Config/defaults.ts @@ -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, }, }; diff --git a/public/app/features/provisioning/File/FileStatusPage.tsx b/public/app/features/provisioning/File/FileStatusPage.tsx index f910d8160ba..5a946122e11 100644 --- a/public/app/features/provisioning/File/FileStatusPage.tsx +++ b/public/app/features/provisioning/File/FileStatusPage.tsx @@ -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 ( ); -} +}); const getStyles = (theme: GrafanaTheme2) => ({ divider: css({ diff --git a/public/app/features/provisioning/Wizard/ConnectPage.tsx b/public/app/features/provisioning/Wizard/ConnectPage.tsx index 07456f5acb5..2e9b60dd724 100644 --- a/public/app/features/provisioning/Wizard/ConnectPage.tsx +++ b/public/app/features/provisioning/Wizard/ConnectPage.tsx @@ -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() { > - + diff --git a/public/app/features/provisioning/Wizard/ConnectStep.tsx b/public/app/features/provisioning/Wizard/ConnectStep.tsx index 4a3f882c1b4..5f6d9db9267 100644 --- a/public/app/features/provisioning/Wizard/ConnectStep.tsx +++ b/public/app/features/provisioning/Wizard/ConnectStep.tsx @@ -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() { )} ); -} +}); diff --git a/public/app/features/provisioning/Wizard/FinishStep.tsx b/public/app/features/provisioning/Wizard/FinishStep.tsx index 7fd54c1d9a6..5414c1929e2 100644 --- a/public/app/features/provisioning/Wizard/FinishStep.tsx +++ b/public/app/features/provisioning/Wizard/FinishStep.tsx @@ -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(); const settings = useGetFrontendSettingsQuery(); @@ -115,4 +115,4 @@ export function FinishStep() { )} ); -} +}); diff --git a/public/app/features/provisioning/Wizard/ProvisioningWizard.tsx b/public/app/features/provisioning/Wizard/ProvisioningWizard.tsx index 0a7d0cf7ca6..5eb4fd174ee 100644 --- a/public/app/features/provisioning/Wizard/ProvisioningWizard.tsx +++ b/public/app/features/provisioning/Wizard/ProvisioningWizard.tsx @@ -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> => { ]; }; -export function ProvisioningWizard({ type }: { type: RepoType }) { +export const ProvisioningWizard = memo(function ProvisioningWizard({ + type, + settingsData, +}: { + type: RepoType; + settingsData?: RepositoryViewList; +}) { const [activeStep, setActiveStep] = useState('connection'); const [completedSteps, setCompletedSteps] = useState([]); @@ -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({ 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 }) {
{activeStep === 'connection' && } - {activeStep === 'bootstrap' && } + {activeStep === 'bootstrap' && } {activeStep === 'synchronize' && ( ); -} +}); const getStyles = (theme: GrafanaTheme2) => ({ form: css({ diff --git a/public/app/features/provisioning/Wizard/SynchronizeStep.tsx b/public/app/features/provisioning/Wizard/SynchronizeStep.tsx index 3e7a70292e6..a763605a9b0 100644 --- a/public/app/features/provisioning/Wizard/SynchronizeStep.tsx +++ b/public/app/features/provisioning/Wizard/SynchronizeStep.tsx @@ -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(); const { setStepStatusInfo } = useStepStatus(); const [repoName = '', repoType] = watch(['repositoryName', 'repository.type']); @@ -163,4 +167,4 @@ export function SynchronizeStep({ isLegacyStorage, onCancel, isCancelling }: Syn ); -} +}); diff --git a/public/app/features/provisioning/Wizard/types.ts b/public/app/features/provisioning/Wizard/types.ts index b98dc293558..43b8545e379 100644 --- a/public/app/features/provisioning/Wizard/types.ts +++ b/public/app/features/provisioning/Wizard/types.ts @@ -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', diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index 02c07532b81..21aef1cd42d 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -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": {