diff --git a/.betterer.results b/.betterer.results index 0cbbf2c2539..2f9307b0639 100644 --- a/.betterer.results +++ b/.betterer.results @@ -2886,8 +2886,7 @@ exports[`better eslint`] = { [0, 0, 0, "No untranslated strings. Wrap text with ", "2"], [0, 0, 0, "No untranslated strings. Wrap text with ", "3"], [0, 0, 0, "No untranslated strings. Wrap text with ", "4"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "5"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "6"] + [0, 0, 0, "No untranslated strings. Wrap text with ", "5"] ], "public/app/features/dashboard-scene/settings/JsonModelEditView.tsx:5381": [ [0, 0, 0, "No untranslated strings. Wrap text with ", "0"], @@ -4097,7 +4096,9 @@ exports[`better eslint`] = { [0, 0, 0, "No untranslated strings. Wrap text with ", "4"], [0, 0, 0, "No untranslated strings. Wrap text with ", "5"], [0, 0, 0, "No untranslated strings. Wrap text with ", "6"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "7"] + [0, 0, 0, "No untranslated strings. Wrap text with ", "7"], + [0, 0, 0, "No untranslated strings. Wrap text with ", "8"], + [0, 0, 0, "No untranslated strings. Wrap text with ", "9"] ], "public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/AccordianReferences.tsx:5381": [ [0, 0, 0, "Styles should be written using objects.", "0"], @@ -4679,12 +4680,7 @@ exports[`better eslint`] = { [0, 0, 0, "Unexpected any. Specify a different type.", "5"], [0, 0, 0, "Unexpected any. Specify a different type.", "6"], [0, 0, 0, "Unexpected any. Specify a different type.", "7"], - [0, 0, 0, "Unexpected any. Specify a different type.", "8"], - [0, 0, 0, "Unexpected any. Specify a different type.", "9"], - [0, 0, 0, "Unexpected any. Specify a different type.", "10"], - [0, 0, 0, "Unexpected any. Specify a different type.", "11"], - [0, 0, 0, "Unexpected any. Specify a different type.", "12"], - [0, 0, 0, "Unexpected any. Specify a different type.", "13"] + [0, 0, 0, "Unexpected any. Specify a different type.", "8"] ], "public/app/features/manage-dashboards/state/reducers.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"], diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 5bc3cab07ce..eeb90d4556a 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -403,7 +403,6 @@ playwright.config.ts @grafana/plugins-platform-frontend /public/app/core/components/TimelineChart/ @grafana/dataviz-squad /public/app/core/components/Form/ @grafana/grafana-frontend-platform /public/app/core/history/ @grafana/explore-squad -/public/app/features/all.ts @grafana/grafana-frontend-platform /public/app/features/admin/ @grafana/identity-access-team # Temp owners until Enterprise team takes over diff --git a/docs/sources/alerting/alerting-rules/create-grafana-managed-rule.md b/docs/sources/alerting/alerting-rules/create-grafana-managed-rule.md index 66d2e30c660..af1d74c7294 100644 --- a/docs/sources/alerting/alerting-rules/create-grafana-managed-rule.md +++ b/docs/sources/alerting/alerting-rules/create-grafana-managed-rule.md @@ -83,7 +83,7 @@ Grafana-managed rules are the most flexible alert rule type. They allow you to c Multiple alert instances can be created as a result of one alert rule (also known as a multi-dimensional alerting). {{% admonition type="note" %}} -For Grafana Cloud, you can create 100 free Grafana-managed alert rules. +For Grafana Cloud Free Forever, you can create up to 100 free Grafana-managed alert rules with each alert rule having a maximum of 1000 alert instances. {{% /admonition %}} Grafana managed alert rules can only be edited or deleted by users with Edit permissions for the folder storing the rules. diff --git a/docs/sources/alerting/set-up/configure-high-availability/_index.md b/docs/sources/alerting/set-up/configure-high-availability/_index.md index 45a4e65c52e..6df014f9808 100644 --- a/docs/sources/alerting/set-up/configure-high-availability/_index.md +++ b/docs/sources/alerting/set-up/configure-high-availability/_index.md @@ -190,3 +190,13 @@ Note that these alerting high availability metrics are exposed via the `/metrics ``` For more information on monitoring alerting metrics, refer to [Alerting meta-monitoring](ref:meta-monitoring). For a demo, see [alerting high availability examples using Docker Compose](https://github.com/grafana/alerting-ha-docker-examples/). + +## Prevent duplicate notifications + +In high-availability mode, each Grafana instance runs its own pre-configured alertmanager to handle alert notifications. + +When multiple Grafana instances are running, all alert rules are evaluated on each instance. By default, each instance sends firing alerts to its respective alertmanager. This results in notification handling being duplicated across all running Grafana instances. + +Alertmanagers in HA mode communicate with each other to coordinate notification delivery. However, this setup can sometimes lead to duplicated or out-of-order notifications. By design, HA prioritizes sending duplicate notifications over the risk of missing notifications. + +To avoid duplicate notifications, you can configure a shared alertmanager to manage notifications for all Grafana instances. For more information, refer to [add an external alertmanager](/docs/grafana//alerting/set-up/configure-alertmanager/). diff --git a/packages/grafana-data/src/types/trace.ts b/packages/grafana-data/src/types/trace.ts index 27b16a9f73c..8346f07dfb9 100644 --- a/packages/grafana-data/src/types/trace.ts +++ b/packages/grafana-data/src/types/trace.ts @@ -13,6 +13,7 @@ export type TraceLog = { // Millisecond epoch time timestamp: number; fields: TraceKeyValuePair[]; + name?: string; }; export type TraceSpanReference = { diff --git a/packages/grafana-ui/src/components/Combobox/Combobox.test.tsx b/packages/grafana-ui/src/components/Combobox/Combobox.test.tsx index b91cde4a618..2263618d601 100644 --- a/packages/grafana-ui/src/components/Combobox/Combobox.test.tsx +++ b/packages/grafana-ui/src/components/Combobox/Combobox.test.tsx @@ -37,7 +37,7 @@ describe('Combobox', () => { render(); const input = screen.getByRole('combobox'); - userEvent.click(input); + await userEvent.click(input); const item = await screen.findByRole('option', { name: 'Option 1' }); await userEvent.click(item); diff --git a/pkg/services/accesscontrol/resourcepermissions/store.go b/pkg/services/accesscontrol/resourcepermissions/store.go index 9ded47f4ab0..5e42f7b2784 100644 --- a/pkg/services/accesscontrol/resourcepermissions/store.go +++ b/pkg/services/accesscontrol/resourcepermissions/store.go @@ -725,7 +725,7 @@ func (s *store) createPermissions(sess *db.Session, roleID int64, cmd SetResourc } func (s *store) shouldStoreActionSet(resource, permission string) bool { - if !(s.features.IsEnabled(context.TODO(), featuremgmt.FlagAccessActionSets) && permission != "") { + if permission == "" { return false } actionSetName := GetActionSetName(resource, permission) diff --git a/pkg/tsdb/tempo/trace_transform.go b/pkg/tsdb/tempo/trace_transform.go index 0c3bf0321c2..a7d0f3c2dea 100644 --- a/pkg/tsdb/tempo/trace_transform.go +++ b/pkg/tsdb/tempo/trace_transform.go @@ -22,6 +22,7 @@ type TraceLog struct { // Millisecond epoch time Timestamp float64 `json:"timestamp"` Fields []*KeyValue `json:"fields"` + Name string `json:"name,omitempty"` } type TraceReference struct { @@ -260,12 +261,6 @@ func spanEventsToLogs(events ptrace.SpanEventSlice) []*TraceLog { for i := 0; i < events.Len(); i++ { event := events.At(i) fields := make([]*KeyValue, 0, event.Attributes().Len()+1) - if event.Name() != "" { - fields = append(fields, &KeyValue{ - Key: TagMessage, - Value: event.Name(), - }) - } event.Attributes().Range(func(key string, attr pcommon.Value) bool { fields = append(fields, &KeyValue{Key: key, Value: getAttributeVal(attr)}) return true @@ -273,6 +268,7 @@ func spanEventsToLogs(events ptrace.SpanEventSlice) []*TraceLog { logs = append(logs, &TraceLog{ Timestamp: float64(event.Timestamp()) / 1_000_000, Fields: fields, + Name: event.Name(), }) } diff --git a/pkg/tsdb/tempo/trace_transform_test.go b/pkg/tsdb/tempo/trace_transform_test.go index 8b901c6c41c..8253220655a 100644 --- a/pkg/tsdb/tempo/trace_transform_test.go +++ b/pkg/tsdb/tempo/trace_transform_test.go @@ -9,6 +9,7 @@ import ( "go.opentelemetry.io/collector/pdata/ptrace" "github.com/grafana/grafana-plugin-sdk-go/data" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -57,7 +58,30 @@ func TestTraceToFrame(t *testing.T) { require.Equal(t, json.RawMessage("[{\"value\":\"loki-all\",\"key\":\"service.name\"},{\"value\":\"Jaeger-Go-2.25.0\",\"key\":\"opencensus.exporterversion\"},{\"value\":\"4d019a031941\",\"key\":\"host.hostname\"},{\"value\":\"172.18.0.6\",\"key\":\"ip\"},{\"value\":\"4b19ace06df8e4de\",\"key\":\"client-uuid\"}]"), span["serviceTags"]) require.Equal(t, 1616072924072.852, span["startTime"]) require.Equal(t, 0.094, span["duration"]) - require.Equal(t, "[{\"timestamp\":1616072924072.856,\"fields\":[{\"value\":\"test event\",\"key\":\"message\"},{\"value\":1,\"key\":\"chunks requested\"}]},{\"timestamp\":1616072924072.9448,\"fields\":[{\"value\":1,\"key\":\"chunks fetched\"}]}]", string(span["logs"].(json.RawMessage))) + expectedLogs := ` + [ + { + "timestamp": 1616072924072.856, + "name": "test event", + "fields": [ + { + "value": 1, + "key": "chunks requested" + } + ] + }, + { + "timestamp": 1616072924072.9448, + "fields": [ + { + "value": 1, + "key": "chunks fetched" + } + ] + } + ] + ` + assert.JSONEq(t, expectedLogs, string(span["logs"].(json.RawMessage))) }) t.Run("should transform correct traceID", func(t *testing.T) { diff --git a/public/app/app.ts b/public/app/app.ts index 6e6141e0d86..bc5dd608787 100644 --- a/public/app/app.ts +++ b/public/app/app.ts @@ -6,8 +6,6 @@ import 'file-saver'; import 'jquery'; import 'vendor/bootstrap/bootstrap'; -import 'app/features/all'; - import _ from 'lodash'; // eslint-disable-line lodash/import-scope import { createElement } from 'react'; import { createRoot } from 'react-dom/client'; diff --git a/public/app/features/alerting/unified/Silences.test.tsx b/public/app/features/alerting/unified/Silences.test.tsx index 204ed9f9bbd..a0e6303c1e3 100644 --- a/public/app/features/alerting/unified/Silences.test.tsx +++ b/public/app/features/alerting/unified/Silences.test.tsx @@ -304,6 +304,14 @@ describe('Silence create/edit', () => { TEST_TIMEOUT ); + it('works when previewing alerts with spaces in label name', async () => { + renderSilences(`${baseUrlPath}?alertmanager=${GRAFANA_RULES_SOURCE_NAME}`); + + await enterSilenceLabel(0, 'label with spaces', MatcherOperator.equal, 'value with spaces'); + + expect((await screen.findAllByTestId('row'))[0]).toBeInTheDocument(); + }); + it('shows an error when existing silence cannot be found', async () => { renderSilences('/alerting/silence/foo-bar/edit'); diff --git a/public/app/features/alerting/unified/api/alertmanagerApi.ts b/public/app/features/alerting/unified/api/alertmanagerApi.ts index fa93bfd0e2d..c7ede90eea5 100644 --- a/public/app/features/alerting/unified/api/alertmanagerApi.ts +++ b/public/app/features/alerting/unified/api/alertmanagerApi.ts @@ -72,7 +72,9 @@ export const alertmanagerApi = alertingApi.injectEndpoints({ // TODO Add support for active, silenced, inhibited, unprocessed filters const filterMatchers = filter?.matchers ?.filter((matcher) => matcher.name && matcher.value) - .map((matcher) => `${matcher.name}${matcherToOperator(matcher)}${wrapWithQuotes(matcher.value)}`); + .map( + (matcher) => `${wrapWithQuotes(matcher.name)}${matcherToOperator(matcher)}${wrapWithQuotes(matcher.value)}` + ); const { silenced, inhibited, unprocessed, active } = filter || {}; diff --git a/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/AlertRuleForm.tsx b/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/AlertRuleForm.tsx index 39ac757ad67..22eda969175 100644 --- a/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/AlertRuleForm.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/AlertRuleForm.tsx @@ -14,6 +14,7 @@ import InfoPausedRule from 'app/features/alerting/unified/components/InfoPausedR import { getRuleGroupLocationFromFormValues, getRuleGroupLocationFromRuleWithLocation, + isCloudRulerRule, isGrafanaManagedRuleByType, isGrafanaRulerRule, isGrafanaRulerRulePaused, @@ -42,7 +43,7 @@ import { formValuesToRulerGrafanaRuleDTO, formValuesToRulerRuleDTO, } from '../../../utils/rule-form'; -import { fromRulerRuleAndRuleGroupIdentifier } from '../../../utils/rule-id'; +import { fromRulerRule, fromRulerRuleAndRuleGroupIdentifier, stringifyIdentifier } from '../../../utils/rule-id'; import { GrafanaRuleExporter } from '../../export/GrafanaRuleExporter'; import { AlertRuleNameAndMetric } from '../AlertRuleNameInput'; import AnnotationsStep from '../AnnotationsStep'; @@ -167,6 +168,10 @@ export const AlertRuleForm = ({ existing, prefill }: Props) => { if (exitOnSave && returnTo) { locationService.push(returnTo); + } else if (isCloudRulerRule(ruleDefinition)) { + const { dataSourceName, namespaceName, groupName } = getRuleGroupLocationFromFormValues(values); + const updatedRuleIdentifier = fromRulerRule(dataSourceName, namespaceName, groupName, ruleDefinition); + locationService.replace(`/alerting/${encodeURIComponent(stringifyIdentifier(updatedRuleIdentifier))}/edit`); } }; diff --git a/public/app/features/alerting/unified/components/silences/SilencedInstancesPreview.tsx b/public/app/features/alerting/unified/components/silences/SilencedInstancesPreview.tsx index 49235dd22e4..1e1b85da218 100644 --- a/public/app/features/alerting/unified/components/silences/SilencedInstancesPreview.tsx +++ b/public/app/features/alerting/unified/components/silences/SilencedInstancesPreview.tsx @@ -66,7 +66,7 @@ export const SilencedInstancesPreview = ({ amSourceName, matchers: inputMatchers if (isError) { return ( - Error occured when generating preview of affected alerts. Are your matchers valid? + Error occurred when generating preview of affected alerts. Are your matchers valid? ); } diff --git a/public/app/features/alerting/unified/mocks/server/handlers/alertmanagers.ts b/public/app/features/alerting/unified/mocks/server/handlers/alertmanagers.ts index 37f7f8e1c46..6677e2cace2 100644 --- a/public/app/features/alerting/unified/mocks/server/handlers/alertmanagers.ts +++ b/public/app/features/alerting/unified/mocks/server/handlers/alertmanagers.ts @@ -12,8 +12,37 @@ export const grafanaAlertingConfigurationStatusHandler = ( response = defaultGrafanaAlertingConfigurationStatusResponse ) => http.get('/api/v1/ngalert', () => HttpResponse.json(response)); +const getInvalidMatcher = (matchers: string[]) => { + return matchers.find((matcher) => { + const split = matcher.split('='); + try { + // Try and parse as JSON, as this will fail if + // we've failed to wrap the label value in quotes + // (e.g. `foo space` can't be parsed, but `"foo space"` can) + JSON.parse(split[0]); + return false; + } catch (e) { + return true; + } + }); +}; + export const alertmanagerAlertsListHandler = () => - http.get<{ datasourceUid: string }>('/api/alertmanager/:datasourceUid/api/v2/alerts', ({ params }) => { + http.get<{ datasourceUid: string }>('/api/alertmanager/:datasourceUid/api/v2/alerts', ({ params, request }) => { + const matchers = new URL(request.url).searchParams.getAll('filter'); + + const invalidMatcher = getInvalidMatcher(matchers); + + if (invalidMatcher) { + return HttpResponse.json( + { + message: `bad matcher format: ${invalidMatcher}: unable to retrieve alerts`, + traceID: '', + }, + { status: 400 } + ); + } + if (params.datasourceUid === MOCK_DATASOURCE_UID_BROKEN_ALERTMANAGER) { return HttpResponse.json({ traceId: '' }, { status: 502 }); } diff --git a/public/app/features/all.ts b/public/app/features/all.ts deleted file mode 100644 index b25b7b50951..00000000000 --- a/public/app/features/all.ts +++ /dev/null @@ -1,2 +0,0 @@ -import './plugins/all'; -import './dashboard'; diff --git a/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataAlertingTab.test.tsx b/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataAlertingTab.test.tsx index 1726c669031..bfc51341d6c 100644 --- a/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataAlertingTab.test.tsx +++ b/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataAlertingTab.test.tsx @@ -143,7 +143,7 @@ const dashboard = { from: 'now-6h', to: 'now', }, - timepicker: { refresh_intervals: 5 }, + timepicker: { refresh_intervals: ['5s', '30s', '1m'] }, meta: { canSave: true, folderId: 1, diff --git a/public/app/features/dashboard-scene/panel-edit/PanelVizTypePicker.tsx b/public/app/features/dashboard-scene/panel-edit/PanelVizTypePicker.tsx index e4a8482622c..0da983abab6 100644 --- a/public/app/features/dashboard-scene/panel-edit/PanelVizTypePicker.tsx +++ b/public/app/features/dashboard-scene/panel-edit/PanelVizTypePicker.tsx @@ -3,7 +3,8 @@ import { useEffect, useMemo, useState } from 'react'; import { useLocalStorage } from 'react-use'; import { GrafanaTheme2, PanelData, SelectableValue } from '@grafana/data'; -import { CustomScrollbar, Field, FilterInput, RadioButtonGroup, useStyles2 } from '@grafana/ui'; +import { selectors } from '@grafana/e2e-selectors'; +import { Button, CustomScrollbar, Field, FilterInput, RadioButtonGroup, useStyles2 } from '@grafana/ui'; import { LS_VISUALIZATION_SELECT_TAB_KEY, LS_WIDGET_SELECT_TAB_KEY } from 'app/core/constants'; import { VisualizationSelectPaneTab } from 'app/features/dashboard/components/PanelEditor/types'; import { VisualizationSuggestions } from 'app/features/panel/components/VizTypePicker/VisualizationSuggestions'; @@ -61,15 +62,29 @@ export function PanelVizTypePicker({ vizManager, data, onChange }: Props) { onChange(); }; + const onCloseVizPicker = () => { + onChange(); + }; + return (
- +
+ +
@@ -106,6 +121,13 @@ const getStyles = (theme: GrafanaTheme2) => ({ borderBottom: 'none', borderTopLeftRadius: theme.shape.radius.default, }), + searchRow: css({ + display: 'flex', + marginBottom: theme.spacing(1), + }), + closeButton: css({ + marginLeft: theme.spacing(1), + }), customFieldMargin: css({ marginBottom: theme.spacing(1), }), diff --git a/public/app/features/dashboard-scene/scene/DashboardScene.test.tsx b/public/app/features/dashboard-scene/scene/DashboardScene.test.tsx index 9e2ca410700..10fb8788a03 100644 --- a/public/app/features/dashboard-scene/scene/DashboardScene.test.tsx +++ b/public/app/features/dashboard-scene/scene/DashboardScene.test.tsx @@ -1000,7 +1000,7 @@ describe('DashboardScene', () => { scene.setState({ isDirty: true }); locationService.push('/d/adsdas'); - await scene.deleteDashboard(); + await scene.onDashboardDelete(); expect(scene.state.isDirty).toBe(false); }); diff --git a/public/app/features/dashboard-scene/scene/DashboardScene.tsx b/public/app/features/dashboard-scene/scene/DashboardScene.tsx index 13f1069c653..3378af54647 100644 --- a/public/app/features/dashboard-scene/scene/DashboardScene.tsx +++ b/public/app/features/dashboard-scene/scene/DashboardScene.tsx @@ -34,7 +34,6 @@ import store from 'app/core/store'; import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv'; import { DashboardModel, PanelModel } from 'app/features/dashboard/state'; import { dashboardWatcher } from 'app/features/live/dashboard/dashboardWatcher'; -import { deleteDashboard } from 'app/features/manage-dashboards/state/actions'; import { getClosestScopesFacade, ScopesFacade } from 'app/features/scopes'; import { VariablesChanged } from 'app/features/variables/types'; import { DashboardDTO, DashboardMeta, KioskMode, SaveDashboardResponseDTO } from 'app/types'; @@ -891,8 +890,7 @@ export class DashboardScene extends SceneObjectBase { this._initialSaveModel = saveModel; } - public async deleteDashboard() { - await deleteDashboard(this.state.uid!, true); + public async onDashboardDelete() { // Need to mark it non dirty to navigate away without unsaved changes warning this.setState({ isDirty: false }); locationService.replace('/'); diff --git a/public/app/features/dashboard-scene/settings/DeleteDashboardButton.tsx b/public/app/features/dashboard-scene/settings/DeleteDashboardButton.tsx index aca70195cf1..19494d5663d 100644 --- a/public/app/features/dashboard-scene/settings/DeleteDashboardButton.tsx +++ b/public/app/features/dashboard-scene/settings/DeleteDashboardButton.tsx @@ -2,17 +2,56 @@ import { useAsyncFn, useToggle } from 'react-use'; import { selectors } from '@grafana/e2e-selectors'; import { config, reportInteraction } from '@grafana/runtime'; -import { Button, ConfirmModal, Modal } from '@grafana/ui'; -import { Trans } from 'app/core/internationalization'; +import { Button, ConfirmModal, Modal, Space, Text } from '@grafana/ui'; +import { t, Trans } from 'app/core/internationalization'; +import { useDeleteItemsMutation } from '../../browse-dashboards/api/browseDashboardsAPI'; import { DashboardScene } from '../scene/DashboardScene'; interface ButtonProps { dashboard: DashboardScene; } +interface ProvisionedDeleteModalProps { + dashboardId: string | undefined; + onClose: () => void; +} + +interface DeleteModalProps { + dashboardTitle: string; + onConfirm: () => void; + onClose: () => void; +} + export function DeleteDashboardButton({ dashboard }: ButtonProps) { const [showModal, toggleModal] = useToggle(false); + const [deleteItems] = useDeleteItemsMutation(); + + const [, onConfirm] = useAsyncFn(async () => { + reportInteraction('grafana_manage_dashboards_delete_clicked', { + item_counts: { + dashboard: 1, + }, + source: 'dashboard_scene_settings', + restore_enabled: config.featureToggles.dashboardRestoreUI, + }); + toggleModal(); + if (dashboard.state.uid) { + await deleteItems({ + selectedItems: { + dashboard: { + [dashboard.state.uid]: true, + }, + folder: {}, + }, + }); + } + await dashboard.onDashboardDelete(); + }, [dashboard, toggleModal]); + + if (dashboard.state.meta.provisioned && showModal) { + return ; + } return ( <> @@ -24,52 +63,48 @@ export function DeleteDashboardButton({ dashboard }: ButtonProps) { Delete dashboard - {showModal && } + {showModal && ( + + )} ); } -interface ModalProps { - dashboard: DashboardScene; - onClose: () => void; -} - -function DeleteDashboardModal({ dashboard, onClose }: ModalProps) { - const [, onConfirm] = useAsyncFn(async () => { - reportInteraction('grafana_manage_dashboards_delete_clicked', { - item_counts: { - dashboard: 1, - }, - source: 'dashboard_scene_settings', - restore_enabled: config.featureToggles.dashboardRestoreUI, - }); - onClose(); - await dashboard.deleteDashboard(); - }, [dashboard, onClose]); - - if (dashboard.state.meta.provisioned) { - return ; - } - +export function DeleteDashboardModal({ dashboardTitle, onConfirm, onClose }: DeleteModalProps) { return ( -

Do you want to delete this dashboard?

-

{dashboard.state.title}

+ {config.featureToggles.dashboardRestore && ( + <> + + + This action will mark the dashboard for deletion in 30 days. Your organization administrator can + restore it anytime before the 30 days expire. + + + + + )} + + Do you want to delete this dashboard? + + {dashboardTitle} + } onConfirm={onConfirm} onDismiss={onClose} - title="Delete" + title={t('dashboard-settings.delete-modal.title', 'Delete')} icon="trash-alt" - confirmText="Delete" + confirmText={t('dashboard-settings.delete-modal.delete-button', 'Delete')} + confirmationText={t('dashboard-settings.delete-modal.confirmation-text', 'Delete')} /> ); } -function ProvisionedDeleteModal({ dashboard, onClose }: ModalProps) { +function ProvisionedDeleteModal({ dashboardId, onClose }: ProvisionedDeleteModalProps) { return (

@@ -90,7 +125,7 @@ function ProvisionedDeleteModal({ dashboard, onClose }: ModalProps) { for more information about provisioning.
- File path: {dashboard.state.meta.provisionedExternalId} + File path: {dashboardId}