diff --git a/.betterer.results b/.betterer.results index 953b94bc7b5..bd77ff7e3a3 100644 --- a/.betterer.results +++ b/.betterer.results @@ -1817,11 +1817,6 @@ exports[`better eslint`] = { [0, 0, 0, "No untranslated strings. Wrap text with ", "3"], [0, 0, 0, "No untranslated strings. Wrap text with ", "4"] ], - "public/app/features/alerting/unified/components/rule-editor/FolderSelector.tsx:5381": [ - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "0"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "1"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "2"] - ], "public/app/features/alerting/unified/components/rule-editor/GrafanaEvaluationBehavior.tsx:5381": [ [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "0"], [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "1"], diff --git a/public/app/features/alerting/unified/components/create-folder/CreateNewFolder.tsx b/public/app/features/alerting/unified/components/create-folder/CreateNewFolder.tsx new file mode 100644 index 00000000000..962a4d23cd9 --- /dev/null +++ b/public/app/features/alerting/unified/components/create-folder/CreateNewFolder.tsx @@ -0,0 +1,111 @@ +import { css } from '@emotion/css'; +import { useState } from 'react'; + +import { GrafanaTheme2 } from '@grafana/data'; +import { selectors } from '@grafana/e2e-selectors'; +import { Button, Field, Input, Label, Modal, Stack, useStyles2 } from '@grafana/ui'; +import { useAppNotification } from 'app/core/copy/appNotification'; +import { contextSrv } from 'app/core/core'; +import { Trans, t } from 'app/core/internationalization'; +import { useNewFolderMutation } from 'app/features/browse-dashboards/api/browseDashboardsAPI'; +import { AccessControlAction } from 'app/types'; + +import { Folder } from '../../types/rule-form'; + +/** + * Provides a button and associated modal for creating a new folder + */ +export const CreateNewFolder = ({ onCreate }: { onCreate: (folder: Folder) => void }) => { + const [isCreatingFolder, setIsCreatingFolder] = useState(false); + const handleCreate = (folder: Folder) => { + onCreate(folder); + setIsCreatingFolder(false); + }; + return ( + <> + + {isCreatingFolder && setIsCreatingFolder(false)} />} + + ); +}; + +function FolderCreationModal({ + onClose, + onCreate, +}: { + onClose: () => void; + onCreate: (folder: Folder) => void; +}): React.ReactElement { + const styles = useStyles2(getStyles); + const notifyApp = useAppNotification(); + const [title, setTitle] = useState(''); + const [createFolder] = useNewFolderMutation(); + + const onSubmit = async () => { + const { data, error } = await createFolder({ title }); + + if (error) { + notifyApp.error('Failed to create folder'); + } else if (data) { + onCreate({ title: data.title, uid: data.uid }); + notifyApp.success('Folder created'); + } + }; + + return ( + + + + Folder name + + } + > + setTitle(e.currentTarget.value)} + /> + + + + + + + + + ); +} + +const getStyles = (theme: GrafanaTheme2) => ({ + modal: css({ + width: `${theme.breakpoints.values.sm}px`, + }), +}); diff --git a/public/app/features/alerting/unified/components/import-to-gma/ConfirmConvertModal.tsx b/public/app/features/alerting/unified/components/import-to-gma/ConfirmConvertModal.tsx index 291484cac65..84bb5ed4713 100644 --- a/public/app/features/alerting/unified/components/import-to-gma/ConfirmConvertModal.tsx +++ b/public/app/features/alerting/unified/components/import-to-gma/ConfirmConvertModal.tsx @@ -4,7 +4,7 @@ import { ComponentProps } from 'react'; import { useFormContext } from 'react-hook-form'; import { locationService } from '@grafana/runtime'; -import { CodeEditor, ConfirmModal, Icon, Stack, Text, useStyles2 } from '@grafana/ui'; +import { Alert, CodeEditor, ConfirmModal, Stack, Text, useStyles2 } from '@grafana/ui'; import { useAppNotification } from 'app/core/copy/appNotification'; import { Trans, t } from 'app/core/internationalization'; import { stringifyErrorLike } from 'app/features/alerting/unified/utils/misc'; @@ -90,15 +90,14 @@ export const ConfirmConversionModal = ({ isOpen, onDismiss }: ModalProps) => { confirmButtonVariant="primary" body={ - - - + + + + If the target folder is not empty, some rules may be overwritten or removed. Are you sure you want to + import these alert rules to Grafana-managed rules? + - - If the target folder is not empty, some rules may be overwritten or removed. Are you sure you want to - import these alert rules to Grafana-managed rules? - - + These are the list of rules that will be imported: diff --git a/public/app/features/alerting/unified/components/import-to-gma/ImportFromDSRules.tsx b/public/app/features/alerting/unified/components/import-to-gma/ImportFromDSRules.tsx index 1609f13d76b..938aeff00df 100644 --- a/public/app/features/alerting/unified/components/import-to-gma/ImportFromDSRules.tsx +++ b/public/app/features/alerting/unified/components/import-to-gma/ImportFromDSRules.tsx @@ -16,12 +16,16 @@ import { Text, } from '@grafana/ui'; import { NestedFolderPicker } from 'app/core/components/NestedFolderPicker/NestedFolderPicker'; +import { useQueryParams } from 'app/core/hooks/useQueryParams'; import { Trans, t } from 'app/core/internationalization'; import { DataSourcePicker } from 'app/features/datasources/components/picker/DataSourcePicker'; +import { useDatasource } from 'app/features/datasources/hooks'; import { Folder } from '../../types/rule-form'; +import { DataSourceType } from '../../utils/datasource'; import { withPageErrorBoundary } from '../../withPageErrorBoundary'; import { AlertingPageWrapper } from '../AlertingPageWrapper'; +import { CreateNewFolder } from '../create-folder/CreateNewFolder'; import { CloudRulesSourcePicker } from '../rule-editor/CloudRulesSourcePicker'; import { ConfirmConversionModal } from './ConfirmConvertModal'; @@ -38,11 +42,22 @@ export interface ImportFormValues { targetDatasourceUID?: string; } +export const supportedImportTypes: string[] = [DataSourceType.Prometheus, DataSourceType.Loki]; + const ImportFromDSRules = () => { + const [queryParams] = useQueryParams(); + const queryParamSelectedDatasourceUID: string = String(queryParams.datasourceUid) || ''; + const defaultDataSourceSettings = useDatasource(queryParamSelectedDatasourceUID); + // useDatasource gets the default data source as a fallback, so we need to check if it's the right type + // before trying to use it + const defaultDataSource = supportedImportTypes.includes(defaultDataSourceSettings?.type || '') + ? defaultDataSourceSettings + : undefined; + const formAPI = useForm({ defaultValues: { - selectedDatasourceUID: undefined, - selectedDatasourceName: '', + selectedDatasourceUID: defaultDataSource?.uid, + selectedDatasourceName: defaultDataSource?.name, pauseAlertingRules: true, pauseRecordingRules: true, targetFolder: undefined, @@ -58,7 +73,7 @@ const ImportFromDSRules = () => { formState: { errors, isSubmitting }, } = formAPI; - const [optionsShowing, toggleOptions] = useToggle(false); + const [optionsShowing, toggleOptions] = useToggle(true); const [targetFolder, selectedDatasourceName] = watch(['targetFolder', 'selectedDatasourceName']); const [showConfirmModal, setShowConfirmModal] = useToggle(false); @@ -70,7 +85,7 @@ const ImportFromDSRules = () => { @@ -92,6 +107,9 @@ const ImportFromDSRules = () => { onChange={(ds: DataSourceInstanceSettings) => { setValue('selectedDatasourceUID', ds.uid); setValue('selectedDatasourceName', ds.name); + // If we've chosen a Prometheus data source, we can set the recording rules target data source to the same as the source + const targetDataSourceUID = ds.type === DataSourceType.Prometheus ? ds.uid : undefined; + setValue('targetDatasourceUID', targetDataSourceUID); }} /> )} @@ -99,7 +117,7 @@ const ImportFromDSRules = () => { rules={{ required: { value: true, - message: t('alerting.import-to-gma.datasource.required-message', 'Please select a datasource'), + message: t('alerting.import-to-gma.datasource.required-message', 'Please select a data source'), }, }} control={control} @@ -107,7 +125,7 @@ const ImportFromDSRules = () => { { error={errors.selectedDatasourceName?.message} htmlFor="folder-picker" > - ( - - { - if (uid && title) { - setValue('targetFolder', { title, uid }); - } else { - setValue('targetFolder', undefined); - } - }} - /> - - )} - name="targetFolder" - control={control} - /> + + ( + + { + if (uid && title) { + setValue('targetFolder', { title, uid }); + } else { + setValue('targetFolder', undefined); + } + }} + /> + + )} + name="targetFolder" + control={control} + /> + { + setValue('targetFolder', folder); + }} + /> + @@ -189,7 +214,7 @@ const ImportFromDSRules = () => { { )} name="targetDatasourceUID" control={control} + rules={{ + required: { value: true, message: 'Please select a target data source' }, + }} /> diff --git a/public/app/features/alerting/unified/components/rule-editor/FolderSelector.tsx b/public/app/features/alerting/unified/components/rule-editor/FolderSelector.tsx index e81d18e251c..0010aaa10fe 100644 --- a/public/app/features/alerting/unified/components/rule-editor/FolderSelector.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/FolderSelector.tsx @@ -1,19 +1,13 @@ -import { css } from '@emotion/css'; -import * as React from 'react'; -import { useCallback, useState } from 'react'; +import { useCallback } from 'react'; import { Controller, useFormContext } from 'react-hook-form'; -import { GrafanaTheme2 } from '@grafana/data'; -import { selectors } from '@grafana/e2e-selectors'; -import { Button, Field, Input, Label, Modal, Stack, Text, useStyles2 } from '@grafana/ui'; +import { Field, Label, Stack } from '@grafana/ui'; import { NestedFolderPicker } from 'app/core/components/NestedFolderPicker/NestedFolderPicker'; -import { useAppNotification } from 'app/core/copy/appNotification'; -import { contextSrv } from 'app/core/services/context_srv'; -import { useNewFolderMutation } from 'app/features/browse-dashboards/api/browseDashboardsAPI'; -import { AccessControlAction } from 'app/types'; +import { t } from 'app/core/internationalization'; import { Trans } from '../../../../../core/internationalization/index'; import { Folder, RuleFormValues } from '../../types/rule-form'; +import { CreateNewFolder } from '../create-folder/CreateNewFolder'; export function FolderSelector() { const { @@ -26,161 +20,61 @@ export function FolderSelector() { setValue('group', ''); }, [setValue]); - const [isCreatingFolder, setIsCreatingFolder] = useState(false); const folder = watch('folder'); - const onOpenFolderCreationModal = () => setIsCreatingFolder(true); - const handleFolderCreation = (folder: Folder) => { resetGroup(); setValue('folder', folder); - setIsCreatingFolder(false); }; return ( - <> - - { - - Folder - - } - error={errors.folder?.message} - data-testid="folder-picker" - > - - {(!isCreatingFolder && ( - <> - ( -
- { - if (uid && title) { - setValue('folder', { title, uid }); - } else { - setValue('folder', undefined); - } + + { + + Folder + + } + error={errors.folder?.message} + data-testid="folder-picker" + > + + ( +
+ { + if (uid && title) { + setValue('folder', { title, uid }); + } else { + setValue('folder', undefined); + } - resetGroup(); - }} - /> -
- )} - name="folder" - rules={{ - required: { value: true, message: 'Select a folder' }, + resetGroup(); }} /> - - or - - - - )) || ( -
- Creating new folder - {'...'}
)} -
-
- } -
- - {isCreatingFolder && ( - setIsCreatingFolder(false)} /> - )} - - ); -} - -function FolderCreationModal({ - onClose, - onCreate, -}: { - onClose: () => void; - onCreate: (folder: Folder) => void; -}): React.ReactElement { - const styles = useStyles2(getStyles); - const notifyApp = useAppNotification(); - const [title, setTitle] = useState(''); - const [createFolder] = useNewFolderMutation(); - - const onSubmit = async () => { - const { data, error } = await createFolder({ title }); - - if (error) { - notifyApp.error('Failed to create folder'); - } else if (data) { - onCreate({ title: data.title, uid: data.uid }); - notifyApp.success('Folder created'); - } - }; - - return ( - - - - - Create a new folder to store your alert rule in. - - - -
- - Folder name - - } - > - setTitle(e.currentTarget.value)} + name="folder" + rules={{ + required: { value: true, message: 'Select a folder' }, + }} /> - - - - - - -
-
-
+ + + + } + ); } - -const getStyles = (theme: GrafanaTheme2) => ({ - modal: css({ - width: `${theme.breakpoints.values.sm}px`, - }), -}); diff --git a/public/app/features/alerting/unified/rule-list/components/DataSourceSection.tsx b/public/app/features/alerting/unified/rule-list/components/DataSourceSection.tsx index 11f4f5cbf01..253db053699 100644 --- a/public/app/features/alerting/unified/rule-list/components/DataSourceSection.tsx +++ b/public/app/features/alerting/unified/rule-list/components/DataSourceSection.tsx @@ -10,6 +10,8 @@ import { RulesSourceApplication } from 'app/types/unified-alerting-dto'; import { Spacer } from '../../components/Spacer'; import { WithReturnButton } from '../../components/WithReturnButton'; +import { supportedImportTypes } from '../../components/import-to-gma/ImportFromDSRules'; +import { useRulesSourcesWithRuler } from '../../hooks/useRuleSourcesWithRuler'; import { isAdmin } from '../../utils/misc'; import { DataSourceIcon } from './Namespace'; @@ -34,6 +36,12 @@ export const DataSourceSection = ({ description = null, }: DataSourceSectionProps) => { const styles = useStyles2(getStyles); + const { rulesSourcesWithRuler } = useRulesSourcesWithRuler(); + + const showImportLink = + uid !== GrafanaRulesSourceSymbol && + rulesSourcesWithRuler.some(({ uid: dsUid, type }) => dsUid === uid && supportedImportTypes.includes(type)); + const [isCollapsed, toggleCollapsed] = useToggle(false); const configureLink = (() => { if (uid === GrafanaRulesSourceSymbol) { @@ -70,6 +78,16 @@ export const DataSourceSection = ({ )} + {showImportLink && ( + + Import to Grafana rules + + )} {configureLink && (