From 1ef4a1a4abeb685954d167bd98694bf497178964 Mon Sep 17 00:00:00 2001 From: Alex Khomenko Date: Thu, 3 Apr 2025 12:34:38 +0300 Subject: [PATCH] Provisioning: Centralise app notifications (#103290) * Provisioning: Centralise app event notifications * Remove test started notification * Remove unused import --- public/app/api/clients/provisioning/index.ts | 162 ++++++++++++++++-- .../provisioning/Config/ConfigForm.tsx | 9 - public/app/features/provisioning/HomePage.tsx | 17 +- .../Repository/CheckRepository.tsx | 19 -- .../Repository/DeleteRepositoryButton.tsx | 13 -- .../Repository/SyncRepository.tsx | 19 +- .../provisioning/Wizard/WizardContent.tsx | 14 -- public/locales/en-US/grafana.json | 7 +- 8 files changed, 156 insertions(+), 104 deletions(-) diff --git a/public/app/api/clients/provisioning/index.ts b/public/app/api/clients/provisioning/index.ts index b31f8c318cb..109b3847102 100644 --- a/public/app/api/clients/provisioning/index.ts +++ b/public/app/api/clients/provisioning/index.ts @@ -1,3 +1,7 @@ +import { notifyApp } from 'app/core/actions'; +import { createSuccessNotification, createErrorNotification } from 'app/core/copy/appNotification'; +import { t } from 'app/core/internationalization'; + import { generatedAPI, JobSpec, @@ -13,26 +17,158 @@ import { createOnCacheEntryAdded } from './utils/createOnCacheEntryAdded'; export const provisioningAPI = generatedAPI.enhanceEndpoints({ endpoints: { - listJob(endpoint) { + listJob: { // Do not include 'watch' in the first query, so we can get the initial list of jobs // and then start watching for changes - endpoint.query = ({ watch, ...queryArg }) => ({ + query: ({ watch, ...queryArg }) => ({ url: `/jobs`, params: queryArg, - }); - endpoint.onCacheEntryAdded = createOnCacheEntryAdded('jobs'); + }), + onCacheEntryAdded: createOnCacheEntryAdded('jobs'), }, - listRepository(endpoint) { - endpoint.query = ({ watch, ...queryArg }) => ({ + listRepository: { + query: ({ watch, ...queryArg }) => ({ url: `/repositories`, params: queryArg, - }); - endpoint.onCacheEntryAdded = createOnCacheEntryAdded< - RepositorySpec, - RepositoryStatus, - Repository, - RepositoryList - >('repositories'); + }), + onCacheEntryAdded: createOnCacheEntryAdded( + 'repositories' + ), + }, + deleteRepository: { + onQueryStarted: async (_, { queryFulfilled, dispatch }) => { + try { + await queryFulfilled; + dispatch( + notifyApp( + createSuccessNotification( + t( + 'provisioning.delete-repository-button.success-repository-deleted', + 'Repository settings queued for deletion' + ) + ) + ) + ); + } catch (e) { + if (e instanceof Error) { + dispatch( + notifyApp( + createErrorNotification( + t('provisioning.delete-repository-button.error-repository-delete', 'Failed to delete repository'), + e + ) + ) + ); + } + } + }, + }, + deletecollectionRepository: { + onQueryStarted: async (_, { queryFulfilled, dispatch }) => { + try { + await queryFulfilled; + dispatch( + notifyApp( + createSuccessNotification( + t('provisioning.home-page.success-all-repositories-deleted', 'All configured repositories deleted') + ) + ) + ); + } catch (e) { + if (e instanceof Error) { + dispatch( + notifyApp( + createErrorNotification( + t('provisioning.home-page.error-delete-all-repositories', 'Failed to delete all repositories'), + e + ) + ) + ); + } + } + }, + }, + createRepositoryTest: { + onQueryStarted: async (_, { queryFulfilled, dispatch }) => { + try { + await queryFulfilled; + } catch (e) { + if (e instanceof Error) { + dispatch(notifyApp(createErrorNotification('Error testing repository', e))); + } + } + }, + }, + createRepositoryJobs: { + onQueryStarted: async (_, { queryFulfilled, dispatch }) => { + try { + await queryFulfilled; + dispatch( + notifyApp(createSuccessNotification(t('provisioning.sync-repository.success-pull-started', 'Pull started'))) + ); + } catch (e) { + if (e instanceof Error) { + dispatch( + notifyApp( + createErrorNotification( + t('provisioning.sync-repository.error-pulling-resources', 'Error pulling resources'), + e + ) + ) + ); + } + } + }, + }, + createRepository: { + onQueryStarted: async (_, { queryFulfilled, dispatch }) => { + try { + await queryFulfilled; + dispatch( + notifyApp( + createSuccessNotification( + t('provisioning.config-form.alert-repository-settings-saved', 'Repository settings saved') + ) + ) + ); + } catch (e) { + if (e instanceof Error) { + dispatch( + notifyApp( + createErrorNotification( + t('provisioning.config-form.error-save-repository', 'Failed to save repository settings'), + e + ) + ) + ); + } + } + }, + }, + replaceRepository: { + onQueryStarted: async (_, { queryFulfilled, dispatch }) => { + try { + await queryFulfilled; + dispatch( + notifyApp( + createSuccessNotification( + t('provisioning.config-form.alert-repository-settings-updated', 'Repository settings updated') + ) + ) + ); + } catch (e) { + if (e instanceof Error) { + dispatch( + notifyApp( + createErrorNotification( + t('provisioning.config-form.error-save-repository', 'Failed to save repository settings'), + e + ) + ) + ); + } + } + }, }, }, }); diff --git a/public/app/features/provisioning/Config/ConfigForm.tsx b/public/app/features/provisioning/Config/ConfigForm.tsx index 7487c1e740f..4030d65e1b7 100644 --- a/public/app/features/provisioning/Config/ConfigForm.tsx +++ b/public/app/features/provisioning/Config/ConfigForm.tsx @@ -2,8 +2,6 @@ import { useEffect, useMemo, useState } from 'react'; import { Controller, useForm } from 'react-hook-form'; import { useNavigate } from 'react-router-dom-v5-compat'; -import { AppEvents } from '@grafana/data'; -import { getAppEvents } from '@grafana/runtime'; import { Button, Combobox, @@ -47,8 +45,6 @@ export function getWorkflowOptions(type?: 'github' | 'local'): Array opt.value === 'write'); // only write } -const appEvents = getAppEvents(); - export function getDefaultValues(repository?: RepositorySpec): RepositoryFormData { if (!repository) { return { @@ -109,11 +105,6 @@ export function ConfigForm({ data }: ConfigFormProps) { useEffect(() => { if (request.isSuccess) { const formData = getValues(); - - appEvents.publish({ - type: AppEvents.alertSuccess.name, - payload: [t('provisioning.config-form.alert-repository-settings-saved', 'Repository settings saved')], - }); reset(formData); setTimeout(() => { navigate('/admin/provisioning'); diff --git a/public/app/features/provisioning/HomePage.tsx b/public/app/features/provisioning/HomePage.tsx index 94ccf8fc6d8..2bc3be3152d 100644 --- a/public/app/features/provisioning/HomePage.tsx +++ b/public/app/features/provisioning/HomePage.tsx @@ -1,7 +1,5 @@ -import { useEffect, useMemo, useState } from 'react'; +import { useMemo, useState } from 'react'; -import { AppEvents } from '@grafana/data'; -import { getAppEvents } from '@grafana/runtime'; import { Alert, ConfirmModal, Stack, Tab, TabContent, TabsBar } from '@grafana/ui'; import { useDeletecollectionRepositoryMutation, useGetFrontendSettingsQuery } from 'app/api/clients/provisioning'; import { Page } from 'app/core/components/Page/Page'; @@ -12,8 +10,6 @@ import GettingStartedPage from './GettingStarted/GettingStartedPage'; import { FolderRepositoryList } from './Shared/FolderRepositoryList'; import { useRepositoryList } from './hooks'; -const appEvents = getAppEvents(); - enum TabSelection { Repositories = 'repositories', GettingStarted = 'getting-started', @@ -22,7 +18,7 @@ enum TabSelection { export default function HomePage() { const [items, isLoading] = useRepositoryList({ watch: true }); const settings = useGetFrontendSettingsQuery(); - const [deleteAll, deleteAllResult] = useDeletecollectionRepositoryMutation(); + const [deleteAll] = useDeletecollectionRepositoryMutation(); const [showDeleteModal, setShowDeleteModal] = useState(false); const [activeTab, setActiveTab] = useState(TabSelection.Repositories); @@ -42,15 +38,6 @@ export default function HomePage() { [] ); - useEffect(() => { - if (deleteAllResult.isSuccess) { - appEvents.publish({ - type: AppEvents.alertSuccess.name, - payload: [t('provisioning.home-page.success-all-repositories-deleted', 'All configured repositories deleted')], - }); - } - }, [deleteAllResult.isSuccess]); - // Early return for onboarding if (!items?.length && !isLoading) { return ; diff --git a/public/app/features/provisioning/Repository/CheckRepository.tsx b/public/app/features/provisioning/Repository/CheckRepository.tsx index 6cbb89f7b0c..c993cdc6f38 100644 --- a/public/app/features/provisioning/Repository/CheckRepository.tsx +++ b/public/app/features/provisioning/Repository/CheckRepository.tsx @@ -1,7 +1,3 @@ -import { useEffect } from 'react'; - -import { AppEvents } from '@grafana/data'; -import { getAppEvents } from '@grafana/runtime'; import { Button, Spinner } from '@grafana/ui'; import { Repository, useCreateRepositoryTestMutation } from 'app/api/clients/provisioning'; import { Trans } from 'app/core/internationalization'; @@ -14,21 +10,6 @@ export function CheckRepository({ repository }: Props) { const [testRepo, testQuery] = useCreateRepositoryTestMutation(); const name = repository.metadata?.name; - useEffect(() => { - const appEvents = getAppEvents(); - if (testQuery.isSuccess) { - appEvents.publish({ - type: AppEvents.alertSuccess.name, - payload: ['Test started'], - }); - } else if (testQuery.isError) { - appEvents.publish({ - type: AppEvents.alertError.name, - payload: ['Error testing repository', testQuery.error], - }); - } - }, [testQuery.error, testQuery.isError, testQuery.isSuccess]); - const onClick = () => { if (!name) { return; diff --git a/public/app/features/provisioning/Repository/DeleteRepositoryButton.tsx b/public/app/features/provisioning/Repository/DeleteRepositoryButton.tsx index f3a28542ddb..e0e7af51b77 100644 --- a/public/app/features/provisioning/Repository/DeleteRepositoryButton.tsx +++ b/public/app/features/provisioning/Repository/DeleteRepositoryButton.tsx @@ -1,14 +1,10 @@ import { useCallback, useEffect, useState } from 'react'; import { useNavigate } from 'react-router-dom-v5-compat'; -import { AppEvents } from '@grafana/data'; -import { getAppEvents } from '@grafana/runtime'; import { ConfirmModal, IconButton } from '@grafana/ui'; import { useDeleteRepositoryMutation } from 'app/api/clients/provisioning'; import { t } from 'app/core/internationalization'; -const appEvents = getAppEvents(); - interface Props { name: string; redirectTo?: string; @@ -21,15 +17,6 @@ export function DeleteRepositoryButton({ name, redirectTo }: Props) { useEffect(() => { if (request.isSuccess) { - appEvents.publish({ - type: AppEvents.alertSuccess.name, - payload: [ - t( - 'provisioning.delete-repository-button.success-repository-deleted', - 'Repository settings queued for deletion' - ), - ], - }); setShowModal(false); if (redirectTo) { navigate(redirectTo); diff --git a/public/app/features/provisioning/Repository/SyncRepository.tsx b/public/app/features/provisioning/Repository/SyncRepository.tsx index 8b8ecd088d4..c4ae51c2ee0 100644 --- a/public/app/features/provisioning/Repository/SyncRepository.tsx +++ b/public/app/features/provisioning/Repository/SyncRepository.tsx @@ -1,8 +1,6 @@ -import { useEffect, useState } from 'react'; +import { useState } from 'react'; import { useNavigate } from 'react-router-dom-v5-compat'; -import { AppEvents } from '@grafana/data'; -import { getAppEvents } from '@grafana/runtime'; import { Button, ConfirmModal } from '@grafana/ui'; import { Repository, useCreateRepositoryJobsMutation } from 'app/api/clients/provisioning'; import { Trans, t } from 'app/core/internationalization'; @@ -19,21 +17,6 @@ export function SyncRepository({ repository }: Props) { const navigate = useNavigate(); const name = repository.metadata?.name; - useEffect(() => { - const appEvents = getAppEvents(); - if (jobQuery.isSuccess) { - appEvents.publish({ - type: AppEvents.alertSuccess.name, - payload: [t('provisioning.sync-repository.success-pull-started', 'Pull started')], - }); - } else if (jobQuery.isError) { - appEvents.publish({ - type: AppEvents.alertError.name, - payload: [t('provisioning.sync-repository.error-pulling-resources', 'Error pulling resources'), jobQuery.error], - }); - } - }, [jobQuery.error, jobQuery.isError, jobQuery.isSuccess]); - const onClick = () => { if (!name) { return; diff --git a/public/app/features/provisioning/Wizard/WizardContent.tsx b/public/app/features/provisioning/Wizard/WizardContent.tsx index b2e4953a4b8..affac97e597 100644 --- a/public/app/features/provisioning/Wizard/WizardContent.tsx +++ b/public/app/features/provisioning/Wizard/WizardContent.tsx @@ -94,19 +94,9 @@ export function WizardContent({ const handleRepositoryDeletion = async (name: string) => { try { await deleteRepository({ name }); - appEvents.publish({ - type: AppEvents.alertSuccess.name, - payload: [t('provisioning.wizard-content.success-repository-deleted', 'Repository deleted')], - }); // Wait before redirecting to ensure deletion is indexed setTimeout(() => navigate(PROVISIONING_URL), 1500); } catch (error) { - appEvents.publish({ - type: AppEvents.alertError.name, - payload: [ - t('provisioning.wizard-content.error-failed-to-delete', 'Failed to delete repository. Please try again.'), - ], - }); setIsCancelling(false); } }; @@ -168,10 +158,6 @@ export function WizardContent({ const newName = saveRequest.data?.metadata?.name; if (newName) { setValue('repositoryName', newName); - appEvents.publish({ - type: AppEvents.alertSuccess.name, - payload: [t('provisioning.wizard-content.success-repository-saved', 'Repository saved')], - }); handleStatusChange(true); } } else if (saveRequest.isError) { diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index 5ab2431e54b..0b39266d6f4 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -5054,6 +5054,7 @@ }, "config-form": { "alert-repository-settings-saved": "Repository settings saved", + "alert-repository-settings-updated": "Repository settings updated", "button-save": "Save", "button-saving": "Saving...", "description-branch": "Create a branch (and pull request) for changes", @@ -5064,6 +5065,7 @@ "description-workflows-makes-repository": "No workflows makes the repository read only", "description-write": "Allow writing updates to the remote repository", "error-required": "This field is required.", + "error-save-repository": "Failed to save repository settings", "error-valid-github-url": "Please enter a valid GitHub repository URL", "label-automatic-pulling": "Automatic pulling", "label-branch": "Branch", @@ -5137,6 +5139,7 @@ "delete-repository-button": { "button-delete": "Delete", "confirm-delete-repository": "Are you sure you want to delete the repository config?", + "error-repository-delete": "Failed to delete repository", "success-repository-deleted": "Repository settings queued for deletion", "title-delete-repository": "Delete repository config", "tooltip-delete-this-repository": "Delete this repository" @@ -5235,6 +5238,7 @@ "button-delete-repositories": "Delete repositories", "configured-repositories-while-running-legacy-storage": "Configured repositories will not work while running legacy storage.", "confirm-delete-repositories": "Are you sure you want to delete all configured repositories? This action cannot be undone.", + "error-delete-all-repositories": "Failed to delete all repositories", "remove-all-configured-repositories": "Remove all configured repositories", "subtitle": "View and manage your configured repositories", "success-all-repositories-deleted": "All configured repositories deleted", @@ -5403,10 +5407,7 @@ "button-cancel": "Cancel", "button-cancelling": "Cancelling...", "button-submitting": "Submitting...", - "error-failed-to-delete": "Failed to delete repository. Please try again.", "error-instance-repository-exists": "Instance repository already exists", - "success-repository-deleted": "Repository deleted", - "success-repository-saved": "Repository saved", "title-repository-verification-failed": "Repository verification failed" } },