From acbdc1f415c032a39ce74db409aad361dd7d54ca Mon Sep 17 00:00:00 2001 From: Joey <90795735+joey-grafana@users.noreply.github.com> Date: Tue, 28 Jan 2025 08:20:23 +0000 Subject: [PATCH 01/39] Tempo: Add separate options groups (#99310) * Separate options groups * Editor row for styling --- .../traceql/TempoQueryBuilderOptions.tsx | 49 ++++++++++++++----- 1 file changed, 36 insertions(+), 13 deletions(-) diff --git a/public/app/plugins/datasource/tempo/traceql/TempoQueryBuilderOptions.tsx b/public/app/plugins/datasource/tempo/traceql/TempoQueryBuilderOptions.tsx index 25a13cc349a..f273bc788ca 100644 --- a/public/app/plugins/datasource/tempo/traceql/TempoQueryBuilderOptions.tsx +++ b/public/app/plugins/datasource/tempo/traceql/TempoQueryBuilderOptions.tsx @@ -1,7 +1,9 @@ +import { css } from '@emotion/css'; import * as React from 'react'; +import { GrafanaTheme2 } from '@grafana/data'; import { EditorField, EditorRow } from '@grafana/experimental'; -import { AutoSizeInput, RadioButtonGroup } from '@grafana/ui'; +import { AutoSizeInput, RadioButtonGroup, useStyles2 } from '@grafana/ui'; import { QueryOptionGroup } from '../_importedDependencies/datasources/prometheus/QueryOptionGroup'; import { SearchTableType } from '../dataquery.gen'; @@ -28,6 +30,8 @@ const parseIntWithFallback = (val: string, fallback: number) => { }; export const TempoQueryBuilderOptions = React.memo(({ onChange, query, isStreaming }) => { + const styles = useStyles2(getStyles); + if (!query.hasOwnProperty('limit')) { query.limit = DEFAULT_LIMIT; } @@ -60,21 +64,23 @@ export const TempoQueryBuilderOptions = React.memo(({ onChange, query, is // } // }; - const collapsedInfoList = [ + const collapsedSearchOptions = [ `Limit: ${query.limit || DEFAULT_LIMIT}`, `Spans Limit: ${query.spss || DEFAULT_SPSS}`, `Table Format: ${query.tableType === SearchTableType.Traces ? 'Traces' : 'Spans'}`, '|', - `Step: ${query.step || 'auto'}`, - // `Exemplars: ${query.exemplars !== undefined ? query.exemplars : 'auto'}`, - '|', `Streaming: ${isStreaming ? 'Enabled' : 'Disabled'}`, ]; + const collapsedMetricsOptions = [ + `Step: ${query.step || 'auto'}`, + // `Exemplars: ${query.exemplars !== undefined ? query.exemplars : 'auto'}`, + ]; + return ( - <> - - + +
+ (({ onChange, query, is onChange={onTableTypeChange} /> + } tooltipInteractive> +
{isStreaming ? 'Enabled' : 'Disabled'}
+
+
+ + (({ onChange, query, is {/* value={query.exemplars}*/} {/* />*/} {/**/} - } tooltipInteractive> -
{isStreaming ? 'Enabled' : 'Disabled'}
-
- - +
+
); }); @@ -163,3 +172,17 @@ const StreamingTooltip = () => { }; TempoQueryBuilderOptions.displayName = 'TempoQueryBuilderOptions'; + +const getStyles = (theme: GrafanaTheme2) => { + return { + options: css({ + display: 'flex', + width: '-webkit-fill-available', + gap: theme.spacing(1), + + '> div': { + width: 'auto', + }, + }), + }; +}; From 3ed138543d9ddbdf33ee5337c0febfd96998c4ce Mon Sep 17 00:00:00 2001 From: Tom Ratcliffe Date: Tue, 28 Jan 2025 08:36:31 +0000 Subject: [PATCH 02/39] Chore: Fix translation imports from `@grafana/ui` (#99614) --- .betterer.results | 36 +++++-------------- .../TimePicker/TimePickerWithHistory.tsx | 2 +- .../components/rule-viewer/RuleViewer.tsx | 2 +- .../share-snapshot/UpsertSnapshot.tsx | 3 +- .../features/dashboard/services/TimeSrv.ts | 2 +- .../explore/ExploreRunQueryButton.tsx | 4 +-- public/app/features/explore/state/time.ts | 2 +- .../ServiceAccountCreatePage.tsx | 2 +- .../app/features/trails/DataTrailSettings.tsx | 2 +- .../MetricSelect/NativeHistogramBadge.tsx | 2 +- .../trails/banners/NativeHistogramBanner.tsx | 2 +- 11 files changed, 19 insertions(+), 40 deletions(-) diff --git a/.betterer.results b/.betterer.results index 2c72e385439..3d15c748b2e 100644 --- a/.betterer.results +++ b/.betterer.results @@ -1194,9 +1194,6 @@ exports[`better eslint`] = { "public/app/core/components/TagFilter/TagOption.tsx:5381": [ [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "0"] ], - "public/app/core/components/TimePicker/TimePickerWithHistory.tsx:5381": [ - [0, 0, 0, "\'@grafana/ui/src/utils/i18n\' import is restricted from being used by a pattern. Import from the public export instead.", "0"] - ], "public/app/core/components/TimeSeries/TimeSeries.tsx:5381": [ [0, 0, 0, "\'@grafana/ui/src/components/uPlot/PlotLegend\' import is restricted from being used by a pattern. Import from the public export instead.", "0"], [0, 0, 0, "\'@grafana/ui/src/components/uPlot/config/UPlotConfigBuilder\' import is restricted from being used by a pattern. Import from the public export instead.", "1"], @@ -2499,12 +2496,11 @@ exports[`better eslint`] = { [0, 0, 0, "No untranslated strings. Wrap text with ", "0"] ], "public/app/features/alerting/unified/components/rule-viewer/RuleViewer.tsx:5381": [ - [0, 0, 0, "\'@grafana/ui/src/utils/i18n\' import is restricted from being used by a pattern. Import from the public export instead.", "0"], + [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"], + [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 ", "4"] ], "public/app/features/alerting/unified/components/rule-viewer/tabs/Details.tsx:5381": [ [0, 0, 0, "No untranslated strings. Wrap text with ", "0"], @@ -3728,8 +3724,7 @@ exports[`better eslint`] = { ], "public/app/features/dashboard-scene/sharing/ShareButton/share-snapshot/UpsertSnapshot.tsx:5381": [ [0, 0, 0, "\'@grafana/ui/src/components/Input/Input\' import is restricted from being used by a pattern. Import from the public export instead.", "0"], - [0, 0, 0, "\'@grafana/ui/src/utils/i18n\' import is restricted from being used by a pattern. Import from the public export instead.", "1"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "2"] + [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "1"] ], "public/app/features/dashboard-scene/sharing/ShareDrawer/ShareDrawerConfirmAction.tsx:5381": [ [0, 0, 0, "\'@grafana/ui/src/components/ConfirmModal/ConfirmContent\' import is restricted from being used by a pattern. Import from the public export instead.", "0"] @@ -4230,9 +4225,6 @@ exports[`better eslint`] = { [0, 0, 0, "Unexpected any. Specify a different type.", "1"], [0, 0, 0, "Unexpected any. Specify a different type.", "2"] ], - "public/app/features/dashboard/services/TimeSrv.ts:5381": [ - [0, 0, 0, "\'@grafana/ui/src/utils/i18n\' import is restricted from being used by a pattern. Import from the public export instead.", "0"] - ], "public/app/features/dashboard/state/DashboardMigrator.test.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"], [0, 0, 0, "Unexpected any. Specify a different type.", "1"], @@ -4654,9 +4646,8 @@ exports[`better eslint`] = { [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "0"] ], "public/app/features/explore/ExploreRunQueryButton.tsx:5381": [ - [0, 0, 0, "\'@grafana/ui/src/utils/i18n\' import is restricted from being used by a pattern. Import from the public export instead.", "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"] + [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"] ], "public/app/features/explore/ExploreToolbar.tsx:5381": [ [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "0"], @@ -5063,9 +5054,6 @@ exports[`better eslint`] = { "public/app/features/explore/state/time.test.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"] ], - "public/app/features/explore/state/time.ts:5381": [ - [0, 0, 0, "\'@grafana/ui/src/utils/i18n\' import is restricted from being used by a pattern. Import from the public export instead.", "0"] - ], "public/app/features/explore/state/utils.ts:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"], [0, 0, 0, "Do not use any type assertions.", "1"], @@ -5970,9 +5958,6 @@ exports[`better eslint`] = { "public/app/features/search/utils.ts:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"] ], - "public/app/features/serviceaccounts/ServiceAccountCreatePage.tsx:5381": [ - [0, 0, 0, "\'@grafana/ui/src/utils/i18n\' import is restricted from being used by a pattern. Import from the public export instead.", "0"] - ], "public/app/features/serviceaccounts/ServiceAccountPage.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"], @@ -6173,8 +6158,7 @@ exports[`better eslint`] = { [0, 0, 0, "No untranslated strings. Wrap text with ", "1"] ], "public/app/features/trails/DataTrailSettings.tsx:5381": [ - [0, 0, 0, "\'@grafana/ui/src/utils/i18n\' import is restricted from being used by a pattern. Import from the public export instead.", "0"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "1"] + [0, 0, 0, "No untranslated strings. Wrap text with ", "0"] ], "public/app/features/trails/DataTrailsHistory.tsx:5381": [ [0, 0, 0, "\'@grafana/data/src/datetime/rangeutil\' import is restricted from being used by a pattern. Import from the public export instead.", "0"], @@ -6201,9 +6185,6 @@ exports[`better eslint`] = { [0, 0, 0, "No untranslated strings. Wrap text with ", "7"], [0, 0, 0, "No untranslated strings. Wrap text with ", "8"] ], - "public/app/features/trails/MetricSelect/NativeHistogramBadge.tsx:5381": [ - [0, 0, 0, "\'@grafana/ui/src/utils/i18n\' import is restricted from being used by a pattern. Import from the public export instead.", "0"] - ], "public/app/features/trails/MetricsHeader.tsx:5381": [ [0, 0, 0, "No untranslated strings. Wrap text with ", "0"], [0, 0, 0, "No untranslated strings. Wrap text with ", "1"] @@ -6217,8 +6198,7 @@ exports[`better eslint`] = { [0, 0, 0, "No untranslated strings. Wrap text with ", "2"] ], "public/app/features/trails/banners/NativeHistogramBanner.tsx:5381": [ - [0, 0, 0, "\'@grafana/ui/src/utils/i18n\' import is restricted from being used by a pattern. Import from the public export instead.", "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()", "0"] ], "public/app/features/transformers/FilterByValueTransformer/FilterByValueFilterEditor.tsx:5381": [ [0, 0, 0, "\'@grafana/data/src/transformations/transformers/filterByValue\' import is restricted from being used by a pattern. Import from the public export instead.", "0"], diff --git a/public/app/core/components/TimePicker/TimePickerWithHistory.tsx b/public/app/core/components/TimePicker/TimePickerWithHistory.tsx index 64c08d31b67..db4ac5ac21d 100644 --- a/public/app/core/components/TimePicker/TimePickerWithHistory.tsx +++ b/public/app/core/components/TimePicker/TimePickerWithHistory.tsx @@ -2,8 +2,8 @@ import { uniqBy } from 'lodash'; import { AppEvents, TimeRange, isDateTime, rangeUtil } from '@grafana/data'; import { TimeRangePickerProps, TimeRangePicker } from '@grafana/ui'; -import { t } from '@grafana/ui/src/utils/i18n'; import appEvents from 'app/core/app_events'; +import { t } from 'app/core/internationalization'; import { LocalStorageValueProvider } from '../LocalStorageValueProvider'; diff --git a/public/app/features/alerting/unified/components/rule-viewer/RuleViewer.tsx b/public/app/features/alerting/unified/components/rule-viewer/RuleViewer.tsx index bb5bd689b32..f1d3064fadb 100644 --- a/public/app/features/alerting/unified/components/rule-viewer/RuleViewer.tsx +++ b/public/app/features/alerting/unified/components/rule-viewer/RuleViewer.tsx @@ -5,9 +5,9 @@ import { useMeasure } from 'react-use'; import { NavModelItem, UrlQueryValue } from '@grafana/data'; import { Alert, LinkButton, LoadingBar, Stack, TabContent, Text, TextLink, useStyles2 } from '@grafana/ui'; -import { Trans, t } from '@grafana/ui/src/utils/i18n'; import { PageInfoItem } from 'app/core/components/Page/types'; import { useQueryParams } from 'app/core/hooks/useQueryParams'; +import { Trans, t } from 'app/core/internationalization'; import InfoPausedRule from 'app/features/alerting/unified/components/InfoPausedRule'; import { RuleActionsButtons } from 'app/features/alerting/unified/components/rules/RuleActionsButtons'; import { AlertInstanceTotalState, CombinedRule, RuleHealth, RuleIdentifier } from 'app/types/unified-alerting'; diff --git a/public/app/features/dashboard-scene/sharing/ShareButton/share-snapshot/UpsertSnapshot.tsx b/public/app/features/dashboard-scene/sharing/ShareButton/share-snapshot/UpsertSnapshot.tsx index 5458db42ec1..57cd584afb1 100644 --- a/public/app/features/dashboard-scene/sharing/ShareButton/share-snapshot/UpsertSnapshot.tsx +++ b/public/app/features/dashboard-scene/sharing/ShareButton/share-snapshot/UpsertSnapshot.tsx @@ -5,8 +5,7 @@ import { GrafanaTheme2, SelectableValue } from '@grafana/data'; import { SceneObjectRef, VizPanel } from '@grafana/scenes'; import { Alert, Button, Divider, Field, RadioButtonGroup, Stack, Text, useStyles2 } from '@grafana/ui'; import { Input } from '@grafana/ui/src/components/Input/Input'; -import { t } from '@grafana/ui/src/utils/i18n'; -import { Trans } from 'app/core/internationalization'; +import { t, Trans } from 'app/core/internationalization'; import { getExpireOptions } from '../../ShareSnapshotTab'; diff --git a/public/app/features/dashboard/services/TimeSrv.ts b/public/app/features/dashboard/services/TimeSrv.ts index 7761ed37fbc..b8261ec1d32 100644 --- a/public/app/features/dashboard/services/TimeSrv.ts +++ b/public/app/features/dashboard/services/TimeSrv.ts @@ -15,9 +15,9 @@ import { } from '@grafana/data'; import { locationService } from '@grafana/runtime'; import { sceneGraph } from '@grafana/scenes'; -import { t } from '@grafana/ui/src/utils/i18n'; import appEvents from 'app/core/app_events'; import { config } from 'app/core/config'; +import { t } from 'app/core/internationalization'; import { AutoRefreshInterval, contextSrv, ContextSrv } from 'app/core/services/context_srv'; import { getCopiedTimeRange, getShiftedTimeRange, getZoomedTimeRange } from 'app/core/utils/timePicker'; import { getTimeRange } from 'app/features/dashboard/utils/timeRange'; diff --git a/public/app/features/explore/ExploreRunQueryButton.tsx b/public/app/features/explore/ExploreRunQueryButton.tsx index 7dd87dbd6c3..7cb37a62a45 100644 --- a/public/app/features/explore/ExploreRunQueryButton.tsx +++ b/public/app/features/explore/ExploreRunQueryButton.tsx @@ -4,7 +4,7 @@ import { ConnectedProps, connect } from 'react-redux'; import { config, reportInteraction } from '@grafana/runtime'; import { DataQuery } from '@grafana/schema'; import { Button, ButtonVariant, Dropdown, Menu, ToolbarButton } from '@grafana/ui'; -import { t } from '@grafana/ui/src/utils/i18n'; +import { t } from 'app/core/internationalization'; import { useSelector } from 'app/types'; import { changeDatasource } from './state/datasource'; @@ -28,7 +28,7 @@ interface ExploreRunQueryButtonProps { export type Props = ConnectedProps & ExploreRunQueryButtonProps; -/* +/* This component does not validate datasources before running them. Root datasource validation should happen outside this component and can pass in an undefined if invalid If query level validation is done and a query datasource is invalid, pass in disabled = true */ diff --git a/public/app/features/explore/state/time.ts b/public/app/features/explore/state/time.ts index 38683f45bf4..f794042c113 100644 --- a/public/app/features/explore/state/time.ts +++ b/public/app/features/explore/state/time.ts @@ -10,8 +10,8 @@ import { } from '@grafana/data'; import { getTemplateSrv } from '@grafana/runtime'; import { RefreshPicker } from '@grafana/ui'; -import { t } from '@grafana/ui/src/utils/i18n'; import appEvents from 'app/core/app_events'; +import { t } from 'app/core/internationalization'; import { getTimeRange, refreshIntervalToSortOrder, stopQueryState } from 'app/core/utils/explore'; import { getCopiedTimeRange, getShiftedTimeRange, getZoomedTimeRange } from 'app/core/utils/timePicker'; import { getTimeSrv } from 'app/features/dashboard/services/TimeSrv'; diff --git a/public/app/features/serviceaccounts/ServiceAccountCreatePage.tsx b/public/app/features/serviceaccounts/ServiceAccountCreatePage.tsx index fa24596b328..c9816d0fb1a 100644 --- a/public/app/features/serviceaccounts/ServiceAccountCreatePage.tsx +++ b/public/app/features/serviceaccounts/ServiceAccountCreatePage.tsx @@ -3,13 +3,13 @@ import { FormProvider, useForm } from 'react-hook-form'; import { config, getBackendSrv, locationService } from '@grafana/runtime'; import { Button, Input, Field, FieldSet } from '@grafana/ui'; -import { t, Trans } from '@grafana/ui/src/utils/i18n'; import { Form } from 'app/core/components/Form/Form'; import { Page } from 'app/core/components/Page/Page'; import { UserRolePicker } from 'app/core/components/RolePicker/UserRolePicker'; import { fetchRoleOptions, updateUserRoles } from 'app/core/components/RolePicker/api'; import { RolePickerSelect } from 'app/core/components/RolePickerDrawer/RolePickerSelect'; import { contextSrv } from 'app/core/core'; +import { t, Trans } from 'app/core/internationalization'; import { AccessControlAction, OrgRole, Role, ServiceAccountCreateApiResponse, ServiceAccountDTO } from 'app/types'; import { OrgRolePicker } from '../admin/OrgRolePicker'; diff --git a/public/app/features/trails/DataTrailSettings.tsx b/public/app/features/trails/DataTrailSettings.tsx index 2584b7bb930..7007b511ad0 100644 --- a/public/app/features/trails/DataTrailSettings.tsx +++ b/public/app/features/trails/DataTrailSettings.tsx @@ -3,7 +3,7 @@ import { css } from '@emotion/css'; import { GrafanaTheme2 } from '@grafana/data'; import { SceneComponentProps, SceneObjectBase, SceneObjectState } from '@grafana/scenes'; import { Dropdown, Switch, ToolbarButton, useStyles2 } from '@grafana/ui'; -import { Trans } from '@grafana/ui/src/utils/i18n'; +import { Trans } from 'app/core/internationalization'; import { MetricScene } from './MetricScene'; import { MetricSelectScene } from './MetricSelect/MetricSelectScene'; diff --git a/public/app/features/trails/MetricSelect/NativeHistogramBadge.tsx b/public/app/features/trails/MetricSelect/NativeHistogramBadge.tsx index f2c551f3d70..21fed5c0ce7 100644 --- a/public/app/features/trails/MetricSelect/NativeHistogramBadge.tsx +++ b/public/app/features/trails/MetricSelect/NativeHistogramBadge.tsx @@ -3,7 +3,7 @@ import { css } from '@emotion/css'; import { GrafanaTheme2 } from '@grafana/data'; import { SceneObjectBase } from '@grafana/scenes'; import { Badge, useStyles2 } from '@grafana/ui'; -import { Trans } from '@grafana/ui/src/utils/i18n'; +import { Trans } from 'app/core/internationalization'; export class NativeHistogramBadge extends SceneObjectBase { public static Component = () => { diff --git a/public/app/features/trails/banners/NativeHistogramBanner.tsx b/public/app/features/trails/banners/NativeHistogramBanner.tsx index 5813c06d30f..68ca35c0c67 100644 --- a/public/app/features/trails/banners/NativeHistogramBanner.tsx +++ b/public/app/features/trails/banners/NativeHistogramBanner.tsx @@ -3,7 +3,7 @@ import { useState, type Dispatch, type SetStateAction } from 'react'; import { GrafanaTheme2 } from '@grafana/data'; import { useStyles2, useTheme2, Alert, Button } from '@grafana/ui'; -import { t, Trans } from '@grafana/ui/src/utils/i18n'; +import { t, Trans } from 'app/core/internationalization'; import { DataTrail } from '../DataTrail'; import { reportExploreMetrics } from '../interactions'; From 9949a56f3b831b1b37633c6343ec5a4bcf000146 Mon Sep 17 00:00:00 2001 From: Stephanie Hingtgen Date: Tue, 28 Jan 2025 02:08:48 -0700 Subject: [PATCH 03/39] K8s: Fix legacy stats (#99623) --- pkg/storage/unified/federated/stats.go | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/pkg/storage/unified/federated/stats.go b/pkg/storage/unified/federated/stats.go index 98440932ace..a126bcb30fb 100644 --- a/pkg/storage/unified/federated/stats.go +++ b/pkg/storage/unified/federated/stats.go @@ -31,7 +31,17 @@ func (s *LegacyStatsGetter) GetStats(ctx context.Context, in *resource.ResourceS rsp := &resource.ResourceStatsResponse{} err = helper.DB.WithDbSession(ctx, func(sess *sqlstore.DBSession) error { - fn := func(table, where, g, r string) error { + fn := func(table, where, g, r string, existCheck bool) error { + // if existCheck is true, do not error out if the table does not exist + if existCheck { + exists, err := sess.IsTableExist(helper.Table(table)) + if !exists { + return nil + } else if err != nil { + return err + } + } + count, err := sess.Table(helper.Table(table)).Where(where, info.OrgID, in.Folder).Count() if err != nil { return err @@ -47,25 +57,25 @@ func (s *LegacyStatsGetter) GetStats(ctx context.Context, in *resource.ResourceS group := "sql-fallback" // Legacy alert rule table - err = fn("alert_rule", "org_id=? AND dashboard_uid=?", group, "alertrules") + err = fn("alert_rule", "org_id=? AND dashboard_uid=?", group, "alertrules", false) if err != nil { return err } // Legacy dashboard table - err = fn("dashboard", "org_id=? AND folder_uid=?", group, "dashboards") + err = fn("dashboard", "org_id=? AND folder_uid=?", group, "dashboards", true) if err != nil { return err } // Legacy folder table - err = fn("folder", "org_id=? AND parent_uid=?", group, "folders") + err = fn("folder", "org_id=? AND parent_uid=?", group, "folders", true) if err != nil { return err } // Legacy library_elements table - err = fn("library_element", "org_id=? AND folder_uid=?", group, "library_elements") + err = fn("library_element", "org_id=? AND folder_uid=?", group, "library_elements", false) if err != nil { return err } From 959a942b5cec3dd024e923874b765b9853f862ac Mon Sep 17 00:00:00 2001 From: Stephanie Hingtgen Date: Tue, 28 Jan 2025 02:28:12 -0700 Subject: [PATCH 04/39] K8s: Dashboards: fix in folder count (#99622) --- .../dashboards/service/dashboard_service.go | 12 ++++ .../service/dashboard_service_test.go | 62 +++++++++++++++++++ 2 files changed, 74 insertions(+) diff --git a/pkg/services/dashboards/service/dashboard_service.go b/pkg/services/dashboards/service/dashboard_service.go index 0c90a37542c..dd8d95348ad 100644 --- a/pkg/services/dashboards/service/dashboard_service.go +++ b/pkg/services/dashboards/service/dashboard_service.go @@ -1401,6 +1401,18 @@ func (dr *DashboardServiceImpl) GetDashboardTags(ctx context.Context, query *das } func (dr DashboardServiceImpl) CountInFolders(ctx context.Context, orgID int64, folderUIDs []string, u identity.Requester) (int64, error) { + if dr.features.IsEnabledGlobally(featuremgmt.FlagKubernetesCliDashboards) { + dashs, err := dr.searchDashboardsThroughK8s(ctx, &dashboards.FindPersistedDashboardsQuery{ + OrgId: orgID, + FolderUIDs: folderUIDs, + }) + if err != nil { + return 0, err + } + + return int64(len(dashs)), nil + } + return dr.dashboardStore.CountDashboardsInFolders(ctx, &dashboards.CountDashboardsInFolderRequest{FolderUIDs: folderUIDs, OrgID: orgID}) } diff --git a/pkg/services/dashboards/service/dashboard_service_test.go b/pkg/services/dashboards/service/dashboard_service_test.go index 27653a41b63..fbe99cf0fb4 100644 --- a/pkg/services/dashboards/service/dashboard_service_test.go +++ b/pkg/services/dashboards/service/dashboard_service_test.go @@ -1818,6 +1818,68 @@ func TestCountDashboardsInOrg(t *testing.T) { }) } +func TestCountInFolders(t *testing.T) { + fakeStore := dashboards.FakeDashboardStore{} + defer fakeStore.AssertExpectations(t) + service := &DashboardServiceImpl{ + cfg: setting.NewCfg(), + dashboardStore: &fakeStore, + } + dashs := &resource.ResourceSearchResponse{ + Results: &resource.ResourceTable{ + Columns: []*resource.ResourceTableColumnDefinition{ + { + Name: "title", + Type: resource.ResourceTableColumnDefinition_STRING, + }, + { + Name: "folder", + Type: resource.ResourceTableColumnDefinition_STRING, + }, + }, + Rows: []*resource.ResourceTableRow{ + { + Key: &resource.ResourceKey{ + Name: "uid", + Resource: "dashboard", + }, + Cells: [][]byte{ + []byte("Dashboard 1"), + []byte("folder 1"), + }, + }, + { + Key: &resource.ResourceKey{ + Name: "uid2", + Resource: "dashboard", + }, + Cells: [][]byte{ + []byte("Dashboard 2"), + []byte("folder 1"), + }, + }, + }, + }, + TotalHits: 2, + } + + t.Run("Should fallback to dashboard store if Kubernetes feature flags are not enabled", func(t *testing.T) { + service.features = featuremgmt.WithFeatures() + fakeStore.On("CountDashboardsInFolders", mock.Anything, mock.Anything).Return(int64(1), nil).Once() + _, err := service.CountInFolders(context.Background(), 1, []string{"folder1"}, &user.SignedInUser{}) + require.NoError(t, err) + fakeStore.AssertExpectations(t) + }) + + t.Run("Should use Kubernetes client if feature flags are enabled", func(t *testing.T) { + ctx, k8sCliMock := setupK8sDashboardTests(service) + k8sCliMock.On("Search", mock.Anything, mock.Anything, mock.Anything).Return(dashs, nil).Once() + result, err := service.CountInFolders(ctx, 1, []string{"folder1"}, &user.SignedInUser{}) + require.NoError(t, err) + require.Equal(t, result, int64(2)) + }) +} + func TestLegacySaveCommandToUnstructured(t *testing.T) { namespace := "test-namespace" t.Run("successfully converts save command to unstructured", func(t *testing.T) { From ae0de61b92316661f9c44cb1a3897665dd7a59ae Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 28 Jan 2025 09:39:55 +0000 Subject: [PATCH 05/39] Update dependency @types/babel__preset-env to v7.10.0 (#99611) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 4c42d41e41d..dfca5abd1a2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8892,9 +8892,9 @@ __metadata: linkType: hard "@types/babel__preset-env@npm:^7": - version: 7.9.7 - resolution: "@types/babel__preset-env@npm:7.9.7" - checksum: 10/624425a84d9149aec04795fed6b1ac2f27dfd5d7976fde479bb1a4d754de34c92cdc28a1a373a5826382a68127b536420a0e090aa5fae522cb62724b7a571cb5 + version: 7.10.0 + resolution: "@types/babel__preset-env@npm:7.10.0" + checksum: 10/7d4d12758d89708afe327079d7d7580e8af3292295f087b8a9a48e12ac1d90aadc18ac3bc00f9b0cbc8778f3ce9fe778801d4d49b7691a75e3f13a901b69fd07 languageName: node linkType: hard From 58a3e0ae869b7161b94993b2c0057e972b8eb7c0 Mon Sep 17 00:00:00 2001 From: Ivana Huckova <30407135+ivanahuckova@users.noreply.github.com> Date: Tue, 28 Jan 2025 10:48:19 +0100 Subject: [PATCH 06/39] Remove @grafana/experimental in @grafana/o11y-ds-frontend and @grafana/sql (#99501) * Remove @grafana/experimental in @grafana/o11y-ds-frontend * Remove @grafana/experimental in @grafana/sql * Fix mock in test --- packages/grafana-o11y-ds-frontend/package.json | 2 +- .../src/NodeGraph/NodeGraphSettings.tsx | 2 +- .../grafana-o11y-ds-frontend/src/SpanBar/SpanBarSettings.tsx | 2 +- .../src/TraceToLogs/TraceToLogsSettings.tsx | 2 +- .../src/TraceToMetrics/TraceToMetricsSettings.tsx | 2 +- .../src/TraceToProfiles/TraceToProfilesSettings.tsx | 2 +- packages/grafana-sql/package.json | 2 +- packages/grafana-sql/src/components/QueryEditor.tsx | 2 +- packages/grafana-sql/src/components/QueryHeader.tsx | 2 +- .../src/components/configuration/ConnectionLimits.tsx | 4 ++-- .../src/components/query-editor-raw/QueryEditorRaw.tsx | 2 +- .../src/components/visual-query-builder/GroupByRow.tsx | 2 +- .../src/components/visual-query-builder/OrderByRow.tsx | 2 +- .../src/components/visual-query-builder/SelectColumn.tsx | 2 +- .../visual-query-builder/SelectFunctionParameters.tsx | 2 +- .../src/components/visual-query-builder/SelectRow.test.tsx | 1 + .../src/components/visual-query-builder/SelectRow.tsx | 2 +- .../src/components/visual-query-builder/VisualEditor.tsx | 2 +- packages/grafana-sql/src/datasource/SqlDatasource.ts | 2 +- packages/grafana-sql/src/defaults.ts | 2 +- packages/grafana-sql/src/types.ts | 2 +- yarn.lock | 4 ++-- 22 files changed, 24 insertions(+), 23 deletions(-) diff --git a/packages/grafana-o11y-ds-frontend/package.json b/packages/grafana-o11y-ds-frontend/package.json index fbd1ee55713..4da97db322a 100644 --- a/packages/grafana-o11y-ds-frontend/package.json +++ b/packages/grafana-o11y-ds-frontend/package.json @@ -20,7 +20,7 @@ "@emotion/css": "11.13.5", "@grafana/data": "11.5.0-pre", "@grafana/e2e-selectors": "11.5.0-pre", - "@grafana/experimental": "2.1.6", + "@grafana/plugin-ui": "0.9.6", "@grafana/runtime": "11.5.0-pre", "@grafana/schema": "11.5.0-pre", "@grafana/ui": "11.5.0-pre", diff --git a/packages/grafana-o11y-ds-frontend/src/NodeGraph/NodeGraphSettings.tsx b/packages/grafana-o11y-ds-frontend/src/NodeGraph/NodeGraphSettings.tsx index 8b6f3c5f4b6..fd9ba9cd6c9 100644 --- a/packages/grafana-o11y-ds-frontend/src/NodeGraph/NodeGraphSettings.tsx +++ b/packages/grafana-o11y-ds-frontend/src/NodeGraph/NodeGraphSettings.tsx @@ -7,7 +7,7 @@ import { GrafanaTheme2, updateDatasourcePluginJsonDataOption, } from '@grafana/data'; -import { ConfigDescriptionLink, ConfigSubSection } from '@grafana/experimental'; +import { ConfigDescriptionLink, ConfigSubSection } from '@grafana/plugin-ui'; import { InlineField, InlineFieldRow, InlineSwitch, useStyles2 } from '@grafana/ui'; export interface NodeGraphOptions { diff --git a/packages/grafana-o11y-ds-frontend/src/SpanBar/SpanBarSettings.tsx b/packages/grafana-o11y-ds-frontend/src/SpanBar/SpanBarSettings.tsx index a612dee380d..11710cef528 100644 --- a/packages/grafana-o11y-ds-frontend/src/SpanBar/SpanBarSettings.tsx +++ b/packages/grafana-o11y-ds-frontend/src/SpanBar/SpanBarSettings.tsx @@ -7,7 +7,7 @@ import { toOption, updateDatasourcePluginJsonDataOption, } from '@grafana/data'; -import { ConfigDescriptionLink, ConfigSubSection } from '@grafana/experimental'; +import { ConfigDescriptionLink, ConfigSubSection } from '@grafana/plugin-ui'; import { InlineField, InlineFieldRow, Input, Select, useStyles2 } from '@grafana/ui'; export interface SpanBarOptions { diff --git a/packages/grafana-o11y-ds-frontend/src/TraceToLogs/TraceToLogsSettings.tsx b/packages/grafana-o11y-ds-frontend/src/TraceToLogs/TraceToLogsSettings.tsx index d94e09b0411..b5bca15f317 100644 --- a/packages/grafana-o11y-ds-frontend/src/TraceToLogs/TraceToLogsSettings.tsx +++ b/packages/grafana-o11y-ds-frontend/src/TraceToLogs/TraceToLogsSettings.tsx @@ -3,7 +3,7 @@ import { useCallback, useMemo } from 'react'; import * as React from 'react'; import { DataSourceJsonData, DataSourceInstanceSettings, DataSourcePluginOptionsEditorProps } from '@grafana/data'; -import { ConfigDescriptionLink, ConfigSection } from '@grafana/experimental'; +import { ConfigDescriptionLink, ConfigSection } from '@grafana/plugin-ui'; import { DataSourcePicker } from '@grafana/runtime'; import { InlineField, InlineFieldRow, Input, InlineSwitch } from '@grafana/ui'; diff --git a/packages/grafana-o11y-ds-frontend/src/TraceToMetrics/TraceToMetricsSettings.tsx b/packages/grafana-o11y-ds-frontend/src/TraceToMetrics/TraceToMetricsSettings.tsx index 98cff44f304..966c1703b54 100644 --- a/packages/grafana-o11y-ds-frontend/src/TraceToMetrics/TraceToMetricsSettings.tsx +++ b/packages/grafana-o11y-ds-frontend/src/TraceToMetrics/TraceToMetricsSettings.tsx @@ -7,7 +7,7 @@ import { GrafanaTheme2, updateDatasourcePluginJsonDataOption, } from '@grafana/data'; -import { ConfigDescriptionLink, ConfigSection } from '@grafana/experimental'; +import { ConfigDescriptionLink, ConfigSection } from '@grafana/plugin-ui'; import { DataSourcePicker } from '@grafana/runtime'; import { Button, InlineField, InlineFieldRow, Input, useStyles2 } from '@grafana/ui'; diff --git a/packages/grafana-o11y-ds-frontend/src/TraceToProfiles/TraceToProfilesSettings.tsx b/packages/grafana-o11y-ds-frontend/src/TraceToProfiles/TraceToProfilesSettings.tsx index 9b2d5ca31cc..cb5af242966 100644 --- a/packages/grafana-o11y-ds-frontend/src/TraceToProfiles/TraceToProfilesSettings.tsx +++ b/packages/grafana-o11y-ds-frontend/src/TraceToProfiles/TraceToProfilesSettings.tsx @@ -9,7 +9,7 @@ import { DataSourcePluginOptionsEditorProps, updateDatasourcePluginJsonDataOption, } from '@grafana/data'; -import { ConfigDescriptionLink, ConfigSection } from '@grafana/experimental'; +import { ConfigDescriptionLink, ConfigSection } from '@grafana/plugin-ui'; import { DataSourcePicker, DataSourceWithBackend, getDataSourceSrv } from '@grafana/runtime'; import { InlineField, InlineFieldRow, Input, InlineSwitch } from '@grafana/ui'; diff --git a/packages/grafana-sql/package.json b/packages/grafana-sql/package.json index 509a4b9832f..d2a690cde8e 100644 --- a/packages/grafana-sql/package.json +++ b/packages/grafana-sql/package.json @@ -17,7 +17,7 @@ "@emotion/css": "11.13.5", "@grafana/data": "11.5.0-pre", "@grafana/e2e-selectors": "11.5.0-pre", - "@grafana/experimental": "2.1.6", + "@grafana/plugin-ui": "0.9.6", "@grafana/runtime": "11.5.0-pre", "@grafana/ui": "11.5.0-pre", "@react-awesome-query-builder/ui": "6.6.4", diff --git a/packages/grafana-sql/src/components/QueryEditor.tsx b/packages/grafana-sql/src/components/QueryEditor.tsx index 487c4f2c1c9..fa7bc199209 100644 --- a/packages/grafana-sql/src/components/QueryEditor.tsx +++ b/packages/grafana-sql/src/components/QueryEditor.tsx @@ -2,7 +2,7 @@ import { useCallback, useEffect, useState } from 'react'; import { useAsync } from 'react-use'; import { QueryEditorProps } from '@grafana/data'; -import { EditorMode } from '@grafana/experimental'; +import { EditorMode } from '@grafana/plugin-ui'; import { Space } from '@grafana/ui'; import { SqlDatasource } from '../datasource/SqlDatasource'; diff --git a/packages/grafana-sql/src/components/QueryHeader.tsx b/packages/grafana-sql/src/components/QueryHeader.tsx index f1bede6cb4f..d7f7412d453 100644 --- a/packages/grafana-sql/src/components/QueryHeader.tsx +++ b/packages/grafana-sql/src/components/QueryHeader.tsx @@ -3,7 +3,7 @@ import { useCopyToClipboard } from 'react-use'; import { SelectableValue } from '@grafana/data'; import { selectors } from '@grafana/e2e-selectors'; -import { EditorField, EditorHeader, EditorMode, EditorRow, FlexItem, InlineSelect } from '@grafana/experimental'; +import { EditorField, EditorHeader, EditorMode, EditorRow, FlexItem, InlineSelect } from '@grafana/plugin-ui'; import { reportInteraction } from '@grafana/runtime'; import { Button, InlineSwitch, RadioButtonGroup, Tooltip, Space } from '@grafana/ui'; diff --git a/packages/grafana-sql/src/components/configuration/ConnectionLimits.tsx b/packages/grafana-sql/src/components/configuration/ConnectionLimits.tsx index 17218d5188a..50bc7e82ae7 100644 --- a/packages/grafana-sql/src/components/configuration/ConnectionLimits.tsx +++ b/packages/grafana-sql/src/components/configuration/ConnectionLimits.tsx @@ -1,7 +1,7 @@ import { DataSourceSettings } from '@grafana/data'; -import { ConfigSubSection, Stack } from '@grafana/experimental'; +import { ConfigSubSection } from '@grafana/plugin-ui'; import { config } from '@grafana/runtime'; -import { Field, Icon, InlineLabel, Label, Switch, Tooltip } from '@grafana/ui'; +import { Field, Icon, InlineLabel, Label, Stack, Switch, Tooltip } from '@grafana/ui'; import { SQLConnectionLimits, SQLOptions } from '../../types'; diff --git a/packages/grafana-sql/src/components/query-editor-raw/QueryEditorRaw.tsx b/packages/grafana-sql/src/components/query-editor-raw/QueryEditorRaw.tsx index 2f9f7bd6eeb..ed79057cd55 100644 --- a/packages/grafana-sql/src/components/query-editor-raw/QueryEditorRaw.tsx +++ b/packages/grafana-sql/src/components/query-editor-raw/QueryEditorRaw.tsx @@ -1,7 +1,7 @@ import { useCallback, useEffect, useRef } from 'react'; import * as React from 'react'; -import { LanguageDefinition, SQLEditor } from '@grafana/experimental'; +import { LanguageDefinition, SQLEditor } from '@grafana/plugin-ui'; import { SQLQuery } from '../../types'; diff --git a/packages/grafana-sql/src/components/visual-query-builder/GroupByRow.tsx b/packages/grafana-sql/src/components/visual-query-builder/GroupByRow.tsx index 33460b4e169..8f6b463c8ee 100644 --- a/packages/grafana-sql/src/components/visual-query-builder/GroupByRow.tsx +++ b/packages/grafana-sql/src/components/visual-query-builder/GroupByRow.tsx @@ -1,7 +1,7 @@ import { useCallback } from 'react'; import { SelectableValue, toOption } from '@grafana/data'; -import { AccessoryButton, EditorList, InputGroup } from '@grafana/experimental'; +import { AccessoryButton, EditorList, InputGroup } from '@grafana/plugin-ui'; import { Select } from '@grafana/ui'; import { QueryEditorGroupByExpression } from '../../expressions'; diff --git a/packages/grafana-sql/src/components/visual-query-builder/OrderByRow.tsx b/packages/grafana-sql/src/components/visual-query-builder/OrderByRow.tsx index 3c2c274eeab..8e7286153a9 100644 --- a/packages/grafana-sql/src/components/visual-query-builder/OrderByRow.tsx +++ b/packages/grafana-sql/src/components/visual-query-builder/OrderByRow.tsx @@ -3,7 +3,7 @@ import { useCallback } from 'react'; import * as React from 'react'; import { SelectableValue, toOption } from '@grafana/data'; -import { EditorField, InputGroup } from '@grafana/experimental'; +import { EditorField, InputGroup } from '@grafana/plugin-ui'; import { Input, RadioButtonGroup, Select, Space } from '@grafana/ui'; import { SQLExpression } from '../../types'; diff --git a/packages/grafana-sql/src/components/visual-query-builder/SelectColumn.tsx b/packages/grafana-sql/src/components/visual-query-builder/SelectColumn.tsx index 774ef7501f8..cea8b01e72a 100644 --- a/packages/grafana-sql/src/components/visual-query-builder/SelectColumn.tsx +++ b/packages/grafana-sql/src/components/visual-query-builder/SelectColumn.tsx @@ -2,7 +2,7 @@ import { useId } from 'react'; import { SelectableValue } from '@grafana/data'; import { selectors } from '@grafana/e2e-selectors'; -import { EditorField } from '@grafana/experimental'; +import { EditorField } from '@grafana/plugin-ui'; import { Select } from '@grafana/ui'; interface Props { diff --git a/packages/grafana-sql/src/components/visual-query-builder/SelectFunctionParameters.tsx b/packages/grafana-sql/src/components/visual-query-builder/SelectFunctionParameters.tsx index 3d5c2203447..16241e3aac4 100644 --- a/packages/grafana-sql/src/components/visual-query-builder/SelectFunctionParameters.tsx +++ b/packages/grafana-sql/src/components/visual-query-builder/SelectFunctionParameters.tsx @@ -3,7 +3,7 @@ import { useCallback, useEffect, useId, useState } from 'react'; import { SelectableValue } from '@grafana/data'; import { selectors } from '@grafana/e2e-selectors'; -import { EditorField } from '@grafana/experimental'; +import { EditorField } from '@grafana/plugin-ui'; import { InlineLabel, Input, Select, Stack, useStyles2 } from '@grafana/ui'; import { QueryEditorExpressionType } from '../../expressions'; diff --git a/packages/grafana-sql/src/components/visual-query-builder/SelectRow.test.tsx b/packages/grafana-sql/src/components/visual-query-builder/SelectRow.test.tsx index a54e1ef72ab..3b31add19ad 100644 --- a/packages/grafana-sql/src/components/visual-query-builder/SelectRow.test.tsx +++ b/packages/grafana-sql/src/components/visual-query-builder/SelectRow.test.tsx @@ -12,6 +12,7 @@ import { SelectRow } from './SelectRow'; // Mock featureToggle sqlQuerybuilderFunctionParameters jest.mock('@grafana/runtime', () => ({ + ...jest.requireActual('@grafana/runtime'), config: { featureToggles: { sqlQuerybuilderFunctionParameters: true, diff --git a/packages/grafana-sql/src/components/visual-query-builder/SelectRow.tsx b/packages/grafana-sql/src/components/visual-query-builder/SelectRow.tsx index 302977d88cd..a553ece4a6c 100644 --- a/packages/grafana-sql/src/components/visual-query-builder/SelectRow.tsx +++ b/packages/grafana-sql/src/components/visual-query-builder/SelectRow.tsx @@ -4,7 +4,7 @@ import { useCallback } from 'react'; import { SelectableValue, toOption } from '@grafana/data'; import { selectors } from '@grafana/e2e-selectors'; -import { EditorField } from '@grafana/experimental'; +import { EditorField } from '@grafana/plugin-ui'; import { config } from '@grafana/runtime'; import { Button, Select, Stack, useStyles2 } from '@grafana/ui'; diff --git a/packages/grafana-sql/src/components/visual-query-builder/VisualEditor.tsx b/packages/grafana-sql/src/components/visual-query-builder/VisualEditor.tsx index ca5e37fd41d..3a267301163 100644 --- a/packages/grafana-sql/src/components/visual-query-builder/VisualEditor.tsx +++ b/packages/grafana-sql/src/components/visual-query-builder/VisualEditor.tsx @@ -1,6 +1,6 @@ import { useAsync } from 'react-use'; -import { EditorRows, EditorRow, EditorField } from '@grafana/experimental'; +import { EditorRows, EditorRow, EditorField } from '@grafana/plugin-ui'; import { DB, QueryEditorProps, QueryRowFilter } from '../../types'; import { QueryToolbox } from '../query-editor-raw/QueryToolbox'; diff --git a/packages/grafana-sql/src/datasource/SqlDatasource.ts b/packages/grafana-sql/src/datasource/SqlDatasource.ts index 121d30a4731..ca4675034c7 100644 --- a/packages/grafana-sql/src/datasource/SqlDatasource.ts +++ b/packages/grafana-sql/src/datasource/SqlDatasource.ts @@ -17,7 +17,7 @@ import { VariableWithMultiSupport, TimeRange, } from '@grafana/data'; -import { EditorMode } from '@grafana/experimental'; +import { EditorMode } from '@grafana/plugin-ui'; import { BackendDataSourceResponse, DataSourceWithBackend, diff --git a/packages/grafana-sql/src/defaults.ts b/packages/grafana-sql/src/defaults.ts index 4532bdcc3a6..06930dcc8cc 100644 --- a/packages/grafana-sql/src/defaults.ts +++ b/packages/grafana-sql/src/defaults.ts @@ -1,4 +1,4 @@ -import { EditorMode } from '@grafana/experimental'; +import { EditorMode } from '@grafana/plugin-ui'; import { QueryFormat, SQLQuery } from './types'; import { createFunctionField, setGroupByField } from './utils/sql.utils'; diff --git a/packages/grafana-sql/src/types.ts b/packages/grafana-sql/src/types.ts index dd2af526b81..00c1a6e0456 100644 --- a/packages/grafana-sql/src/types.ts +++ b/packages/grafana-sql/src/types.ts @@ -9,7 +9,7 @@ import { TimeRange, toOption as toOptionFromData, } from '@grafana/data'; -import { CompletionItemKind, EditorMode, LanguageDefinition } from '@grafana/experimental'; +import { CompletionItemKind, EditorMode, LanguageDefinition } from '@grafana/plugin-ui'; import { QueryWithDefaults } from './defaults'; import { diff --git a/yarn.lock b/yarn.lock index dfca5abd1a2..899a9ca41b1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3497,7 +3497,7 @@ __metadata: "@emotion/css": "npm:11.13.5" "@grafana/data": "npm:11.5.0-pre" "@grafana/e2e-selectors": "npm:11.5.0-pre" - "@grafana/experimental": "npm:2.1.6" + "@grafana/plugin-ui": "npm:0.9.6" "@grafana/runtime": "npm:11.5.0-pre" "@grafana/schema": "npm:11.5.0-pre" "@grafana/tsconfig": "npm:^2.0.0" @@ -3875,7 +3875,7 @@ __metadata: "@emotion/css": "npm:11.13.5" "@grafana/data": "npm:11.5.0-pre" "@grafana/e2e-selectors": "npm:11.5.0-pre" - "@grafana/experimental": "npm:2.1.6" + "@grafana/plugin-ui": "npm:0.9.6" "@grafana/runtime": "npm:11.5.0-pre" "@grafana/tsconfig": "npm:^2.0.0" "@grafana/ui": "npm:11.5.0-pre" From 9c7618160d8fc8aa27f153fa228bbca4d483feb6 Mon Sep 17 00:00:00 2001 From: Stephanie Closson Date: Tue, 28 Jan 2025 04:52:50 -0500 Subject: [PATCH 07/39] Update queries-conditions.md (#99592) --- .../alerting/fundamentals/alert-rules/queries-conditions.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/sources/alerting/fundamentals/alert-rules/queries-conditions.md b/docs/sources/alerting/fundamentals/alert-rules/queries-conditions.md index 229816db306..877d1b5e76a 100644 --- a/docs/sources/alerting/fundamentals/alert-rules/queries-conditions.md +++ b/docs/sources/alerting/fundamentals/alert-rules/queries-conditions.md @@ -103,7 +103,7 @@ The threshold expression allows you to compare two single values. It returns `0` - Is above (x > y) - Is below (x < y) - Is within range (x > y1 AND x < y2) -- Is outside range (x < y1 AND x > y2) +- Is outside range (x < y1 OR x > y2) **Classic condition (legacy)** From a05f539dd2413cab20b72466928c789f01d2a3b8 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 28 Jan 2025 10:02:23 +0000 Subject: [PATCH 08/39] Update dependency @types/lodash to v4.17.15 (#99632) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package.json | 2 +- packages/grafana-data/package.json | 2 +- packages/grafana-flamegraph/package.json | 2 +- packages/grafana-prometheus/package.json | 2 +- packages/grafana-runtime/package.json | 2 +- packages/grafana-sql/package.json | 2 +- packages/grafana-ui/package.json | 2 +- .../datasource/azuremonitor/package.json | 2 +- .../datasource/cloud-monitoring/package.json | 2 +- .../package.json | 2 +- .../grafana-pyroscope-datasource/package.json | 2 +- .../grafana-testdata-datasource/package.json | 2 +- .../plugins/datasource/jaeger/package.json | 2 +- .../app/plugins/datasource/mssql/package.json | 2 +- .../app/plugins/datasource/mysql/package.json | 2 +- .../app/plugins/datasource/parca/package.json | 2 +- .../app/plugins/datasource/tempo/package.json | 2 +- .../plugins/datasource/zipkin/package.json | 2 +- yarn.lock | 44 +++++++++---------- 19 files changed, 40 insertions(+), 40 deletions(-) diff --git a/package.json b/package.json index b7c6dc24ac1..54a32f06f62 100644 --- a/package.json +++ b/package.json @@ -120,7 +120,7 @@ "@types/jquery": "3.5.32", "@types/js-yaml": "^4.0.5", "@types/jsurl": "^1.2.28", - "@types/lodash": "4.17.14", + "@types/lodash": "4.17.15", "@types/logfmt": "^1.2.3", "@types/lucene": "^2", "@types/node": "22.10.10", diff --git a/packages/grafana-data/package.json b/packages/grafana-data/package.json index db7b6f85e1c..8531d80b874 100644 --- a/packages/grafana-data/package.json +++ b/packages/grafana-data/package.json @@ -64,7 +64,7 @@ "@grafana/tsconfig": "^2.0.0", "@rollup/plugin-node-resolve": "16.0.0", "@types/history": "4.7.11", - "@types/lodash": "4.17.14", + "@types/lodash": "4.17.15", "@types/node": "22.10.10", "@types/papaparse": "5.3.15", "@types/react": "18.3.18", diff --git a/packages/grafana-flamegraph/package.json b/packages/grafana-flamegraph/package.json index 0ff1830c8a2..66de5e2225d 100644 --- a/packages/grafana-flamegraph/package.json +++ b/packages/grafana-flamegraph/package.json @@ -67,7 +67,7 @@ "@testing-library/user-event": "14.5.2", "@types/d3": "^7", "@types/jest": "^29.5.4", - "@types/lodash": "4.17.14", + "@types/lodash": "4.17.15", "@types/node": "22.10.10", "@types/react": "18.3.18", "@types/react-virtualized-auto-sizer": "1.0.4", diff --git a/packages/grafana-prometheus/package.json b/packages/grafana-prometheus/package.json index 44a22efae28..740e9309832 100644 --- a/packages/grafana-prometheus/package.json +++ b/packages/grafana-prometheus/package.json @@ -92,7 +92,7 @@ "@types/eslint": "9.6.1", "@types/jest": "29.5.14", "@types/jquery": "3.5.32", - "@types/lodash": "4.17.14", + "@types/lodash": "4.17.15", "@types/node": "22.10.10", "@types/pluralize": "^0.0.33", "@types/prismjs": "1.26.5", diff --git a/packages/grafana-runtime/package.json b/packages/grafana-runtime/package.json index 6df8bf7b619..56876aab254 100644 --- a/packages/grafana-runtime/package.json +++ b/packages/grafana-runtime/package.json @@ -58,7 +58,7 @@ "@types/angular": "1.8.9", "@types/history": "4.7.11", "@types/jest": "29.5.14", - "@types/lodash": "4.17.14", + "@types/lodash": "4.17.15", "@types/react": "18.3.18", "@types/react-dom": "18.3.5", "@types/systemjs": "6.15.1", diff --git a/packages/grafana-sql/package.json b/packages/grafana-sql/package.json index d2a690cde8e..a5093fc6aee 100644 --- a/packages/grafana-sql/package.json +++ b/packages/grafana-sql/package.json @@ -40,7 +40,7 @@ "@testing-library/react": "16.1.0", "@testing-library/user-event": "14.5.2", "@types/jest": "^29.5.4", - "@types/lodash": "4.17.14", + "@types/lodash": "4.17.15", "@types/node": "22.10.10", "@types/react": "18.3.18", "@types/react-dom": "18.3.5", diff --git a/packages/grafana-ui/package.json b/packages/grafana-ui/package.json index 548c93ee375..a8eb8f1be2b 100644 --- a/packages/grafana-ui/package.json +++ b/packages/grafana-ui/package.json @@ -64,7 +64,7 @@ "@react-aria/utils": "3.27.0", "@tanstack/react-virtual": "^3.5.1", "@types/jquery": "3.5.32", - "@types/lodash": "4.17.14", + "@types/lodash": "4.17.15", "@types/react-table": "7.7.20", "calculate-size": "1.1.1", "classnames": "2.5.1", diff --git a/public/app/plugins/datasource/azuremonitor/package.json b/public/app/plugins/datasource/azuremonitor/package.json index 1a6ea15bb03..69ca46d32f5 100644 --- a/public/app/plugins/datasource/azuremonitor/package.json +++ b/public/app/plugins/datasource/azuremonitor/package.json @@ -32,7 +32,7 @@ "@testing-library/react": "16.1.0", "@testing-library/user-event": "14.5.2", "@types/jest": "29.5.14", - "@types/lodash": "4.17.14", + "@types/lodash": "4.17.15", "@types/node": "22.10.10", "@types/prismjs": "1.26.5", "@types/react": "18.3.18", diff --git a/public/app/plugins/datasource/cloud-monitoring/package.json b/public/app/plugins/datasource/cloud-monitoring/package.json index c278483c99f..8aee980ffa1 100644 --- a/public/app/plugins/datasource/cloud-monitoring/package.json +++ b/public/app/plugins/datasource/cloud-monitoring/package.json @@ -34,7 +34,7 @@ "@testing-library/user-event": "14.5.2", "@types/debounce-promise": "3.1.9", "@types/jest": "29.5.14", - "@types/lodash": "4.17.14", + "@types/lodash": "4.17.15", "@types/node": "22.10.10", "@types/prismjs": "1.26.5", "@types/react": "18.3.18", diff --git a/public/app/plugins/datasource/grafana-postgresql-datasource/package.json b/public/app/plugins/datasource/grafana-postgresql-datasource/package.json index 5ebdb88eb3e..4094ca3e08b 100644 --- a/public/app/plugins/datasource/grafana-postgresql-datasource/package.json +++ b/public/app/plugins/datasource/grafana-postgresql-datasource/package.json @@ -22,7 +22,7 @@ "@testing-library/react": "16.1.0", "@testing-library/user-event": "14.5.2", "@types/jest": "29.5.14", - "@types/lodash": "4.17.14", + "@types/lodash": "4.17.15", "@types/node": "22.10.10", "@types/react": "18.3.18", "ts-node": "10.9.2", diff --git a/public/app/plugins/datasource/grafana-pyroscope-datasource/package.json b/public/app/plugins/datasource/grafana-pyroscope-datasource/package.json index 75c7b27c73d..bba725e85e2 100644 --- a/public/app/plugins/datasource/grafana-pyroscope-datasource/package.json +++ b/public/app/plugins/datasource/grafana-pyroscope-datasource/package.json @@ -26,7 +26,7 @@ "@testing-library/react": "16.1.0", "@testing-library/user-event": "14.5.2", "@types/jest": "29.5.14", - "@types/lodash": "4.17.14", + "@types/lodash": "4.17.15", "@types/node": "22.10.10", "@types/prismjs": "1.26.5", "@types/react": "18.3.18", diff --git a/public/app/plugins/datasource/grafana-testdata-datasource/package.json b/public/app/plugins/datasource/grafana-testdata-datasource/package.json index 3726f299ff1..a1666e38d59 100644 --- a/public/app/plugins/datasource/grafana-testdata-datasource/package.json +++ b/public/app/plugins/datasource/grafana-testdata-datasource/package.json @@ -29,7 +29,7 @@ "@testing-library/user-event": "14.5.2", "@types/d3-random": "^3.0.2", "@types/jest": "29.5.14", - "@types/lodash": "4.17.14", + "@types/lodash": "4.17.15", "@types/node": "22.10.10", "@types/react": "18.3.18", "@types/react-dom": "18.3.5", diff --git a/public/app/plugins/datasource/jaeger/package.json b/public/app/plugins/datasource/jaeger/package.json index a0aefef0d6f..5f1fd12a7ad 100644 --- a/public/app/plugins/datasource/jaeger/package.json +++ b/public/app/plugins/datasource/jaeger/package.json @@ -29,7 +29,7 @@ "@testing-library/react": "16.1.0", "@testing-library/user-event": "14.5.2", "@types/jest": "29.5.14", - "@types/lodash": "4.17.14", + "@types/lodash": "4.17.15", "@types/logfmt": "^1.2.3", "@types/node": "22.10.10", "@types/react": "18.3.18", diff --git a/public/app/plugins/datasource/mssql/package.json b/public/app/plugins/datasource/mssql/package.json index af7fab99453..1c422fdda68 100644 --- a/public/app/plugins/datasource/mssql/package.json +++ b/public/app/plugins/datasource/mssql/package.json @@ -22,7 +22,7 @@ "@testing-library/react": "16.1.0", "@testing-library/user-event": "14.5.2", "@types/jest": "29.5.14", - "@types/lodash": "4.17.14", + "@types/lodash": "4.17.15", "@types/node": "22.10.10", "@types/react": "18.3.18", "ts-node": "10.9.2", diff --git a/public/app/plugins/datasource/mysql/package.json b/public/app/plugins/datasource/mysql/package.json index b35ff254ad9..b1a791dd64b 100644 --- a/public/app/plugins/datasource/mysql/package.json +++ b/public/app/plugins/datasource/mysql/package.json @@ -22,7 +22,7 @@ "@testing-library/react": "16.1.0", "@testing-library/user-event": "14.5.2", "@types/jest": "29.5.14", - "@types/lodash": "4.17.14", + "@types/lodash": "4.17.15", "@types/node": "22.10.10", "@types/react": "18.3.18", "ts-node": "10.9.2", diff --git a/public/app/plugins/datasource/parca/package.json b/public/app/plugins/datasource/parca/package.json index df78383ef64..becc966e421 100644 --- a/public/app/plugins/datasource/parca/package.json +++ b/public/app/plugins/datasource/parca/package.json @@ -22,7 +22,7 @@ "@testing-library/dom": "10.4.0", "@testing-library/react": "16.1.0", "@testing-library/user-event": "14.5.2", - "@types/lodash": "4.17.14", + "@types/lodash": "4.17.15", "@types/node": "22.10.10", "@types/react": "18.3.18", "@types/react-dom": "18.3.5", diff --git a/public/app/plugins/datasource/tempo/package.json b/public/app/plugins/datasource/tempo/package.json index b42d506a96c..9f6e20f1d5b 100644 --- a/public/app/plugins/datasource/tempo/package.json +++ b/public/app/plugins/datasource/tempo/package.json @@ -45,7 +45,7 @@ "@testing-library/react": "16.1.0", "@testing-library/user-event": "14.5.2", "@types/jest": "29.5.14", - "@types/lodash": "4.17.14", + "@types/lodash": "4.17.15", "@types/node": "22.10.10", "@types/prismjs": "1.26.5", "@types/react": "18.3.18", diff --git a/public/app/plugins/datasource/zipkin/package.json b/public/app/plugins/datasource/zipkin/package.json index b52961af94a..f9f8a40298c 100644 --- a/public/app/plugins/datasource/zipkin/package.json +++ b/public/app/plugins/datasource/zipkin/package.json @@ -25,7 +25,7 @@ "@testing-library/jest-dom": "6.6.3", "@testing-library/react": "16.1.0", "@types/jest": "29.5.14", - "@types/lodash": "4.17.14", + "@types/lodash": "4.17.15", "@types/node": "22.10.10", "@types/react": "18.3.18", "@types/react-dom": "18.3.5", diff --git a/yarn.lock b/yarn.lock index 899a9ca41b1..14402fbda83 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2717,7 +2717,7 @@ __metadata: "@testing-library/react": "npm:16.1.0" "@testing-library/user-event": "npm:14.5.2" "@types/jest": "npm:29.5.14" - "@types/lodash": "npm:4.17.14" + "@types/lodash": "npm:4.17.15" "@types/node": "npm:22.10.10" "@types/prismjs": "npm:1.26.5" "@types/react": "npm:18.3.18" @@ -2759,7 +2759,7 @@ __metadata: "@testing-library/react": "npm:16.1.0" "@testing-library/user-event": "npm:14.5.2" "@types/jest": "npm:29.5.14" - "@types/lodash": "npm:4.17.14" + "@types/lodash": "npm:4.17.15" "@types/node": "npm:22.10.10" "@types/react": "npm:18.3.18" lodash: "npm:4.17.21" @@ -2789,7 +2789,7 @@ __metadata: "@testing-library/react": "npm:16.1.0" "@testing-library/user-event": "npm:14.5.2" "@types/jest": "npm:29.5.14" - "@types/lodash": "npm:4.17.14" + "@types/lodash": "npm:4.17.15" "@types/node": "npm:22.10.10" "@types/prismjs": "npm:1.26.5" "@types/react": "npm:18.3.18" @@ -2831,7 +2831,7 @@ __metadata: "@testing-library/user-event": "npm:14.5.2" "@types/d3-random": "npm:^3.0.2" "@types/jest": "npm:29.5.14" - "@types/lodash": "npm:4.17.14" + "@types/lodash": "npm:4.17.15" "@types/node": "npm:22.10.10" "@types/react": "npm:18.3.18" "@types/react-dom": "npm:18.3.5" @@ -2871,7 +2871,7 @@ __metadata: "@testing-library/react": "npm:16.1.0" "@testing-library/user-event": "npm:14.5.2" "@types/jest": "npm:29.5.14" - "@types/lodash": "npm:4.17.14" + "@types/lodash": "npm:4.17.15" "@types/logfmt": "npm:^1.2.3" "@types/node": "npm:22.10.10" "@types/react": "npm:18.3.18" @@ -2912,7 +2912,7 @@ __metadata: "@testing-library/react": "npm:16.1.0" "@testing-library/user-event": "npm:14.5.2" "@types/jest": "npm:29.5.14" - "@types/lodash": "npm:4.17.14" + "@types/lodash": "npm:4.17.15" "@types/node": "npm:22.10.10" "@types/react": "npm:18.3.18" lodash: "npm:4.17.21" @@ -2943,7 +2943,7 @@ __metadata: "@testing-library/react": "npm:16.1.0" "@testing-library/user-event": "npm:14.5.2" "@types/jest": "npm:29.5.14" - "@types/lodash": "npm:4.17.14" + "@types/lodash": "npm:4.17.15" "@types/node": "npm:22.10.10" "@types/react": "npm:18.3.18" lodash: "npm:4.17.21" @@ -2971,7 +2971,7 @@ __metadata: "@testing-library/dom": "npm:10.4.0" "@testing-library/react": "npm:16.1.0" "@testing-library/user-event": "npm:14.5.2" - "@types/lodash": "npm:4.17.14" + "@types/lodash": "npm:4.17.15" "@types/node": "npm:22.10.10" "@types/react": "npm:18.3.18" "@types/react-dom": "npm:18.3.5" @@ -3009,7 +3009,7 @@ __metadata: "@testing-library/user-event": "npm:14.5.2" "@types/debounce-promise": "npm:3.1.9" "@types/jest": "npm:29.5.14" - "@types/lodash": "npm:4.17.14" + "@types/lodash": "npm:4.17.15" "@types/node": "npm:22.10.10" "@types/prismjs": "npm:1.26.5" "@types/react": "npm:18.3.18" @@ -3062,7 +3062,7 @@ __metadata: "@testing-library/react": "npm:16.1.0" "@testing-library/user-event": "npm:14.5.2" "@types/jest": "npm:29.5.14" - "@types/lodash": "npm:4.17.14" + "@types/lodash": "npm:4.17.15" "@types/node": "npm:22.10.10" "@types/prismjs": "npm:1.26.5" "@types/react": "npm:18.3.18" @@ -3112,7 +3112,7 @@ __metadata: "@testing-library/jest-dom": "npm:6.6.3" "@testing-library/react": "npm:16.1.0" "@types/jest": "npm:29.5.14" - "@types/lodash": "npm:4.17.14" + "@types/lodash": "npm:4.17.15" "@types/node": "npm:22.10.10" "@types/react": "npm:18.3.18" "@types/react-dom": "npm:18.3.5" @@ -3205,7 +3205,7 @@ __metadata: "@rollup/plugin-node-resolve": "npm:16.0.0" "@types/d3-interpolate": "npm:^3.0.0" "@types/history": "npm:4.7.11" - "@types/lodash": "npm:4.17.14" + "@types/lodash": "npm:4.17.15" "@types/node": "npm:22.10.10" "@types/papaparse": "npm:5.3.15" "@types/react": "npm:18.3.18" @@ -3398,7 +3398,7 @@ __metadata: "@testing-library/user-event": "npm:14.5.2" "@types/d3": "npm:^7" "@types/jest": "npm:^29.5.4" - "@types/lodash": "npm:4.17.14" + "@types/lodash": "npm:4.17.15" "@types/node": "npm:22.10.10" "@types/react": "npm:18.3.18" "@types/react-virtualized-auto-sizer": "npm:1.0.4" @@ -3627,7 +3627,7 @@ __metadata: "@types/eslint": "npm:9.6.1" "@types/jest": "npm:29.5.14" "@types/jquery": "npm:3.5.32" - "@types/lodash": "npm:4.17.14" + "@types/lodash": "npm:4.17.15" "@types/node": "npm:22.10.10" "@types/pluralize": "npm:^0.0.33" "@types/prismjs": "npm:1.26.5" @@ -3719,7 +3719,7 @@ __metadata: "@types/angular": "npm:1.8.9" "@types/history": "npm:4.7.11" "@types/jest": "npm:29.5.14" - "@types/lodash": "npm:4.17.14" + "@types/lodash": "npm:4.17.15" "@types/react": "npm:18.3.18" "@types/react-dom": "npm:18.3.5" "@types/systemjs": "npm:6.15.1" @@ -3885,7 +3885,7 @@ __metadata: "@testing-library/react": "npm:16.1.0" "@testing-library/user-event": "npm:14.5.2" "@types/jest": "npm:^29.5.4" - "@types/lodash": "npm:4.17.14" + "@types/lodash": "npm:4.17.15" "@types/node": "npm:22.10.10" "@types/react": "npm:18.3.18" "@types/react-dom": "npm:18.3.5" @@ -4048,7 +4048,7 @@ __metadata: "@types/is-hotkey": "npm:0.1.10" "@types/jest": "npm:29.5.14" "@types/jquery": "npm:3.5.32" - "@types/lodash": "npm:4.17.14" + "@types/lodash": "npm:4.17.15" "@types/mock-raf": "npm:1.0.6" "@types/node": "npm:22.10.10" "@types/prismjs": "npm:1.26.5" @@ -9647,10 +9647,10 @@ __metadata: languageName: node linkType: hard -"@types/lodash@npm:*, @types/lodash@npm:4.17.14, @types/lodash@npm:^4.14.172": - version: 4.17.14 - resolution: "@types/lodash@npm:4.17.14" - checksum: 10/6ee40725f3e192f5ef1f493caca19210aa7acd7adc3136b8dba84d418a35be0abea0668105aed9f696ad62a54310a9c0d328971ad4b157f5bcda700424ed5aae +"@types/lodash@npm:*, @types/lodash@npm:4.17.15, @types/lodash@npm:^4.14.172": + version: 4.17.15 + resolution: "@types/lodash@npm:4.17.15" + checksum: 10/27b348b5971b9c670215331b52448a13d7d65bf1fbd320a7049c9c153c1186ff5d116ba75f05f07d32d7ece8a992b26a30c7bdc9be22a3d1e4e3e6068aa04603 languageName: node linkType: hard @@ -17819,7 +17819,7 @@ __metadata: "@types/jquery": "npm:3.5.32" "@types/js-yaml": "npm:^4.0.5" "@types/jsurl": "npm:^1.2.28" - "@types/lodash": "npm:4.17.14" + "@types/lodash": "npm:4.17.15" "@types/logfmt": "npm:^1.2.3" "@types/lucene": "npm:^2" "@types/node": "npm:22.10.10" From 8c2824cf3b0ab8fa46d01b9b10aca400679e9215 Mon Sep 17 00:00:00 2001 From: Tobias Skarhed <1438972+tskarhed@users.noreply.github.com> Date: Tue, 28 Jan 2025 11:03:08 +0100 Subject: [PATCH 09/39] MultiCombobox: Autosize (#99510) * Add input auto resizing * Initial auotsize * Initial implementation * Remove px * Remove unused import * Handle backspace and support the width prop * Make sizing work with useComboboxFloat * Remove unused expression * Add supoport for min and max width * Change space for clicking --- .../Combobox/MultiCombobox.internal.story.tsx | 18 ++++++++ .../src/components/Combobox/MultiCombobox.tsx | 43 ++++++++++++------- .../src/components/Combobox/SuffixIcon.tsx | 17 ++++++++ .../Combobox/getMultiComboboxStyles.ts | 20 ++++++++- .../Combobox/useMultiInputAutoSize.tsx | 33 ++++++++++++++ 5 files changed, 113 insertions(+), 18 deletions(-) create mode 100644 packages/grafana-ui/src/components/Combobox/SuffixIcon.tsx create mode 100644 packages/grafana-ui/src/components/Combobox/useMultiInputAutoSize.tsx diff --git a/packages/grafana-ui/src/components/Combobox/MultiCombobox.internal.story.tsx b/packages/grafana-ui/src/components/Combobox/MultiCombobox.internal.story.tsx index 0dc4f2e9128..5d27d2ac594 100644 --- a/packages/grafana-ui/src/components/Combobox/MultiCombobox.internal.story.tsx +++ b/packages/grafana-ui/src/components/Combobox/MultiCombobox.internal.story.tsx @@ -48,6 +48,24 @@ export const Basic: Story = { }, }; +export const AutoSize: Story = { + args: { ...commonArgs, width: 'auto', minWidth: 20 }, + render: (args) => { + const [{ value }, setArgs] = useArgs(); + + return ( + { + action('onChange')(val); + setArgs({ value: val }); + }} + /> + ); + }, +}; + const ManyOptionsStory: StoryFn = ({ numberOfOptions = 1e4, ...args }) => { const [value, setValue] = useState([]); const [options, setOptions] = useState([]); diff --git a/packages/grafana-ui/src/components/Combobox/MultiCombobox.tsx b/packages/grafana-ui/src/components/Combobox/MultiCombobox.tsx index 0cede71af9e..4b819ab4a28 100644 --- a/packages/grafana-ui/src/components/Combobox/MultiCombobox.tsx +++ b/packages/grafana-ui/src/components/Combobox/MultiCombobox.tsx @@ -10,13 +10,13 @@ import { Box } from '../Layout/Box/Box'; import { Stack } from '../Layout/Stack/Stack'; import { Portal } from '../Portal/Portal'; import { ScrollContainer } from '../ScrollContainer/ScrollContainer'; -import { Spinner } from '../Spinner/Spinner'; import { Text } from '../Text/Text'; import { Tooltip } from '../Tooltip'; import { ComboboxBaseProps, AutoSizeConditionals, VIRTUAL_OVERSCAN_ITEMS } from './Combobox'; import { NotFoundError } from './MessageRows'; import { OptionListItem } from './OptionListItem'; +import { SuffixIcon } from './SuffixIcon'; import { ValuePill } from './ValuePill'; import { itemFilter, itemToString } from './filter'; import { getComboboxStyles, MENU_OPTION_HEIGHT, MENU_OPTION_HEIGHT_DESCRIPTION } from './getComboboxStyles'; @@ -24,6 +24,7 @@ import { getMultiComboboxStyles } from './getMultiComboboxStyles'; import { ALL_OPTION_VALUE, ComboboxOption } from './types'; import { useComboboxFloat } from './useComboboxFloat'; import { MAX_SHOWN_ITEMS, useMeasureMulti } from './useMeasureMulti'; +import { useMultiInputAutoSize } from './useMultiInputAutoSize'; interface MultiComboboxBaseProps extends Omit, 'value' | 'onChange'> { value?: T[] | Array>; @@ -34,7 +35,19 @@ interface MultiComboboxBaseProps extends Omit = MultiComboboxBaseProps & AutoSizeConditionals; export const MultiCombobox = (props: MultiComboboxProps) => { - const { options, placeholder, onChange, value, width, enableAllOption, invalid, loading, disabled } = props; + const { + options, + placeholder, + onChange, + value, + width, + enableAllOption, + invalid, + loading, + disabled, + minWidth, + maxWidth, + } = props; const isAsync = typeof options === 'function'; const selectedItems = useMemo(() => { @@ -129,7 +142,7 @@ export const MultiCombobox = (props: MultiComboboxPro }); const { - //getToggleButtonProps, + getToggleButtonProps, //getLabelProps, isOpen, highlightedIndex, @@ -199,7 +212,7 @@ export const MultiCombobox = (props: MultiComboboxPro }); const { inputRef: containerRef, floatingRef, floatStyles, scrollRef } = useComboboxFloat(items, isOpen); - const multiStyles = useStyles2(getMultiComboboxStyles, isOpen, invalid, disabled); + const multiStyles = useStyles2(getMultiComboboxStyles, isOpen, invalid, disabled, width, minWidth, maxWidth); const virtualizerOptions = { count: items.length, @@ -214,13 +227,10 @@ export const MultiCombobox = (props: MultiComboboxPro // Selected items that show up in the input field const visibleItems = isOpen ? selectedItems.slice(0, MAX_SHOWN_ITEMS) : selectedItems.slice(0, shownItems); + const { inputRef, inputWidth } = useMultiInputAutoSize(inputValue); return ( -
-
+
+
{visibleItems.map((item, index) => ( (props: MultiComboboxPro getDropdownProps({ disabled, preventKeyAction: isOpen, - placeholder: selectedItems.length > 0 ? undefined : placeholder, + placeholder, + ref: inputRef, + style: { width: inputWidth }, }) )} /> - {loading && ( -
- -
- )} + +
+ +
diff --git a/packages/grafana-ui/src/components/Combobox/SuffixIcon.tsx b/packages/grafana-ui/src/components/Combobox/SuffixIcon.tsx new file mode 100644 index 00000000000..04fa29b699b --- /dev/null +++ b/packages/grafana-ui/src/components/Combobox/SuffixIcon.tsx @@ -0,0 +1,17 @@ +import { Icon } from '../Icon/Icon'; + +interface Props { + isLoading: boolean; + isOpen: boolean; +} + +export const SuffixIcon = ({ isLoading, isOpen }: Props) => { + const suffixIcon = isLoading + ? 'spinner' + : // If it's loading, show loading icon. Otherwise, icon indicating menu state + isOpen + ? 'search' + : 'angle-down'; + + return ; +}; diff --git a/packages/grafana-ui/src/components/Combobox/getMultiComboboxStyles.ts b/packages/grafana-ui/src/components/Combobox/getMultiComboboxStyles.ts index 4347acdb0e7..06bd2dd2a22 100644 --- a/packages/grafana-ui/src/components/Combobox/getMultiComboboxStyles.ts +++ b/packages/grafana-ui/src/components/Combobox/getMultiComboboxStyles.ts @@ -9,18 +9,33 @@ export const getMultiComboboxStyles = ( theme: GrafanaTheme2, isOpen: boolean, invalid?: boolean, - disabled?: boolean + disabled?: boolean, + width?: number | 'auto', + minWidth?: number, + maxWidth?: number ) => { const inputStyles = getInputStyles({ theme, invalid }); const focusStyles = getFocusStyles(theme); + const wrapperWidth = width && width !== 'auto' ? theme.spacing(width) : '100%'; + const wrapperMinWidth = minWidth ? theme.spacing(minWidth) : ''; + const wrapperMaxWidth = maxWidth ? theme.spacing(maxWidth) : ''; + return { + container: css({ + width: width === 'auto' ? 'auto' : wrapperWidth, + minWidth: wrapperMinWidth, + maxWidth: wrapperMaxWidth, + display: width === 'auto' ? 'inline-block' : 'block', + }), // wraps everything wrapper: cx( inputStyles.input, css({ display: 'flex', + width: '100%', gap: theme.spacing(0.5), padding: theme.spacing(0.5), + paddingRight: 28, // Account for suffix '&:focus-within': { ...focusStyles, }, @@ -31,7 +46,8 @@ export const getMultiComboboxStyles = ( outline: 'none', background: 'transparent', flexGrow: 1, - minWidth: '0', + maxWidth: '100%', + minWidth: 40, // This is a bit arbitrary, but is used to leave some space for clicking. This will override the minWidth property '&::placeholder': { color: theme.colors.text.disabled, }, diff --git a/packages/grafana-ui/src/components/Combobox/useMultiInputAutoSize.tsx b/packages/grafana-ui/src/components/Combobox/useMultiInputAutoSize.tsx new file mode 100644 index 00000000000..30ade93a632 --- /dev/null +++ b/packages/grafana-ui/src/components/Combobox/useMultiInputAutoSize.tsx @@ -0,0 +1,33 @@ +import { useLayoutEffect, useRef, useState } from 'react'; + +import { measureText } from '../../utils'; + +export function useMultiInputAutoSize(inputValue: string) { + const inputRef = useRef(null); + const initialInputWidth = useRef(0); // Store initial width to prevent resizing on backspace + const [inputWidth, setInputWidth] = useState(''); + + useLayoutEffect(() => { + if (inputRef.current && inputValue == null && initialInputWidth.current === 0) { + initialInputWidth.current = inputRef?.current.getBoundingClientRect().width; + } + + if (!inputRef.current || inputValue == null) { + setInputWidth(''); + return; + } + + const fontSize = window.getComputedStyle(inputRef.current).fontSize; + const textWidth = measureText(inputRef.current.value || '', parseInt(fontSize, 10)).width; + + if (textWidth < initialInputWidth.current) { + // Let input fill all space before resizing + setInputWidth(''); + } else { + // Add pixels to prevent clipping + setInputWidth(`${textWidth + 5}px`); + } + }, [inputValue]); + + return { inputRef, inputWidth }; +} From 58a279e109571cf4f2d4128f62e15347ece8e88f Mon Sep 17 00:00:00 2001 From: "grafana-delivery-bot[bot]" <132647405+grafana-delivery-bot[bot]@users.noreply.github.com> Date: Tue, 28 Jan 2025 10:28:22 +0000 Subject: [PATCH 10/39] Release: update changelog for 10.4.15 (#99634) Update changelog Co-authored-by: github-actions[bot] --- CHANGELOG.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6bb3ea46a8e..34fd866c684 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,17 @@ + + +# 10.4.15 (2025-01-28) + +### Features and enhancements + +- **Security:** Update to Go 1.22.11 - Backport to v10.4.x [#99128](https://github.com/grafana/grafana/pull/99128), [@Proximyst](https://github.com/Proximyst) +- **Security:** Update to Go 1.22.11 - Backport to v10.4.x (Enterprise) + +### Bug fixes + +- **Azure/GCM:** Improve error display [#97590](https://github.com/grafana/grafana/pull/97590), [@aangelisc](https://github.com/aangelisc) + + # 11.4.0 (2024-12-05) From f6fc39e71fa02fd454bfcb5b0672b2a2cd08ab16 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 28 Jan 2025 10:30:33 +0000 Subject: [PATCH 11/39] Update dependency @types/webpack-env to v1.18.7 (#99633) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 14402fbda83..3485996dde5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10280,9 +10280,9 @@ __metadata: linkType: hard "@types/webpack-env@npm:^1.18.4": - version: 1.18.6 - resolution: "@types/webpack-env@npm:1.18.6" - checksum: 10/463e545441d206e52bf24b4577a503bf9a96198a69b3ad1234d15bf96e9a6d5ccb25bb1197e520541cb8da968232305fe8d8400595d5745e16abfdc4089e73d0 + version: 1.18.7 + resolution: "@types/webpack-env@npm:1.18.7" + checksum: 10/b07ca300b8e8af9ffad2bfdd6a662adffe49f86710a72cec01e4d10bb99444ed1ce45efbedda66ccd399b26ee22e7eadefd7c118a64047f5ea465b556ba86cf3 languageName: node linkType: hard From a28328d7645dbbde185aae1bca6b620899621b61 Mon Sep 17 00:00:00 2001 From: Alexander Akhmetov Date: Tue, 28 Jan 2025 11:34:26 +0100 Subject: [PATCH 12/39] Alerting: Call the deletion reason provider even if the rule is no longer scheduled (#99571) Alerting: Call the deletion reason provider even if the rule is not scheduled anymore --- pkg/services/ngalert/schedule/alert_rule.go | 8 +++++++- pkg/services/ngalert/schedule/alert_rule_test.go | 8 ++++++++ pkg/services/ngalert/schedule/recording_rule.go | 14 +++++++++----- .../ngalert/schedule/recording_rule_test.go | 11 ++++++++++- pkg/services/ngalert/schedule/schedule.go | 10 +++++----- .../ngalert/schedule/schedule_unit_test.go | 6 +++--- 6 files changed, 42 insertions(+), 15 deletions(-) diff --git a/pkg/services/ngalert/schedule/alert_rule.go b/pkg/services/ngalert/schedule/alert_rule.go index 8f56fdfa8a9..f4a301aff78 100644 --- a/pkg/services/ngalert/schedule/alert_rule.go +++ b/pkg/services/ngalert/schedule/alert_rule.go @@ -42,6 +42,8 @@ type Rule interface { Type() ngmodels.RuleType // Status indicates the status of the evaluating rule. Status() ngmodels.RuleStatus + // Identifier returns the identifier of the rule. + Identifier() ngmodels.AlertRuleKeyWithGroup } type ruleFactoryFunc func(context.Context, *ngmodels.AlertRule) Rule @@ -71,7 +73,7 @@ func newRuleFactory( if rule.Type() == ngmodels.RuleTypeRecording { return newRecordingRule( ctx, - rule.GetKey(), + rule.GetKeyWithGroup(), maxAttempts, clock, evalFactory, @@ -178,6 +180,10 @@ func newAlertRule( } } +func (a *alertRule) Identifier() ngmodels.AlertRuleKeyWithGroup { + return a.key +} + func (a *alertRule) Type() ngmodels.RuleType { return ngmodels.RuleTypeAlerting } diff --git a/pkg/services/ngalert/schedule/alert_rule_test.go b/pkg/services/ngalert/schedule/alert_rule_test.go index bb9e648c291..4593a4052e0 100644 --- a/pkg/services/ngalert/schedule/alert_rule_test.go +++ b/pkg/services/ngalert/schedule/alert_rule_test.go @@ -269,6 +269,14 @@ func TestAlertRule(t *testing.T) { }) } +func TestAlertRuleIdentifier(t *testing.T) { + t.Run("should return correct identifier", func(t *testing.T) { + key := models.GenerateRuleKeyWithGroup(1) + r := blankRuleForTests(context.Background(), key) + require.Equal(t, key, r.Identifier()) + }) +} + func blankRuleForTests(ctx context.Context, key models.AlertRuleKeyWithGroup) *alertRule { return newAlertRule(ctx, key, nil, false, 0, nil, nil, nil, nil, nil, nil, log.NewNopLogger(), nil, nil, nil) } diff --git a/pkg/services/ngalert/schedule/recording_rule.go b/pkg/services/ngalert/schedule/recording_rule.go index 3abf44b6457..954b0869a4a 100644 --- a/pkg/services/ngalert/schedule/recording_rule.go +++ b/pkg/services/ngalert/schedule/recording_rule.go @@ -30,7 +30,7 @@ type RuleStatus struct { } type recordingRule struct { - key ngmodels.AlertRuleKey + key ngmodels.AlertRuleKeyWithGroup ctx context.Context evalCh chan *Evaluation @@ -56,8 +56,8 @@ type recordingRule struct { tracer tracing.Tracer } -func newRecordingRule(parent context.Context, key ngmodels.AlertRuleKey, maxAttempts int64, clock clock.Clock, evalFactory eval.EvaluatorFactory, cfg setting.RecordingRuleSettings, logger log.Logger, metrics *metrics.Scheduler, tracer tracing.Tracer, writer RecordingWriter, evalAppliedHook evalAppliedFunc, stopAppliedHook stopAppliedFunc) *recordingRule { - ctx, stop := util.WithCancelCause(ngmodels.WithRuleKey(parent, key)) +func newRecordingRule(parent context.Context, key ngmodels.AlertRuleKeyWithGroup, maxAttempts int64, clock clock.Clock, evalFactory eval.EvaluatorFactory, cfg setting.RecordingRuleSettings, logger log.Logger, metrics *metrics.Scheduler, tracer tracing.Tracer, writer RecordingWriter, evalAppliedHook evalAppliedFunc, stopAppliedHook stopAppliedFunc) *recordingRule { + ctx, stop := util.WithCancelCause(ngmodels.WithRuleKey(parent, key.AlertRuleKey)) return &recordingRule{ key: key, ctx: ctx, @@ -80,6 +80,10 @@ func newRecordingRule(parent context.Context, key ngmodels.AlertRuleKey, maxAtte } } +func (r *recordingRule) Identifier() ngmodels.AlertRuleKeyWithGroup { + return r.key +} + func (r *recordingRule) Type() ngmodels.RuleType { return ngmodels.RuleTypeRecording } @@ -301,7 +305,7 @@ func (r *recordingRule) evaluationDoneTestHook(ev *Evaluation) { return } - r.evalAppliedHook(r.key, ev.scheduledAt) + r.evalAppliedHook(r.key.AlertRuleKey, ev.scheduledAt) } // frameRef gets frames from a QueryDataResponse for a particular refID. It returns an error if the frames do not exist or have no data. @@ -328,5 +332,5 @@ func (r *recordingRule) stopApplied() { return } - r.stopAppliedHook(r.key) + r.stopAppliedHook(r.key.AlertRuleKey) } diff --git a/pkg/services/ngalert/schedule/recording_rule_test.go b/pkg/services/ngalert/schedule/recording_rule_test.go index 3a85921fdfe..7e72453bb9f 100644 --- a/pkg/services/ngalert/schedule/recording_rule_test.go +++ b/pkg/services/ngalert/schedule/recording_rule_test.go @@ -152,11 +152,20 @@ func TestRecordingRule(t *testing.T) { }) } +func TestRecordingRuleIdentifier(t *testing.T) { + t.Run("should return correct identifier", func(t *testing.T) { + key := models.GenerateRuleKeyWithGroup(1) + r := blankRecordingRuleForTests(context.Background()) + r.key = key + require.Equal(t, key, r.Identifier()) + }) +} + func blankRecordingRuleForTests(ctx context.Context) *recordingRule { st := setting.RecordingRuleSettings{ Enabled: true, } - return newRecordingRule(context.Background(), models.AlertRuleKey{}, 0, nil, nil, st, log.NewNopLogger(), nil, nil, writer.FakeWriter{}, nil, nil) + return newRecordingRule(context.Background(), models.AlertRuleKeyWithGroup{}, 0, nil, nil, st, log.NewNopLogger(), nil, nil, writer.FakeWriter{}, nil, nil) } func TestRecordingRule_Integration(t *testing.T) { diff --git a/pkg/services/ngalert/schedule/schedule.go b/pkg/services/ngalert/schedule/schedule.go index e02755eb0ec..3bc21115ca0 100644 --- a/pkg/services/ngalert/schedule/schedule.go +++ b/pkg/services/ngalert/schedule/schedule.go @@ -198,7 +198,7 @@ func (sch *schedule) deleteAlertRule(ctx context.Context, keys ...ngmodels.Alert // It can happen that the scheduler has deleted the alert rule before the // Ruler API has called DeleteAlertRule. This can happen as requests to // the Ruler API do not hold an exclusive lock over all scheduler operations. - rule, ok := sch.schedulableAlertRules.del(key) + _, ok := sch.schedulableAlertRules.del(key) if !ok { sch.log.Info("Alert rule cannot be removed from the scheduler as it is not scheduled", key.LogContext()...) } @@ -210,7 +210,7 @@ func (sch *schedule) deleteAlertRule(ctx context.Context, keys ...ngmodels.Alert } // stop rule evaluation - reason := sch.getRuleStopReason(ctx, key, rule) + reason := sch.getRuleStopReason(ctx, ruleRoutine.Identifier()) ruleRoutine.Stop(reason) } // Our best bet at this point is that we update the metrics with what we hope to schedule in the next tick. @@ -218,14 +218,14 @@ func (sch *schedule) deleteAlertRule(ctx context.Context, keys ...ngmodels.Alert sch.updateRulesMetrics(alertRules) } -func (sch *schedule) getRuleStopReason(ctx context.Context, key ngmodels.AlertRuleKey, rule *ngmodels.AlertRule) error { +func (sch *schedule) getRuleStopReason(ctx context.Context, key ngmodels.AlertRuleKeyWithGroup) error { // If the ruleStopReasonProvider is defined, we will use it to get the reason why the // alert rule was stopped. If it returns an error, we will use the default reason. - if sch.ruleStopReasonProvider == nil || rule == nil { + if sch.ruleStopReasonProvider == nil { return errRuleDeleted } - stopReason, err := sch.ruleStopReasonProvider.FindReason(ctx, sch.log, rule.GetKeyWithGroup()) + stopReason, err := sch.ruleStopReasonProvider.FindReason(ctx, sch.log, key) if err != nil { sch.log.New(key.LogContext()...).Error("Failed to get stop reason", "error", err) return errRuleDeleted diff --git a/pkg/services/ngalert/schedule/schedule_unit_test.go b/pkg/services/ngalert/schedule/schedule_unit_test.go index 311a419f88b..575f19cda27 100644 --- a/pkg/services/ngalert/schedule/schedule_unit_test.go +++ b/pkg/services/ngalert/schedule/schedule_unit_test.go @@ -991,7 +991,7 @@ func TestSchedule_deleteAlertRule(t *testing.T) { require.False(t, sch.registry.exists(key)) }) - t.Run("it should not call ruleStopReasonProvider if the rule is not found in the registry", func(t *testing.T) { + t.Run("it should still call ruleStopReasonProvider if the rule is not found in the registry", func(t *testing.T) { mockReasonProvider := new(mockAlertRuleStopReasonProvider) expectedReason := errors.New("some rule deletion reason") mockReasonProvider.On("FindReason", mock.Anything, mock.Anything, mock.Anything).Return(expectedReason, nil) @@ -1008,9 +1008,9 @@ func TestSchedule_deleteAlertRule(t *testing.T) { sch.deleteAlertRule(ctx, key) - mockReasonProvider.AssertNotCalled(t, "FindReason") + mockReasonProvider.AssertCalled(t, "FindReason", mock.Anything, mock.Anything, rule.GetKeyWithGroup()) - require.ErrorIs(t, info.(*alertRule).ctx.Err(), errRuleDeleted) + require.ErrorIs(t, info.(*alertRule).ctx.Err(), expectedReason) require.False(t, sch.registry.exists(key)) }) }) From a66857dfbea5c1d7c111742095859bf04e124e7c Mon Sep 17 00:00:00 2001 From: "grafana-delivery-bot[bot]" <132647405+grafana-delivery-bot[bot]@users.noreply.github.com> Date: Tue, 28 Jan 2025 10:54:23 +0000 Subject: [PATCH 13/39] Release: update changelog for 11.0.10 (#99636) Update changelog Co-authored-by: github-actions[bot] --- CHANGELOG.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 34fd866c684..337028dfe77 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,17 @@ + + +# 11.0.10 (2025-01-28) + +### Features and enhancements + +- **Security:** Update to Go 1.22.11 - Backport to v11.0.x [#99127](https://github.com/grafana/grafana/pull/99127), [@Proximyst](https://github.com/Proximyst) +- **Security:** Update to Go 1.22.11 - Backport to v11.0.x (Enterprise) + +### Bug fixes + +- **Azure/GCM:** Improve error display [#97592](https://github.com/grafana/grafana/pull/97592), [@aangelisc](https://github.com/aangelisc) + + # 10.4.15 (2025-01-28) From 8e6a868178623a4ec7b479246f33ddc630a6f8d7 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 28 Jan 2025 10:56:34 +0000 Subject: [PATCH 14/39] Update dependency papaparse to v5.5.2 (#99635) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- packages/grafana-data/package.json | 2 +- yarn.lock | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/grafana-data/package.json b/packages/grafana-data/package.json index 8531d80b874..8ba4574e8df 100644 --- a/packages/grafana-data/package.json +++ b/packages/grafana-data/package.json @@ -51,7 +51,7 @@ "moment": "2.30.1", "moment-timezone": "0.5.46", "ol": "7.4.0", - "papaparse": "5.5.1", + "papaparse": "5.5.2", "react-use": "17.6.0", "rxjs": "7.8.1", "string-hash": "^1.1.3", diff --git a/yarn.lock b/yarn.lock index 3485996dde5..7b812284201 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3225,7 +3225,7 @@ __metadata: moment: "npm:2.30.1" moment-timezone: "npm:0.5.46" ol: "npm:7.4.0" - papaparse: "npm:5.5.1" + papaparse: "npm:5.5.2" react: "npm:18.3.1" react-dom: "npm:18.3.1" react-use: "npm:17.6.0" @@ -23759,10 +23759,10 @@ __metadata: languageName: node linkType: hard -"papaparse@npm:5.5.1": - version: 5.5.1 - resolution: "papaparse@npm:5.5.1" - checksum: 10/6c3b47bd316cf11936641abe5590c6fb1e5b04297d1c594965a44dba89cc0f26b4e4d61eb1933352c538a30c4915d110628d14cf4d60cef0b8245c9a26477602 +"papaparse@npm:5.5.2": + version: 5.5.2 + resolution: "papaparse@npm:5.5.2" + checksum: 10/f5d25ccb10b1b9e87a0fe2d2965c7f8a83223cd79ec8963dd7e8766e8e97a6228dc6c20738a78148dfc74f32b2372003de732a7d50593f38b6a2bb5092710bc7 languageName: node linkType: hard From d2eac460cf04f2fbd876f923be61400133b7849b Mon Sep 17 00:00:00 2001 From: "grafana-delivery-bot[bot]" <132647405+grafana-delivery-bot[bot]@users.noreply.github.com> Date: Tue, 28 Jan 2025 11:25:01 +0000 Subject: [PATCH 15/39] Release: update changelog for 11.1.11 (#99639) Update changelog Co-authored-by: github-actions[bot] --- CHANGELOG.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 337028dfe77..f5ca2310ca3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,17 @@ + + +# 11.1.11 (2025-01-28) + +### Features and enhancements + +- **Security:** Update to Go 1.22.11 - Backport to v11.1.x [#99126](https://github.com/grafana/grafana/pull/99126), [@Proximyst](https://github.com/Proximyst) +- **Security:** Update to Go 1.22.11 - Backport to v11.1.x (Enterprise) + +### Bug fixes + +- **Azure/GCM:** Improve error display [#97595](https://github.com/grafana/grafana/pull/97595), [@aangelisc](https://github.com/aangelisc) + + # 11.0.10 (2025-01-28) From ecc9b0c10c6693d596bcd961d2e522978d94176b Mon Sep 17 00:00:00 2001 From: Gareth Dawson Date: Tue, 28 Jan 2025 18:44:22 +0700 Subject: [PATCH 16/39] partner data sources: remove experimental imports from datasources (#99370) * remove experimental imports from datasources * add plugin-ui deps * remove * trigger workflow * Make versions of plugin-ui fixed * Fix import in test --------- Co-authored-by: Ivana Huckova --- .../components/ArgQueryEditor/ArgQueryEditor.tsx | 2 +- .../ConfigEditor/AzureCredentialsForm.tsx | 2 +- .../components/ConfigEditor/ConfigEditor.tsx | 2 +- .../ConfigEditor/CurrentUserFallbackCredentials.tsx | 2 +- .../LogsQueryEditor/AdvancedResourcePicker.tsx | 2 +- .../components/LogsQueryEditor/LogsQueryEditor.tsx | 2 +- .../MetricsQueryEditor/AdvancedResourcePicker.tsx | 2 +- .../MetricsQueryEditor/DimensionFields.tsx | 2 +- .../MetricsQueryEditor/MetricsQueryEditor.tsx | 2 +- .../components/QueryEditor/QueryHeader.tsx | 2 +- .../components/TracesQueryEditor/Filter.tsx | 2 +- .../components/TracesQueryEditor/Filters.tsx | 2 +- .../TracesQueryEditor/TracesQueryEditor.tsx | 2 +- .../azuremonitor/components/shared/Field.tsx | 2 +- .../plugins/datasource/azuremonitor/package.json | 2 +- .../cloud-monitoring/components/Aggregation.tsx | 2 +- .../cloud-monitoring/components/AliasBy.tsx | 2 +- .../cloud-monitoring/components/Alignment.tsx | 2 +- .../components/AnnotationQueryEditor.tsx | 2 +- .../components/ConfigEditor/ConfigEditor.tsx | 2 +- .../cloud-monitoring/components/GraphPeriod.tsx | 2 +- .../cloud-monitoring/components/GroupBy.tsx | 2 +- .../cloud-monitoring/components/LabelFilter.tsx | 2 +- .../components/LookbackPeriodSelect.tsx | 2 +- .../components/MetricQueryEditor.tsx | 3 ++- .../cloud-monitoring/components/Preprocessor.tsx | 2 +- .../cloud-monitoring/components/Project.tsx | 2 +- .../cloud-monitoring/components/PromQLEditor.tsx | 2 +- .../cloud-monitoring/components/QueryEditor.tsx | 2 +- .../cloud-monitoring/components/QueryHeader.tsx | 2 +- .../datasource/cloud-monitoring/components/SLO.tsx | 2 +- .../cloud-monitoring/components/SLOQueryEditor.tsx | 2 +- .../cloud-monitoring/components/Selector.tsx | 2 +- .../cloud-monitoring/components/Service.tsx | 2 +- .../components/VisualMetricQueryEditor.tsx | 2 +- .../datasource/cloud-monitoring/package.json | 2 +- .../editor/query/influxql/visual/FromSection.tsx | 2 +- .../query/influxql/visual/PartListSection.tsx | 2 +- .../editor/query/influxql/visual/TagsSection.tsx | 2 +- .../visual/VisualInfluxQLEditor.tags.test.tsx | 13 ++++++------- .../influxdb/fsql/datasource.flightsql.ts | 2 +- .../influxdb/fsql/sqlCompletionProvider.ts | 2 +- .../mssql/configuration/ConfigurationEditor.tsx | 2 +- .../datasource/mssql/configuration/Kerberos.tsx | 2 +- public/app/plugins/datasource/mssql/datasource.ts | 2 +- public/app/plugins/datasource/mssql/package.json | 2 +- .../datasource/mssql/sqlCompletionProvider.ts | 2 +- yarn.lock | 6 +++--- 48 files changed, 56 insertions(+), 56 deletions(-) diff --git a/public/app/plugins/datasource/azuremonitor/components/ArgQueryEditor/ArgQueryEditor.tsx b/public/app/plugins/datasource/azuremonitor/components/ArgQueryEditor/ArgQueryEditor.tsx index a06b3739072..4c326a44fb3 100644 --- a/public/app/plugins/datasource/azuremonitor/components/ArgQueryEditor/ArgQueryEditor.tsx +++ b/public/app/plugins/datasource/azuremonitor/components/ArgQueryEditor/ArgQueryEditor.tsx @@ -1,7 +1,7 @@ import { intersection } from 'lodash'; import { useState, useMemo } from 'react'; -import { EditorFieldGroup, EditorRow, EditorRows } from '@grafana/experimental'; +import { EditorFieldGroup, EditorRow, EditorRows } from '@grafana/plugin-ui'; import Datasource from '../../datasource'; import { selectors } from '../../e2e/selectors'; diff --git a/public/app/plugins/datasource/azuremonitor/components/ConfigEditor/AzureCredentialsForm.tsx b/public/app/plugins/datasource/azuremonitor/components/ConfigEditor/AzureCredentialsForm.tsx index fa80caadc82..6ab8bc69a18 100644 --- a/public/app/plugins/datasource/azuremonitor/components/ConfigEditor/AzureCredentialsForm.tsx +++ b/public/app/plugins/datasource/azuremonitor/components/ConfigEditor/AzureCredentialsForm.tsx @@ -2,7 +2,7 @@ import { useMemo } from 'react'; import { AzureAuthType, AzureCredentials, getAzureClouds } from '@grafana/azure-sdk'; import { SelectableValue } from '@grafana/data'; -import { ConfigSection } from '@grafana/experimental'; +import { ConfigSection } from '@grafana/plugin-ui'; import { Select, Field } from '@grafana/ui'; import { selectors } from '../../e2e/selectors'; diff --git a/public/app/plugins/datasource/azuremonitor/components/ConfigEditor/ConfigEditor.tsx b/public/app/plugins/datasource/azuremonitor/components/ConfigEditor/ConfigEditor.tsx index e4246af0996..bdf2129e11d 100644 --- a/public/app/plugins/datasource/azuremonitor/components/ConfigEditor/ConfigEditor.tsx +++ b/public/app/plugins/datasource/azuremonitor/components/ConfigEditor/ConfigEditor.tsx @@ -1,7 +1,7 @@ import { PureComponent } from 'react'; import { DataSourcePluginOptionsEditorProps, SelectableValue, updateDatasourcePluginOption } from '@grafana/data'; -import { ConfigSection, DataSourceDescription } from '@grafana/experimental'; +import { ConfigSection, DataSourceDescription } from '@grafana/plugin-ui'; import { getBackendSrv, getTemplateSrv, isFetchError, TemplateSrv, config } from '@grafana/runtime'; import { Alert, Divider, SecureSocksProxySettings } from '@grafana/ui'; diff --git a/public/app/plugins/datasource/azuremonitor/components/ConfigEditor/CurrentUserFallbackCredentials.tsx b/public/app/plugins/datasource/azuremonitor/components/ConfigEditor/CurrentUserFallbackCredentials.tsx index 951afa88344..d13296dc1c5 100644 --- a/public/app/plugins/datasource/azuremonitor/components/ConfigEditor/CurrentUserFallbackCredentials.tsx +++ b/public/app/plugins/datasource/azuremonitor/components/ConfigEditor/CurrentUserFallbackCredentials.tsx @@ -2,7 +2,7 @@ import { useMemo } from 'react'; import { AadCurrentUserCredentials, AzureCredentials, instanceOfAzureCredential } from '@grafana/azure-sdk'; import { SelectableValue } from '@grafana/data'; -import { ConfigSection } from '@grafana/experimental'; +import { ConfigSection } from '@grafana/plugin-ui'; import { config } from '@grafana/runtime'; import { Select, Field, RadioButtonGroup, Alert, Stack } from '@grafana/ui'; diff --git a/public/app/plugins/datasource/azuremonitor/components/LogsQueryEditor/AdvancedResourcePicker.tsx b/public/app/plugins/datasource/azuremonitor/components/LogsQueryEditor/AdvancedResourcePicker.tsx index 26efe10eb20..640fdd1a6c7 100644 --- a/public/app/plugins/datasource/azuremonitor/components/LogsQueryEditor/AdvancedResourcePicker.tsx +++ b/public/app/plugins/datasource/azuremonitor/components/LogsQueryEditor/AdvancedResourcePicker.tsx @@ -2,7 +2,7 @@ import { css } from '@emotion/css'; import { useEffect } from 'react'; import { GrafanaTheme2 } from '@grafana/data'; -import { AccessoryButton } from '@grafana/experimental'; +import { AccessoryButton } from '@grafana/plugin-ui'; import { Icon, Input, Tooltip, Label, Button, useStyles2 } from '@grafana/ui'; export interface ResourcePickerProps { diff --git a/public/app/plugins/datasource/azuremonitor/components/LogsQueryEditor/LogsQueryEditor.tsx b/public/app/plugins/datasource/azuremonitor/components/LogsQueryEditor/LogsQueryEditor.tsx index b1b13313f7c..622d6b660c8 100644 --- a/public/app/plugins/datasource/azuremonitor/components/LogsQueryEditor/LogsQueryEditor.tsx +++ b/public/app/plugins/datasource/azuremonitor/components/LogsQueryEditor/LogsQueryEditor.tsx @@ -1,7 +1,7 @@ import { useEffect, useState } from 'react'; import { PanelData, TimeRange } from '@grafana/data'; -import { EditorFieldGroup, EditorRow, EditorRows } from '@grafana/experimental'; +import { EditorFieldGroup, EditorRow, EditorRows } from '@grafana/plugin-ui'; import { getTemplateSrv } from '@grafana/runtime'; import { Alert, LinkButton, Text, TextLink } from '@grafana/ui'; diff --git a/public/app/plugins/datasource/azuremonitor/components/MetricsQueryEditor/AdvancedResourcePicker.tsx b/public/app/plugins/datasource/azuremonitor/components/MetricsQueryEditor/AdvancedResourcePicker.tsx index b052df9448c..7ab12383dc5 100644 --- a/public/app/plugins/datasource/azuremonitor/components/MetricsQueryEditor/AdvancedResourcePicker.tsx +++ b/public/app/plugins/datasource/azuremonitor/components/MetricsQueryEditor/AdvancedResourcePicker.tsx @@ -2,7 +2,7 @@ import { css } from '@emotion/css'; import { useEffect } from 'react'; import { GrafanaTheme2 } from '@grafana/data'; -import { AccessoryButton } from '@grafana/experimental'; +import { AccessoryButton } from '@grafana/plugin-ui'; import { Input, Label, InlineField, Button, useStyles2 } from '@grafana/ui'; import { selectors } from '../../e2e/selectors'; diff --git a/public/app/plugins/datasource/azuremonitor/components/MetricsQueryEditor/DimensionFields.tsx b/public/app/plugins/datasource/azuremonitor/components/MetricsQueryEditor/DimensionFields.tsx index b7b7c2f13d6..9a4e1975a74 100644 --- a/public/app/plugins/datasource/azuremonitor/components/MetricsQueryEditor/DimensionFields.tsx +++ b/public/app/plugins/datasource/azuremonitor/components/MetricsQueryEditor/DimensionFields.tsx @@ -1,7 +1,7 @@ import { useEffect, useMemo, useState } from 'react'; import { SelectableValue, DataFrame, PanelData, Labels } from '@grafana/data'; -import { EditorList, AccessoryButton } from '@grafana/experimental'; +import { EditorList, AccessoryButton } from '@grafana/plugin-ui'; import { Select, HorizontalGroup, MultiSelect } from '@grafana/ui'; import { AzureMetricDimension, AzureMonitorOption, AzureMonitorQuery, AzureQueryEditorFieldProps } from '../../types'; diff --git a/public/app/plugins/datasource/azuremonitor/components/MetricsQueryEditor/MetricsQueryEditor.tsx b/public/app/plugins/datasource/azuremonitor/components/MetricsQueryEditor/MetricsQueryEditor.tsx index 751392acf44..6048ebca21e 100644 --- a/public/app/plugins/datasource/azuremonitor/components/MetricsQueryEditor/MetricsQueryEditor.tsx +++ b/public/app/plugins/datasource/azuremonitor/components/MetricsQueryEditor/MetricsQueryEditor.tsx @@ -1,5 +1,5 @@ import { PanelData } from '@grafana/data'; -import { EditorRows, EditorRow, EditorFieldGroup } from '@grafana/experimental'; +import { EditorRows, EditorRow, EditorFieldGroup } from '@grafana/plugin-ui'; import { multiResourceCompatibleTypes } from '../../azureMetadata'; import type Datasource from '../../datasource'; diff --git a/public/app/plugins/datasource/azuremonitor/components/QueryEditor/QueryHeader.tsx b/public/app/plugins/datasource/azuremonitor/components/QueryEditor/QueryHeader.tsx index 737053e81db..7fddd79ce0c 100644 --- a/public/app/plugins/datasource/azuremonitor/components/QueryEditor/QueryHeader.tsx +++ b/public/app/plugins/datasource/azuremonitor/components/QueryEditor/QueryHeader.tsx @@ -1,7 +1,7 @@ import { useCallback } from 'react'; import { SelectableValue } from '@grafana/data'; -import { EditorHeader, InlineSelect } from '@grafana/experimental'; +import { EditorHeader, InlineSelect } from '@grafana/plugin-ui'; import { selectors } from '../../e2e/selectors'; import { AzureMonitorQuery, AzureQueryType } from '../../types'; diff --git a/public/app/plugins/datasource/azuremonitor/components/TracesQueryEditor/Filter.tsx b/public/app/plugins/datasource/azuremonitor/components/TracesQueryEditor/Filter.tsx index 12f985dbb92..15d4f7793ee 100644 --- a/public/app/plugins/datasource/azuremonitor/components/TracesQueryEditor/Filter.tsx +++ b/public/app/plugins/datasource/azuremonitor/components/TracesQueryEditor/Filter.tsx @@ -5,7 +5,7 @@ import { lastValueFrom } from 'rxjs'; import { CoreApp, DataFrame, getDefaultTimeRange, SelectableValue, TimeRange } from '@grafana/data'; import { selectors } from '@grafana/e2e-selectors'; -import { AccessoryButton } from '@grafana/experimental'; +import { AccessoryButton } from '@grafana/plugin-ui'; import { HorizontalGroup, Select, diff --git a/public/app/plugins/datasource/azuremonitor/components/TracesQueryEditor/Filters.tsx b/public/app/plugins/datasource/azuremonitor/components/TracesQueryEditor/Filters.tsx index 9c8c84d1bc6..8628766f6ba 100644 --- a/public/app/plugins/datasource/azuremonitor/components/TracesQueryEditor/Filters.tsx +++ b/public/app/plugins/datasource/azuremonitor/components/TracesQueryEditor/Filters.tsx @@ -2,7 +2,7 @@ import { uniq } from 'lodash'; import { useEffect, useMemo, useState } from 'react'; import { SelectableValue } from '@grafana/data'; -import { EditorList } from '@grafana/experimental'; +import { EditorList } from '@grafana/plugin-ui'; import { Field } from '@grafana/ui'; import { AzureQueryEditorFieldProps, AzureTracesFilter } from '../../types'; diff --git a/public/app/plugins/datasource/azuremonitor/components/TracesQueryEditor/TracesQueryEditor.tsx b/public/app/plugins/datasource/azuremonitor/components/TracesQueryEditor/TracesQueryEditor.tsx index 7ed37f8fa21..b550ecf09a8 100644 --- a/public/app/plugins/datasource/azuremonitor/components/TracesQueryEditor/TracesQueryEditor.tsx +++ b/public/app/plugins/datasource/azuremonitor/components/TracesQueryEditor/TracesQueryEditor.tsx @@ -3,7 +3,7 @@ import * as React from 'react'; import { usePrevious } from 'react-use'; import { TimeRange } from '@grafana/data'; -import { EditorFieldGroup, EditorRow, EditorRows } from '@grafana/experimental'; +import { EditorFieldGroup, EditorRow, EditorRows } from '@grafana/plugin-ui'; import { Input } from '@grafana/ui'; import Datasource from '../../datasource'; diff --git a/public/app/plugins/datasource/azuremonitor/components/shared/Field.tsx b/public/app/plugins/datasource/azuremonitor/components/shared/Field.tsx index 2657c066115..3c5a9cf1123 100644 --- a/public/app/plugins/datasource/azuremonitor/components/shared/Field.tsx +++ b/public/app/plugins/datasource/azuremonitor/components/shared/Field.tsx @@ -1,4 +1,4 @@ -import { EditorField } from '@grafana/experimental'; +import { EditorField } from '@grafana/plugin-ui'; import { InlineField } from '@grafana/ui'; import { Props as InlineFieldProps } from '@grafana/ui/src/components/Forms/InlineField'; diff --git a/public/app/plugins/datasource/azuremonitor/package.json b/public/app/plugins/datasource/azuremonitor/package.json index 69ca46d32f5..38ed9e50920 100644 --- a/public/app/plugins/datasource/azuremonitor/package.json +++ b/public/app/plugins/datasource/azuremonitor/package.json @@ -6,7 +6,7 @@ "dependencies": { "@emotion/css": "11.13.5", "@grafana/data": "11.5.0-pre", - "@grafana/experimental": "2.1.6", + "@grafana/plugin-ui": "0.9.6", "@grafana/runtime": "11.5.0-pre", "@grafana/schema": "11.5.0-pre", "@grafana/ui": "11.5.0-pre", diff --git a/public/app/plugins/datasource/cloud-monitoring/components/Aggregation.tsx b/public/app/plugins/datasource/cloud-monitoring/components/Aggregation.tsx index 8d9c874bb38..2d5089f06fc 100644 --- a/public/app/plugins/datasource/cloud-monitoring/components/Aggregation.tsx +++ b/public/app/plugins/datasource/cloud-monitoring/components/Aggregation.tsx @@ -1,7 +1,7 @@ import { useMemo } from 'react'; import { SelectableValue } from '@grafana/data'; -import { EditorField } from '@grafana/experimental'; +import { EditorField } from '@grafana/plugin-ui'; import { Select } from '@grafana/ui'; import { getAggregationOptionsByMetric } from '../functions'; diff --git a/public/app/plugins/datasource/cloud-monitoring/components/AliasBy.tsx b/public/app/plugins/datasource/cloud-monitoring/components/AliasBy.tsx index a275b89e15f..6b92a061e66 100644 --- a/public/app/plugins/datasource/cloud-monitoring/components/AliasBy.tsx +++ b/public/app/plugins/datasource/cloud-monitoring/components/AliasBy.tsx @@ -2,7 +2,7 @@ import { debounce } from 'lodash'; import { useState } from 'react'; import * as React from 'react'; -import { EditorField } from '@grafana/experimental'; +import { EditorField } from '@grafana/plugin-ui'; import { Input } from '@grafana/ui'; export interface Props { diff --git a/public/app/plugins/datasource/cloud-monitoring/components/Alignment.tsx b/public/app/plugins/datasource/cloud-monitoring/components/Alignment.tsx index c9a313e48ed..1a3d21c3a9d 100644 --- a/public/app/plugins/datasource/cloud-monitoring/components/Alignment.tsx +++ b/public/app/plugins/datasource/cloud-monitoring/components/Alignment.tsx @@ -1,7 +1,7 @@ import { useMemo } from 'react'; import { SelectableValue } from '@grafana/data'; -import { EditorField, EditorFieldGroup } from '@grafana/experimental'; +import { EditorField, EditorFieldGroup } from '@grafana/plugin-ui'; import { ALIGNMENT_PERIODS } from '../constants'; import CloudMonitoringDatasource from '../datasource'; diff --git a/public/app/plugins/datasource/cloud-monitoring/components/AnnotationQueryEditor.tsx b/public/app/plugins/datasource/cloud-monitoring/components/AnnotationQueryEditor.tsx index 35f524cc556..6925e724d68 100644 --- a/public/app/plugins/datasource/cloud-monitoring/components/AnnotationQueryEditor.tsx +++ b/public/app/plugins/datasource/cloud-monitoring/components/AnnotationQueryEditor.tsx @@ -3,7 +3,7 @@ import * as React from 'react'; import { useDebounce } from 'react-use'; import { QueryEditorProps, getDefaultTimeRange, toOption } from '@grafana/data'; -import { EditorField, EditorRows } from '@grafana/experimental'; +import { EditorField, EditorRows } from '@grafana/plugin-ui'; import { Input } from '@grafana/ui'; import CloudMonitoringDatasource from '../datasource'; diff --git a/public/app/plugins/datasource/cloud-monitoring/components/ConfigEditor/ConfigEditor.tsx b/public/app/plugins/datasource/cloud-monitoring/components/ConfigEditor/ConfigEditor.tsx index 02943e876ef..9889693efae 100644 --- a/public/app/plugins/datasource/cloud-monitoring/components/ConfigEditor/ConfigEditor.tsx +++ b/public/app/plugins/datasource/cloud-monitoring/components/ConfigEditor/ConfigEditor.tsx @@ -1,8 +1,8 @@ import { PureComponent } from 'react'; import { DataSourcePluginOptionsEditorProps } from '@grafana/data'; -import { ConfigSection, DataSourceDescription } from '@grafana/experimental'; import { ConnectionConfig } from '@grafana/google-sdk'; +import { ConfigSection, DataSourceDescription } from '@grafana/plugin-ui'; import { reportInteraction, config } from '@grafana/runtime'; import { Divider, SecureSocksProxySettings } from '@grafana/ui'; diff --git a/public/app/plugins/datasource/cloud-monitoring/components/GraphPeriod.tsx b/public/app/plugins/datasource/cloud-monitoring/components/GraphPeriod.tsx index 13dd8e3d201..9e6c7aed0c3 100644 --- a/public/app/plugins/datasource/cloud-monitoring/components/GraphPeriod.tsx +++ b/public/app/plugins/datasource/cloud-monitoring/components/GraphPeriod.tsx @@ -1,5 +1,5 @@ import { SelectableValue } from '@grafana/data'; -import { EditorField, EditorRow } from '@grafana/experimental'; +import { EditorField, EditorRow } from '@grafana/plugin-ui'; import { HorizontalGroup, Switch } from '@grafana/ui'; import { GRAPH_PERIODS } from '../constants'; diff --git a/public/app/plugins/datasource/cloud-monitoring/components/GroupBy.tsx b/public/app/plugins/datasource/cloud-monitoring/components/GroupBy.tsx index d9fd0e4c70c..356a81d5ade 100644 --- a/public/app/plugins/datasource/cloud-monitoring/components/GroupBy.tsx +++ b/public/app/plugins/datasource/cloud-monitoring/components/GroupBy.tsx @@ -1,7 +1,7 @@ import { useMemo } from 'react'; import { SelectableValue } from '@grafana/data'; -import { EditorField, EditorFieldGroup } from '@grafana/experimental'; +import { EditorField, EditorFieldGroup } from '@grafana/plugin-ui'; import { MultiSelect } from '@grafana/ui'; import { SYSTEM_LABELS } from '../constants'; diff --git a/public/app/plugins/datasource/cloud-monitoring/components/LabelFilter.tsx b/public/app/plugins/datasource/cloud-monitoring/components/LabelFilter.tsx index 102bc22ef2f..dca57374c91 100644 --- a/public/app/plugins/datasource/cloud-monitoring/components/LabelFilter.tsx +++ b/public/app/plugins/datasource/cloud-monitoring/components/LabelFilter.tsx @@ -1,7 +1,7 @@ import { useMemo } from 'react'; import { SelectableValue, toOption } from '@grafana/data'; -import { AccessoryButton, EditorField, EditorList, EditorRow } from '@grafana/experimental'; +import { AccessoryButton, EditorField, EditorList, EditorRow } from '@grafana/plugin-ui'; import { HorizontalGroup, Select } from '@grafana/ui'; import { labelsToGroupedOptions, stringArrayToFilters } from '../functions'; diff --git a/public/app/plugins/datasource/cloud-monitoring/components/LookbackPeriodSelect.tsx b/public/app/plugins/datasource/cloud-monitoring/components/LookbackPeriodSelect.tsx index 0e7d991327a..92cce156c87 100644 --- a/public/app/plugins/datasource/cloud-monitoring/components/LookbackPeriodSelect.tsx +++ b/public/app/plugins/datasource/cloud-monitoring/components/LookbackPeriodSelect.tsx @@ -1,5 +1,5 @@ import { SelectableValue } from '@grafana/data'; -import { EditorField } from '@grafana/experimental'; +import { EditorField } from '@grafana/plugin-ui'; import { Select } from '@grafana/ui'; import { LOOKBACK_PERIODS } from '../constants'; diff --git a/public/app/plugins/datasource/cloud-monitoring/components/MetricQueryEditor.tsx b/public/app/plugins/datasource/cloud-monitoring/components/MetricQueryEditor.tsx index 8a245c46503..7cd44f61ffb 100644 --- a/public/app/plugins/datasource/cloud-monitoring/components/MetricQueryEditor.tsx +++ b/public/app/plugins/datasource/cloud-monitoring/components/MetricQueryEditor.tsx @@ -2,7 +2,8 @@ import { useCallback, useEffect } from 'react'; import * as React from 'react'; import { SelectableValue, TimeRange } from '@grafana/data'; -import { EditorRows, Stack } from '@grafana/experimental'; +import { EditorRows } from '@grafana/plugin-ui'; +import { Stack } from '@grafana/ui'; import CloudMonitoringDatasource from '../datasource'; import { AlignmentTypes, CloudMonitoringQuery, QueryType, TimeSeriesList, TimeSeriesQuery } from '../types/query'; diff --git a/public/app/plugins/datasource/cloud-monitoring/components/Preprocessor.tsx b/public/app/plugins/datasource/cloud-monitoring/components/Preprocessor.tsx index c7f6c66d355..3ca7d101942 100644 --- a/public/app/plugins/datasource/cloud-monitoring/components/Preprocessor.tsx +++ b/public/app/plugins/datasource/cloud-monitoring/components/Preprocessor.tsx @@ -1,7 +1,7 @@ import { useMemo } from 'react'; import { SelectableValue } from '@grafana/data'; -import { EditorField } from '@grafana/experimental'; +import { EditorField } from '@grafana/plugin-ui'; import { RadioButtonGroup } from '@grafana/ui'; import { getAlignmentPickerData } from '../functions'; diff --git a/public/app/plugins/datasource/cloud-monitoring/components/Project.tsx b/public/app/plugins/datasource/cloud-monitoring/components/Project.tsx index de42fbdd788..289b4745ba0 100644 --- a/public/app/plugins/datasource/cloud-monitoring/components/Project.tsx +++ b/public/app/plugins/datasource/cloud-monitoring/components/Project.tsx @@ -1,7 +1,7 @@ import { useEffect, useMemo, useState } from 'react'; import { SelectableValue } from '@grafana/data'; -import { EditorField } from '@grafana/experimental'; +import { EditorField } from '@grafana/plugin-ui'; import { Select } from '@grafana/ui'; import CloudMonitoringDatasource from '../datasource'; diff --git a/public/app/plugins/datasource/cloud-monitoring/components/PromQLEditor.tsx b/public/app/plugins/datasource/cloud-monitoring/components/PromQLEditor.tsx index 8e54fbe372c..deaa5e7eaf8 100644 --- a/public/app/plugins/datasource/cloud-monitoring/components/PromQLEditor.tsx +++ b/public/app/plugins/datasource/cloud-monitoring/components/PromQLEditor.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; import { SelectableValue } from '@grafana/data'; -import { EditorField, EditorRow } from '@grafana/experimental'; +import { EditorField, EditorRow } from '@grafana/plugin-ui'; import { TextArea, Input } from '@grafana/ui'; import CloudMonitoringDatasource from '../datasource'; diff --git a/public/app/plugins/datasource/cloud-monitoring/components/QueryEditor.tsx b/public/app/plugins/datasource/cloud-monitoring/components/QueryEditor.tsx index 9464e273ea2..7b701df34da 100644 --- a/public/app/plugins/datasource/cloud-monitoring/components/QueryEditor.tsx +++ b/public/app/plugins/datasource/cloud-monitoring/components/QueryEditor.tsx @@ -3,7 +3,7 @@ import { isEqual } from 'lodash'; import { useEffect, useState } from 'react'; import { QueryEditorProps, getDefaultTimeRange, toOption } from '@grafana/data'; -import { EditorRows } from '@grafana/experimental'; +import { EditorRows } from '@grafana/plugin-ui'; import { ConfirmModal } from '@grafana/ui'; import CloudMonitoringDatasource from '../datasource'; diff --git a/public/app/plugins/datasource/cloud-monitoring/components/QueryHeader.tsx b/public/app/plugins/datasource/cloud-monitoring/components/QueryHeader.tsx index 150bb3c1f1f..71431f0fc20 100644 --- a/public/app/plugins/datasource/cloud-monitoring/components/QueryHeader.tsx +++ b/public/app/plugins/datasource/cloud-monitoring/components/QueryHeader.tsx @@ -1,4 +1,4 @@ -import { EditorHeader, FlexItem, InlineSelect } from '@grafana/experimental'; +import { EditorHeader, FlexItem, InlineSelect } from '@grafana/plugin-ui'; import { QUERY_TYPES } from '../constants'; import { CloudMonitoringQuery } from '../types/query'; diff --git a/public/app/plugins/datasource/cloud-monitoring/components/SLO.tsx b/public/app/plugins/datasource/cloud-monitoring/components/SLO.tsx index 2e0117cfe70..be4606155d7 100644 --- a/public/app/plugins/datasource/cloud-monitoring/components/SLO.tsx +++ b/public/app/plugins/datasource/cloud-monitoring/components/SLO.tsx @@ -1,7 +1,7 @@ import { useEffect, useState } from 'react'; import { SelectableValue } from '@grafana/data'; -import { EditorField } from '@grafana/experimental'; +import { EditorField } from '@grafana/plugin-ui'; import { Select } from '@grafana/ui'; import CloudMonitoringDatasource from '../datasource'; diff --git a/public/app/plugins/datasource/cloud-monitoring/components/SLOQueryEditor.tsx b/public/app/plugins/datasource/cloud-monitoring/components/SLOQueryEditor.tsx index 0d62b1cff1e..1a4ec805596 100644 --- a/public/app/plugins/datasource/cloud-monitoring/components/SLOQueryEditor.tsx +++ b/public/app/plugins/datasource/cloud-monitoring/components/SLOQueryEditor.tsx @@ -2,7 +2,7 @@ import { useMemo } from 'react'; import * as React from 'react'; import { SelectableValue } from '@grafana/data'; -import { EditorField, EditorFieldGroup, EditorRow } from '@grafana/experimental'; +import { EditorField, EditorFieldGroup, EditorRow } from '@grafana/plugin-ui'; import { ALIGNMENT_PERIODS, SLO_BURN_RATE_SELECTOR_NAME } from '../constants'; import CloudMonitoringDatasource from '../datasource'; diff --git a/public/app/plugins/datasource/cloud-monitoring/components/Selector.tsx b/public/app/plugins/datasource/cloud-monitoring/components/Selector.tsx index 690e03219b6..ea0e5903af5 100644 --- a/public/app/plugins/datasource/cloud-monitoring/components/Selector.tsx +++ b/public/app/plugins/datasource/cloud-monitoring/components/Selector.tsx @@ -1,5 +1,5 @@ import { SelectableValue } from '@grafana/data'; -import { EditorField } from '@grafana/experimental'; +import { EditorField } from '@grafana/plugin-ui'; import { Select } from '@grafana/ui'; import { SELECTORS } from '../constants'; diff --git a/public/app/plugins/datasource/cloud-monitoring/components/Service.tsx b/public/app/plugins/datasource/cloud-monitoring/components/Service.tsx index 35093b92a7b..9425437d7b3 100644 --- a/public/app/plugins/datasource/cloud-monitoring/components/Service.tsx +++ b/public/app/plugins/datasource/cloud-monitoring/components/Service.tsx @@ -1,7 +1,7 @@ import { useEffect, useState } from 'react'; import { SelectableValue } from '@grafana/data'; -import { EditorField } from '@grafana/experimental'; +import { EditorField } from '@grafana/plugin-ui'; import { Select } from '@grafana/ui'; import CloudMonitoringDatasource from '../datasource'; diff --git a/public/app/plugins/datasource/cloud-monitoring/components/VisualMetricQueryEditor.tsx b/public/app/plugins/datasource/cloud-monitoring/components/VisualMetricQueryEditor.tsx index d50b6011e8e..56824f89358 100644 --- a/public/app/plugins/datasource/cloud-monitoring/components/VisualMetricQueryEditor.tsx +++ b/public/app/plugins/datasource/cloud-monitoring/components/VisualMetricQueryEditor.tsx @@ -5,7 +5,7 @@ import { useCallback, useEffect, useState } from 'react'; import * as React from 'react'; import { GrafanaTheme2, SelectableValue, TimeRange } from '@grafana/data'; -import { EditorField, EditorFieldGroup, EditorRow } from '@grafana/experimental'; +import { EditorField, EditorFieldGroup, EditorRow } from '@grafana/plugin-ui'; import { reportInteraction } from '@grafana/runtime'; import { getSelectStyles, Select, AsyncSelect, useStyles2, useTheme2 } from '@grafana/ui'; diff --git a/public/app/plugins/datasource/cloud-monitoring/package.json b/public/app/plugins/datasource/cloud-monitoring/package.json index 8aee980ffa1..32a61eb300d 100644 --- a/public/app/plugins/datasource/cloud-monitoring/package.json +++ b/public/app/plugins/datasource/cloud-monitoring/package.json @@ -6,8 +6,8 @@ "dependencies": { "@emotion/css": "11.13.5", "@grafana/data": "11.5.0-pre", - "@grafana/experimental": "2.1.6", "@grafana/google-sdk": "0.1.2", + "@grafana/plugin-ui": "0.9.6", "@grafana/runtime": "11.5.0-pre", "@grafana/schema": "11.5.0-pre", "@grafana/ui": "11.5.0-pre", diff --git a/public/app/plugins/datasource/influxdb/components/editor/query/influxql/visual/FromSection.tsx b/public/app/plugins/datasource/influxdb/components/editor/query/influxql/visual/FromSection.tsx index 458f2eb9176..153ec2aa039 100644 --- a/public/app/plugins/datasource/influxdb/components/editor/query/influxql/visual/FromSection.tsx +++ b/public/app/plugins/datasource/influxdb/components/editor/query/influxql/visual/FromSection.tsx @@ -1,4 +1,4 @@ -import { AccessoryButton } from '@grafana/experimental'; +import { AccessoryButton } from '@grafana/plugin-ui'; import { DEFAULT_POLICY } from '../../../../../types'; import { toSelectableValue } from '../utils/toSelectableValue'; diff --git a/public/app/plugins/datasource/influxdb/components/editor/query/influxql/visual/PartListSection.tsx b/public/app/plugins/datasource/influxdb/components/editor/query/influxql/visual/PartListSection.tsx index 3bb380ee126..14f188bd683 100644 --- a/public/app/plugins/datasource/influxdb/components/editor/query/influxql/visual/PartListSection.tsx +++ b/public/app/plugins/datasource/influxdb/components/editor/query/influxql/visual/PartListSection.tsx @@ -2,7 +2,7 @@ import { css, cx } from '@emotion/css'; import { Fragment, useMemo } from 'react'; import { GrafanaTheme2, SelectableValue } from '@grafana/data'; -import { AccessoryButton } from '@grafana/experimental'; +import { AccessoryButton } from '@grafana/plugin-ui'; import { useTheme2 } from '@grafana/ui'; import { toSelectableValue } from '../utils/toSelectableValue'; diff --git a/public/app/plugins/datasource/influxdb/components/editor/query/influxql/visual/TagsSection.tsx b/public/app/plugins/datasource/influxdb/components/editor/query/influxql/visual/TagsSection.tsx index 9079bc056c7..3edc8320144 100644 --- a/public/app/plugins/datasource/influxdb/components/editor/query/influxql/visual/TagsSection.tsx +++ b/public/app/plugins/datasource/influxdb/components/editor/query/influxql/visual/TagsSection.tsx @@ -1,5 +1,5 @@ import { SelectableValue } from '@grafana/data'; -import { AccessoryButton } from '@grafana/experimental'; +import { AccessoryButton } from '@grafana/plugin-ui'; import { InfluxQueryTag } from '../../../../../types'; import { adjustOperatorIfNeeded, getCondition, getOperator } from '../utils/tagUtils'; diff --git a/public/app/plugins/datasource/influxdb/components/editor/query/influxql/visual/VisualInfluxQLEditor.tags.test.tsx b/public/app/plugins/datasource/influxdb/components/editor/query/influxql/visual/VisualInfluxQLEditor.tags.test.tsx index 7d6b19d637f..910bb67fca5 100644 --- a/public/app/plugins/datasource/influxdb/components/editor/query/influxql/visual/VisualInfluxQLEditor.tags.test.tsx +++ b/public/app/plugins/datasource/influxdb/components/editor/query/influxql/visual/VisualInfluxQLEditor.tags.test.tsx @@ -38,13 +38,12 @@ jest.mock('../../../../../influxql_metadata_query', () => { }; }); -jest.mock('@grafana/runtime', () => { - return { - getTemplateSrv: jest.fn().mockReturnValueOnce({ - getVariables: jest.fn().mockReturnValueOnce([]), - }), - }; -}); +jest.mock('@grafana/runtime', () => ({ + ...jest.requireActual('@grafana/runtime'), + getTemplateSrv: jest.fn().mockReturnValueOnce({ + getVariables: jest.fn().mockReturnValueOnce([]), + }), +})); beforeEach(() => { (mockedMeta.getTagKeys as jest.Mock).mockClear(); diff --git a/public/app/plugins/datasource/influxdb/fsql/datasource.flightsql.ts b/public/app/plugins/datasource/influxdb/fsql/datasource.flightsql.ts index ce40ff3c53a..da0a3ed5f25 100644 --- a/public/app/plugins/datasource/influxdb/fsql/datasource.flightsql.ts +++ b/public/app/plugins/datasource/influxdb/fsql/datasource.flightsql.ts @@ -1,7 +1,7 @@ import { v4 as uuidv4 } from 'uuid'; import { DataSourceInstanceSettings, TimeRange } from '@grafana/data'; -import { CompletionItemKind, LanguageDefinition, TableIdentifier } from '@grafana/experimental'; +import { CompletionItemKind, LanguageDefinition, TableIdentifier } from '@grafana/plugin-ui'; import { TemplateSrv, config, getTemplateSrv } from '@grafana/runtime'; import { COMMON_FNS, DB, FuncParameter, SQLQuery, SqlDatasource, formatSQL } from '@grafana/sql'; diff --git a/public/app/plugins/datasource/influxdb/fsql/sqlCompletionProvider.ts b/public/app/plugins/datasource/influxdb/fsql/sqlCompletionProvider.ts index b44b2475027..19fca7d6f56 100644 --- a/public/app/plugins/datasource/influxdb/fsql/sqlCompletionProvider.ts +++ b/public/app/plugins/datasource/influxdb/fsql/sqlCompletionProvider.ts @@ -11,7 +11,7 @@ import { TableDefinition, TableIdentifier, TokenType, -} from '@grafana/experimental'; +} from '@grafana/plugin-ui'; interface CompletionProviderGetterArgs { getMeta: (t?: TableIdentifier) => Promise; diff --git a/public/app/plugins/datasource/mssql/configuration/ConfigurationEditor.tsx b/public/app/plugins/datasource/mssql/configuration/ConfigurationEditor.tsx index 607dc32f114..287257b4eaf 100644 --- a/public/app/plugins/datasource/mssql/configuration/ConfigurationEditor.tsx +++ b/public/app/plugins/datasource/mssql/configuration/ConfigurationEditor.tsx @@ -10,7 +10,7 @@ import { updateDatasourcePluginJsonDataOption, updateDatasourcePluginResetOption, } from '@grafana/data'; -import { ConfigSection, ConfigSubSection, DataSourceDescription } from '@grafana/experimental'; +import { ConfigSection, ConfigSubSection, DataSourceDescription } from '@grafana/plugin-ui'; import { config } from '@grafana/runtime'; import { ConnectionLimits, useMigrateDatabaseFields } from '@grafana/sql'; import { NumberInput } from '@grafana/sql/src/components/configuration/NumberInput'; diff --git a/public/app/plugins/datasource/mssql/configuration/Kerberos.tsx b/public/app/plugins/datasource/mssql/configuration/Kerberos.tsx index f5dfc8759c3..d1166ba79e0 100644 --- a/public/app/plugins/datasource/mssql/configuration/Kerberos.tsx +++ b/public/app/plugins/datasource/mssql/configuration/Kerberos.tsx @@ -1,7 +1,7 @@ import { SyntheticEvent } from 'react'; import { DataSourcePluginOptionsEditorProps, updateDatasourcePluginJsonDataOption } from '@grafana/data'; -import { ConfigSubSection } from '@grafana/experimental'; +import { ConfigSubSection } from '@grafana/plugin-ui'; import { FieldSet, Input, Field } from '@grafana/ui'; import { MSSQLAuthenticationType, MssqlOptions } from '../types'; diff --git a/public/app/plugins/datasource/mssql/datasource.ts b/public/app/plugins/datasource/mssql/datasource.ts index abe0ee75b13..b020d051975 100644 --- a/public/app/plugins/datasource/mssql/datasource.ts +++ b/public/app/plugins/datasource/mssql/datasource.ts @@ -1,7 +1,7 @@ import { v4 as uuidv4 } from 'uuid'; import { DataSourceInstanceSettings, ScopedVars } from '@grafana/data'; -import { LanguageDefinition } from '@grafana/experimental'; +import { LanguageDefinition } from '@grafana/plugin-ui'; import { TemplateSrv, config } from '@grafana/runtime'; import { COMMON_FNS, diff --git a/public/app/plugins/datasource/mssql/package.json b/public/app/plugins/datasource/mssql/package.json index 1c422fdda68..7a54d042db0 100644 --- a/public/app/plugins/datasource/mssql/package.json +++ b/public/app/plugins/datasource/mssql/package.json @@ -6,7 +6,7 @@ "dependencies": { "@emotion/css": "11.13.5", "@grafana/data": "11.5.0-pre", - "@grafana/experimental": "2.1.6", + "@grafana/plugin-ui": "0.9.6", "@grafana/runtime": "11.5.0-pre", "@grafana/sql": "11.5.0-pre", "@grafana/ui": "11.5.0-pre", diff --git a/public/app/plugins/datasource/mssql/sqlCompletionProvider.ts b/public/app/plugins/datasource/mssql/sqlCompletionProvider.ts index 871c2172dfc..72cc15f421d 100644 --- a/public/app/plugins/datasource/mssql/sqlCompletionProvider.ts +++ b/public/app/plugins/datasource/mssql/sqlCompletionProvider.ts @@ -6,7 +6,7 @@ import { TableDefinition, TableIdentifier, TokenType, -} from '@grafana/experimental'; +} from '@grafana/plugin-ui'; import { DB, SQLQuery } from '@grafana/sql'; interface CompletionProviderGetterArgs { diff --git a/yarn.lock b/yarn.lock index 7b812284201..b6e5c37717e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2706,8 +2706,8 @@ __metadata: "@emotion/css": "npm:11.13.5" "@grafana/data": "npm:11.5.0-pre" "@grafana/e2e-selectors": "npm:11.5.0-pre" - "@grafana/experimental": "npm:2.1.6" "@grafana/plugin-configs": "npm:11.5.0-pre" + "@grafana/plugin-ui": "npm:0.9.6" "@grafana/runtime": "npm:11.5.0-pre" "@grafana/schema": "npm:11.5.0-pre" "@grafana/ui": "npm:11.5.0-pre" @@ -2903,8 +2903,8 @@ __metadata: "@emotion/css": "npm:11.13.5" "@grafana/data": "npm:11.5.0-pre" "@grafana/e2e-selectors": "npm:11.5.0-pre" - "@grafana/experimental": "npm:2.1.6" "@grafana/plugin-configs": "npm:11.5.0-pre" + "@grafana/plugin-ui": "npm:0.9.6" "@grafana/runtime": "npm:11.5.0-pre" "@grafana/sql": "npm:11.5.0-pre" "@grafana/ui": "npm:11.5.0-pre" @@ -2997,9 +2997,9 @@ __metadata: "@emotion/css": "npm:11.13.5" "@grafana/data": "npm:11.5.0-pre" "@grafana/e2e-selectors": "npm:11.5.0-pre" - "@grafana/experimental": "npm:2.1.6" "@grafana/google-sdk": "npm:0.1.2" "@grafana/plugin-configs": "npm:11.5.0-pre" + "@grafana/plugin-ui": "npm:0.9.6" "@grafana/runtime": "npm:11.5.0-pre" "@grafana/schema": "npm:11.5.0-pre" "@grafana/ui": "npm:11.5.0-pre" From 1cc9f8f0c217ec59fc11472213a17c0199bfbaae Mon Sep 17 00:00:00 2001 From: "grafana-delivery-bot[bot]" <132647405+grafana-delivery-bot[bot]@users.noreply.github.com> Date: Tue, 28 Jan 2025 11:46:25 +0000 Subject: [PATCH 17/39] Release: update changelog for 11.2.6 (#99645) Update changelog Co-authored-by: github-actions[bot] --- CHANGELOG.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f5ca2310ca3..3b736827d26 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,18 @@ + + +# 11.2.6 (2025-01-28) + +### Features and enhancements + +- **Azure Monitor:** Add a feature flag to toggle user auth for Azure Monitor only [#97565](https://github.com/grafana/grafana/pull/97565), [@adamyeats](https://github.com/adamyeats) +- **Security:** Update to Go 1.22.11 - Backport to v11.2.x [#99125](https://github.com/grafana/grafana/pull/99125), [@Proximyst](https://github.com/Proximyst) +- **Security:** Update to Go 1.22.11 - Backport to v11.2.x (Enterprise) + +### Bug fixes + +- **Azure/GCM:** Improve error display [#97591](https://github.com/grafana/grafana/pull/97591), [@aangelisc](https://github.com/aangelisc) + + # 11.1.11 (2025-01-28) From f8e7e9e0244750a1527d35820dbb33e2bd1297ae Mon Sep 17 00:00:00 2001 From: Fayzal Ghantiwala <114010985+fayzal-g@users.noreply.github.com> Date: Tue, 28 Jan 2025 11:54:11 +0000 Subject: [PATCH 18/39] Alerting: Make pagination token empty if an invalid token is passed (#99644) Reset token to empty if invalid --- pkg/services/ngalert/api/api_prometheus.go | 5 +++++ pkg/services/ngalert/api/api_prometheus_test.go | 13 ++++++++++--- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/pkg/services/ngalert/api/api_prometheus.go b/pkg/services/ngalert/api/api_prometheus.go index 22d09a91668..364a7027253 100644 --- a/pkg/services/ngalert/api/api_prometheus.go +++ b/pkg/services/ngalert/api/api_prometheus.go @@ -334,6 +334,11 @@ func PrepareRuleGroupStatuses(log log.Logger, manager state.AlertInstanceManager maxGroups := getInt64WithDefault(opts.Query, "group_limit", -1) nextToken := opts.Query.Get("group_next_token") + if nextToken != "" { + if _, err := base64.URLEncoding.DecodeString(nextToken); err != nil { + nextToken = "" + } + } groupedRules := getGroupedRules(log, ruleList, ruleNamesSet, opts.Namespaces) rulesTotals := make(map[string]int64, len(groupedRules)) diff --git a/pkg/services/ngalert/api/api_prometheus_test.go b/pkg/services/ngalert/api/api_prometheus_test.go index 120755d2252..8ac41c3606e 100644 --- a/pkg/services/ngalert/api/api_prometheus_test.go +++ b/pkg/services/ngalert/api/api_prometheus_test.go @@ -790,8 +790,8 @@ func TestRouteGetRuleStatuses(t *testing.T) { } }) - t.Run("bad token should return no results", func(t *testing.T) { - r, err := http.NewRequest("GET", "/api/v1/rules?group_limit=10&group_next_token=foobar", nil) + t.Run("bad token should return first group_limit results", func(t *testing.T) { + r, err := http.NewRequest("GET", "/api/v1/rules?group_limit=1&group_next_token=foobar", nil) require.NoError(t, err) c.Context = &web.Context{Req: r} @@ -801,7 +801,14 @@ func TestRouteGetRuleStatuses(t *testing.T) { result := &apimodels.RuleResponse{} require.NoError(t, json.Unmarshal(resp.Body(), result)) - require.Len(t, result.Data.RuleGroups, 0) + require.Len(t, result.Data.RuleGroups, 1) + require.Len(t, result.Data.Totals, 0) + require.NotEmpty(t, result.Data.NextToken) + + folder, err := api.store.GetNamespaceByUID(context.Background(), "namespace_0", orgID, user) + require.NoError(t, err) + require.Equal(t, folder.Fullpath, result.Data.RuleGroups[0].File) + require.Equal(t, "rule_group_0", result.Data.RuleGroups[0].Name) }) t.Run("should return nothing when using group_limit=0", func(t *testing.T) { From b1f39f4b8ba01cc5d96fccbe16e0f9bed1016c6c Mon Sep 17 00:00:00 2001 From: "grafana-delivery-bot[bot]" <132647405+grafana-delivery-bot[bot]@users.noreply.github.com> Date: Tue, 28 Jan 2025 12:06:25 +0000 Subject: [PATCH 19/39] Release: update changelog for 11.3.3 (#99647) Update changelog Co-authored-by: github-actions[bot] --- CHANGELOG.md | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3b736827d26..9ab5bca9c62 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,31 @@ + + +# 11.3.3 (2025-01-28) + +### Features and enhancements + +- **Azure Monitor:** Add a feature flag to toggle user auth for Azure Monitor only [#97576](https://github.com/grafana/grafana/pull/97576), [@adamyeats](https://github.com/adamyeats) +- **Security:** Update to Go 1.23.5 - Backport to v11.3.x [#99124](https://github.com/grafana/grafana/pull/99124), [@Proximyst](https://github.com/Proximyst) +- **Security:** Update to Go 1.23.5 - Backport to v11.3.x (Enterprise) + +### Bug fixes + +- **Alerting:** AlertingQueryRunner should skip descendant nodes of invalid queries [#97829](https://github.com/grafana/grafana/pull/97829), [@gillesdemey](https://github.com/gillesdemey) +- **Azure/GCM:** Improve error display [#97593](https://github.com/grafana/grafana/pull/97593), [@aangelisc](https://github.com/aangelisc) +- **Dashboard:** Fixes issue with compatability of old DashboardModel.annotations [#97467](https://github.com/grafana/grafana/pull/97467), [@torkelo](https://github.com/torkelo) +- **Dashboards:** Fix issue where filtered panels would not react to variable changes [#98733](https://github.com/grafana/grafana/pull/98733), [@oscarkilhed](https://github.com/oscarkilhed) +- **Dashboards:** Fixes issue with panel header showing even when hide time override was enabled [#97389](https://github.com/grafana/grafana/pull/97389), [@torkelo](https://github.com/torkelo) +- **Dashboards:** Fixes week relative time ranges when weekStart was changed [#98268](https://github.com/grafana/grafana/pull/98268), [@torkelo](https://github.com/torkelo) +- **DateTimePicker:** Fixes issue with date picker showing invalid date [#97970](https://github.com/grafana/grafana/pull/97970), [@torkelo](https://github.com/torkelo) +- **Fix:** Add support for datasource variable queries [#98118](https://github.com/grafana/grafana/pull/98118), [@sunker](https://github.com/sunker) +- **InfluxDB:** Adhoc filters can use template vars as values [#98785](https://github.com/grafana/grafana/pull/98785), [@bossinc](https://github.com/bossinc) +- **Unified Storage:** Use tls preferred when grafana db using ssl [#97379](https://github.com/grafana/grafana/pull/97379), [@owensmallwood](https://github.com/owensmallwood) + +### Plugin development fixes & changes + +- **Grafana UI:** Re-add react-router-dom as a dependency [#98421](https://github.com/grafana/grafana/pull/98421), [@leventebalogh](https://github.com/leventebalogh) + + # 11.2.6 (2025-01-28) From af2c7a19d1da2960be78c0864f4ba95c4356631f Mon Sep 17 00:00:00 2001 From: "grafana-delivery-bot[bot]" <132647405+grafana-delivery-bot[bot]@users.noreply.github.com> Date: Tue, 28 Jan 2025 12:29:59 +0000 Subject: [PATCH 20/39] Release: update changelog for 11.4.1 (#99650) Update changelog Co-authored-by: github-actions[bot] --- CHANGELOG.md | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9ab5bca9c62..c1c7060ba7a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,33 @@ + + +# 11.4.1 (2025-01-28) + +### Features and enhancements + +- **Security:** Update to Go 1.23.5 - Backport to v11.4.x [#99123](https://github.com/grafana/grafana/pull/99123), [@Proximyst](https://github.com/Proximyst) +- **Security:** Update to Go 1.23.5 - Backport to v11.4.x (Enterprise) + +### Bug fixes + +- **Alerting:** AlertingQueryRunner should skip descendant nodes of invalid queries [#97830](https://github.com/grafana/grafana/pull/97830), [@gillesdemey](https://github.com/gillesdemey) +- **Alerting:** Fix alert rules unpausing after moving rule to different folder [#97583](https://github.com/grafana/grafana/pull/97583), [@santihernandezc](https://github.com/santihernandezc) +- **Alerting:** Fix label escaping in rule export [#98649](https://github.com/grafana/grafana/pull/98649), [@moustafab](https://github.com/moustafab) +- **Alerting:** Fix slack image uploading to use new api [#98066](https://github.com/grafana/grafana/pull/98066), [@moustafab](https://github.com/moustafab) +- **Azure/GCM:** Improve error display [#97594](https://github.com/grafana/grafana/pull/97594), [@aangelisc](https://github.com/aangelisc) +- **Dashboards:** Fix issue where filtered panels would not react to variable changes [#98734](https://github.com/grafana/grafana/pull/98734), [@oscarkilhed](https://github.com/oscarkilhed) +- **Dashboards:** Fixes issue with panel header showing even when hide time override was enabled [#98747](https://github.com/grafana/grafana/pull/98747), [@torkelo](https://github.com/torkelo) +- **Dashboards:** Fixes week relative time ranges when weekStart was changed [#98269](https://github.com/grafana/grafana/pull/98269), [@torkelo](https://github.com/torkelo) +- **Dashboards:** Panel react for `timeFrom` and `timeShift` changes using variables [#98659](https://github.com/grafana/grafana/pull/98659), [@Sergej-Vlasov](https://github.com/Sergej-Vlasov) +- **DateTimePicker:** Fixes issue with date picker showing invalid date [#97971](https://github.com/grafana/grafana/pull/97971), [@torkelo](https://github.com/torkelo) +- **Fix:** Add support for datasource variable queries [#98119](https://github.com/grafana/grafana/pull/98119), [@sunker](https://github.com/sunker) +- **InfluxDB:** Adhoc filters can use template vars as values [#98786](https://github.com/grafana/grafana/pull/98786), [@bossinc](https://github.com/bossinc) +- **LibraryPanel:** Fallback to panel title if library panel title is not set [#99410](https://github.com/grafana/grafana/pull/99410), [@ivanortegaalba](https://github.com/ivanortegaalba) + +### Plugin development fixes & changes + +- **Grafana UI:** Re-add react-router-dom as a dependency [#98422](https://github.com/grafana/grafana/pull/98422), [@leventebalogh](https://github.com/leventebalogh) + + # 11.3.3 (2025-01-28) From 9d635edd0e173f732f3ca8e2e0923d79b66a0ce7 Mon Sep 17 00:00:00 2001 From: Josh Hunt Date: Tue, 28 Jan 2025 13:36:59 +0000 Subject: [PATCH 21/39] MultiCombobox: Async options (#99469) * remove managed isOpen state, add hook to abstract away options/async functionality * split useOptions into new file * refactor stories revert combobox stories to what's in main. I screwed up that rebase * change onChange type, clean up what calls onChange, add debounce and useLatestAsyncCall * tests (mid trying to figure out the act stuff) * tests * debounce-promise doesn't work with rollup? * just some minor code clean up * fix type import --- .../components/Combobox/Combobox.story.tsx | 10 +- .../Combobox/MultiCombobox.internal.story.tsx | 97 ++++++-- .../Combobox/MultiCombobox.test.tsx | 156 ++++++++++++- .../src/components/Combobox/MultiCombobox.tsx | 211 +++++++++--------- .../src/components/Combobox/useOptions.ts | 82 +++++++ 5 files changed, 428 insertions(+), 128 deletions(-) create mode 100644 packages/grafana-ui/src/components/Combobox/useOptions.ts diff --git a/packages/grafana-ui/src/components/Combobox/Combobox.story.tsx b/packages/grafana-ui/src/components/Combobox/Combobox.story.tsx index 9d3bc5fbfb8..5575d21df0e 100644 --- a/packages/grafana-ui/src/components/Combobox/Combobox.story.tsx +++ b/packages/grafana-ui/src/components/Combobox/Combobox.story.tsx @@ -162,15 +162,15 @@ export const AsyncOptionsWithLabels: Story = { return ( { - onChangeAction(val); - setArgs({ value: val }); + onChange={(value: ComboboxOption | null) => { + onChangeAction(value); + setArgs({ value }); }} /> @@ -205,7 +205,7 @@ export const AsyncOptionsWithOnlyValues: Story = { {...dynamicArgs} onChange={(value: ComboboxOption | null) => { onChangeAction(value); - setArgs({ value: value }); + setArgs({ value }); }} /> diff --git a/packages/grafana-ui/src/components/Combobox/MultiCombobox.internal.story.tsx b/packages/grafana-ui/src/components/Combobox/MultiCombobox.internal.story.tsx index 5d27d2ac594..36c368291ee 100644 --- a/packages/grafana-ui/src/components/Combobox/MultiCombobox.internal.story.tsx +++ b/packages/grafana-ui/src/components/Combobox/MultiCombobox.internal.story.tsx @@ -2,8 +2,10 @@ import { action } from '@storybook/addon-actions'; import { useArgs, useEffect, useState } from '@storybook/preview-api'; import type { Meta, StoryFn, StoryObj } from '@storybook/react'; +import { Field } from '../Forms/Field'; + import { MultiCombobox } from './MultiCombobox'; -import { generateOptions } from './storyUtils'; +import { generateOptions, fakeSearchAPI } from './storyUtils'; import { ComboboxOption } from './types'; const meta: Meta = { @@ -11,6 +13,9 @@ const meta: Meta = { component: MultiCombobox, }; +const loadOptionsAction = action('options called'); +const onChangeAction = action('onChange called'); + const commonArgs = { options: [ { label: 'wasd - 1', value: 'option1' }, @@ -40,7 +45,7 @@ export const Basic: Story = { {...args} value={value} onChange={(val) => { - action('onChange')(val); + onChangeAction(val); setArgs({ value: val }); }} /> @@ -67,17 +72,14 @@ export const AutoSize: Story = { }; const ManyOptionsStory: StoryFn = ({ numberOfOptions = 1e4, ...args }) => { - const [value, setValue] = useState([]); + const [dynamicArgs, setArgs] = useArgs(); + const [options, setOptions] = useState([]); - const [isLoading, setIsLoading] = useState(true); useEffect(() => { - setTimeout(() => { - generateOptions(numberOfOptions).then((options) => { - setIsLoading(false); - setOptions(options); - setValue([options[5].value]); - }); + setTimeout(async () => { + const options = await generateOptions(numberOfOptions); + setOptions(options); }, 1000); }, [numberOfOptions]); @@ -85,12 +87,11 @@ const ManyOptionsStory: StoryFn = ({ numberOfOptions = 1e4, ... return ( { - setValue(opts || []); - action('onChange')(opts); + setArgs({ value: opts }); + onChangeAction(opts); }} /> ); @@ -104,3 +105,71 @@ export const ManyOptions: StoryObj = { }, render: ManyOptionsStory, }; + +function loadOptionsWithLabels(inputValue: string) { + loadOptionsAction(inputValue); + return fakeSearchAPI(`http://example.com/search?errorOnQuery=break&query=${inputValue}`); +} + +export const AsyncOptionsWithLabels: Story = { + name: 'Async - options returns labels', + args: { + options: loadOptionsWithLabels, + value: [{ label: 'Option 69', value: '69' }], + placeholder: 'Select an option', + }, + render: (args) => { + const [dynamicArgs, setArgs] = useArgs(); + + return ( + + { + onChangeAction(val); + setArgs({ value: val }); + }} + /> + + ); + }, +}; + +function loadOptionsOnlyValues(inputValue: string) { + loadOptionsAction(inputValue); + return fakeSearchAPI(`http://example.com/search?errorOnQuery=break&query=${inputValue}`).then((options) => + options.map((opt) => ({ value: opt.label! })) + ); +} + +export const AsyncOptionsWithOnlyValues: Story = { + name: 'Async - options returns only values', + args: { + options: loadOptionsOnlyValues, + value: [{ value: 'Option 69' }], + placeholder: 'Select an option', + }, + render: (args) => { + const [dynamicArgs, setArgs] = useArgs(); + + return ( + + { + onChangeAction(val); + setArgs({ value: val }); + }} + /> + + ); + }, +}; diff --git a/packages/grafana-ui/src/components/Combobox/MultiCombobox.test.tsx b/packages/grafana-ui/src/components/Combobox/MultiCombobox.test.tsx index 70be3c4148d..66444644ca4 100644 --- a/packages/grafana-ui/src/components/Combobox/MultiCombobox.test.tsx +++ b/packages/grafana-ui/src/components/Combobox/MultiCombobox.test.tsx @@ -1,8 +1,9 @@ -import { render, screen } from '@testing-library/react'; +import { act, render, screen } from '@testing-library/react'; import userEvent, { UserEvent } from '@testing-library/user-event'; import React from 'react'; import { MultiCombobox, MultiComboboxProps } from './MultiCombobox'; +import { ComboboxOption } from './types'; describe('MultiCombobox', () => { beforeAll(() => { @@ -91,15 +92,18 @@ describe('MultiCombobox', () => { await user.click(input); await user.click(await screen.findByRole('option', { name: 'A' })); - //Second option + // Second option await user.click(screen.getByRole('option', { name: 'C' })); - //Deselect + // Deselect await user.click(screen.getByRole('option', { name: 'A' })); - expect(onChange).toHaveBeenNthCalledWith(1, [first]); - expect(onChange).toHaveBeenNthCalledWith(2, [first, third]); - expect(onChange).toHaveBeenNthCalledWith(3, [third]); + expect(onChange).toHaveBeenNthCalledWith(1, [{ label: 'A', value: first }]); + expect(onChange).toHaveBeenNthCalledWith(2, [ + { label: 'A', value: first }, + { label: 'C', value: third }, + ]); + expect(onChange).toHaveBeenNthCalledWith(3, [{ label: 'C', value: third }]); }); it('should be able to render a value that is not in the options', async () => { @@ -138,7 +142,11 @@ describe('MultiCombobox', () => { await user.click(input); await user.click(await screen.findByText('All')); - expect(onChange).toHaveBeenCalledWith(['a', 'b', 'c']); + expect(onChange).toHaveBeenCalledWith([ + { label: 'A', value: 'a' }, + { label: 'B', value: 'b' }, + { label: 'C', value: 'c' }, + ]); }); it('should deselect all option', async () => { @@ -157,4 +165,138 @@ describe('MultiCombobox', () => { expect(onChange).toHaveBeenCalledWith([]); }); }); + + describe('async', () => { + const onChangeHandler = jest.fn(); + let user: ReturnType; + + beforeAll(() => { + user = userEvent.setup({ delay: null }); + jest.useFakeTimers(); + }); + + afterAll(() => { + jest.useRealTimers(); + }); + + afterEach(() => { + onChangeHandler.mockReset(); + }); + + // Assume that most apis only return with the value + const simpleAsyncOptions = [{ value: 'Option 1' }, { value: 'Option 2' }, { value: 'Option 3' }]; + + it('should allow async options', async () => { + const asyncOptions = jest.fn(() => Promise.resolve(simpleAsyncOptions)); + render(); + + const input = screen.getByRole('combobox'); + await user.click(input); + + // Debounce + await act(async () => jest.advanceTimersByTime(200)); + + expect(asyncOptions).toHaveBeenCalled(); + }); + + it('should allow async options and select value', async () => { + const asyncOptions = jest.fn(() => Promise.resolve(simpleAsyncOptions)); + render(); + + const input = screen.getByRole('combobox'); + await user.click(input); + + const item = await screen.findByRole('option', { name: 'Option 3' }); + await user.click(item); + + expect(onChangeHandler).toHaveBeenCalledWith([simpleAsyncOptions[2]]); + }); + + it('should retain values not returned by the async function', async () => { + const asyncOptions = jest.fn(() => Promise.resolve(simpleAsyncOptions)); + render(); + + const input = screen.getByRole('combobox'); + await user.click(input); + + const item = await screen.findByRole('option', { name: 'Option 3' }); + await user.click(item); + + expect(onChangeHandler).toHaveBeenCalledWith([{ value: 'Option 69' }, { value: 'Option 3' }]); + }); + + it('should ignore late responses', async () => { + const asyncOptions = jest.fn(async (searchTerm: string) => { + if (searchTerm === 'a') { + return promiseResolvesWith([{ value: 'first' }], 1500); + } else if (searchTerm === 'ab') { + return promiseResolvesWith([{ value: 'second' }], 500); + } else if (searchTerm === 'abc') { + return promiseResolvesWith([{ value: 'third' }], 800); + } + + return Promise.resolve([]); + }); + + render(); + + const input = screen.getByRole('combobox'); + await user.click(input); + + await user.keyboard('a'); + act(() => jest.advanceTimersByTime(200)); // Skip debounce + + await user.keyboard('b'); + act(() => jest.advanceTimersByTime(200)); // Skip debounce + + await user.keyboard('c'); + act(() => jest.advanceTimersByTime(500)); // Resolve the second request, should be ignored + + expect(screen.queryByRole('option', { name: 'first' })).not.toBeInTheDocument(); + expect(screen.queryByRole('option', { name: 'second' })).not.toBeInTheDocument(); + expect(screen.queryByRole('option', { name: 'third' })).not.toBeInTheDocument(); + + jest.advanceTimersByTime(800); // Resolve the third request, should be shown + expect(screen.queryByRole('option', { name: 'first' })).not.toBeInTheDocument(); + expect(screen.queryByRole('option', { name: 'second' })).not.toBeInTheDocument(); + expect(await screen.findByRole('option', { name: 'third' })).toBeInTheDocument(); + + jest.advanceTimersByTime(1500); // Resolve the first request, should be ignored + expect(screen.queryByRole('option', { name: 'first' })).not.toBeInTheDocument(); + expect(screen.queryByRole('option', { name: 'second' })).not.toBeInTheDocument(); + expect(screen.queryByRole('option', { name: 'third' })).toBeInTheDocument(); + + jest.clearAllTimers(); + }); + + it('should debounce requests', async () => { + const asyncOptions = jest.fn(async () => { + return promiseResolvesWith([{ value: 'Option 3' }], 1); + }); + + render(); + + const input = screen.getByRole('combobox'); + await user.click(input); + + await user.keyboard('a'); + act(() => jest.advanceTimersByTime(10)); + + await user.keyboard('b'); + act(() => jest.advanceTimersByTime(10)); + + await user.keyboard('c'); + act(() => jest.advanceTimersByTime(200)); + + const item = await screen.findByRole('option', { name: 'Option 3' }); + expect(item).toBeInTheDocument(); + + expect(asyncOptions).toHaveBeenCalledTimes(1); + expect(asyncOptions).toHaveBeenCalledWith('abc'); + }); + }); }); + +function promiseResolvesWith(value: ComboboxOption[], timeout = 0) { + return new Promise((resolve) => setTimeout(() => resolve(value), timeout)); +} diff --git a/packages/grafana-ui/src/components/Combobox/MultiCombobox.tsx b/packages/grafana-ui/src/components/Combobox/MultiCombobox.tsx index 4b819ab4a28..104f638de60 100644 --- a/packages/grafana-ui/src/components/Combobox/MultiCombobox.tsx +++ b/packages/grafana-ui/src/components/Combobox/MultiCombobox.tsx @@ -18,46 +18,25 @@ import { NotFoundError } from './MessageRows'; import { OptionListItem } from './OptionListItem'; import { SuffixIcon } from './SuffixIcon'; import { ValuePill } from './ValuePill'; -import { itemFilter, itemToString } from './filter'; +import { itemToString } from './filter'; import { getComboboxStyles, MENU_OPTION_HEIGHT, MENU_OPTION_HEIGHT_DESCRIPTION } from './getComboboxStyles'; import { getMultiComboboxStyles } from './getMultiComboboxStyles'; import { ALL_OPTION_VALUE, ComboboxOption } from './types'; import { useComboboxFloat } from './useComboboxFloat'; import { MAX_SHOWN_ITEMS, useMeasureMulti } from './useMeasureMulti'; import { useMultiInputAutoSize } from './useMultiInputAutoSize'; +import { useOptions } from './useOptions'; interface MultiComboboxBaseProps extends Omit, 'value' | 'onChange'> { value?: T[] | Array>; - onChange: (items?: T[]) => void; + onChange: (option: Array>) => void; enableAllOption?: boolean; } export type MultiComboboxProps = MultiComboboxBaseProps & AutoSizeConditionals; export const MultiCombobox = (props: MultiComboboxProps) => { - const { - options, - placeholder, - onChange, - value, - width, - enableAllOption, - invalid, - loading, - disabled, - minWidth, - maxWidth, - } = props; - const isAsync = typeof options === 'function'; - - const selectedItems = useMemo(() => { - if (!value || isAsync) { - //TODO handle async - return []; - } - - return getSelectedItemsFromValue(value, options); - }, [value, options, isAsync]); + const { placeholder, onChange, value, width, enableAllOption, invalid, disabled, minWidth, maxWidth } = props; const styles = useStyles2(getComboboxStyles); const [inputValue, setInputValue] = useState(''); @@ -73,19 +52,22 @@ export const MultiCombobox = (props: MultiComboboxPro } as ComboboxOption; }, [inputValue]); - const baseItems = useMemo(() => { - return isAsync ? [] : enableAllOption ? [allOptionItem, ...options] : options; - }, [options, enableAllOption, allOptionItem, isAsync]); + // Handle async options and the 'All' option + const { options: baseOptions, updateOptions, asyncLoading } = useOptions(props.options); + const options = useMemo(() => { + // Only add the 'All' option if there's more than 1 option + const addAllOption = enableAllOption && baseOptions.length > 1; + return addAllOption ? [allOptionItem, ...baseOptions] : baseOptions; + }, [baseOptions, enableAllOption, allOptionItem]); + const loading = props.loading || asyncLoading; - const items = useMemo(() => { - const newItems = baseItems.filter(itemFilter(inputValue)); - - if (enableAllOption && newItems.length === 1 && newItems[0] === allOptionItem) { + const selectedItems = useMemo(() => { + if (!value) { return []; } - return newItems; - }, [baseItems, inputValue, enableAllOption, allOptionItem]); + return getSelectedItemsFromValue(value, baseOptions); + }, [value, baseOptions]); const { measureRef, counterMeasureRef, suffixMeasureRef, shownItems } = useMeasureMulti( selectedItems, @@ -98,48 +80,50 @@ export const MultiCombobox = (props: MultiComboboxPro [selectedItems] ); - const { getSelectedItemProps, getDropdownProps, removeSelectedItem } = useMultipleSelection({ - selectedItems, //initally selected items, - onStateChange: ({ type, selectedItems: newSelectedItems }) => { - switch (type) { - case useMultipleSelection.stateChangeTypes.SelectedItemKeyDownBackspace: - case useMultipleSelection.stateChangeTypes.SelectedItemKeyDownDelete: - case useMultipleSelection.stateChangeTypes.DropdownKeyDownBackspace: - case useMultipleSelection.stateChangeTypes.FunctionRemoveSelectedItem: - if (newSelectedItems) { - onChange(getComboboxOptionsValues(newSelectedItems)); - } - break; + const { getSelectedItemProps, getDropdownProps, setSelectedItems, addSelectedItem, removeSelectedItem } = + useMultipleSelection({ + selectedItems, // initally selected items, + onStateChange: ({ type, selectedItems: newSelectedItems }) => { + switch (type) { + case useMultipleSelection.stateChangeTypes.SelectedItemKeyDownBackspace: + case useMultipleSelection.stateChangeTypes.SelectedItemKeyDownDelete: + case useMultipleSelection.stateChangeTypes.DropdownKeyDownBackspace: + case useMultipleSelection.stateChangeTypes.FunctionRemoveSelectedItem: + case useMultipleSelection.stateChangeTypes.FunctionAddSelectedItem: + case useMultipleSelection.stateChangeTypes.FunctionSetSelectedItems: + // Unclear why newSelectedItems would be undefined, but this seems logical + onChange(newSelectedItems ?? []); + break; - default: - break; - } - }, - stateReducer: (state, actionAndChanges) => { - const { changes } = actionAndChanges; - return { - ...changes, + default: + break; + } + }, + stateReducer: (state, actionAndChanges) => { + const { changes } = actionAndChanges; + return { + ...changes, - /** - * TODO: Fix Hack! - * This prevents the menu from closing when the user unselects an item in the dropdown at the expense - * of breaking keyboard navigation. - * - * Downshift isn't really designed to keep selected items in the dropdown menu, so when you unselect an item - * in a multiselect, the stateReducer tries to move focus onto another item which causes the menu to be closed. - * This only seems to happen when you deselect the last item in the selectedItems list. - * - * Check out: - * - FunctionRemoveSelectedItem in the useMultipleSelection reducer https://github.com/downshift-js/downshift/blob/master/src/hooks/useMultipleSelection/reducer.js#L75 - * - The activeIndex useEffect in useMultipleSelection https://github.com/downshift-js/downshift/blob/master/src/hooks/useMultipleSelection/index.js#L68-L72 - * - * Forcing the activeIndex to -999 both prevents the useEffect that changes the focus from triggering (value never changes) - * and prevents the if statement in useMultipleSelection from focusing anything. - */ - activeIndex: -999, - }; - }, - }); + /** + * TODO: Fix Hack! + * This prevents the menu from closing when the user unselects an item in the dropdown at the expense + * of breaking keyboard navigation. + * + * Downshift isn't really designed to keep selected items in the dropdown menu, so when you unselect an item + * in a multiselect, the stateReducer tries to move focus onto another item which causes the menu to be closed. + * This only seems to happen when you deselect the last item in the selectedItems list. + * + * Check out: + * - FunctionRemoveSelectedItem in the useMultipleSelection reducer https://github.com/downshift-js/downshift/blob/master/src/hooks/useMultipleSelection/reducer.js#L75 + * - The activeIndex useEffect in useMultipleSelection https://github.com/downshift-js/downshift/blob/master/src/hooks/useMultipleSelection/index.js#L68-L72 + * + * Forcing the activeIndex to -999 both prevents the useEffect that changes the focus from triggering (value never changes) + * and prevents the if statement in useMultipleSelection from focusing anything. + */ + activeIndex: -999, + }; + }, + }); const { getToggleButtonProps, @@ -150,12 +134,25 @@ export const MultiCombobox = (props: MultiComboboxPro getInputProps, getItemProps, } = useCombobox({ - items, + items: options, itemToString, inputValue, selectedItem: null, stateReducer: (state, actionAndChanges) => { - const { changes, type } = actionAndChanges; + const { type } = actionAndChanges; + let { changes } = actionAndChanges; + const menuBeingOpened = state.isOpen === false && changes.isOpen === true; + + // Reset the input value when the menu is opened. If the menu is opened due to an input change + // then make sure we keep that. + // This will trigger onInputValueChange to load async options + if (menuBeingOpened && changes.inputValue === state.inputValue) { + changes = { + ...changes, + inputValue: '', + }; + } + switch (type) { case useCombobox.stateChangeTypes.InputKeyDownEnter: case useCombobox.stateChangeTypes.ItemClick: @@ -171,39 +168,50 @@ export const MultiCombobox = (props: MultiComboboxPro } }, + onIsOpenChange: ({ isOpen, inputValue }) => { + if (isOpen && inputValue === '') { + updateOptions(inputValue); + } + }, + onStateChange: ({ inputValue: newInputValue, type, selectedItem: newSelectedItem }) => { switch (type) { case useCombobox.stateChangeTypes.InputKeyDownEnter: case useCombobox.stateChangeTypes.ItemClick: // Handle All functionality if (newSelectedItem?.value === ALL_OPTION_VALUE) { - const allFilteredSelected = selectedItems.length === items.length - 1; - let newSelectedItems = allFilteredSelected && inputValue === '' ? [] : baseItems.slice(1); + // TODO: fix bug where if the search filtered items list is the + // same length, but different, than the selected items (ask tobias) + const isAllFilteredSelected = selectedItems.length === options.length - 1; - if (!allFilteredSelected && inputValue !== '') { + // if every option is already selected, clear the selection. + // otherwise, select all the options (excluding the first ALL_OTION) + const realOptions = options.slice(1); + let newSelectedItems = isAllFilteredSelected && inputValue === '' ? [] : realOptions; + + if (!isAllFilteredSelected && inputValue !== '') { // Select all currently filtered items and deduplicate - newSelectedItems = [...new Set([...selectedItems, ...items.slice(1)])]; + newSelectedItems = [...new Set([...selectedItems, ...realOptions])]; } - if (allFilteredSelected && inputValue !== '') { + if (isAllFilteredSelected && inputValue !== '') { // Deselect all currently filtered items - const filteredSet = new Set(items.slice(1).map((item) => item.value)); + const filteredSet = new Set(realOptions.map((item) => item.value)); newSelectedItems = selectedItems.filter((item) => !filteredSet.has(item.value)); } - onChange(getComboboxOptionsValues(newSelectedItems)); - break; - } - if (newSelectedItem) { - if (!isOptionSelected(newSelectedItem)) { - onChange(getComboboxOptionsValues([...selectedItems, newSelectedItem])); - break; - } - removeSelectedItem(newSelectedItem); // onChange is handled by multiselect here + setSelectedItems(newSelectedItems); + } else if (newSelectedItem && isOptionSelected(newSelectedItem)) { + removeSelectedItem(newSelectedItem); + } else if (newSelectedItem) { + addSelectedItem(newSelectedItem); } + break; case useCombobox.stateChangeTypes.InputChange: setInputValue(newInputValue ?? ''); + updateOptions(newInputValue ?? ''); + break; default: break; @@ -211,14 +219,14 @@ export const MultiCombobox = (props: MultiComboboxPro }, }); - const { inputRef: containerRef, floatingRef, floatStyles, scrollRef } = useComboboxFloat(items, isOpen); + const { inputRef: containerRef, floatingRef, floatStyles, scrollRef } = useComboboxFloat(options, isOpen); const multiStyles = useStyles2(getMultiComboboxStyles, isOpen, invalid, disabled, width, minWidth, maxWidth); const virtualizerOptions = { - count: items.length, + count: options.length, getScrollElement: () => scrollRef.current, estimateSize: (index: number) => - 'description' in items[index] ? MENU_OPTION_HEIGHT_DESCRIPTION : MENU_OPTION_HEIGHT, + 'description' in options[index] ? MENU_OPTION_HEIGHT_DESCRIPTION : MENU_OPTION_HEIGHT, overscan: VIRTUAL_OVERSCAN_ITEMS, }; @@ -291,13 +299,16 @@ export const MultiCombobox = (props: MultiComboboxPro
    {rowVirtualizer.getVirtualItems().map((virtualRow) => { const index = virtualRow.index; - const item = items[index]; + const item = options[index]; const itemProps = getItemProps({ item, index }); const isSelected = isOptionSelected(item); const id = 'multicombobox-option-' + item.value.toString(); const isAll = item.value === ALL_OPTION_VALUE; + + // TODO: fix bug where if the search filtered items list is the + // same length, but different, than the selected items (ask tobias) const allItemsSelected = - items[0]?.value === ALL_OPTION_VALUE && selectedItems.length === items.length - 1; + options[0]?.value === ALL_OPTION_VALUE && selectedItems.length === options.length - 1; return (
  • (props: MultiComboboxPro label={ isAll ? (item.label ?? item.value.toString()) + - (isAll && inputValue !== '' ? ` (${items.length - 1})` : '') + (isAll && inputValue !== '' ? ` (${options.length - 1})` : '') : (item.label ?? item.value.toString()) } description={item?.description} @@ -332,7 +343,7 @@ export const MultiCombobox = (props: MultiComboboxPro ); })}
-
{items.length === 0 && }
+
{options.length === 0 && }
)}
@@ -375,7 +386,3 @@ function isComboboxOptions( ): value is Array> { return typeof value[0] === 'object'; } - -function getComboboxOptionsValues(optionArray: Array>) { - return optionArray.map((option) => option.value); -} diff --git a/packages/grafana-ui/src/components/Combobox/useOptions.ts b/packages/grafana-ui/src/components/Combobox/useOptions.ts new file mode 100644 index 00000000000..504f3584e1d --- /dev/null +++ b/packages/grafana-ui/src/components/Combobox/useOptions.ts @@ -0,0 +1,82 @@ +import { debounce } from 'lodash'; +import { useState, useCallback, useMemo } from 'react'; + +import { itemFilter } from './filter'; +import { ComboboxOption } from './types'; +import { StaleResultError, useLatestAsyncCall } from './useLatestAsyncCall'; + +type AsyncOptions = + | Array> + | ((inputValue: string) => Promise>>); + +const asyncNoop = () => Promise.resolve([]); + +/** + * Abstracts away sync/async options for MultiCombobox (and later Combobox). + * It also filters options based on the user's input. + * + * Returns: + * - options either filtered by user's input, or from async options fn + * - function to call when user types (to filter, or call async fn) + * - loading and error states + */ +export function useOptions(rawOptions: AsyncOptions) { + const isAsync = typeof rawOptions === 'function'; + + const loadOptions = useLatestAsyncCall(isAsync ? rawOptions : asyncNoop); + + const debouncedLoadOptions = useMemo( + () => + debounce((searchTerm: string) => { + return loadOptions(searchTerm) + .then((options) => { + setAsyncOptions(options); + setAsyncLoading(false); + setAsyncError(false); + }) + .catch((error) => { + if (!(error instanceof StaleResultError)) { + setAsyncError(true); + setAsyncLoading(false); + + if (error) { + console.error('Error loading async options for Combobox', error); + } + } + }); + }, 200), + [loadOptions] + ); + + const [asyncOptions, setAsyncOptions] = useState>>([]); + const [asyncLoading, setAsyncLoading] = useState(false); + const [asyncError, setAsyncError] = useState(false); + + // This hook keeps its own inputValue state (rather than accepting it as an arg) because it needs to be + // told it for async options loading anyway. + const [userTypedSearch, setUserTypedSearch] = useState(''); + + const updateOptions = useCallback( + (inputValue: string) => { + if (!isAsync) { + setUserTypedSearch(inputValue); + return; + } + + setAsyncLoading(true); + + debouncedLoadOptions(inputValue); + }, + [debouncedLoadOptions, isAsync] + ); + + const finalOptions = useMemo(() => { + if (isAsync) { + return asyncOptions; + } else { + return rawOptions.filter(itemFilter(userTypedSearch)); + } + }, [rawOptions, asyncOptions, isAsync, userTypedSearch]); + + return { options: finalOptions, updateOptions, asyncLoading, asyncError }; +} From 8b9d4d13586b55509e7f0b26daff2a1ede6e41c4 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 28 Jan 2025 13:37:10 +0000 Subject: [PATCH 22/39] Update dependency @types/node to v22.12.0 (#99638) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package.json | 2 +- packages/grafana-data/package.json | 2 +- packages/grafana-e2e-selectors/package.json | 2 +- packages/grafana-flamegraph/package.json | 2 +- packages/grafana-icons/package.json | 2 +- .../grafana-o11y-ds-frontend/package.json | 2 +- packages/grafana-prometheus/package.json | 2 +- packages/grafana-sql/package.json | 2 +- packages/grafana-ui/package.json | 2 +- .../datasource/azuremonitor/package.json | 2 +- .../datasource/cloud-monitoring/package.json | 2 +- .../package.json | 2 +- .../grafana-pyroscope-datasource/package.json | 2 +- .../grafana-testdata-datasource/package.json | 2 +- .../plugins/datasource/jaeger/package.json | 2 +- .../app/plugins/datasource/mssql/package.json | 2 +- .../app/plugins/datasource/mysql/package.json | 2 +- .../app/plugins/datasource/parca/package.json | 2 +- .../app/plugins/datasource/tempo/package.json | 2 +- .../plugins/datasource/zipkin/package.json | 2 +- yarn.lock | 48 +++++++++---------- 21 files changed, 44 insertions(+), 44 deletions(-) diff --git a/package.json b/package.json index 54a32f06f62..92651a8dbd1 100644 --- a/package.json +++ b/package.json @@ -123,7 +123,7 @@ "@types/lodash": "4.17.15", "@types/logfmt": "^1.2.3", "@types/lucene": "^2", - "@types/node": "22.10.10", + "@types/node": "22.12.0", "@types/node-forge": "^1", "@types/ol-ext": "npm:@siedlerchr/types-ol-ext@3.2.4", "@types/pluralize": "^0.0.33", diff --git a/packages/grafana-data/package.json b/packages/grafana-data/package.json index 8ba4574e8df..ff3a9928802 100644 --- a/packages/grafana-data/package.json +++ b/packages/grafana-data/package.json @@ -65,7 +65,7 @@ "@rollup/plugin-node-resolve": "16.0.0", "@types/history": "4.7.11", "@types/lodash": "4.17.15", - "@types/node": "22.10.10", + "@types/node": "22.12.0", "@types/papaparse": "5.3.15", "@types/react": "18.3.18", "@types/react-dom": "18.3.5", diff --git a/packages/grafana-e2e-selectors/package.json b/packages/grafana-e2e-selectors/package.json index f6da48b7630..5eaf8277ad1 100644 --- a/packages/grafana-e2e-selectors/package.json +++ b/packages/grafana-e2e-selectors/package.json @@ -40,7 +40,7 @@ }, "devDependencies": { "@rollup/plugin-node-resolve": "16.0.0", - "@types/node": "22.10.10", + "@types/node": "22.12.0", "@types/semver": "7.5.8", "esbuild": "0.24.2", "rimraf": "6.0.1", diff --git a/packages/grafana-flamegraph/package.json b/packages/grafana-flamegraph/package.json index 66de5e2225d..e48e6113c6b 100644 --- a/packages/grafana-flamegraph/package.json +++ b/packages/grafana-flamegraph/package.json @@ -68,7 +68,7 @@ "@types/d3": "^7", "@types/jest": "^29.5.4", "@types/lodash": "4.17.15", - "@types/node": "22.10.10", + "@types/node": "22.12.0", "@types/react": "18.3.18", "@types/react-virtualized-auto-sizer": "1.0.4", "@types/tinycolor2": "1.4.6", diff --git a/packages/grafana-icons/package.json b/packages/grafana-icons/package.json index 5e4ea1556f8..a39c28777a5 100644 --- a/packages/grafana-icons/package.json +++ b/packages/grafana-icons/package.json @@ -45,7 +45,7 @@ "@svgr/plugin-prettier": "^8.1.0", "@svgr/plugin-svgo": "^8.1.0", "@types/babel__core": "^7", - "@types/node": "22.10.10", + "@types/node": "22.12.0", "@types/react": "18.3.18", "@types/react-dom": "18.3.5", "esbuild": "0.24.2", diff --git a/packages/grafana-o11y-ds-frontend/package.json b/packages/grafana-o11y-ds-frontend/package.json index 4da97db322a..a668fa0280b 100644 --- a/packages/grafana-o11y-ds-frontend/package.json +++ b/packages/grafana-o11y-ds-frontend/package.json @@ -36,7 +36,7 @@ "@testing-library/react": "16.1.0", "@testing-library/user-event": "14.5.2", "@types/jest": "^29.5.4", - "@types/node": "22.10.10", + "@types/node": "22.12.0", "@types/react": "18.3.18", "@types/systemjs": "6.15.1", "jest": "^29.6.4", diff --git a/packages/grafana-prometheus/package.json b/packages/grafana-prometheus/package.json index 740e9309832..3f3060e08cc 100644 --- a/packages/grafana-prometheus/package.json +++ b/packages/grafana-prometheus/package.json @@ -93,7 +93,7 @@ "@types/jest": "29.5.14", "@types/jquery": "3.5.32", "@types/lodash": "4.17.15", - "@types/node": "22.10.10", + "@types/node": "22.12.0", "@types/pluralize": "^0.0.33", "@types/prismjs": "1.26.5", "@types/react": "18.3.18", diff --git a/packages/grafana-sql/package.json b/packages/grafana-sql/package.json index a5093fc6aee..6bafb8a31ed 100644 --- a/packages/grafana-sql/package.json +++ b/packages/grafana-sql/package.json @@ -41,7 +41,7 @@ "@testing-library/user-event": "14.5.2", "@types/jest": "^29.5.4", "@types/lodash": "4.17.15", - "@types/node": "22.10.10", + "@types/node": "22.12.0", "@types/react": "18.3.18", "@types/react-dom": "18.3.5", "@types/react-virtualized-auto-sizer": "1.0.4", diff --git a/packages/grafana-ui/package.json b/packages/grafana-ui/package.json index a8eb8f1be2b..aa162a98040 100644 --- a/packages/grafana-ui/package.json +++ b/packages/grafana-ui/package.json @@ -145,7 +145,7 @@ "@types/is-hotkey": "0.1.10", "@types/jest": "29.5.14", "@types/mock-raf": "1.0.6", - "@types/node": "22.10.10", + "@types/node": "22.12.0", "@types/prismjs": "1.26.5", "@types/react": "18.3.18", "@types/react-color": "3.0.13", diff --git a/public/app/plugins/datasource/azuremonitor/package.json b/public/app/plugins/datasource/azuremonitor/package.json index 38ed9e50920..0f8b460420e 100644 --- a/public/app/plugins/datasource/azuremonitor/package.json +++ b/public/app/plugins/datasource/azuremonitor/package.json @@ -33,7 +33,7 @@ "@testing-library/user-event": "14.5.2", "@types/jest": "29.5.14", "@types/lodash": "4.17.15", - "@types/node": "22.10.10", + "@types/node": "22.12.0", "@types/prismjs": "1.26.5", "@types/react": "18.3.18", "@types/react-dom": "18.3.5", diff --git a/public/app/plugins/datasource/cloud-monitoring/package.json b/public/app/plugins/datasource/cloud-monitoring/package.json index 32a61eb300d..b281d43e7a3 100644 --- a/public/app/plugins/datasource/cloud-monitoring/package.json +++ b/public/app/plugins/datasource/cloud-monitoring/package.json @@ -35,7 +35,7 @@ "@types/debounce-promise": "3.1.9", "@types/jest": "29.5.14", "@types/lodash": "4.17.15", - "@types/node": "22.10.10", + "@types/node": "22.12.0", "@types/prismjs": "1.26.5", "@types/react": "18.3.18", "@types/react-dom": "18.3.5", diff --git a/public/app/plugins/datasource/grafana-postgresql-datasource/package.json b/public/app/plugins/datasource/grafana-postgresql-datasource/package.json index 4094ca3e08b..2f85083d376 100644 --- a/public/app/plugins/datasource/grafana-postgresql-datasource/package.json +++ b/public/app/plugins/datasource/grafana-postgresql-datasource/package.json @@ -23,7 +23,7 @@ "@testing-library/user-event": "14.5.2", "@types/jest": "29.5.14", "@types/lodash": "4.17.15", - "@types/node": "22.10.10", + "@types/node": "22.12.0", "@types/react": "18.3.18", "ts-node": "10.9.2", "typescript": "5.7.3", diff --git a/public/app/plugins/datasource/grafana-pyroscope-datasource/package.json b/public/app/plugins/datasource/grafana-pyroscope-datasource/package.json index bba725e85e2..ca1682f1758 100644 --- a/public/app/plugins/datasource/grafana-pyroscope-datasource/package.json +++ b/public/app/plugins/datasource/grafana-pyroscope-datasource/package.json @@ -27,7 +27,7 @@ "@testing-library/user-event": "14.5.2", "@types/jest": "29.5.14", "@types/lodash": "4.17.15", - "@types/node": "22.10.10", + "@types/node": "22.12.0", "@types/prismjs": "1.26.5", "@types/react": "18.3.18", "@types/react-dom": "18.3.5", diff --git a/public/app/plugins/datasource/grafana-testdata-datasource/package.json b/public/app/plugins/datasource/grafana-testdata-datasource/package.json index a1666e38d59..47acd088aeb 100644 --- a/public/app/plugins/datasource/grafana-testdata-datasource/package.json +++ b/public/app/plugins/datasource/grafana-testdata-datasource/package.json @@ -30,7 +30,7 @@ "@types/d3-random": "^3.0.2", "@types/jest": "29.5.14", "@types/lodash": "4.17.15", - "@types/node": "22.10.10", + "@types/node": "22.12.0", "@types/react": "18.3.18", "@types/react-dom": "18.3.5", "@types/uuid": "10.0.0", diff --git a/public/app/plugins/datasource/jaeger/package.json b/public/app/plugins/datasource/jaeger/package.json index 5f1fd12a7ad..a882e2093de 100644 --- a/public/app/plugins/datasource/jaeger/package.json +++ b/public/app/plugins/datasource/jaeger/package.json @@ -31,7 +31,7 @@ "@types/jest": "29.5.14", "@types/lodash": "4.17.15", "@types/logfmt": "^1.2.3", - "@types/node": "22.10.10", + "@types/node": "22.12.0", "@types/react": "18.3.18", "@types/react-dom": "18.3.5", "@types/react-window": "1.8.8", diff --git a/public/app/plugins/datasource/mssql/package.json b/public/app/plugins/datasource/mssql/package.json index 7a54d042db0..008012c3d27 100644 --- a/public/app/plugins/datasource/mssql/package.json +++ b/public/app/plugins/datasource/mssql/package.json @@ -23,7 +23,7 @@ "@testing-library/user-event": "14.5.2", "@types/jest": "29.5.14", "@types/lodash": "4.17.15", - "@types/node": "22.10.10", + "@types/node": "22.12.0", "@types/react": "18.3.18", "ts-node": "10.9.2", "typescript": "5.7.3", diff --git a/public/app/plugins/datasource/mysql/package.json b/public/app/plugins/datasource/mysql/package.json index b1a791dd64b..2cbed20190a 100644 --- a/public/app/plugins/datasource/mysql/package.json +++ b/public/app/plugins/datasource/mysql/package.json @@ -23,7 +23,7 @@ "@testing-library/user-event": "14.5.2", "@types/jest": "29.5.14", "@types/lodash": "4.17.15", - "@types/node": "22.10.10", + "@types/node": "22.12.0", "@types/react": "18.3.18", "ts-node": "10.9.2", "typescript": "5.7.3", diff --git a/public/app/plugins/datasource/parca/package.json b/public/app/plugins/datasource/parca/package.json index becc966e421..cf4dcec1268 100644 --- a/public/app/plugins/datasource/parca/package.json +++ b/public/app/plugins/datasource/parca/package.json @@ -23,7 +23,7 @@ "@testing-library/react": "16.1.0", "@testing-library/user-event": "14.5.2", "@types/lodash": "4.17.15", - "@types/node": "22.10.10", + "@types/node": "22.12.0", "@types/react": "18.3.18", "@types/react-dom": "18.3.5", "ts-node": "10.9.2", diff --git a/public/app/plugins/datasource/tempo/package.json b/public/app/plugins/datasource/tempo/package.json index 9f6e20f1d5b..55a87c3c5f3 100644 --- a/public/app/plugins/datasource/tempo/package.json +++ b/public/app/plugins/datasource/tempo/package.json @@ -46,7 +46,7 @@ "@testing-library/user-event": "14.5.2", "@types/jest": "29.5.14", "@types/lodash": "4.17.15", - "@types/node": "22.10.10", + "@types/node": "22.12.0", "@types/prismjs": "1.26.5", "@types/react": "18.3.18", "@types/react-dom": "18.3.5", diff --git a/public/app/plugins/datasource/zipkin/package.json b/public/app/plugins/datasource/zipkin/package.json index f9f8a40298c..efddda4e217 100644 --- a/public/app/plugins/datasource/zipkin/package.json +++ b/public/app/plugins/datasource/zipkin/package.json @@ -26,7 +26,7 @@ "@testing-library/react": "16.1.0", "@types/jest": "29.5.14", "@types/lodash": "4.17.15", - "@types/node": "22.10.10", + "@types/node": "22.12.0", "@types/react": "18.3.18", "@types/react-dom": "18.3.5", "ts-node": "10.9.2", diff --git a/yarn.lock b/yarn.lock index b6e5c37717e..2714fa57eea 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2718,7 +2718,7 @@ __metadata: "@testing-library/user-event": "npm:14.5.2" "@types/jest": "npm:29.5.14" "@types/lodash": "npm:4.17.15" - "@types/node": "npm:22.10.10" + "@types/node": "npm:22.12.0" "@types/prismjs": "npm:1.26.5" "@types/react": "npm:18.3.18" "@types/react-dom": "npm:18.3.5" @@ -2760,7 +2760,7 @@ __metadata: "@testing-library/user-event": "npm:14.5.2" "@types/jest": "npm:29.5.14" "@types/lodash": "npm:4.17.15" - "@types/node": "npm:22.10.10" + "@types/node": "npm:22.12.0" "@types/react": "npm:18.3.18" lodash: "npm:4.17.21" react: "npm:18.3.1" @@ -2790,7 +2790,7 @@ __metadata: "@testing-library/user-event": "npm:14.5.2" "@types/jest": "npm:29.5.14" "@types/lodash": "npm:4.17.15" - "@types/node": "npm:22.10.10" + "@types/node": "npm:22.12.0" "@types/prismjs": "npm:1.26.5" "@types/react": "npm:18.3.18" "@types/react-dom": "npm:18.3.5" @@ -2832,7 +2832,7 @@ __metadata: "@types/d3-random": "npm:^3.0.2" "@types/jest": "npm:29.5.14" "@types/lodash": "npm:4.17.15" - "@types/node": "npm:22.10.10" + "@types/node": "npm:22.12.0" "@types/react": "npm:18.3.18" "@types/react-dom": "npm:18.3.5" "@types/uuid": "npm:10.0.0" @@ -2873,7 +2873,7 @@ __metadata: "@types/jest": "npm:29.5.14" "@types/lodash": "npm:4.17.15" "@types/logfmt": "npm:^1.2.3" - "@types/node": "npm:22.10.10" + "@types/node": "npm:22.12.0" "@types/react": "npm:18.3.18" "@types/react-dom": "npm:18.3.5" "@types/react-window": "npm:1.8.8" @@ -2913,7 +2913,7 @@ __metadata: "@testing-library/user-event": "npm:14.5.2" "@types/jest": "npm:29.5.14" "@types/lodash": "npm:4.17.15" - "@types/node": "npm:22.10.10" + "@types/node": "npm:22.12.0" "@types/react": "npm:18.3.18" lodash: "npm:4.17.21" react: "npm:18.3.1" @@ -2944,7 +2944,7 @@ __metadata: "@testing-library/user-event": "npm:14.5.2" "@types/jest": "npm:29.5.14" "@types/lodash": "npm:4.17.15" - "@types/node": "npm:22.10.10" + "@types/node": "npm:22.12.0" "@types/react": "npm:18.3.18" lodash: "npm:4.17.21" react: "npm:18.3.1" @@ -2972,7 +2972,7 @@ __metadata: "@testing-library/react": "npm:16.1.0" "@testing-library/user-event": "npm:14.5.2" "@types/lodash": "npm:4.17.15" - "@types/node": "npm:22.10.10" + "@types/node": "npm:22.12.0" "@types/react": "npm:18.3.18" "@types/react-dom": "npm:18.3.5" lodash: "npm:4.17.21" @@ -3010,7 +3010,7 @@ __metadata: "@types/debounce-promise": "npm:3.1.9" "@types/jest": "npm:29.5.14" "@types/lodash": "npm:4.17.15" - "@types/node": "npm:22.10.10" + "@types/node": "npm:22.12.0" "@types/prismjs": "npm:1.26.5" "@types/react": "npm:18.3.18" "@types/react-dom": "npm:18.3.5" @@ -3063,7 +3063,7 @@ __metadata: "@testing-library/user-event": "npm:14.5.2" "@types/jest": "npm:29.5.14" "@types/lodash": "npm:4.17.15" - "@types/node": "npm:22.10.10" + "@types/node": "npm:22.12.0" "@types/prismjs": "npm:1.26.5" "@types/react": "npm:18.3.18" "@types/react-dom": "npm:18.3.5" @@ -3113,7 +3113,7 @@ __metadata: "@testing-library/react": "npm:16.1.0" "@types/jest": "npm:29.5.14" "@types/lodash": "npm:4.17.15" - "@types/node": "npm:22.10.10" + "@types/node": "npm:22.12.0" "@types/react": "npm:18.3.18" "@types/react-dom": "npm:18.3.5" lodash: "npm:4.17.21" @@ -3206,7 +3206,7 @@ __metadata: "@types/d3-interpolate": "npm:^3.0.0" "@types/history": "npm:4.7.11" "@types/lodash": "npm:4.17.15" - "@types/node": "npm:22.10.10" + "@types/node": "npm:22.12.0" "@types/papaparse": "npm:5.3.15" "@types/react": "npm:18.3.18" "@types/react-dom": "npm:18.3.5" @@ -3264,7 +3264,7 @@ __metadata: dependencies: "@grafana/tsconfig": "npm:^2.0.0" "@rollup/plugin-node-resolve": "npm:16.0.0" - "@types/node": "npm:22.10.10" + "@types/node": "npm:22.12.0" "@types/semver": "npm:7.5.8" esbuild: "npm:0.24.2" rimraf: "npm:6.0.1" @@ -3399,7 +3399,7 @@ __metadata: "@types/d3": "npm:^7" "@types/jest": "npm:^29.5.4" "@types/lodash": "npm:4.17.15" - "@types/node": "npm:22.10.10" + "@types/node": "npm:22.12.0" "@types/react": "npm:18.3.18" "@types/react-virtualized-auto-sizer": "npm:1.0.4" "@types/tinycolor2": "npm:1.4.6" @@ -3507,7 +3507,7 @@ __metadata: "@testing-library/react": "npm:16.1.0" "@testing-library/user-event": "npm:14.5.2" "@types/jest": "npm:^29.5.4" - "@types/node": "npm:22.10.10" + "@types/node": "npm:22.12.0" "@types/react": "npm:18.3.18" "@types/systemjs": "npm:6.15.1" jest: "npm:^29.6.4" @@ -3628,7 +3628,7 @@ __metadata: "@types/jest": "npm:29.5.14" "@types/jquery": "npm:3.5.32" "@types/lodash": "npm:4.17.15" - "@types/node": "npm:22.10.10" + "@types/node": "npm:22.12.0" "@types/pluralize": "npm:^0.0.33" "@types/prismjs": "npm:1.26.5" "@types/react": "npm:18.3.18" @@ -3779,7 +3779,7 @@ __metadata: "@svgr/plugin-prettier": "npm:^8.1.0" "@svgr/plugin-svgo": "npm:^8.1.0" "@types/babel__core": "npm:^7" - "@types/node": "npm:22.10.10" + "@types/node": "npm:22.12.0" "@types/react": "npm:18.3.18" "@types/react-dom": "npm:18.3.5" esbuild: "npm:0.24.2" @@ -3886,7 +3886,7 @@ __metadata: "@testing-library/user-event": "npm:14.5.2" "@types/jest": "npm:^29.5.4" "@types/lodash": "npm:4.17.15" - "@types/node": "npm:22.10.10" + "@types/node": "npm:22.12.0" "@types/react": "npm:18.3.18" "@types/react-dom": "npm:18.3.5" "@types/react-virtualized-auto-sizer": "npm:1.0.4" @@ -4050,7 +4050,7 @@ __metadata: "@types/jquery": "npm:3.5.32" "@types/lodash": "npm:4.17.15" "@types/mock-raf": "npm:1.0.6" - "@types/node": "npm:22.10.10" + "@types/node": "npm:22.12.0" "@types/prismjs": "npm:1.26.5" "@types/react": "npm:18.3.18" "@types/react-color": "npm:3.0.13" @@ -9745,12 +9745,12 @@ __metadata: languageName: node linkType: hard -"@types/node@npm:*, @types/node@npm:22.10.10, @types/node@npm:>=10.0.0, @types/node@npm:>=13.7.0, @types/node@npm:>=13.7.4, @types/node@npm:^22.0.0": - version: 22.10.10 - resolution: "@types/node@npm:22.10.10" +"@types/node@npm:*, @types/node@npm:22.12.0, @types/node@npm:>=10.0.0, @types/node@npm:>=13.7.0, @types/node@npm:>=13.7.4, @types/node@npm:^22.0.0": + version: 22.12.0 + resolution: "@types/node@npm:22.12.0" dependencies: undici-types: "npm:~6.20.0" - checksum: 10/61559b62bc7e62b947876e097c99472dd01317dd3b5916b538e2c40db8d68a1fc23c8de48149ef04cd2790a97bf2c005ea60cbb067e55ddbefd013d73da7a147 + checksum: 10/aac2b6f6a845ec3540c3d979b3150efe3162165bfda953af10b579df2d1cc4f5c48506922bf6bf661a2e5a7ebb571c5729bf1f9f12488a810bb1a5fa9522ef9d languageName: node linkType: hard @@ -17822,7 +17822,7 @@ __metadata: "@types/lodash": "npm:4.17.15" "@types/logfmt": "npm:^1.2.3" "@types/lucene": "npm:^2" - "@types/node": "npm:22.10.10" + "@types/node": "npm:22.12.0" "@types/node-forge": "npm:^1" "@types/ol-ext": "npm:@siedlerchr/types-ol-ext@3.2.4" "@types/pluralize": "npm:^0.0.33" From 0bf31c14a7e295af089ecc682d7eedb434349d41 Mon Sep 17 00:00:00 2001 From: Konrad Lalik Date: Tue, 28 Jan 2025 14:49:47 +0100 Subject: [PATCH 23/39] Alerting: Improve default form values handling (#97564) This PR refactors how rule form state is managed and relies less on prop drilling and removes dependency on redux store. --- .betterer.results | 41 ++- public/app/features/alerting/routes.tsx | 4 +- .../alerting/unified/api/alertRuleApi.ts | 1 + .../components/export/GrafanaModifyExport.tsx | 2 +- .../ExpressionStatusIndicator.test.tsx | 2 +- .../rule-editor/AnnotationsStep.test.tsx | 2 +- .../rule-editor/GrafanaEvaluationBehavior.tsx | 180 ++++----- .../alert-rule-form/AlertRuleForm.tsx | 155 ++------ .../alert-rule-form/ModifyExportRuleForm.tsx | 17 +- .../getPayloadToExport.test.ts | 2 +- .../SimplifiedRuleEditor.test.tsx | 10 +- .../rule-editor/labels/LabelsField.test.tsx | 1 + .../QueryAndExpressionsStep.tsx | 63 +--- ...riesTransformableToSimpleCondition.test.ts | 3 +- .../useAdvancedMode.ts | 5 +- .../unified/components/rule-editor/util.ts | 14 - .../rule-viewer/RuleViewer.test.tsx | 12 +- .../rules/EditRuleGroupModal.test.tsx | 206 ++++++----- .../components/rules/EditRuleGroupModal.tsx | 346 ++++++++++-------- .../components/rules/RulesGroup.test.tsx | 52 +-- .../unified/components/rules/RulesGroup.tsx | 24 +- .../unified/components/rules/RulesTable.tsx | 17 +- .../hooks/ruleGroup/useProduceNewRuleGroup.ts | 2 +- .../alerting/unified/hooks/useHasRuler.ts | 7 +- .../hooks/useUnifiedAlertingSelector.ts | 3 + public/app/features/alerting/unified/mocks.ts | 9 +- .../unified/mocks/server/configure.ts | 9 +- .../alerting/unified/mocks/server/db.ts | 62 +++- .../__snapshots__/ruleGroups.test.ts.snap | 4 - .../CloneRuleEditor.test.tsx | 32 +- .../{ => rule-editor}/CloneRuleEditor.tsx | 23 +- .../{ => rule-editor}/ExistingRuleEditor.tsx | 15 +- .../unified/{ => rule-editor}/RuleEditor.tsx | 55 ++- .../RuleEditorCloudOnlyAllowed.test.tsx | 26 +- .../RuleEditorCloudRules.test.tsx | 16 +- .../RuleEditorExisting.test.tsx | 12 +- .../RuleEditorGrafanaRules.test.tsx | 8 +- .../RuleEditorRecordingRule.test.tsx | 14 +- .../RuleEditorCloudRules.test.tsx.snap | 0 .../RuleEditorGrafanaRules.test.tsx.snap | 0 .../RuleEditorRecordingRule.test.tsx.snap | 0 .../unified/rule-editor/formDefaults.test.ts | 196 ++++++++++ .../unified/rule-editor/formDefaults.ts | 157 ++++++++ .../unified/rule-editor/formProcessing.ts | 150 ++++++++ .../unified/rule-list/FilterView.test.tsx | 6 +- .../unified/rule-list/GroupedView.test.tsx | 6 +- .../alerting/unified/test/test-utils.ts | 14 + .../alerting/unified/utils/rule-form.test.ts | 35 +- .../alerting/unified/utils/rule-form.ts | 116 +----- public/test/helpers/alertingRuleEditor.tsx | 3 +- 50 files changed, 1203 insertions(+), 936 deletions(-) rename public/app/features/alerting/unified/{ => rule-editor}/CloneRuleEditor.test.tsx (93%) rename public/app/features/alerting/unified/{ => rule-editor}/CloneRuleEditor.tsx (72%) rename public/app/features/alerting/unified/{ => rule-editor}/ExistingRuleEditor.tsx (71%) rename public/app/features/alerting/unified/{ => rule-editor}/RuleEditor.tsx (69%) rename public/app/features/alerting/unified/{ => rule-editor}/RuleEditorCloudOnlyAllowed.test.tsx (88%) rename public/app/features/alerting/unified/{ => rule-editor}/RuleEditorCloudRules.test.tsx (85%) rename public/app/features/alerting/unified/{ => rule-editor}/RuleEditorExisting.test.tsx (95%) rename public/app/features/alerting/unified/{ => rule-editor}/RuleEditorGrafanaRules.test.tsx (94%) rename public/app/features/alerting/unified/{ => rule-editor}/RuleEditorRecordingRule.test.tsx (88%) rename public/app/features/alerting/unified/{ => rule-editor}/__snapshots__/RuleEditorCloudRules.test.tsx.snap (100%) rename public/app/features/alerting/unified/{ => rule-editor}/__snapshots__/RuleEditorGrafanaRules.test.tsx.snap (100%) rename public/app/features/alerting/unified/{ => rule-editor}/__snapshots__/RuleEditorRecordingRule.test.tsx.snap (100%) create mode 100644 public/app/features/alerting/unified/rule-editor/formDefaults.test.ts create mode 100644 public/app/features/alerting/unified/rule-editor/formDefaults.ts create mode 100644 public/app/features/alerting/unified/rule-editor/formProcessing.ts diff --git a/.betterer.results b/.betterer.results index 3d15c748b2e..c08d07d5459 100644 --- a/.betterer.results +++ b/.betterer.results @@ -1555,19 +1555,6 @@ exports[`better eslint`] = { [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "3"], [0, 0, 0, "No untranslated strings. Wrap text with ", "4"] ], - "public/app/features/alerting/unified/CloneRuleEditor.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/ExistingRuleEditor.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"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "3"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "4"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "5"] - ], "public/app/features/alerting/unified/GrafanaRuleQueryViewer.tsx:5381": [ [0, 0, 0, "\'@grafana/data/src/datetime/rangeutil\' import is restricted from being used by a pattern. Import from the public export instead.", "0"], [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "1"], @@ -1606,12 +1593,6 @@ exports[`better eslint`] = { [0, 0, 0, "No untranslated strings. Wrap text with ", "6"], [0, 0, 0, "No untranslated strings. Wrap text with ", "7"] ], - "public/app/features/alerting/unified/RuleEditor.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. Wrap text with ", "2"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "3"] - ], "public/app/features/alerting/unified/RuleViewer.tsx:5381": [ [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "0"] ], @@ -2563,7 +2544,8 @@ exports[`better eslint`] = { [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"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "10"] + [0, 0, 0, "No untranslated strings. Wrap text with ", "10"], + [0, 0, 0, "No untranslated strings. Wrap text with ", "11"] ], "public/app/features/alerting/unified/components/rules/Filter/RulesFilter.v1.tsx:5381": [ [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "0"], @@ -2925,6 +2907,25 @@ exports[`better eslint`] = { "public/app/features/alerting/unified/plugins/PluginOriginBadge.tsx:5381": [ [0, 0, 0, "\'@grafana/ui/src/components/Icon/utils\' import is restricted from being used by a pattern. Import from the public export instead.", "0"] ], + "public/app/features/alerting/unified/rule-editor/CloneRuleEditor.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/rule-editor/ExistingRuleEditor.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"], + [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "3"], + [0, 0, 0, "No untranslated strings. Wrap text with ", "4"], + [0, 0, 0, "No untranslated strings. Wrap text with ", "5"] + ], + "public/app/features/alerting/unified/rule-editor/RuleEditor.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. Wrap text with ", "2"], + [0, 0, 0, "No untranslated strings. Wrap text with ", "3"] + ], "public/app/features/alerting/unified/rule-list/FilterView.tsx:5381": [ [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "0"] ], diff --git a/public/app/features/alerting/routes.tsx b/public/app/features/alerting/routes.tsx index 839d5db06c4..490d264c5fa 100644 --- a/public/app/features/alerting/routes.tsx +++ b/public/app/features/alerting/routes.tsx @@ -214,7 +214,7 @@ export function getAlertingRoutes(cfg = config): RouteDescriptor[] { pageClass: 'page-alerting', roles: evaluateAccess([AccessControlAction.AlertingRuleCreate, AccessControlAction.AlertingRuleExternalWrite]), component: importAlertingComponent( - () => import(/* webpackChunkName: "AlertingRuleForm"*/ 'app/features/alerting/unified/RuleEditor') + () => import(/* webpackChunkName: "AlertingRuleForm"*/ 'app/features/alerting/unified/rule-editor/RuleEditor') ), }, { @@ -222,7 +222,7 @@ export function getAlertingRoutes(cfg = config): RouteDescriptor[] { pageClass: 'page-alerting', roles: evaluateAccess([AccessControlAction.AlertingRuleUpdate, AccessControlAction.AlertingRuleExternalWrite]), component: importAlertingComponent( - () => import(/* webpackChunkName: "AlertingRuleForm"*/ 'app/features/alerting/unified/RuleEditor') + () => import(/* webpackChunkName: "AlertingRuleForm"*/ 'app/features/alerting/unified/rule-editor/RuleEditor') ), }, { diff --git a/public/app/features/alerting/unified/api/alertRuleApi.ts b/public/app/features/alerting/unified/api/alertRuleApi.ts index f483df956e7..806b782a2bf 100644 --- a/public/app/features/alerting/unified/api/alertRuleApi.ts +++ b/public/app/features/alerting/unified/api/alertRuleApi.ts @@ -248,6 +248,7 @@ export const alertRuleApi = alertingApi.injectEndpoints({ const { path, params } = rulerUrlBuilder(rulerConfig).namespace(namespace); return { url: path, params }; }, + providesTags: (_result, _error, { namespace }) => [{ type: 'RuleNamespace', id: namespace }], }), // TODO This should be probably a separate ruler API file diff --git a/public/app/features/alerting/unified/components/export/GrafanaModifyExport.tsx b/public/app/features/alerting/unified/components/export/GrafanaModifyExport.tsx index 108532e5e41..5aca1d77824 100644 --- a/public/app/features/alerting/unified/components/export/GrafanaModifyExport.tsx +++ b/public/app/features/alerting/unified/components/export/GrafanaModifyExport.tsx @@ -7,8 +7,8 @@ import { Alert, LoadingPlaceholder } from '@grafana/ui'; import { RuleIdentifier } from '../../../../../types/unified-alerting'; import { useRuleWithLocation } from '../../hooks/useCombinedRule'; +import { formValuesFromExistingRule } from '../../rule-editor/formDefaults'; import { stringifyErrorLike } from '../../utils/misc'; -import { formValuesFromExistingRule } from '../../utils/rule-form'; import * as ruleId from '../../utils/rule-id'; import { isGrafanaRulerRule } from '../../utils/rules'; import { createRelativeUrl } from '../../utils/url'; diff --git a/public/app/features/alerting/unified/components/expressions/ExpressionStatusIndicator.test.tsx b/public/app/features/alerting/unified/components/expressions/ExpressionStatusIndicator.test.tsx index 3b76b141690..c82097c0df0 100644 --- a/public/app/features/alerting/unified/components/expressions/ExpressionStatusIndicator.test.tsx +++ b/public/app/features/alerting/unified/components/expressions/ExpressionStatusIndicator.test.tsx @@ -1,8 +1,8 @@ import { render, screen } from '@testing-library/react'; import { FormProvider, useForm } from 'react-hook-form'; +import { getDefaultFormValues } from '../../rule-editor/formDefaults'; import { RuleFormValues } from '../../types/rule-form'; -import { getDefaultFormValues } from '../../utils/rule-form'; import { ExpressionStatusIndicator } from './ExpressionStatusIndicator'; diff --git a/public/app/features/alerting/unified/components/rule-editor/AnnotationsStep.test.tsx b/public/app/features/alerting/unified/components/rule-editor/AnnotationsStep.test.tsx index df1ee304e11..9f1067761ca 100644 --- a/public/app/features/alerting/unified/components/rule-editor/AnnotationsStep.test.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/AnnotationsStep.test.tsx @@ -6,9 +6,9 @@ import { byRole, byTestId } from 'testing-library-selector'; import { DashboardSearchItemType } from '../../../../search/types'; import { mockDashboardApi, setupMswServer } from '../../mockApi'; import { mockDashboardDto, mockDashboardSearchItem } from '../../mocks'; +import { getDefaultFormValues } from '../../rule-editor/formDefaults'; import { RuleFormValues } from '../../types/rule-form'; import { Annotation } from '../../utils/constants'; -import { getDefaultFormValues } from '../../utils/rule-form'; import AnnotationsStep from './AnnotationsStep'; diff --git a/public/app/features/alerting/unified/components/rule-editor/GrafanaEvaluationBehavior.tsx b/public/app/features/alerting/unified/components/rule-editor/GrafanaEvaluationBehavior.tsx index 3a918eb6369..6104c3b2fa6 100644 --- a/public/app/features/alerting/unified/components/rule-editor/GrafanaEvaluationBehavior.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/GrafanaEvaluationBehavior.tsx @@ -1,12 +1,11 @@ import { css } from '@emotion/css'; -import { debounce, take, uniqueId } from 'lodash'; -import { useCallback, useEffect, useMemo, useState } from 'react'; +import { uniqueId } from 'lodash'; +import { useEffect, useMemo, useState } from 'react'; import { Controller, FormProvider, RegisterOptions, useForm, useFormContext } from 'react-hook-form'; import { GrafanaTheme2, SelectableValue } from '@grafana/data'; import { selectors } from '@grafana/e2e-selectors'; import { - AsyncSelect, Box, Button, Field, @@ -15,6 +14,7 @@ import { Input, Label, Modal, + Select, Stack, Switch, Text, @@ -22,17 +22,13 @@ import { useStyles2, } from '@grafana/ui'; import { Trans, t } from 'app/core/internationalization'; -import { CombinedRuleGroup, CombinedRuleNamespace } from 'app/types/unified-alerting'; import { RulerRuleGroupDTO, RulerRulesConfigDTO } from 'app/types/unified-alerting-dto'; -import { LogMessages, logInfo } from '../../Analytics'; import { alertRuleApi } from '../../api/alertRuleApi'; import { GRAFANA_RULER_CONFIG } from '../../api/featureDiscoveryApi'; -import { useCombinedRuleNamespaces } from '../../hooks/useCombinedRuleNamespaces'; -import { useUnifiedAlertingSelector } from '../../hooks/useUnifiedAlertingSelector'; +import { DEFAULT_GROUP_EVALUATION_INTERVAL } from '../../rule-editor/formDefaults'; import { RuleFormValues } from '../../types/rule-form'; import { GRAFANA_RULES_SOURCE_NAME } from '../../utils/datasource'; -import { DEFAULT_GROUP_EVALUATION_INTERVAL } from '../../utils/rule-form'; import { isGrafanaAlertingRuleByType, isGrafanaManagedRuleByType, @@ -53,48 +49,38 @@ import { RuleEditorSection } from './RuleEditorSection'; export const MIN_TIME_RANGE_STEP_S = 10; // 10 seconds export const MAX_GROUP_RESULTS = 1000; -export const useFolderGroupOptions = (folderUid: string, enableProvisionedGroups: boolean) => { +const useFetchGroupsForFolder = (folderUid: string) => { // fetch the ruler rules from the database so we can figure out what other "groups" are already defined // for our folders - const { isLoading: isLoadingRulerNamespace, currentData: rulerNamespace } = - alertRuleApi.endpoints.rulerNamespace.useQuery( - { - namespace: folderUid, - rulerConfig: GRAFANA_RULER_CONFIG, - }, - { - skip: !folderUid, - refetchOnMountOrArgChange: true, - } - ); - - // There should be only one entry in the rulerNamespace object - // However it uses folder name as key, so to avoid fetching folder name, we use Object.values - const groupOptions = useMemo(() => { - if (!rulerNamespace) { - // still waiting for namespace information to be fetched - return []; + return alertRuleApi.endpoints.rulerNamespace.useQuery( + { + namespace: folderUid, + rulerConfig: GRAFANA_RULER_CONFIG, + }, + { + refetchOnMountOrArgChange: true, + skip: !folderUid, } + ); +}; - const folderGroups = Object.values(rulerNamespace).flat() ?? []; +const namespaceToGroupOptions = (rulerNamespace: RulerRulesConfigDTO, enableProvisionedGroups: boolean) => { + const folderGroups = Object.values(rulerNamespace).flat(); - return folderGroups - .map>((group) => { - const isProvisioned = isProvisionedGroup(group); - return { - label: group.name, - value: group.name, - description: group.interval ?? DEFAULT_GROUP_EVALUATION_INTERVAL, - // we include provisioned folders, but disable the option to select them - isDisabled: !enableProvisionedGroups ? isProvisioned : false, - isProvisioned: isProvisioned, - }; - }) + return folderGroups + .map>((group) => { + const isProvisioned = isProvisionedGroup(group); + return { + label: group.name, + value: group.name, + description: group.interval ?? DEFAULT_GROUP_EVALUATION_INTERVAL, + // we include provisioned folders, but disable the option to select them + isDisabled: !enableProvisionedGroups ? isProvisioned : false, + isProvisioned: isProvisioned, + }; + }) - .sort(sortByLabel); - }, [rulerNamespace, enableProvisionedGroups]); - - return { groupOptions, loading: isLoadingRulerNamespace }; + .sort(sortByLabel); }; const isProvisionedGroup = (group: RulerRuleGroupDTO) => { @@ -105,10 +91,6 @@ const sortByLabel = (a: SelectableValue, b: SelectableValue) => return a.label?.localeCompare(b.label ?? '') || 0; }; -const findGroupMatchingLabel = (group: SelectableValue, query: string) => { - return group.label?.toLowerCase().includes(query.toLowerCase()); -}; - const forValidationOptions = (evaluateEvery: string): RegisterOptions<{ evaluateFor: string }> => ({ required: { value: true, @@ -149,24 +131,10 @@ const forValidationOptions = (evaluateEvery: string): RegisterOptions<{ evaluate }, }); -const useIsNewGroup = (folder: string, group: string) => { - const { groupOptions } = useFolderGroupOptions(folder, false); - - const groupIsInGroupOptions = useCallback( - (group_: string) => groupOptions.some((groupInList: SelectableValue) => groupInList.label === group_), - [groupOptions] - ); - return !groupIsInGroupOptions(group); -}; - export function GrafanaEvaluationBehaviorStep({ - evaluateEvery, - setEvaluateEvery, existing, enableProvisionedGroups, }: { - evaluateEvery: string; - setEvaluateEvery: (value: string) => void; existing: boolean; enableProvisionedGroups: boolean; }) { @@ -181,51 +149,39 @@ export function GrafanaEvaluationBehaviorStep({ control, } = useFormContext(); - const [folder, group, type, isPaused, folderUid, folderName] = watch([ - 'folder', + const [group, type, isPaused, folder, evaluateEvery] = watch([ 'group', 'type', 'isPaused', - 'folder.uid', - 'folder.title', + 'folder', + 'evaluateEvery', ]); const isGrafanaAlertingRule = isGrafanaAlertingRuleByType(type); const isGrafanaRecordingRule = isGrafanaRecordingRuleByType(type); - const { groupOptions, loading } = useFolderGroupOptions(folder?.uid ?? '', enableProvisionedGroups); + const { currentData: rulerNamespace, isLoading: loadingGroups } = useFetchGroupsForFolder(folder?.uid ?? ''); const [isEditingGroup, setIsEditingGroup] = useState(false); - const rulerRuleRequests = useUnifiedAlertingSelector((state) => state.rulerRules); - const groupfoldersForGrafana = rulerRuleRequests[GRAFANA_RULES_SOURCE_NAME]; + const groupOptions = useMemo(() => { + return rulerNamespace ? namespaceToGroupOptions(rulerNamespace, enableProvisionedGroups) : []; + }, [enableProvisionedGroups, rulerNamespace]); - const grafanaNamespaces = useCombinedRuleNamespaces(GRAFANA_RULES_SOURCE_NAME); - const existingNamespace = grafanaNamespaces.find((ns) => ns.uid === folderUid); - const existingGroup = existingNamespace?.groups.find((g) => g.name === group); - - const isNewGroup = useIsNewGroup(folderUid ?? '', group); + const existingGroup = Object.values(rulerNamespace ?? {}) + .flat() + .find((ruleGroup) => ruleGroup.name === group); + const isNewGroup = !existingGroup && !loadingGroups; + // synchronize the evaluation interval with the group name when it's an existing group useEffect(() => { - if (!isNewGroup && existingGroup?.interval) { - setEvaluateEvery(existingGroup.interval); + if (existingGroup) { + setValue('evaluateEvery', existingGroup.interval ?? DEFAULT_GROUP_EVALUATION_INTERVAL); } - }, [setEvaluateEvery, isNewGroup, setValue, existingGroup]); - - const closeEditGroupModal = (saved = false) => { - if (!saved) { - logInfo(LogMessages.leavingRuleGroupEdit); - } - setIsEditingGroup(false); - }; + }, [existingGroup, setValue]); + const closeEditGroupModal = () => setIsEditingGroup(false); const onOpenEditGroupModal = () => setIsEditingGroup(true); - const editGroupDisabled = groupfoldersForGrafana?.loading || isNewGroup || !folderUid || !group; - const emptyNamespace: CombinedRuleNamespace = { - name: folderName, - rulesSource: GRAFANA_RULES_SOURCE_NAME, - groups: [], - }; - const emptyGroup: CombinedRuleGroup = { name: group, interval: evaluateEvery, rules: [], totals: {} }; + const editGroupDisabled = loadingGroups || isNewGroup || !folder?.uid || !group; const [isCreatingEvaluationGroup, setIsCreatingEvaluationGroup] = useState(false); @@ -235,18 +191,6 @@ export function GrafanaEvaluationBehaviorStep({ setIsCreatingEvaluationGroup(false); }; - const getOptions = useCallback( - async (query: string) => { - const results = query ? groupOptions.filter((group) => findGroupMatchingLabel(group, query)) : groupOptions; - return take(results, MAX_GROUP_RESULTS); - }, - [groupOptions] - ); - - const debouncedSearch = useMemo(() => { - return debounce(getOptions, 300, { leading: true }); - }, [getOptions]); - const defaultGroupValue = group ? { value: group, label: group } : undefined; const pauseContentText = isGrafanaRecordingRule @@ -257,7 +201,7 @@ export function GrafanaEvaluationBehaviorStep({ const step = isGrafanaManagedRuleByType(type) ? 4 : 3; const label = - isGrafanaManagedRuleByType(type) && !folder + isGrafanaManagedRuleByType(type) && !folder?.uid ? t( 'alerting.rule-form.evaluation.select-folder-before', 'Select a folder before setting evaluation group and interval' @@ -284,21 +228,20 @@ export function GrafanaEvaluationBehaviorStep({ > ( - { field.onChange(group.label ?? ''); }} - isLoading={loading} - invalid={Boolean(folder) && !group && Boolean(fieldState.error)} - loadOptions={debouncedSearch} + isLoading={loadingGroups} + invalid={Boolean(folder?.uid) && !group && Boolean(fieldState.error)} cacheOptions loadingMessage={'Loading groups...'} defaultValue={defaultGroupValue} - defaultOptions={groupOptions} + options={groupOptions} getOptionLabel={(option: SelectableValue) => (
{option.label} @@ -329,7 +272,7 @@ export function GrafanaEvaluationBehaviorStep({ icon="plus" fill="outline" variant="secondary" - disabled={!folder} + disabled={!folder?.uid} data-testid={selectors.components.AlertRules.newEvaluationGroupButton} > New evaluation group @@ -339,22 +282,25 @@ export function GrafanaEvaluationBehaviorStep({ setIsCreatingEvaluationGroup(false)} - groupfoldersForGrafana={groupfoldersForGrafana?.result} + groupfoldersForGrafana={rulerNamespace} /> )} - {folderName && isEditingGroup && ( + {folder?.uid && isEditingGroup && ( closeEditGroupModal()} intervalEditOnly hideFolder={true} /> )} - {folderName && group && ( + {folder?.title && group && (
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 54d22db90ac..3d46e0873e7 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 @@ -1,5 +1,5 @@ import { css } from '@emotion/css'; -import { useEffect, useMemo, useState } from 'react'; +import { useMemo, useState } from 'react'; import { FormProvider, SubmitErrorHandler, UseFormWatch, useForm } from 'react-hook-form'; import { useParams } from 'react-router-dom-v5-compat'; @@ -22,7 +22,6 @@ import { isGrafanaRulerRulePaused, isRecordingRuleByType, } from 'app/features/alerting/unified/utils/rules'; -import { isExpressionQuery } from 'app/features/expressions/guards'; import { RuleGroupIdentifier, RuleIdentifier, RuleWithLocation } from 'app/types/unified-alerting'; import { PostableRuleGrafanaRuleDTO, RulerRuleDTO } from 'app/types/unified-alerting-dto'; @@ -40,24 +39,21 @@ import { shouldUsePrometheusRulesPrimary } from '../../../featureToggles'; import { useDeleteRuleFromGroup } from '../../../hooks/ruleGroup/useDeleteRuleFromGroup'; import { useAddRuleToRuleGroup, useUpdateRuleInRuleGroup } from '../../../hooks/ruleGroup/useUpsertRuleFromRuleGroup'; import { useReturnTo } from '../../../hooks/useReturnTo'; -import { useURLSearchParams } from '../../../hooks/useURLSearchParams'; -import { RuleFormType, RuleFormValues } from '../../../types/rule-form'; -import { DataSourceType } from '../../../utils/datasource'; import { - DEFAULT_GROUP_EVALUATION_INTERVAL, + defaultFormValuesForRuleType, + formValuesFromExistingRule, + formValuesFromPrefill, + translateRouteParamToRuleType, +} from '../../../rule-editor/formDefaults'; +import { RuleFormType, RuleFormValues } from '../../../types/rule-form'; +import { MANUAL_ROUTING_KEY, SIMPLIFIED_QUERY_EDITOR_KEY, - formValuesFromExistingRule, formValuesToRulerGrafanaRuleDTO, formValuesToRulerRuleDTO, - getDefaultFormValues, - getDefaultQueries, - ignoreHiddenQueries, - normalizeDefaultAnnotations, } from '../../../utils/rule-form'; import * as ruleId from '../../../utils/rule-id'; import { fromRulerRule, fromRulerRuleAndRuleGroupIdentifier, stringifyIdentifier } from '../../../utils/rule-id'; -import { isGrafanaRecordingRuleByType } from '../../../utils/rules'; import { createRelativeUrl } from '../../../utils/url'; import { GrafanaRuleExporter } from '../../export/GrafanaRuleExporter'; import { AlertRuleNameAndMetric } from '../AlertRuleNameInput'; @@ -68,12 +64,7 @@ import { GrafanaFolderAndLabelsStep } from '../GrafanaFolderAndLabelsStep'; import { NotificationsStep } from '../NotificationsStep'; import { RecordingRulesNameSpaceAndGroupStep } from '../RecordingRulesNameSpaceAndGroupStep'; import { RuleInspector } from '../RuleInspector'; -import { - QueryAndExpressionsStep, - areQueriesTransformableToSimpleCondition, - isExpressionQueryInAlert, -} from '../query-and-alert-condition/QueryAndExpressionsStep'; -import { translateRouteParamToRuleType } from '../util'; +import { QueryAndExpressionsStep } from '../query-and-alert-condition/QueryAndExpressionsStep'; type Props = { existing?: RuleWithLocation; @@ -85,9 +76,7 @@ const prometheusRulesPrimary = shouldUsePrometheusRulesPrimary(); export const AlertRuleForm = ({ existing, prefill }: Props) => { const styles = useStyles2(getStyles); const notifyApp = useAppNotification(); - const [queryParams] = useURLSearchParams(); const [showEditYaml, setShowEditYaml] = useState(false); - const [evaluateEvery, setEvaluateEvery] = useState(existing?.group.interval ?? DEFAULT_GROUP_EVALUATION_INTERVAL); const [deleteRuleFromGroup] = useDeleteRuleFromGroup(); const [addRuleToRuleGroup] = useAddRuleToRuleGroup(); @@ -110,19 +99,10 @@ export const AlertRuleForm = ({ existing, prefill }: Props) => { return formValuesFromPrefill(prefill); } - if (queryParams.has('defaults')) { - return formValuesFromQueryParams(queryParams.get('defaults') ?? '', ruleType); - } const defaultRuleType = ruleType || RuleFormType.grafana; - return { - ...getDefaultFormValues(), - condition: 'C', - queries: getDefaultQueries(isGrafanaRecordingRuleByType(defaultRuleType)), - type: defaultRuleType, - evaluateEvery: evaluateEvery, - }; - }, [existing, prefill, queryParams, evaluateEvery, ruleType]); + return defaultFormValuesForRuleType(defaultRuleType); + }, [existing, prefill, ruleType]); const formAPI = useForm({ mode: 'onSubmit', @@ -151,6 +131,8 @@ export const AlertRuleForm = ({ existing, prefill }: Props) => { // @todo why is error not propagated to form? const submit = async (values: RuleFormValues, exitOnSave: boolean) => { + const { type, evaluateEvery } = values; + if (conditionErrorMsg !== '') { notifyApp.error(conditionErrorMsg); if (!existing && grafanaTypeRule) { @@ -160,7 +142,7 @@ export const AlertRuleForm = ({ existing, prefill }: Props) => { return; } - trackAlertRuleFormSaved({ formAction: existing ? 'update' : 'create', ruleType: values.type }); + trackAlertRuleFormSaved({ formAction: existing ? 'update' : 'create', ruleType: type }); const ruleDefinition = grafanaTypeRule ? formValuesToRulerGrafanaRuleDTO(values) : formValuesToRulerRuleDTO(values); @@ -206,12 +188,11 @@ export const AlertRuleForm = ({ existing, prefill }: Props) => { const deleteRule = async () => { if (existing) { - const returnTo = queryParams.get('returnTo') || '/alerting/list'; const ruleGroupIdentifier = getRuleGroupLocationFromRuleWithLocation(existing); const ruleIdentifier = fromRulerRuleAndRuleGroupIdentifier(ruleGroupIdentifier, existing.rule); await deleteRuleFromGroup.execute(ruleGroupIdentifier, ruleIdentifier); - locationService.replace(returnTo); + locationService.replace(returnTo ?? '/alerting/list'); } }; @@ -236,9 +217,6 @@ export const AlertRuleForm = ({ existing, prefill }: Props) => { locationService.getHistory().goBack(); }; - const evaluateEveryInForm = watch('evaluateEvery'); - useEffect(() => setEvaluateEvery(evaluateEveryInForm), [evaluateEveryInForm]); - const actionButtons = ( {existing && ( @@ -314,12 +292,7 @@ export const AlertRuleForm = ({ existing, prefill }: Props) => { {/* Step 4 & 5 & 6*/} {isGrafanaManagedRuleByType(type) && ( - + )} {/* Notifications step*/} @@ -379,101 +352,17 @@ const isCortexLokiOrRecordingRule = (watch: UseFormWatch) => { return (ruleType === RuleFormType.cloudAlerting || ruleType === RuleFormType.cloudRecording) && dataSourceName !== ''; }; -function formValuesFromQueryParams(ruleDefinition: string, type: RuleFormType): RuleFormValues { - let ruleFromQueryParams: Partial; - - try { - ruleFromQueryParams = JSON.parse(ruleDefinition); - } catch (err) { - return { - ...getDefaultFormValues(), - queries: getDefaultQueries(), - }; - } - - return setQueryEditorSettings( - setInstantOrRange( - ignoreHiddenQueries({ - ...getDefaultFormValues(), - ...ruleFromQueryParams, - annotations: normalizeDefaultAnnotations(ruleFromQueryParams.annotations ?? []), - queries: ruleFromQueryParams.queries ?? getDefaultQueries(), - type: type || RuleFormType.grafana, - evaluateEvery: DEFAULT_GROUP_EVALUATION_INTERVAL, - }) - ) - ); -} - -function formValuesFromPrefill(rule: Partial): RuleFormValues { - return ignoreHiddenQueries({ - ...getDefaultFormValues(), - ...rule, - }); -} - -function setQueryEditorSettings(values: RuleFormValues): RuleFormValues { - const isQuerySwitchModeEnabled = config.featureToggles.alertingQueryAndExpressionsStepMode ?? false; - - if (!isQuerySwitchModeEnabled) { - return { - ...values, - editorSettings: { - simplifiedQueryEditor: false, - simplifiedNotificationEditor: true, // actually it doesn't matter in this case - }, - }; - } - - // data queries only - const dataQueries = values.queries.filter((query) => !isExpressionQuery(query.model)); - - // expression queries only - const expressionQueries = values.queries.filter((query) => isExpressionQueryInAlert(query)); - - const queryParamsAreTransformable = areQueriesTransformableToSimpleCondition(dataQueries, expressionQueries); - return { - ...values, - editorSettings: { - simplifiedQueryEditor: queryParamsAreTransformable, - simplifiedNotificationEditor: true, - }, - }; -} - -function setInstantOrRange(values: RuleFormValues): RuleFormValues { - return { - ...values, - queries: values.queries?.map((query) => { - if (isExpressionQuery(query.model)) { - return query; - } - // data query - const defaultToInstant = - query.model.datasource?.type === DataSourceType.Loki || - query.model.datasource?.type === DataSourceType.Prometheus; - const isInstant = - 'instant' in query.model && query.model.instant !== undefined ? query.model.instant : defaultToInstant; - return { - ...query, - model: { - ...query.model, - instant: isInstant, - range: !isInstant, // we cannot have both instant and range queries in alerting - }, - }; - }), - }; -} - function storeInLocalStorageValues(values: RuleFormValues) { - if (values.manualRouting) { + const { manualRouting, editorSettings } = values; + + if (manualRouting) { localStorage.setItem(MANUAL_ROUTING_KEY, 'true'); } else { localStorage.setItem(MANUAL_ROUTING_KEY, 'false'); } - if (values.editorSettings) { - if (values.editorSettings.simplifiedQueryEditor) { + + if (editorSettings) { + if (editorSettings.simplifiedQueryEditor) { localStorage.setItem(SIMPLIFIED_QUERY_EDITOR_KEY, 'true'); } else { localStorage.setItem(SIMPLIFIED_QUERY_EDITOR_KEY, 'false'); diff --git a/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/ModifyExportRuleForm.tsx b/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/ModifyExportRuleForm.tsx index 38598f08784..72093ecc490 100644 --- a/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/ModifyExportRuleForm.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/ModifyExportRuleForm.tsx @@ -16,14 +16,10 @@ import { alertRuleApi } from '../../../api/alertRuleApi'; import { fetchRulerRulesGroup } from '../../../api/ruler'; import { useDataSourceFeatures } from '../../../hooks/useCombinedRule'; import { useReturnTo } from '../../../hooks/useReturnTo'; +import { DEFAULT_GROUP_EVALUATION_INTERVAL, getDefaultFormValues } from '../../../rule-editor/formDefaults'; import { RuleFormType, RuleFormValues } from '../../../types/rule-form'; import { GRAFANA_RULES_SOURCE_NAME } from '../../../utils/datasource'; -import { - DEFAULT_GROUP_EVALUATION_INTERVAL, - formValuesToRulerGrafanaRuleDTO, - getDefaultFormValues, - getDefaultQueries, -} from '../../../utils/rule-form'; +import { formValuesToRulerGrafanaRuleDTO, getDefaultQueries } from '../../../utils/rule-form'; import { isGrafanaRulerRule } from '../../../utils/rules'; import { FileExportPreview } from '../../export/FileExportPreview'; import { GrafanaExportDrawer } from '../../export/GrafanaExportDrawer'; @@ -64,9 +60,7 @@ export function ModifyExportRuleForm({ ruleForm, alertUid }: ModifyExportRuleFor const { returnTo } = useReturnTo('/alerting/list'); const [exportData, setExportData] = useState(undefined); - const [conditionErrorMsg, setConditionErrorMsg] = useState(''); - const [evaluateEvery, setEvaluateEvery] = useState(ruleForm?.evaluateEvery ?? DEFAULT_GROUP_EVALUATION_INTERVAL); const onInvalid = (): void => { notifyApp.error('There are errors in the form. Please correct them and try again!'); @@ -112,12 +106,7 @@ export function ModifyExportRuleForm({ ruleForm, alertUid }: ModifyExportRuleFor {/* Step 4 & 5 */} - + {/* Notifications step*/} {/* Annotations only for cloud and Grafana */} diff --git a/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/getPayloadToExport.test.ts b/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/getPayloadToExport.test.ts index 6fc5ba37a35..2a1e67855e3 100644 --- a/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/getPayloadToExport.test.ts +++ b/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/getPayloadToExport.test.ts @@ -1,9 +1,9 @@ import { RulerRuleDTO, RulerRuleGroupDTO } from 'app/types/unified-alerting-dto'; import { mockRulerGrafanaRecordingRule, mockRulerGrafanaRule } from '../../../mocks'; +import { getDefaultFormValues } from '../../../rule-editor/formDefaults'; import { RuleFormType, RuleFormValues } from '../../../types/rule-form'; import { Annotation } from '../../../utils/constants'; -import { getDefaultFormValues } from '../../../utils/rule-form'; import { getPayloadToExport } from './ModifyExportRuleForm'; diff --git a/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/simplifiedRouting/SimplifiedRuleEditor.test.tsx b/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/simplifiedRouting/SimplifiedRuleEditor.test.tsx index 11b3a0f5dfc..116374475da 100644 --- a/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/simplifiedRouting/SimplifiedRuleEditor.test.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/simplifiedRouting/SimplifiedRuleEditor.test.tsx @@ -106,8 +106,10 @@ describe('Can create a new grafana managed alert using simplified routing', () = it('simplified routing is not available when Grafana AM is not enabled', async () => { setAlertmanagerChoices(AlertmanagerChoice.External, 1); - renderRuleEditor(); + const { user } = renderRuleEditor(); + // Just to make sure all dropdowns have been loaded + await selectFolderAndGroup(user); await waitFor(() => expect(ui.inputs.simplifiedRouting.contactPointRouting.query()).not.toBeInTheDocument()); }); @@ -147,6 +149,7 @@ describe('Can create a new grafana managed alert using simplified routing', () = expect(await screen.findByText('Email')).toBeInTheDocument(); }); }); + describe('switch modes enabled', () => { testWithFeatureToggles(['alertingQueryAndExpressionsStepMode', 'alertingNotificationsStepMode']); @@ -168,6 +171,7 @@ describe('Can create a new grafana managed alert using simplified routing', () = const serializedRequests = await serializeRequests(requests); expect(serializedRequests).toMatchSnapshot(); }); + it('can create the new grafana-managed rule with advanced modes', async () => { const capture = captureRequests((r) => r.method === 'POST' && r.url.includes('/api/ruler/')); @@ -185,6 +189,7 @@ describe('Can create a new grafana managed alert using simplified routing', () = const serializedRequests = await serializeRequests(requests); expect(serializedRequests).toMatchSnapshot(); }); + it('can create the new grafana-managed rule with only notifications step advanced mode', async () => { const capture = captureRequests((r) => r.method === 'POST' && r.url.includes('/api/ruler/')); @@ -202,6 +207,7 @@ describe('Can create a new grafana managed alert using simplified routing', () = const serializedRequests = await serializeRequests(requests); expect(serializedRequests).toMatchSnapshot(); }); + it('can create the new grafana-managed rule with only query step advanced mode', async () => { const contactPointName = 'lotsa-emails'; const capture = captureRequests((r) => r.method === 'POST' && r.url.includes('/api/ruler/')); @@ -221,6 +227,7 @@ describe('Can create a new grafana managed alert using simplified routing', () = const serializedRequests = await serializeRequests(requests); expect(serializedRequests).toMatchSnapshot(); }); + it('switch modes are intiallized depending on the local storage - 1', async () => { localStorage.setItem(SIMPLIFIED_QUERY_EDITOR_KEY, 'false'); localStorage.setItem(MANUAL_ROUTING_KEY, 'true'); @@ -231,6 +238,7 @@ describe('Can create a new grafana managed alert using simplified routing', () = expect(ui.inputs.switchModeAdvanced(GrafanaRuleFormStep.Query).get()).toBeInTheDocument(); expect(ui.inputs.switchModeBasic(GrafanaRuleFormStep.Notification).get()).toBeInTheDocument(); }); + it('switch modes are intiallized depending on the local storage - 2', async () => { localStorage.setItem(SIMPLIFIED_QUERY_EDITOR_KEY, 'true'); localStorage.setItem(MANUAL_ROUTING_KEY, 'false'); diff --git a/public/app/features/alerting/unified/components/rule-editor/labels/LabelsField.test.tsx b/public/app/features/alerting/unified/components/rule-editor/labels/LabelsField.test.tsx index 752d9e71101..a0983cba07f 100644 --- a/public/app/features/alerting/unified/components/rule-editor/labels/LabelsField.test.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/labels/LabelsField.test.tsx @@ -136,6 +136,7 @@ describe('LabelsField with suggestions', () => { expect(screen.getByTestId('labelsInSubform-key-2')).toHaveTextContent('key3'); expect(screen.getByTestId('labelsInSubform-value-2')).toHaveTextContent('value3'); }); + it('Should be able to write new keys and values using the dropdowns, case sensitive', async () => { const { user } = await renderLabelsWithSuggestions(); diff --git a/public/app/features/alerting/unified/components/rule-editor/query-and-alert-condition/QueryAndExpressionsStep.tsx b/public/app/features/alerting/unified/components/rule-editor/query-and-alert-condition/QueryAndExpressionsStep.tsx index f43e3a14b67..2ecd8131b3b 100644 --- a/public/app/features/alerting/unified/components/rule-editor/query-and-alert-condition/QueryAndExpressionsStep.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/query-and-alert-condition/QueryAndExpressionsStep.tsx @@ -23,16 +23,14 @@ import { import { Text } from '@grafana/ui/src/components/Text/Text'; import { Trans, t } from 'app/core/internationalization'; import { isExpressionQuery } from 'app/features/expressions/guards'; -import { - ExpressionDatasourceUID, - ExpressionQuery, - ExpressionQueryType, - ReducerMode, - expressionTypes, -} from 'app/features/expressions/types'; -import { AlertDataQuery, AlertQuery } from 'app/types/unified-alerting-dto'; +import { ExpressionDatasourceUID, ExpressionQueryType, expressionTypes } from 'app/features/expressions/types'; +import { AlertQuery } from 'app/types/unified-alerting-dto'; import { useRulesSourcesWithRuler } from '../../../hooks/useRuleSourcesWithRuler'; +import { + areQueriesTransformableToSimpleCondition, + isExpressionQueryInAlert, +} from '../../../rule-editor/formProcessing'; import { RuleFormType, RuleFormValues } from '../../../types/rule-form'; import { getDefaultOrFirstCompatibleDataSource } from '../../../utils/datasource'; import { PromOrLokiQuery, isPromOrLokiQuery } from '../../../utils/rule-form'; @@ -76,49 +74,6 @@ import { import { useAdvancedMode } from './useAdvancedMode'; import { useAlertQueryRunner } from './useAlertQueryRunner'; -export function areQueriesTransformableToSimpleCondition( - dataQueries: Array>, - expressionQueries: Array> -) { - if (dataQueries.length !== 1) { - return false; - } - const singleReduceExpressionInInstantQuery = - 'instant' in dataQueries[0].model && dataQueries[0].model.instant && expressionQueries.length === 1; - - if (expressionQueries.length !== 2 && !singleReduceExpressionInInstantQuery) { - return false; - } - - const query = dataQueries[0]; - - if (query.refId !== SimpleConditionIdentifier.queryId) { - return false; - } - - const reduceExpressionIndex = expressionQueries.findIndex( - (query) => query.model.type === ExpressionQueryType.reduce && query.refId === SimpleConditionIdentifier.reducerId - ); - const reduceExpression = expressionQueries.at(reduceExpressionIndex); - const reduceOk = - reduceExpression && - reduceExpressionIndex === 0 && - (reduceExpression.model.settings?.mode === ReducerMode.Strict || - reduceExpression.model.settings?.mode === undefined); - - const thresholdExpressionIndex = expressionQueries.findIndex( - (query) => - query.model.type === ExpressionQueryType.threshold && query.refId === SimpleConditionIdentifier.thresholdId - ); - const thresholdExpression = expressionQueries.at(thresholdExpressionIndex); - const conditions = thresholdExpression?.model.conditions ?? []; - const thresholdIndexOk = singleReduceExpressionInInstantQuery - ? thresholdExpressionIndex === 0 - : thresholdExpressionIndex === 1; - const thresholdOk = thresholdExpression && thresholdIndexOk && conditions[0]?.unloadEvaluator === undefined; - return (Boolean(reduceOk) || Boolean(singleReduceExpressionInInstantQuery)) && Boolean(thresholdOk); -} - interface Props { editingExistingRule: boolean; onDataChange: (error: string) => void; @@ -777,9 +732,3 @@ const useSetExpressionAndDataSource = () => { } }; }; - -export function isExpressionQueryInAlert( - query: AlertQuery -): query is AlertQuery { - return isExpressionQuery(query.model); -} diff --git a/public/app/features/alerting/unified/components/rule-editor/query-and-alert-condition/__snapshots__/areQueriesTransformableToSimpleCondition.test.ts b/public/app/features/alerting/unified/components/rule-editor/query-and-alert-condition/__snapshots__/areQueriesTransformableToSimpleCondition.test.ts index 208342735e6..2a9294ada7d 100644 --- a/public/app/features/alerting/unified/components/rule-editor/query-and-alert-condition/__snapshots__/areQueriesTransformableToSimpleCondition.test.ts +++ b/public/app/features/alerting/unified/components/rule-editor/query-and-alert-condition/__snapshots__/areQueriesTransformableToSimpleCondition.test.ts @@ -2,11 +2,10 @@ import { produce } from 'immer'; import { EvalFunction } from 'app/features/alerting/state/alertDef'; import { dataQuery, reduceExpression, thresholdExpression } from 'app/features/alerting/unified/mocks'; +import { areQueriesTransformableToSimpleCondition } from 'app/features/alerting/unified/rule-editor/formProcessing'; import { ExpressionQuery, ReducerMode } from 'app/features/expressions/types'; import { AlertDataQuery, AlertQuery } from 'app/types/unified-alerting-dto'; -import { areQueriesTransformableToSimpleCondition } from '../QueryAndExpressionsStep'; - const expressionQueries: Array> = [reduceExpression, thresholdExpression]; describe('areQueriesTransformableToSimpleCondition', () => { diff --git a/public/app/features/alerting/unified/components/rule-editor/query-and-alert-condition/useAdvancedMode.ts b/public/app/features/alerting/unified/components/rule-editor/query-and-alert-condition/useAdvancedMode.ts index 0c793dfdaac..08e9d889bbf 100644 --- a/public/app/features/alerting/unified/components/rule-editor/query-and-alert-condition/useAdvancedMode.ts +++ b/public/app/features/alerting/unified/components/rule-editor/query-and-alert-condition/useAdvancedMode.ts @@ -5,7 +5,8 @@ import { EvalFunction } from 'app/features/alerting/state/alertDef'; import { ExpressionQuery } from 'app/features/expressions/types'; import { AlertDataQuery, AlertQuery } from 'app/types/unified-alerting-dto'; -import { areQueriesTransformableToSimpleCondition } from './QueryAndExpressionsStep'; +import { areQueriesTransformableToSimpleCondition } from '../../../rule-editor/formProcessing'; + import { SimpleCondition, getSimpleConditionFromExpressions } from './SimpleCondition'; function initializeSimpleCondition( @@ -30,7 +31,7 @@ export function determineAdvancedMode(simplifiedQueryEditor: boolean | undefined } /* - This hook is used mantain the state of the advanced mode, and the simple condition, + This hook is used mantain the state of the advanced mode, and the simple condition, depending on the editor settings, the alert type, and the queries. */ export const useAdvancedMode = ( diff --git a/public/app/features/alerting/unified/components/rule-editor/util.ts b/public/app/features/alerting/unified/components/rule-editor/util.ts index 1c432344b1c..3933de4f48e 100644 --- a/public/app/features/alerting/unified/components/rule-editor/util.ts +++ b/public/app/features/alerting/unified/components/rule-editor/util.ts @@ -15,8 +15,6 @@ import { isExpressionQuery } from 'app/features/expressions/guards'; import { ClassicCondition, ExpressionQueryType } from 'app/features/expressions/types'; import { AlertQuery } from 'app/types/unified-alerting-dto'; -import { RuleFormType } from '../../types/rule-form'; - import { createDagFromQueries, getOriginOfRefId } from './dag'; export function queriesWithUpdatedReferences( @@ -312,18 +310,6 @@ export function getStatusMessage(data: PanelData): string | undefined { return data.error?.message ?? genericErrorMessage; } -export function translateRouteParamToRuleType(param = ''): RuleFormType { - if (param === 'recording') { - return RuleFormType.cloudRecording; - } - - if (param === 'grafana-recording') { - return RuleFormType.grafanaRecording; - } - - return RuleFormType.grafana; -} - /** * This function finds what refIds have been updated given the previous Array of queries and an Array of updated data queries. * All expression queries are discarded from the arrays, since we have separate handlers for those (see "onUpdateRefId") of the ExpressionEditor diff --git a/public/app/features/alerting/unified/components/rule-viewer/RuleViewer.test.tsx b/public/app/features/alerting/unified/components/rule-viewer/RuleViewer.test.tsx index be7153bc131..7f41a873797 100644 --- a/public/app/features/alerting/unified/components/rule-viewer/RuleViewer.test.tsx +++ b/public/app/features/alerting/unified/components/rule-viewer/RuleViewer.test.tsx @@ -4,7 +4,6 @@ import { byRole, byText } from 'testing-library-selector'; import { setPluginLinksHook } from '@grafana/runtime'; import { setupMswServer } from 'app/features/alerting/unified/mockApi'; -import { setFolderAccessControl } from 'app/features/alerting/unified/mocks/server/configure'; import { AlertManagerDataSourceJsonData } from 'app/plugins/datasource/alertmanager/types'; import { AccessControlAction } from 'app/types'; import { CombinedRule, RuleIdentifier } from 'app/types/unified-alerting'; @@ -20,6 +19,7 @@ import { mockPromAlertingRule, } from '../../mocks'; import { grafanaRulerRule } from '../../mocks/grafanaRulerApi'; +import { grantPermissionsHelper } from '../../test/test-utils'; import { setupDataSources } from '../../testSetup/datasources'; import { Annotation } from '../../utils/constants'; import { DataSourceType } from '../../utils/datasource'; @@ -76,16 +76,6 @@ setPluginLinksHook(() => ({ isLoading: false, })); -/** - * "Grants" permissions via contextSrv mock, and additionally sets folder access control - * API response to match - */ -const grantPermissionsHelper = (permissions: AccessControlAction[]) => { - const permissionsHash = permissions.reduce((hash, permission) => ({ ...hash, [permission]: true }), {}); - grantUserPermissions(permissions); - setFolderAccessControl(permissionsHash); -}; - const openSilenceDrawer = async () => { const user = userEvent.setup(); await user.click(ELEMENTS.actions.more.button.get()); diff --git a/public/app/features/alerting/unified/components/rules/EditRuleGroupModal.test.tsx b/public/app/features/alerting/unified/components/rules/EditRuleGroupModal.test.tsx index dcbc646ae03..f755d6ff9e3 100644 --- a/public/app/features/alerting/unified/components/rules/EditRuleGroupModal.test.tsx +++ b/public/app/features/alerting/unified/components/rules/EditRuleGroupModal.test.tsx @@ -1,17 +1,17 @@ +import { HttpResponse } from 'msw'; import { render } from 'test/test-utils'; import { byLabelText, byTestId, byText, byTitle } from 'testing-library-selector'; -import { CombinedRuleNamespace } from 'app/types/unified-alerting'; +import { AccessControlAction } from 'app/types'; +import { RuleGroupIdentifier } from 'app/types/unified-alerting'; -import { - mockCombinedRule, - mockCombinedRuleNamespace, - mockDataSource, - mockPromAlertingRule, - mockPromRecordingRule, - mockRulerAlertingRule, - mockRulerRecordingRule, -} from '../../mocks'; +import { GRAFANA_RULER_CONFIG } from '../../api/featureDiscoveryApi'; +import server, { setupMswServer } from '../../mockApi'; +import { mimirDataSource } from '../../mocks/server/configure'; +import { alertingFactory } from '../../mocks/server/db'; +import { rulerRuleGroupHandler as grafanaRulerRuleGroupHandler } from '../../mocks/server/handlers/grafanaRuler'; +import { rulerRuleGroupHandler } from '../../mocks/server/handlers/mimirRuler'; +import { grantPermissionsHelper } from '../../test/test-utils'; import { GRAFANA_RULES_SOURCE_NAME } from '../../utils/datasource'; import { EditRuleGroupModal } from './EditRuleGroupModal'; @@ -29,133 +29,165 @@ const ui = { }; const noop = () => jest.fn(); +setupMswServer(); -describe('EditGroupModal', () => { +jest.mock('@grafana/runtime', () => ({ + ...jest.requireActual('@grafana/runtime'), + useReturnToPrevious: jest.fn(), +})); + +describe('EditGroupModal component on cloud alert rules', () => { it('Should disable all inputs but interval when intervalEditOnly is set', async () => { - const namespace = mockCombinedRuleNamespace({ - name: 'my-alerts', - rulesSource: mockDataSource(), - groups: [{ name: 'default-group', interval: '90s', rules: [], totals: {} }], + const { rulerConfig } = mimirDataSource(); + + const group = alertingFactory.ruler.group.build({ + rules: [alertingFactory.ruler.alertingRule.build(), alertingFactory.ruler.recordingRule.build()], }); - const group = namespace.groups[0]; + // @TODO need to simplify this a bit I think, ideally there would be a higher-level function that simply sets up a few rules + // and attaches the ruler and prometheus endpoint(s) – including the namespaces and group endpoints. + server.use( + rulerRuleGroupHandler({ + response: HttpResponse.json(group), + }) + ); - render(); + const rulerGroupIdentifier: RuleGroupIdentifier = { + dataSourceName: rulerConfig.dataSourceName, + groupName: 'default-group', + namespaceName: 'my-namespace', + }; + + render( + + ); expect(await ui.input.namespace.find()).toHaveAttribute('readonly'); expect(ui.input.group.get()).toHaveAttribute('readonly'); expect(ui.input.interval.get()).not.toHaveAttribute('readonly'); }); -}); - -describe('EditGroupModal component on cloud alert rules', () => { - const promDsSettings = mockDataSource({ name: 'Prometheus-1', uid: 'Prometheus-1' }); - - const alertingRule = mockCombinedRule({ - namespace: undefined, - promRule: mockPromAlertingRule({ name: 'alerting-rule-cpu' }), - rulerRule: mockRulerAlertingRule({ alert: 'alerting-rule-cpu' }), - }); - - const recordingRule1 = mockCombinedRule({ - namespace: undefined, - promRule: mockPromRecordingRule({ name: 'recording-rule-memory' }), - rulerRule: mockRulerRecordingRule({ record: 'recording-rule-memory' }), - }); - - const recordingRule2 = mockCombinedRule({ - namespace: undefined, - promRule: mockPromRecordingRule({ name: 'recording-rule-cpu' }), - rulerRule: mockRulerRecordingRule({ record: 'recording-rule-cpu' }), - }); it('Should show alert table in case of having some non-recording rules in the group', async () => { - const promNs = mockCombinedRuleNamespace({ - name: 'prometheus-ns', - rulesSource: promDsSettings, - groups: [ - { name: 'default-group', interval: '90s', rules: [alertingRule, recordingRule1, recordingRule2], totals: {} }, - ], + const { dataSource, rulerConfig } = mimirDataSource(); + + const group = alertingFactory.ruler.group.build({ + rules: [alertingFactory.ruler.alertingRule.build(), alertingFactory.ruler.recordingRule.build()], }); - const group = promNs.groups[0]; + // @TODO need to simplify this a bit I think, ideally there would be a higher-level function that simply sets up a few rules + // and attaches the ruler and prometheus endpoint(s) – including the namespaces and group endpoints. + server.use( + rulerRuleGroupHandler({ + response: HttpResponse.json(group), + }) + ); - render(); + const ruleGroupIdentifier: RuleGroupIdentifier = { + dataSourceName: dataSource.name, + groupName: group.name, + namespaceName: 'ns1', + }; - expect(await ui.input.namespace.find()).toHaveValue('prometheus-ns'); + render(); + + expect(await ui.input.namespace.find()).toHaveValue('ns1'); expect(ui.input.namespace.get()).not.toHaveAttribute('readonly'); - expect(ui.input.group.get()).toHaveValue('default-group'); + expect(ui.input.group.get()).toHaveValue(group.name); + + // @ts-ignore + const ruleName = group.rules.at(0).alert; expect(ui.tableRows.getAll()).toHaveLength(1); // Only one rule is non-recording - expect(ui.tableRows.getAll()[0]).toHaveTextContent('alerting-rule-cpu'); + expect(ui.tableRows.getAll().at(0)).toHaveTextContent(ruleName); }); it('Should not show alert table in case of having exclusively recording rules in the group', async () => { - const promNs = mockCombinedRuleNamespace({ - name: 'prometheus-ns', - rulesSource: promDsSettings, - groups: [{ name: 'default-group', interval: '90s', rules: [recordingRule1, recordingRule2], totals: {} }], + const { dataSource, rulerConfig } = mimirDataSource(); + + const group = alertingFactory.ruler.group.build({ + rules: [alertingFactory.ruler.recordingRule.build(), alertingFactory.ruler.recordingRule.build()], }); - const group = promNs.groups[0]; + // @TODO need to simplify this a bit I think + server.use( + rulerRuleGroupHandler({ + response: HttpResponse.json(group), + }) + ); - render(); + const ruleGroupIdentifier: RuleGroupIdentifier = { + dataSourceName: dataSource.name, + groupName: group.name, + namespaceName: 'ns1', + }; + + render(); expect(ui.table.query()).not.toBeInTheDocument(); expect(await ui.noRulesText.find()).toBeInTheDocument(); }); }); describe('EditGroupModal component on grafana-managed alert rules', () => { - const grafanaNamespace: CombinedRuleNamespace = { - name: 'namespace1', - rulesSource: GRAFANA_RULES_SOURCE_NAME, - groups: [ - { - name: 'grafanaGroup1', - interval: '30s', - rules: [ - mockCombinedRule({ - namespace: undefined, - promRule: mockPromAlertingRule({ name: 'high-cpu-1' }), - rulerRule: mockRulerAlertingRule({ alert: 'high-cpu-1' }), - }), - mockCombinedRule({ - namespace: undefined, - promRule: mockPromAlertingRule({ name: 'high-memory' }), - rulerRule: mockRulerAlertingRule({ alert: 'high-memory' }), - }), - ], - totals: {}, - }, - ], + // @TODO simplify folder stuff, should also have a higher-level function to set these up + const folder = alertingFactory.folder.build(); + const NAMESPACE_UID = folder.uid; + + const group = alertingFactory.ruler.group.build({ + rules: [alertingFactory.ruler.alertingRule.build(), alertingFactory.ruler.alertingRule.build()], + }); + + const ruleGroupIdentifier: RuleGroupIdentifier = { + dataSourceName: GRAFANA_RULES_SOURCE_NAME, + groupName: group.name, + namespaceName: NAMESPACE_UID, }; - const grafanaGroup1 = grafanaNamespace.groups[0]; + beforeEach(() => { + grantPermissionsHelper([ + AccessControlAction.AlertingRuleCreate, + AccessControlAction.AlertingRuleRead, + AccessControlAction.AlertingRuleUpdate, + ]); + + server.use( + grafanaRulerRuleGroupHandler({ + response: HttpResponse.json(group), + }) + ); + }); const renderWithGrafanaGroup = () => - render(); + render( + + ); it('Should show alert table', async () => { renderWithGrafanaGroup(); - expect(await ui.input.namespace.find()).toHaveValue('namespace1'); - expect(ui.input.group.get()).toHaveValue('grafanaGroup1'); - expect(ui.input.interval.get()).toHaveValue('30s'); + expect(await ui.input.namespace.find()).toHaveValue(NAMESPACE_UID); + expect(ui.input.group.get()).toHaveValue(group.name); + expect(ui.input.interval.get()).toHaveValue(group.interval); expect(ui.tableRows.getAll()).toHaveLength(2); - expect(ui.tableRows.getAll()[0]).toHaveTextContent('high-cpu-1'); - expect(ui.tableRows.getAll()[1]).toHaveTextContent('high-memory'); + // @ts-ignore + expect(ui.tableRows.getAll().at(0)).toHaveTextContent(group.rules.at(0).alert); + // @ts-ignore + expect(ui.tableRows.getAll().at(1)).toHaveTextContent(group.rules.at(1).alert); }); it('Should have folder input in readonly mode', async () => { renderWithGrafanaGroup(); - expect(await ui.input.namespace.find()).toHaveAttribute('readonly'); }); it('Should not display folder link if no folderUrl provided', async () => { renderWithGrafanaGroup(); - expect(await ui.input.namespace.find()).toHaveValue('namespace1'); + expect(await ui.input.namespace.find()).toHaveValue(NAMESPACE_UID); expect(ui.folderLink.query()).not.toBeInTheDocument(); }); }); diff --git a/public/app/features/alerting/unified/components/rules/EditRuleGroupModal.tsx b/public/app/features/alerting/unified/components/rules/EditRuleGroupModal.tsx index 76427e7d5fa..828920a7e52 100644 --- a/public/app/features/alerting/unified/components/rules/EditRuleGroupModal.tsx +++ b/public/app/features/alerting/unified/components/rules/EditRuleGroupModal.tsx @@ -4,32 +4,46 @@ import { useMemo } from 'react'; import { FieldValues, FormProvider, RegisterOptions, useForm, useFormContext } from 'react-hook-form'; import { GrafanaTheme2 } from '@grafana/data'; -import { Alert, Badge, Button, Field, Input, Label, LinkButton, Modal, Stack, useStyles2 } from '@grafana/ui'; +import { + Alert, + Badge, + Button, + Field, + Input, + Label, + LinkButton, + LoadingPlaceholder, + Modal, + Stack, + useStyles2, +} from '@grafana/ui'; import { useAppNotification } from 'app/core/copy/appNotification'; -import { Trans } from 'app/core/internationalization'; +import { Trans, t } from 'app/core/internationalization'; import { dispatch } from 'app/store/store'; -import { CombinedRuleGroup, CombinedRuleNamespace, RuleGroupIdentifier } from 'app/types/unified-alerting'; -import { RulerRuleDTO } from 'app/types/unified-alerting-dto'; +import { RuleGroupIdentifier, RulerDataSourceConfig } from 'app/types/unified-alerting'; +import { RulerRuleDTO, RulerRuleGroupDTO } from 'app/types/unified-alerting-dto'; +import { alertRuleApi } from '../../api/alertRuleApi'; import { useMoveRuleGroup, useRenameRuleGroup, useUpdateRuleGroupConfiguration, } from '../../hooks/ruleGroup/useUpdateRuleGroup'; import { anyOfRequestState } from '../../hooks/useAsync'; +import { DEFAULT_GROUP_EVALUATION_INTERVAL } from '../../rule-editor/formDefaults'; import { fetchRulerRulesAction, rulesInSameGroupHaveInvalidFor } from '../../state/actions'; import { checkEvaluationIntervalGlobalLimit } from '../../utils/config'; -import { GRAFANA_RULES_SOURCE_NAME, getRulesSourceName } from '../../utils/datasource'; +import { GRAFANA_RULES_SOURCE_NAME } from '../../utils/datasource'; import { stringifyErrorLike } from '../../utils/misc'; -import { DEFAULT_GROUP_EVALUATION_INTERVAL } from '../../utils/rule-form'; import { AlertInfo, getAlertInfo, isGrafanaOrDataSourceRecordingRule } from '../../utils/rules'; import { formatPrometheusDuration, parsePrometheusDuration, safeParsePrometheusDuration } from '../../utils/time'; import { DynamicTable, DynamicTableColumnProps, DynamicTableItemProps } from '../DynamicTable'; import { EvaluationIntervalLimitExceeded } from '../InvalidIntervalWarning'; -import { decodeGrafanaNamespace, encodeGrafanaNamespace } from '../expressions/util'; import { EvaluationGroupQuickPick } from '../rule-editor/EvaluationGroupQuickPick'; import { MIN_TIME_RANGE_STEP_S } from '../rule-editor/GrafanaEvaluationBehavior'; +const useRuleGroupDefinition = alertRuleApi.endpoints.getRuleGroupForNamespace.useQuery; + const ITEMS_PER_PAGE = 10; function ForBadge({ message, error }: { message: string; error?: boolean }) { @@ -170,17 +184,59 @@ export const evaluateEveryValidationOptions = (rules: Rul }); export interface ModalProps { - namespace: CombinedRuleNamespace; - group: CombinedRuleGroup; + ruleGroupIdentifier: RuleGroupIdentifier; + folderTitle?: string; + rulerConfig: RulerDataSourceConfig; onClose: (saved?: boolean) => void; intervalEditOnly?: boolean; folderUrl?: string; - folderUid?: string; hideFolder?: boolean; } -export function EditRuleGroupModal(props: ModalProps): React.ReactElement { - const { namespace, group, onClose, intervalEditOnly, folderUid } = props; +export interface ModalFormProps { + ruleGroupIdentifier: RuleGroupIdentifier; + folderTitle?: string; // used to display the GMA folder title + ruleGroup: RulerRuleGroupDTO; + onClose: (saved?: boolean) => void; + intervalEditOnly?: boolean; + folderUrl?: string; + hideFolder?: boolean; +} + +// this component just wraps the modal with some loading state for grabbing rules and such +export function EditRuleGroupModal(props: ModalProps) { + const { ruleGroupIdentifier, rulerConfig, intervalEditOnly, onClose } = props; + const rulesSourceName = ruleGroupIdentifier.dataSourceName; + const isGrafanaManagedGroup = rulesSourceName === GRAFANA_RULES_SOURCE_NAME; + + const modalTitle = + intervalEditOnly || isGrafanaManagedGroup ? 'Edit evaluation group' : 'Edit namespace or evaluation group'; + + const styles = useStyles2(getStyles); + + const { + data: ruleGroup, + error, + isLoading, + } = useRuleGroupDefinition({ + group: ruleGroupIdentifier.groupName, + namespace: ruleGroupIdentifier.namespaceName, + rulerConfig, + }); + + const loadingText = t('alerting.common.loading', 'Loading...'); + + return ( + + {isLoading && } + {error ? stringifyErrorLike(error) : null} + {ruleGroup && } + + ); +} + +export function EditRuleGroupModalForm(props: ModalFormProps): React.ReactElement { + const { ruleGroup, ruleGroupIdentifier, folderTitle, onClose, intervalEditOnly } = props; const styles = useStyles2(getStyles); const notifyApp = useAppNotification(); @@ -200,32 +256,21 @@ export function EditRuleGroupModal(props: ModalProps): React.ReactElement { const defaultValues = useMemo( (): FormValues => ({ - namespaceName: decodeGrafanaNamespace(namespace).name, - groupName: group.name, - groupInterval: group.interval ?? DEFAULT_GROUP_EVALUATION_INTERVAL, + namespaceName: ruleGroupIdentifier.namespaceName, + groupName: ruleGroupIdentifier.groupName, + groupInterval: ruleGroup?.interval ?? DEFAULT_GROUP_EVALUATION_INTERVAL, }), - [namespace, group.name, group.interval] + [ruleGroup?.interval, ruleGroupIdentifier.groupName, ruleGroupIdentifier.namespaceName] ); - const rulesSourceName = getRulesSourceName(namespace.rulesSource); + const rulesSourceName = ruleGroupIdentifier.dataSourceName; const isGrafanaManagedGroup = rulesSourceName === GRAFANA_RULES_SOURCE_NAME; - // parse any parent folders the alert rule might be stored in - const nestedFolderParents = decodeGrafanaNamespace(namespace).parents; - const nameSpaceLabel = isGrafanaManagedGroup ? 'Folder' : 'Namespace'; const onSubmit = async (values: FormValues) => { - const ruleGroupIdentifier: RuleGroupIdentifier = { - dataSourceName: rulesSourceName, - groupName: group.name, - namespaceName: isGrafanaManagedGroup ? folderUid! : namespace.name, - }; - // make sure that when dealing with a nested folder for Grafana managed rules we encode the folder properly - const updatedNamespaceName = isGrafanaManagedGroup - ? encodeGrafanaNamespace(values.namespaceName, nestedFolderParents) - : values.namespaceName; + const updatedNamespaceName = values.namespaceName; const updatedGroupName = values.groupName; const updatedInterval = values.groupInterval; @@ -266,136 +311,133 @@ export function EditRuleGroupModal(props: ModalProps): React.ReactElement { }; const rulesWithoutRecordingRules = compact( - group.rules.map((r) => r.rulerRule).filter((rule) => !isGrafanaOrDataSourceRecordingRule(rule)) + ruleGroup?.rules.filter((rule) => !isGrafanaOrDataSourceRecordingRule(rule)) ); const hasSomeNoRecordingRules = rulesWithoutRecordingRules.length > 0; - const modalTitle = - intervalEditOnly || isGrafanaManagedGroup ? 'Edit evaluation group' : 'Edit namespace or evaluation group'; return ( - - -
- <> - {!props.hideFolder && ( - - - {nameSpaceLabel} - - } - invalid={Boolean(errors.namespaceName) ? true : undefined} - error={errors.namespaceName?.message} - > - - - {isGrafanaManagedGroup && props.folderUrl && ( - - )} - - )} - - Evaluation group - - } - invalid={!!errors.groupName} - error={errors.groupName?.message} - > - - - - Evaluation interval - - } - invalid={Boolean(errors.groupInterval) ? true : undefined} - error={errors.groupInterval?.message} - > - + + + <> + {!props.hideFolder && ( + + + {nameSpaceLabel} + + } + invalid={Boolean(errors.namespaceName) ? true : undefined} + error={errors.namespaceName?.message} + > - setValue('groupInterval', value, { shouldValidate: true, shouldDirty: true })} - /> - - - - {/* if we're dealing with a Grafana-managed group, check if the evaluation interval is valid / permitted */} - {isGrafanaManagedGroup && checkEvaluationIntervalGlobalLimit(watch('groupInterval')).exceedsLimit && ( - - )} - - {!hasSomeNoRecordingRules &&
This group does not contain alert rules.
} - {hasSomeNoRecordingRules && ( - <> -
List of rules that belong to this group
-
- #Eval column represents the number of evaluations needed before alert starts firing. -
- - - )} - {error && {stringifyErrorLike(error)}} -
- - - - -
- - -
-
+ icon="folder-open" + target="_blank" + /> + )} +
+ )} + + Evaluation group + + } + invalid={!!errors.groupName} + error={errors.groupName?.message} + > + + + + Evaluation interval + + } + invalid={Boolean(errors.groupInterval) ? true : undefined} + error={errors.groupInterval?.message} + > + + + setValue('groupInterval', value, { shouldValidate: true, shouldDirty: true })} + /> + + + + {/* if we're dealing with a Grafana-managed group, check if the evaluation interval is valid / permitted */} + {isGrafanaManagedGroup && checkEvaluationIntervalGlobalLimit(watch('groupInterval')).exceedsLimit && ( + + )} + + {!hasSomeNoRecordingRules &&
This group does not contain alert rules.
} + {hasSomeNoRecordingRules && ( + <> +
List of rules that belong to this group
+
+ #Eval column represents the number of evaluations needed before alert starts firing. +
+ + + )} + {error && {stringifyErrorLike(error)}} +
+ + + + +
+ + + ); } diff --git a/public/app/features/alerting/unified/components/rules/RulesGroup.test.tsx b/public/app/features/alerting/unified/components/rules/RulesGroup.test.tsx index d79e630ffb5..9baab7600cd 100644 --- a/public/app/features/alerting/unified/components/rules/RulesGroup.test.tsx +++ b/public/app/features/alerting/unified/components/rules/RulesGroup.test.tsx @@ -7,12 +7,14 @@ import { byRole, byTestId, byText } from 'testing-library-selector'; import { contextSrv } from 'app/core/services/context_srv'; import { configureStore } from 'app/store/configureStore'; import { AccessControlAction } from 'app/types'; -import { CombinedRuleGroup, CombinedRuleNamespace } from 'app/types/unified-alerting'; +import { CombinedRuleGroup, CombinedRuleNamespace, RulerDataSourceConfig } from 'app/types/unified-alerting'; import * as analytics from '../../Analytics'; +import { GRAFANA_RULER_CONFIG } from '../../api/featureDiscoveryApi'; import { useHasRuler } from '../../hooks/useHasRuler'; import { mockExportApi, mockFolderApi, setupMswServer } from '../../mockApi'; import { grantUserPermissions, mockCombinedRule, mockDataSource, mockFolder, mockGrafanaRulerRule } from '../../mocks'; +import { mimirDataSource } from '../../mocks/server/configure'; import { RulesGroup } from './RulesGroup'; @@ -38,10 +40,10 @@ const mocks = { useHasRuler: jest.mocked(useHasRuler), }; -function mockUseHasRuler(hasRuler: boolean, rulerRulesLoaded: boolean) { +function mockUseHasRuler(hasRuler: boolean, rulerConfig: RulerDataSourceConfig) { mocks.useHasRuler.mockReturnValue({ hasRuler, - rulerRulesLoaded, + rulerConfig, }); } @@ -107,7 +109,7 @@ describe('Rules group tests', () => { it('Should hide delete and edit group buttons', async () => { // Act - mockUseHasRuler(true, true); + mockUseHasRuler(true, GRAFANA_RULER_CONFIG); mockFolderApi(server).folder('cpu-usage', mockFolder({ uid: 'cpu-usage', canSave: false })); renderRulesGroup(namespace, group); expect(await screen.findByTestId('rule-group')).toBeInTheDocument(); @@ -119,7 +121,7 @@ describe('Rules group tests', () => { it('Should allow exporting rules group', async () => { // Arrange - mockUseHasRuler(true, true); + mockUseHasRuler(true, GRAFANA_RULER_CONFIG); mockFolderApi(server).folder('cpu-usage', mockFolder({ uid: 'cpu-usage' })); mockExportApi(server).exportRulesGroup('cpu-usage', 'TestGroup', { yaml: 'Yaml Export Content', @@ -151,6 +153,8 @@ describe('Rules group tests', () => { }); describe('Cloud rules', () => { + const { rulerConfig } = mimirDataSource(); + beforeEach(() => { contextSrv.isEditor = true; }); @@ -169,7 +173,7 @@ describe('Rules group tests', () => { it('When ruler enabled should display delete and edit group buttons', () => { // Arrange - mockUseHasRuler(true, true); + mockUseHasRuler(true, rulerConfig); // Act renderRulesGroup(namespace, group); @@ -182,7 +186,7 @@ describe('Rules group tests', () => { it('When ruler disabled should hide delete and edit group buttons', () => { // Arrange - mockUseHasRuler(false, false); + mockUseHasRuler(false, rulerConfig); // Act renderRulesGroup(namespace, group); @@ -195,7 +199,7 @@ describe('Rules group tests', () => { it('Delete button click should display confirmation modal', async () => { // Arrange - mockUseHasRuler(true, true); + mockUseHasRuler(true, rulerConfig); // Act renderRulesGroup(namespace, group); @@ -206,36 +210,4 @@ describe('Rules group tests', () => { expect(ui.confirmDeleteModal.confirmButton.get()).toBeInTheDocument(); }); }); - - describe('Analytics', () => { - beforeEach(() => { - contextSrv.isEditor = true; - }); - - const group: CombinedRuleGroup = { - name: 'TestGroup', - rules: [mockCombinedRule()], - totals: {}, - }; - - const namespace: CombinedRuleNamespace = { - name: 'TestNamespace', - rulesSource: mockDataSource(), - groups: [group], - }; - - it('Should log info when closing the edit group rule modal without saving', async () => { - mockUseHasRuler(true, true); - renderRulesGroup(namespace, group); - - await userEvent.click(ui.editGroupButton.get()); - - expect(screen.getByText('Cancel')).toBeInTheDocument(); - - await userEvent.click(screen.getByText('Cancel')); - - expect(screen.queryByText('Cancel')).not.toBeInTheDocument(); - expect(analytics.logInfo).toHaveBeenCalledWith(analytics.LogMessages.leavingRuleGroupEdit); - }); - }); }); diff --git a/public/app/features/alerting/unified/components/rules/RulesGroup.tsx b/public/app/features/alerting/unified/components/rules/RulesGroup.tsx index b1a5bddd43c..ccf725b5f99 100644 --- a/public/app/features/alerting/unified/components/rules/RulesGroup.tsx +++ b/public/app/features/alerting/unified/components/rules/RulesGroup.tsx @@ -1,6 +1,6 @@ import { css } from '@emotion/css'; import pluralize from 'pluralize'; -import React, { useEffect, useState } from 'react'; +import React, { useEffect, useMemo, useState } from 'react'; import { GrafanaTheme2 } from '@grafana/data'; import { selectors } from '@grafana/e2e-selectors'; @@ -26,7 +26,7 @@ import { ActionIcon } from './ActionIcon'; import { EditRuleGroupModal } from './EditRuleGroupModal'; import { ReorderCloudGroupModal } from './ReorderRuleGroupModal'; import { RuleGroupStats } from './RuleStats'; -import { RulesTable } from './RulesTable'; +import { RulesTable, useIsRulesLoading } from './RulesTable'; type ViewMode = 'grouped' | 'list'; @@ -42,6 +42,7 @@ const { useDiscoverDsFeaturesQuery } = featureDiscoveryApi; export const RulesGroup = React.memo(({ group, namespace, expandAll, viewMode }: Props) => { const { rulesSource } = namespace; const rulesSourceName = getRulesSourceName(rulesSource); + const rulerRulesLoaded = useIsRulesLoading(rulesSource); const [deleteRuleGroup] = useDeleteRuleGroup(); const styles = useStyles2(getStyles); @@ -58,7 +59,7 @@ export const RulesGroup = React.memo(({ group, namespace, expandAll, viewMode }: setIsCollapsed(!expandAll); }, [expandAll]); - const { hasRuler, rulerRulesLoaded } = useHasRuler(namespace.rulesSource); + const { hasRuler, rulerConfig } = useHasRuler(namespace.rulesSource); const { currentData: dsFeatures } = useDiscoverDsFeaturesQuery({ rulesSourceName }); const rulerRule = group.rules[0]?.rulerRule; @@ -78,12 +79,15 @@ export const RulesGroup = React.memo(({ group, namespace, expandAll, viewMode }: const isListView = viewMode === 'list'; const isGroupView = viewMode === 'grouped'; - const deleteGroup = async () => { - const namespaceName = decodeGrafanaNamespace(namespace).name; + const ruleGroupIdentifier = useMemo(() => { + const namespaceName = namespace.uid ?? namespace.name; const groupName = group.name; const dataSourceName = getRulesSourceName(namespace.rulesSource); - const ruleGroupIdentifier: RuleGroupIdentifier = { namespaceName, groupName, dataSourceName }; + return { namespaceName, groupName, dataSourceName }; + }, [namespace, group.name]); + + const deleteGroup = async () => { await deleteRuleGroup.execute(ruleGroupIdentifier); setIsDeletingGroup(false); }; @@ -274,13 +278,13 @@ export const RulesGroup = React.memo(({ group, namespace, expandAll, viewMode }: rules={group.rules} /> )} - {isEditingGroup && ( + {isEditingGroup && rulerConfig && ( closeEditModal()} folderUrl={folder?.canEdit ? makeFolderSettingsLink(folder.uid) : undefined} - folderUid={folderUID} /> )} {isReorderingGroup && dsFeatures?.rulerConfig && ( diff --git a/public/app/features/alerting/unified/components/rules/RulesTable.tsx b/public/app/features/alerting/unified/components/rules/RulesTable.tsx index 38eb1045e40..1f7c6c44b11 100644 --- a/public/app/features/alerting/unified/components/rules/RulesTable.tsx +++ b/public/app/features/alerting/unified/components/rules/RulesTable.tsx @@ -4,7 +4,7 @@ import Skeleton from 'react-loading-skeleton'; import { GrafanaTheme2 } from '@grafana/data'; import { Pagination, Tooltip, useStyles2 } from '@grafana/ui'; -import { CombinedRule } from 'app/types/unified-alerting'; +import { CombinedRule, RulesSource } from 'app/types/unified-alerting'; import { DEFAULT_PER_PAGE_PAGINATION } from '../../../../../core/constants'; import { alertRuleApi } from '../../api/alertRuleApi'; @@ -14,6 +14,7 @@ import { useAsync } from '../../hooks/useAsync'; import { attachRulerRuleToCombinedRule } from '../../hooks/useCombinedRuleNamespaces'; import { useHasRuler } from '../../hooks/useHasRuler'; import { usePagination } from '../../hooks/usePagination'; +import { useUnifiedAlertingSelector } from '../../hooks/useUnifiedAlertingSelector'; import { PluginOriginBadge } from '../../plugins/PluginOriginBadge'; import { calculateNextEvaluationEstimate } from '../../rule-list/components/util'; import { Annotation } from '../../utils/constants'; @@ -328,8 +329,20 @@ function RuleActionsCell({ rule, isLoadingRuler }: { rule: CombinedRule; isLoadi ); } +export function useIsRulesLoading(rulesSource: RulesSource) { + const rulerRules = useUnifiedAlertingSelector((state) => state.rulerRules); + const rulesSourceName = getRulesSourceName(rulesSource); + + const rulerRulesLoaded = Boolean(rulerRules[rulesSourceName]?.result); + return rulerRulesLoaded; +} + function useRuleStatus(rule: CombinedRule) { - const { hasRuler, rulerRulesLoaded } = useHasRuler(rule.namespace.rulesSource); + const rulesSource = rule.namespace.rulesSource; + + const rulerRulesLoaded = useIsRulesLoading(rulesSource); + const { hasRuler } = useHasRuler(rulesSource); + const { promRule, rulerRule } = rule; // If prometheusRulesPrimary is enabled, we don't fetch rules from the Ruler API (except for Grafana managed rules) diff --git a/public/app/features/alerting/unified/hooks/ruleGroup/useProduceNewRuleGroup.ts b/public/app/features/alerting/unified/hooks/ruleGroup/useProduceNewRuleGroup.ts index f7c54c1121b..6d8d890e737 100644 --- a/public/app/features/alerting/unified/hooks/ruleGroup/useProduceNewRuleGroup.ts +++ b/public/app/features/alerting/unified/hooks/ruleGroup/useProduceNewRuleGroup.ts @@ -7,7 +7,7 @@ import { alertRuleApi } from '../../api/alertRuleApi'; import { featureDiscoveryApi } from '../../api/featureDiscoveryApi'; import { notFoundToNullOrThrow } from '../../api/util'; import { ruleGroupReducer } from '../../reducers/ruler/ruleGroups'; -import { DEFAULT_GROUP_EVALUATION_INTERVAL } from '../../utils/rule-form'; +import { DEFAULT_GROUP_EVALUATION_INTERVAL } from '../../rule-editor/formDefaults'; const { useLazyGetRuleGroupForNamespaceQuery } = alertRuleApi; const { useLazyDiscoverDsFeaturesQuery } = featureDiscoveryApi; diff --git a/public/app/features/alerting/unified/hooks/useHasRuler.ts b/public/app/features/alerting/unified/hooks/useHasRuler.ts index db2e85f09ee..399aca6081f 100644 --- a/public/app/features/alerting/unified/hooks/useHasRuler.ts +++ b/public/app/features/alerting/unified/hooks/useHasRuler.ts @@ -3,19 +3,14 @@ import { RulesSource } from 'app/types/unified-alerting'; import { featureDiscoveryApi } from '../api/featureDiscoveryApi'; import { getRulesSourceName } from '../utils/datasource'; -import { useUnifiedAlertingSelector } from './useUnifiedAlertingSelector'; - const { useDiscoverDsFeaturesQuery } = featureDiscoveryApi; // datasource has ruler if the discovery api returns a rulerConfig export function useHasRuler(rulesSource: RulesSource) { - const rulerRules = useUnifiedAlertingSelector((state) => state.rulerRules); const rulesSourceName = getRulesSourceName(rulesSource); const { currentData: dsFeatures } = useDiscoverDsFeaturesQuery({ rulesSourceName }); - const hasRuler = Boolean(dsFeatures?.rulerConfig); - const rulerRulesLoaded = Boolean(rulerRules[rulesSourceName]?.result); - return { hasRuler, rulerRulesLoaded }; + return { hasRuler, rulerConfig: dsFeatures?.rulerConfig }; } diff --git a/public/app/features/alerting/unified/hooks/useUnifiedAlertingSelector.ts b/public/app/features/alerting/unified/hooks/useUnifiedAlertingSelector.ts index 7b50599934c..5e6daa81e8b 100644 --- a/public/app/features/alerting/unified/hooks/useUnifiedAlertingSelector.ts +++ b/public/app/features/alerting/unified/hooks/useUnifiedAlertingSelector.ts @@ -4,6 +4,9 @@ import { StoreState, useSelector } from 'app/types'; import { UnifiedAlertingState } from '../state/reducers'; +/** + * @deprecated: DO NOT USE THIS; when using this you are INCORRECTLY assuming that we already have dispatched an action to populate the redux store values + */ export function useUnifiedAlertingSelector( selector: (state: UnifiedAlertingState) => TSelected, equalityFn?: (left: TSelected, right: TSelected) => boolean diff --git a/public/app/features/alerting/unified/mocks.ts b/public/app/features/alerting/unified/mocks.ts index 5676dbb9a04..278178b395e 100644 --- a/public/app/features/alerting/unified/mocks.ts +++ b/public/app/features/alerting/unified/mocks.ts @@ -196,15 +196,12 @@ export const mockRulerAlertingRule = (partial: Partial = { ...partial, }); -export const mockRulerRecordingRule = (partial: Partial = {}): RulerAlertingRuleDTO => ({ - alert: 'alert1', +export const mockRulerRecordingRule = (partial: Partial = {}): RulerRecordingRuleDTO => ({ + record: 'alert1', expr: 'up = 1', labels: { severity: 'warning', }, - annotations: { - summary: 'test alert', - }, ...partial, }); @@ -735,7 +732,7 @@ export function mockStore(recipe: (state: StoreState) => void) { return configureStore(produce(defaultState, recipe)); } -export function mockAlertQuery(query: Partial): AlertQuery { +export function mockAlertQuery(query: Partial = {}): AlertQuery { return { datasourceUid: '--uid--', refId: 'A', diff --git a/public/app/features/alerting/unified/mocks/server/configure.ts b/public/app/features/alerting/unified/mocks/server/configure.ts index 063a070a10f..bc638438e14 100644 --- a/public/app/features/alerting/unified/mocks/server/configure.ts +++ b/public/app/features/alerting/unified/mocks/server/configure.ts @@ -20,6 +20,7 @@ import { SupportedPlugin } from 'app/features/alerting/unified/types/pluginBridg import { clearPluginSettingsCache } from 'app/features/plugins/pluginSettings'; import { AlertmanagerChoice } from 'app/plugins/datasource/alertmanager/types'; import { FolderDTO } from 'app/types'; +import { RulerDataSourceConfig } from 'app/types/unified-alerting'; import { PromRuleGroupDTO } from 'app/types/unified-alerting-dto'; import { setupDataSources } from '../../testSetup/datasources'; @@ -108,9 +109,15 @@ export function mimirDataSource() { { alerting: true, module: 'core:plugin/prometheus' } ); + const rulerConfig: RulerDataSourceConfig = { + apiVersion: 'config', + dataSourceUid: dataSource.uid, + dataSourceName: dataSource.name, + }; + setupDataSources(dataSource); - return { dataSource }; + return { dataSource, rulerConfig }; } export function setPrometheusRules(ds: DataSourceInstanceSettings, groups: PromRuleGroupDTO[]) { diff --git a/public/app/features/alerting/unified/mocks/server/db.ts b/public/app/features/alerting/unified/mocks/server/db.ts index 55e76f78208..ba78b0be31a 100644 --- a/public/app/features/alerting/unified/mocks/server/db.ts +++ b/public/app/features/alerting/unified/mocks/server/db.ts @@ -1,18 +1,23 @@ import { Factory } from 'fishery'; +import { uniqueId } from 'lodash'; import { DataSourceInstanceSettings, PluginType } from '@grafana/data'; import { config, setDataSourceSrv } from '@grafana/runtime'; +import { FolderDTO } from 'app/types'; import { PromAlertingRuleDTO, PromAlertingRuleState, PromRuleGroupDTO, PromRuleType, + RulerAlertingRuleDTO, + RulerRecordingRuleDTO, + RulerRuleGroupDTO, } from 'app/types/unified-alerting-dto'; import { MockDataSourceSrv } from '../../mocks'; import { DataSourceType } from '../../utils/datasource'; -const ruleFactory = Factory.define(({ sequence }) => ({ +const prometheusRuleFactory = Factory.define(({ sequence }) => ({ name: `test-rule-${sequence}`, query: 'test-query', state: PromAlertingRuleState.Inactive, @@ -21,15 +26,35 @@ const ruleFactory = Factory.define(({ sequence }) => ({ labels: { team: 'infra' }, })); -const groupFactory = Factory.define(({ sequence }) => { +const rulerAlertingRuleFactory = Factory.define(({ sequence }) => ({ + alert: `ruler-alerting-rule-${sequence}`, + expr: 'vector(0)', + annotations: { 'annotation-key-1': 'annotation-value-1' }, + labels: { 'label-key-1': 'label-value-1' }, + for: '5m', +})); + +const rulerRecordingRuleFactory = Factory.define(({ sequence }) => ({ + record: `ruler-recording-rule-${sequence}`, + expr: 'vector(0)', + labels: { 'label-key-1': 'label-value-1' }, +})); + +const rulerRuleGroupFactory = Factory.define(({ sequence }) => ({ + name: `ruler-rule-group-${sequence}`, + rules: [], + interval: '1m', +})); + +const prometheusRuleGroupFactory = Factory.define(({ sequence }) => { const group = { name: `test-group-${sequence}`, file: `test-namespace`, interval: 10, - rules: ruleFactory.buildList(10), + rules: prometheusRuleFactory.buildList(10), }; - ruleFactory.rewindSequence(); + prometheusRuleFactory.rewindSequence(); return group; }); @@ -72,8 +97,33 @@ const dataSourceFactory = Factory.define(({ sequence }; }); +const grafanaFolderFactory = Factory.define(({ sequence }) => ({ + id: sequence, + uid: uniqueId(), + title: `Mock Folder ${sequence}`, + version: 1, + url: '', + canAdmin: true, + canDelete: true, + canEdit: true, + canSave: true, + created: '', + createdBy: '', + hasAcl: false, + updated: '', + updatedBy: '', +})); + export const alertingFactory = { - group: groupFactory, - rule: ruleFactory, + folder: grafanaFolderFactory, + prometheus: { + group: prometheusRuleGroupFactory, + rule: prometheusRuleFactory, + }, + ruler: { + group: rulerRuleGroupFactory, + alertingRule: rulerAlertingRuleFactory, + recordingRule: rulerRecordingRuleFactory, + }, dataSource: dataSourceFactory, }; diff --git a/public/app/features/alerting/unified/reducers/ruler/__snapshots__/ruleGroups.test.ts.snap b/public/app/features/alerting/unified/reducers/ruler/__snapshots__/ruleGroups.test.ts.snap index 95329f16627..42aac7884e8 100644 --- a/public/app/features/alerting/unified/reducers/ruler/__snapshots__/ruleGroups.test.ts.snap +++ b/public/app/features/alerting/unified/reducers/ruler/__snapshots__/ruleGroups.test.ts.snap @@ -104,10 +104,6 @@ exports[`removing a rule should remove a Data source managed ruler rule without }, }, { - "alert": "alert1", - "annotations": { - "summary": "test alert", - }, "expr": "up = 1", "labels": { "severity": "warning", diff --git a/public/app/features/alerting/unified/CloneRuleEditor.test.tsx b/public/app/features/alerting/unified/rule-editor/CloneRuleEditor.test.tsx similarity index 93% rename from public/app/features/alerting/unified/CloneRuleEditor.test.tsx rename to public/app/features/alerting/unified/rule-editor/CloneRuleEditor.test.tsx index 7399bcae26b..298be9b1529 100644 --- a/public/app/features/alerting/unified/CloneRuleEditor.test.tsx +++ b/public/app/features/alerting/unified/rule-editor/CloneRuleEditor.test.tsx @@ -4,36 +4,36 @@ import { getWrapper, render, waitFor, waitForElementToBeRemoved, within } from ' import { byRole, byTestId, byText } from 'testing-library-selector'; import { MIMIR_DATASOURCE_UID } from 'app/features/alerting/unified/mocks/server/constants'; +import { AccessControlAction } from 'app/types'; import { RuleWithLocation } from 'app/types/unified-alerting'; - -import { AccessControlAction } from '../../../types'; import { RulerAlertingRuleDTO, RulerGrafanaRuleDTO, RulerRecordingRuleDTO, RulerRuleDTO, -} from '../../../types/unified-alerting-dto'; +} from 'app/types/unified-alerting-dto'; -import { CloneRuleEditor, cloneRuleDefinition } from './CloneRuleEditor'; -import { ExpressionEditorProps } from './components/rule-editor/ExpressionEditor'; -import { setupMswServer } from './mockApi'; +import { ExpressionEditorProps } from '../components/rule-editor/ExpressionEditor'; +import { setupMswServer } from '../mockApi'; import { grantUserPermissions, mockDataSource, mockRulerAlertingRule, mockRulerGrafanaRule, mockRulerRuleGroup, -} from './mocks'; -import { grafanaRulerRule } from './mocks/grafanaRulerApi'; -import { mockRulerRulesApiResponse, mockRulerRulesGroupApiResponse } from './mocks/rulerApi'; -import { AlertingQueryRunner } from './state/AlertingQueryRunner'; -import { setupDataSources } from './testSetup/datasources'; -import { RuleFormValues } from './types/rule-form'; -import { Annotation } from './utils/constants'; -import { getDefaultFormValues } from './utils/rule-form'; -import { hashRulerRule } from './utils/rule-id'; +} from '../mocks'; +import { grafanaRulerRule } from '../mocks/grafanaRulerApi'; +import { mockRulerRulesApiResponse, mockRulerRulesGroupApiResponse } from '../mocks/rulerApi'; +import { AlertingQueryRunner } from '../state/AlertingQueryRunner'; +import { setupDataSources } from '../testSetup/datasources'; +import { RuleFormValues } from '../types/rule-form'; +import { Annotation } from '../utils/constants'; +import { hashRulerRule } from '../utils/rule-id'; -jest.mock('./components/rule-editor/ExpressionEditor', () => ({ +import { CloneRuleEditor, cloneRuleDefinition } from './CloneRuleEditor'; +import { getDefaultFormValues } from './formDefaults'; + +jest.mock('../components/rule-editor/ExpressionEditor', () => ({ // eslint-disable-next-line react/display-name ExpressionEditor: ({ value, onChange }: ExpressionEditorProps) => ( onChange(e.target.value)} /> diff --git a/public/app/features/alerting/unified/CloneRuleEditor.tsx b/public/app/features/alerting/unified/rule-editor/CloneRuleEditor.tsx similarity index 72% rename from public/app/features/alerting/unified/CloneRuleEditor.tsx rename to public/app/features/alerting/unified/rule-editor/CloneRuleEditor.tsx index 92745f20bde..e92ad4b46e6 100644 --- a/public/app/features/alerting/unified/CloneRuleEditor.tsx +++ b/public/app/features/alerting/unified/rule-editor/CloneRuleEditor.tsx @@ -1,18 +1,17 @@ import { cloneDeep } from 'lodash'; -import { locationService } from '@grafana/runtime/src'; -import { Alert, LoadingPlaceholder } from '@grafana/ui/src'; +import { locationService } from '@grafana/runtime'; +import { Alert, LoadingPlaceholder } from '@grafana/ui'; +import { RuleIdentifier, RuleWithLocation } from 'app/types/unified-alerting'; +import { RulerRuleDTO } from 'app/types/unified-alerting-dto'; -import { RuleIdentifier, RuleWithLocation } from '../../../types/unified-alerting'; -import { RulerRuleDTO } from '../../../types/unified-alerting-dto'; - -import { AlertRuleForm } from './components/rule-editor/alert-rule-form/AlertRuleForm'; -import { useRuleWithLocation } from './hooks/useCombinedRule'; -import { generateCopiedName } from './utils/duplicate'; -import { stringifyErrorLike } from './utils/misc'; -import { rulerRuleToFormValues } from './utils/rule-form'; -import { getRuleName, isAlertingRulerRule, isGrafanaRulerRule, isRecordingRulerRule } from './utils/rules'; -import { createRelativeUrl } from './utils/url'; +import { AlertRuleForm } from '../components/rule-editor/alert-rule-form/AlertRuleForm'; +import { useRuleWithLocation } from '../hooks/useCombinedRule'; +import { generateCopiedName } from '../utils/duplicate'; +import { stringifyErrorLike } from '../utils/misc'; +import { rulerRuleToFormValues } from '../utils/rule-form'; +import { getRuleName, isAlertingRulerRule, isGrafanaRulerRule, isRecordingRulerRule } from '../utils/rules'; +import { createRelativeUrl } from '../utils/url'; export function CloneRuleEditor({ sourceRuleId }: { sourceRuleId: RuleIdentifier }) { const { loading, result: rule, error } = useRuleWithLocation({ ruleIdentifier: sourceRuleId }); diff --git a/public/app/features/alerting/unified/ExistingRuleEditor.tsx b/public/app/features/alerting/unified/rule-editor/ExistingRuleEditor.tsx similarity index 71% rename from public/app/features/alerting/unified/ExistingRuleEditor.tsx rename to public/app/features/alerting/unified/rule-editor/ExistingRuleEditor.tsx index 17dc0d74753..7d1b9d92fcf 100644 --- a/public/app/features/alerting/unified/ExistingRuleEditor.tsx +++ b/public/app/features/alerting/unified/rule-editor/ExistingRuleEditor.tsx @@ -1,19 +1,18 @@ import { Alert, LoadingPlaceholder } from '@grafana/ui'; import { RuleIdentifier } from 'app/types/unified-alerting'; -import { AlertWarning } from './AlertWarning'; -import { AlertRuleForm } from './components/rule-editor/alert-rule-form/AlertRuleForm'; -import { useRuleWithLocation } from './hooks/useCombinedRule'; -import { useIsRuleEditable } from './hooks/useIsRuleEditable'; -import { stringifyErrorLike } from './utils/misc'; -import * as ruleId from './utils/rule-id'; +import { AlertWarning } from '../AlertWarning'; +import { AlertRuleForm } from '../components/rule-editor/alert-rule-form/AlertRuleForm'; +import { useRuleWithLocation } from '../hooks/useCombinedRule'; +import { useIsRuleEditable } from '../hooks/useIsRuleEditable'; +import { stringifyErrorLike } from '../utils/misc'; +import * as ruleId from '../utils/rule-id'; interface ExistingRuleEditorProps { identifier: RuleIdentifier; - id?: string; } -export function ExistingRuleEditor({ identifier, id }: ExistingRuleEditorProps) { +export function ExistingRuleEditor({ identifier }: ExistingRuleEditorProps) { const { loading: loadingAlertRule, result: ruleWithLocation, diff --git a/public/app/features/alerting/unified/RuleEditor.tsx b/public/app/features/alerting/unified/rule-editor/RuleEditor.tsx similarity index 69% rename from public/app/features/alerting/unified/RuleEditor.tsx rename to public/app/features/alerting/unified/rule-editor/RuleEditor.tsx index bdda3e5d28b..b5adba57cf2 100644 --- a/public/app/features/alerting/unified/RuleEditor.tsx +++ b/public/app/features/alerting/unified/rule-editor/RuleEditor.tsx @@ -5,14 +5,16 @@ import { NavModelItem } from '@grafana/data'; import { withErrorBoundary } from '@grafana/ui'; import { RuleIdentifier } from 'app/types/unified-alerting'; -import { AlertWarning } from './AlertWarning'; +import { AlertWarning } from '../AlertWarning'; +import { AlertingPageWrapper } from '../components/AlertingPageWrapper'; +import { AlertRuleForm } from '../components/rule-editor/alert-rule-form/AlertRuleForm'; +import { useURLSearchParams } from '../hooks/useURLSearchParams'; +import { useRulesAccess } from '../utils/accessControlHooks'; +import * as ruleId from '../utils/rule-id'; + import { CloneRuleEditor } from './CloneRuleEditor'; import { ExistingRuleEditor } from './ExistingRuleEditor'; -import { AlertingPageWrapper } from './components/AlertingPageWrapper'; -import { AlertRuleForm } from './components/rule-editor/alert-rule-form/AlertRuleForm'; -import { useURLSearchParams } from './hooks/useURLSearchParams'; -import { useRulesAccess } from './utils/accessControlHooks'; -import * as ruleId from './utils/rule-id'; +import { formValuesFromQueryParams, translateRouteParamToRuleType } from './formDefaults'; type RuleEditorPathParams = { id?: string; @@ -44,14 +46,8 @@ const getPageNav = (identifier?: RuleIdentifier, type?: RuleEditorPathParams['ty }; const RuleEditor = () => { - const [searchParams] = useURLSearchParams(); - const params = useParams(); - const { type } = params; - const id = ruleId.getRuleIdFromPathname(params); - const identifier = ruleId.tryParse(id, true); - - const copyFromId = searchParams.get('copyFrom') ?? undefined; - const copyFromIdentifier = ruleId.tryParse(copyFromId); + const { identifier, type } = useRuleEditorPathParams(); + const { copyFromIdentifier, queryDefaults } = useRuleEditorQueryParams(); const { canCreateGrafanaRules, canCreateCloudRules, canEditRules } = useRulesAccess(); @@ -65,15 +61,15 @@ const RuleEditor = () => { } if (identifier) { - return ; + return ; } if (copyFromIdentifier) { return ; } // new alert rule - return ; - }, [canCreateCloudRules, canCreateGrafanaRules, canEditRules, copyFromIdentifier, id, identifier]); + return ; + }, [canCreateCloudRules, canCreateGrafanaRules, canEditRules, copyFromIdentifier, identifier, queryDefaults]); return ( @@ -83,3 +79,28 @@ const RuleEditor = () => { }; export default withErrorBoundary(RuleEditor, { style: 'page' }); + +function useRuleEditorPathParams() { + const params = useParams(); + const { type } = params; + const id = ruleId.getRuleIdFromPathname(params); + const identifier = ruleId.tryParse(id, true); + + return { identifier, type }; +} + +function useRuleEditorQueryParams() { + const { type } = useParams(); + + const [searchParams] = useURLSearchParams(); + const copyFromId = searchParams.get('copyFrom') ?? undefined; + const copyFromIdentifier = ruleId.tryParse(copyFromId); + + const ruleType = translateRouteParamToRuleType(type); + + const queryDefaults = searchParams.has('defaults') + ? formValuesFromQueryParams(searchParams.get('defaults') ?? '', ruleType) + : undefined; + + return { copyFromIdentifier, queryDefaults }; +} diff --git a/public/app/features/alerting/unified/RuleEditorCloudOnlyAllowed.test.tsx b/public/app/features/alerting/unified/rule-editor/RuleEditorCloudOnlyAllowed.test.tsx similarity index 88% rename from public/app/features/alerting/unified/RuleEditorCloudOnlyAllowed.test.tsx rename to public/app/features/alerting/unified/rule-editor/RuleEditorCloudOnlyAllowed.test.tsx index e99c56856d2..3671d13d232 100644 --- a/public/app/features/alerting/unified/RuleEditorCloudOnlyAllowed.test.tsx +++ b/public/app/features/alerting/unified/rule-editor/RuleEditorCloudOnlyAllowed.test.tsx @@ -6,24 +6,24 @@ import { contextSrv } from 'app/core/services/context_srv'; import { AccessControlAction } from 'app/types'; import { PromApiFeatures, PromApplication } from 'app/types/unified-alerting-dto'; -import { discoverFeaturesByUid } from './api/buildInfo'; -import { fetchRulerRulesGroup } from './api/ruler'; -import { ExpressionEditorProps } from './components/rule-editor/ExpressionEditor'; -import { setupMswServer } from './mockApi'; -import { grantUserPermissions, mockDataSource } from './mocks'; -import { setupDataSources } from './testSetup/datasources'; -import { DataSourceType, GRAFANA_DATASOURCE_NAME, GRAFANA_RULES_SOURCE_NAME } from './utils/datasource'; +import { discoverFeaturesByUid } from '../api/buildInfo'; +import { fetchRulerRulesGroup } from '../api/ruler'; +import { ExpressionEditorProps } from '../components/rule-editor/ExpressionEditor'; +import { setupMswServer } from '../mockApi'; +import { grantUserPermissions, mockDataSource } from '../mocks'; +import { setupDataSources } from '../testSetup/datasources'; +import { DataSourceType, GRAFANA_DATASOURCE_NAME, GRAFANA_RULES_SOURCE_NAME } from '../utils/datasource'; -jest.mock('./components/rule-editor/ExpressionEditor', () => ({ +jest.mock('../components/rule-editor/ExpressionEditor', () => ({ // eslint-disable-next-line react/display-name ExpressionEditor: ({ value, onChange }: ExpressionEditorProps) => ( onChange(e.target.value)} /> ), })); -jest.mock('./api/buildInfo'); -jest.mock('./api/ruler', () => ({ - rulerUrlBuilder: jest.requireActual('./api/ruler').rulerUrlBuilder, +jest.mock('../api/buildInfo'); +jest.mock('../api/ruler', () => ({ + rulerUrlBuilder: jest.requireActual('../api/ruler').rulerUrlBuilder, fetchRulerRules: jest.fn(), fetchRulerRulesGroup: jest.fn(), fetchRulerRulesNamespace: jest.fn(), @@ -36,8 +36,8 @@ jest.mock('app/features/query/components/QueryEditorRow', () => ({ QueryEditorRow: () =>

hi

, })); -jest.mock('./components/rule-editor/util', () => { - const originalModule = jest.requireActual('./components/rule-editor/util'); +jest.mock('../components/rule-editor/util', () => { + const originalModule = jest.requireActual('../components/rule-editor/util'); return { ...originalModule, getThresholdsForQueries: jest.fn(() => ({})), diff --git a/public/app/features/alerting/unified/RuleEditorCloudRules.test.tsx b/public/app/features/alerting/unified/rule-editor/RuleEditorCloudRules.test.tsx similarity index 85% rename from public/app/features/alerting/unified/RuleEditorCloudRules.test.tsx rename to public/app/features/alerting/unified/rule-editor/RuleEditorCloudRules.test.tsx index 21ba09cb12f..d70177fc7a9 100644 --- a/public/app/features/alerting/unified/RuleEditorCloudRules.test.tsx +++ b/public/app/features/alerting/unified/rule-editor/RuleEditorCloudRules.test.tsx @@ -5,15 +5,15 @@ import { screen } from 'test/test-utils'; import { selectors } from '@grafana/e2e-selectors'; import { AccessControlAction } from 'app/types'; -import { ExpressionEditorProps } from './components/rule-editor/ExpressionEditor'; -import { setupMswServer } from './mockApi'; -import { grantUserPermissions } from './mocks'; -import { GROUP_3, NAMESPACE_2 } from './mocks/mimirRulerApi'; -import { mimirDataSource } from './mocks/server/configure'; -import { MIMIR_DATASOURCE_UID } from './mocks/server/constants'; -import { captureRequests, serializeRequests } from './mocks/server/events'; +import { ExpressionEditorProps } from '../components/rule-editor/ExpressionEditor'; +import { setupMswServer } from '../mockApi'; +import { grantUserPermissions } from '../mocks'; +import { GROUP_3, NAMESPACE_2 } from '../mocks/mimirRulerApi'; +import { mimirDataSource } from '../mocks/server/configure'; +import { MIMIR_DATASOURCE_UID } from '../mocks/server/constants'; +import { captureRequests, serializeRequests } from '../mocks/server/events'; -jest.mock('./components/rule-editor/ExpressionEditor', () => ({ +jest.mock('../components/rule-editor/ExpressionEditor', () => ({ // eslint-disable-next-line react/display-name ExpressionEditor: ({ value, onChange }: ExpressionEditorProps) => ( onChange(e.target.value)} /> diff --git a/public/app/features/alerting/unified/RuleEditorExisting.test.tsx b/public/app/features/alerting/unified/rule-editor/RuleEditorExisting.test.tsx similarity index 95% rename from public/app/features/alerting/unified/RuleEditorExisting.test.tsx rename to public/app/features/alerting/unified/rule-editor/RuleEditorExisting.test.tsx index 15ecae4e3ec..1781ab75d2b 100644 --- a/public/app/features/alerting/unified/RuleEditorExisting.test.tsx +++ b/public/app/features/alerting/unified/rule-editor/RuleEditorExisting.test.tsx @@ -7,15 +7,15 @@ import { setFolderResponse } from 'app/features/alerting/unified/mocks/server/co import { MIMIR_DATASOURCE_UID } from 'app/features/alerting/unified/mocks/server/constants'; import { captureRequests } from 'app/features/alerting/unified/mocks/server/events'; import { DashboardSearchItemType } from 'app/features/search/types'; +import { AccessControlAction } from 'app/types'; -import { AccessControlAction } from '../../../types'; +import { setupMswServer } from '../mockApi'; +import { grantUserPermissions, mockDataSource, mockFolder } from '../mocks'; +import { grafanaRulerRule } from '../mocks/grafanaRulerApi'; +import { setupDataSources } from '../testSetup/datasources'; +import { Annotation } from '../utils/constants'; import RuleEditor from './RuleEditor'; -import { setupMswServer } from './mockApi'; -import { grantUserPermissions, mockDataSource, mockFolder } from './mocks'; -import { grafanaRulerRule } from './mocks/grafanaRulerApi'; -import { setupDataSources } from './testSetup/datasources'; -import { Annotation } from './utils/constants'; jest.mock('app/core/components/AppChrome/AppChromeUpdate', () => ({ AppChromeUpdate: ({ actions }: { actions: React.ReactNode }) =>
{actions}
, diff --git a/public/app/features/alerting/unified/RuleEditorGrafanaRules.test.tsx b/public/app/features/alerting/unified/rule-editor/RuleEditorGrafanaRules.test.tsx similarity index 94% rename from public/app/features/alerting/unified/RuleEditorGrafanaRules.test.tsx rename to public/app/features/alerting/unified/rule-editor/RuleEditorGrafanaRules.test.tsx index e07f62cd7c1..94888d4c9b1 100644 --- a/public/app/features/alerting/unified/RuleEditorGrafanaRules.test.tsx +++ b/public/app/features/alerting/unified/rule-editor/RuleEditorGrafanaRules.test.tsx @@ -9,10 +9,10 @@ import { setupMswServer } from 'app/features/alerting/unified/mockApi'; import { PROMETHEUS_DATASOURCE_UID } from 'app/features/alerting/unified/mocks/server/constants'; import { AccessControlAction } from 'app/types'; -import { grantUserPermissions, mockDataSource } from './mocks'; -import { grafanaRulerGroup } from './mocks/grafanaRulerApi'; -import { captureRequests, serializeRequests } from './mocks/server/events'; -import { setupDataSources } from './testSetup/datasources'; +import { grantUserPermissions, mockDataSource } from '../mocks'; +import { grafanaRulerGroup } from '../mocks/grafanaRulerApi'; +import { captureRequests, serializeRequests } from '../mocks/server/events'; +import { setupDataSources } from '../testSetup/datasources'; jest.mock('app/core/components/AppChrome/AppChromeUpdate', () => ({ AppChromeUpdate: ({ actions }: { actions: React.ReactNode }) =>
{actions}
, diff --git a/public/app/features/alerting/unified/RuleEditorRecordingRule.test.tsx b/public/app/features/alerting/unified/rule-editor/RuleEditorRecordingRule.test.tsx similarity index 88% rename from public/app/features/alerting/unified/RuleEditorRecordingRule.test.tsx rename to public/app/features/alerting/unified/rule-editor/RuleEditorRecordingRule.test.tsx index 766253febf1..8e979828aa9 100644 --- a/public/app/features/alerting/unified/RuleEditorRecordingRule.test.tsx +++ b/public/app/features/alerting/unified/rule-editor/RuleEditorRecordingRule.test.tsx @@ -7,14 +7,14 @@ import { byText } from 'testing-library-selector'; import { setupMswServer } from 'app/features/alerting/unified/mockApi'; import { AccessControlAction } from 'app/types'; -import { RecordingRuleEditorProps } from './components/rule-editor/RecordingRuleEditor'; -import { grantUserPermissions } from './mocks'; -import { GROUP_3, NAMESPACE_2 } from './mocks/mimirRulerApi'; -import { mimirDataSource } from './mocks/server/configure'; -import { MIMIR_DATASOURCE_UID } from './mocks/server/constants'; -import { captureRequests, serializeRequests } from './mocks/server/events'; +import { RecordingRuleEditorProps } from '../components/rule-editor/RecordingRuleEditor'; +import { grantUserPermissions } from '../mocks'; +import { GROUP_3, NAMESPACE_2 } from '../mocks/mimirRulerApi'; +import { mimirDataSource } from '../mocks/server/configure'; +import { MIMIR_DATASOURCE_UID } from '../mocks/server/constants'; +import { captureRequests, serializeRequests } from '../mocks/server/events'; -jest.mock('./components/rule-editor/RecordingRuleEditor', () => ({ +jest.mock('../components/rule-editor/RecordingRuleEditor', () => ({ RecordingRuleEditor: ({ queries, onChangeQuery }: Pick) => { const onChange = (expr: string) => { const query = queries[0]; diff --git a/public/app/features/alerting/unified/__snapshots__/RuleEditorCloudRules.test.tsx.snap b/public/app/features/alerting/unified/rule-editor/__snapshots__/RuleEditorCloudRules.test.tsx.snap similarity index 100% rename from public/app/features/alerting/unified/__snapshots__/RuleEditorCloudRules.test.tsx.snap rename to public/app/features/alerting/unified/rule-editor/__snapshots__/RuleEditorCloudRules.test.tsx.snap diff --git a/public/app/features/alerting/unified/__snapshots__/RuleEditorGrafanaRules.test.tsx.snap b/public/app/features/alerting/unified/rule-editor/__snapshots__/RuleEditorGrafanaRules.test.tsx.snap similarity index 100% rename from public/app/features/alerting/unified/__snapshots__/RuleEditorGrafanaRules.test.tsx.snap rename to public/app/features/alerting/unified/rule-editor/__snapshots__/RuleEditorGrafanaRules.test.tsx.snap diff --git a/public/app/features/alerting/unified/__snapshots__/RuleEditorRecordingRule.test.tsx.snap b/public/app/features/alerting/unified/rule-editor/__snapshots__/RuleEditorRecordingRule.test.tsx.snap similarity index 100% rename from public/app/features/alerting/unified/__snapshots__/RuleEditorRecordingRule.test.tsx.snap rename to public/app/features/alerting/unified/rule-editor/__snapshots__/RuleEditorRecordingRule.test.tsx.snap diff --git a/public/app/features/alerting/unified/rule-editor/formDefaults.test.ts b/public/app/features/alerting/unified/rule-editor/formDefaults.test.ts new file mode 100644 index 00000000000..800b95a6193 --- /dev/null +++ b/public/app/features/alerting/unified/rule-editor/formDefaults.test.ts @@ -0,0 +1,196 @@ +import { config } from '@grafana/runtime'; + +import { mockAlertQuery, mockDataSource, reduceExpression, thresholdExpression } from '../mocks'; +import { testWithFeatureToggles } from '../test/test-utils'; +import { RuleFormType } from '../types/rule-form'; +import { Annotation } from '../utils/constants'; +import { DataSourceType, getDefaultOrFirstCompatibleDataSource } from '../utils/datasource'; +import { MANUAL_ROUTING_KEY, getDefaultQueries } from '../utils/rule-form'; + +import { formValuesFromQueryParams, getDefaultFormValues, getDefautManualRouting } from './formDefaults'; +import { isAlertQueryOfAlertData } from './formProcessing'; + +jest.mock('../utils/datasource'); + +const mocks = { + getDefaultOrFirstCompatibleDataSource: jest.mocked(getDefaultOrFirstCompatibleDataSource), +}; + +// Setup mock implementation +mocks.getDefaultOrFirstCompatibleDataSource.mockReturnValue( + mockDataSource({ + type: DataSourceType.Prometheus, + }) +); + +// TODO Not sure why queries are an empty array in the default form values +const defaultFormValues = { + ...getDefaultFormValues(), + queries: getDefaultQueries(), +}; + +describe('formValuesFromQueryParams', () => { + it('should return default values when given invalid JSON', () => { + const result = formValuesFromQueryParams('invalid json', RuleFormType.grafana); + + expect(result).toEqual(defaultFormValues); + }); + + it('should normalize annotations', () => { + const ruleDefinition = JSON.stringify({ + annotations: [ + { key: 'custom', value: 'my custom annotation' }, + { key: Annotation.runbookURL, value: 'runbook annotation' }, + { key: 'custom-2', value: 'custom annotation v2' }, + { key: Annotation.summary, value: 'summary annotation' }, + { key: 'custom-3', value: 'custom annotation v3' }, + { key: Annotation.description, value: 'description annotation' }, + ], + }); + + const result = formValuesFromQueryParams(ruleDefinition, RuleFormType.grafana); + + const [summary, description, runbookURL, ...rest] = result.annotations; + + expect(summary).toEqual({ key: Annotation.summary, value: 'summary annotation' }); + expect(description).toEqual({ key: Annotation.description, value: 'description annotation' }); + expect(runbookURL).toEqual({ key: Annotation.runbookURL, value: 'runbook annotation' }); + expect(rest).toContainEqual({ key: 'custom', value: 'my custom annotation' }); + expect(rest).toContainEqual({ key: 'custom-2', value: 'custom annotation v2' }); + expect(rest).toContainEqual({ key: 'custom-3', value: 'custom annotation v3' }); + }); + + it('should disable simplified query editor when query switch mode is disabled', () => { + const result = formValuesFromQueryParams(JSON.stringify({}), RuleFormType.grafana); + + expect(result.editorSettings).toBeDefined(); + expect(result.editorSettings!.simplifiedQueryEditor).toBe(false); + }); + + describe('when simplified query editor is enabled', () => { + testWithFeatureToggles(['alertingQueryAndExpressionsStepMode']); + + it('should enable simplified query editor if queries are transformable to simple condition', () => { + const result = formValuesFromQueryParams( + JSON.stringify({ + queries: [mockAlertQuery(), reduceExpression, thresholdExpression], + }), + RuleFormType.grafana + ); + + expect(result.editorSettings).toBeDefined(); + expect(result.editorSettings!.simplifiedQueryEditor).toBe(true); + }); + + it('should disable simplified query editor if queries are not transformable to simple condition', () => { + const result = formValuesFromQueryParams( + JSON.stringify({ + queries: [mockAlertQuery(), mockAlertQuery(), thresholdExpression], + }), + RuleFormType.grafana + ); + + expect(result.editorSettings).toBeDefined(); + expect(result.editorSettings!.simplifiedQueryEditor).toBe(false); + }); + }); + + it('should default to instant queries for loki and prometheus if not specified', () => { + const result = formValuesFromQueryParams( + JSON.stringify({ + queries: [ + mockAlertQuery({ datasourceUid: 'loki', model: { refId: 'A', datasource: { type: DataSourceType.Loki } } }), + mockAlertQuery({ + datasourceUid: 'prometheus', + model: { refId: 'B', datasource: { type: DataSourceType.Prometheus } }, + }), + ], + }), + RuleFormType.grafana + ); + + const [lokiQuery, prometheusQuery] = result.queries.filter(isAlertQueryOfAlertData); + + expect(lokiQuery.model.instant).toBe(true); + expect(lokiQuery.model.range).toBe(false); + expect(prometheusQuery.model.instant).toBe(true); + expect(prometheusQuery.model.range).toBe(false); + }); + + it('should preserver instant and range values if specified', () => { + const result = formValuesFromQueryParams( + JSON.stringify({ + queries: [ + mockAlertQuery({ + datasourceUid: 'loki', + model: { refId: 'A', datasource: { type: DataSourceType.Loki }, instant: true, range: false }, + }), + mockAlertQuery({ + datasourceUid: 'prometheus', + model: { refId: 'B', datasource: { type: DataSourceType.Prometheus }, instant: false, range: true }, + }), + ], + }), + RuleFormType.grafana + ); + + const [lokiQuery, prometheusQuery] = result.queries.filter(isAlertQueryOfAlertData); + + expect(lokiQuery.model.instant).toBe(true); + expect(lokiQuery.model.range).toBe(false); + expect(prometheusQuery.model.range).toBe(true); + expect(prometheusQuery.model.instant).toBe(false); + }); + + it('should reveal hidden queries', () => { + const ruleDefinition = JSON.stringify({ + queries: [ + { refId: 'A', model: { refId: 'A', hide: true } }, + { refId: 'B', model: { refId: 'B', hide: false } }, + { refId: 'C', model: { refId: 'C' } }, + ], + }); + + const result = formValuesFromQueryParams(ruleDefinition, RuleFormType.grafana); + + expect(result.queries.length).toBe(3); + + const [q1, q2, q3] = result.queries; + expect(q1.refId).toBe('A'); + expect(q2.refId).toBe('B'); + expect(q3.refId).toBe('C'); + expect(q1.model).not.toHaveProperty('hide'); + expect(q2.model).not.toHaveProperty('hide'); + expect(q3.model).not.toHaveProperty('hide'); + }); +}); + +describe('getDefaultManualRouting', () => { + afterEach(() => { + window.localStorage.clear(); + }); + + it('returns false if the feature toggle is not enabled', () => { + config.featureToggles.alertingSimplifiedRouting = false; + expect(getDefautManualRouting()).toBe(false); + }); + + it('returns true if the feature toggle is enabled and localStorage is not set', () => { + config.featureToggles.alertingSimplifiedRouting = true; + expect(getDefautManualRouting()).toBe(true); + }); + + it('returns false if the feature toggle is enabled and localStorage is set to "false"', () => { + config.featureToggles.alertingSimplifiedRouting = true; + localStorage.setItem(MANUAL_ROUTING_KEY, 'false'); + expect(getDefautManualRouting()).toBe(false); + }); + + it('returns true if the feature toggle is enabled and localStorage is set to any value other than "false"', () => { + config.featureToggles.alertingSimplifiedRouting = true; + localStorage.setItem(MANUAL_ROUTING_KEY, 'true'); + expect(getDefautManualRouting()).toBe(true); + localStorage.removeItem(MANUAL_ROUTING_KEY); + expect(getDefautManualRouting()).toBe(true); + }); +}); diff --git a/public/app/features/alerting/unified/rule-editor/formDefaults.ts b/public/app/features/alerting/unified/rule-editor/formDefaults.ts new file mode 100644 index 00000000000..7c6a0c824c0 --- /dev/null +++ b/public/app/features/alerting/unified/rule-editor/formDefaults.ts @@ -0,0 +1,157 @@ +import { clamp } from 'lodash'; + +import { config } from '@grafana/runtime'; +import { RuleWithLocation } from 'app/types/unified-alerting'; +import { GrafanaAlertStateDecision, RulerRuleDTO } from 'app/types/unified-alerting-dto'; + +import { RuleFormType, RuleFormValues } from '../types/rule-form'; +// TODO Ideally all of these should be moved here +import { getRulesAccess } from '../utils/access-control'; +import { defaultAnnotations } from '../utils/constants'; +import { GRAFANA_RULES_SOURCE_NAME } from '../utils/datasource'; +import { + MANUAL_ROUTING_KEY, + SIMPLIFIED_QUERY_EDITOR_KEY, + getDefaultQueries, + rulerRuleToFormValues, +} from '../utils/rule-form'; +import { isGrafanaRecordingRuleByType } from '../utils/rules'; +import { formatPrometheusDuration, safeParsePrometheusDuration } from '../utils/time'; + +import { + normalizeDefaultAnnotations, + revealHiddenQueries, + setInstantOrRange, + setQueryEditorSettings, +} from './formProcessing'; + +// even if the min interval is < 1m we should default to 1m, but allow arbitrary values for minInterval > 1m +const GROUP_EVALUATION_MIN_INTERVAL_MS = safeParsePrometheusDuration(config.unifiedAlerting?.minInterval ?? '10s'); +const GROUP_EVALUATION_INTERVAL_LOWER_BOUND = safeParsePrometheusDuration('1m'); +const GROUP_EVALUATION_INTERVAL_UPPER_BOUND = Infinity; + +export const DEFAULT_GROUP_EVALUATION_INTERVAL = formatPrometheusDuration( + clamp(GROUP_EVALUATION_MIN_INTERVAL_MS, GROUP_EVALUATION_INTERVAL_LOWER_BOUND, GROUP_EVALUATION_INTERVAL_UPPER_BOUND) +); +export const getDefaultFormValues = (): RuleFormValues => { + const { canCreateGrafanaRules, canCreateCloudRules } = getRulesAccess(); + + return Object.freeze({ + name: '', + uid: '', + labels: [{ key: '', value: '' }], + annotations: defaultAnnotations, + dataSourceName: GRAFANA_RULES_SOURCE_NAME, // let's use Grafana-managed alert rule by default + type: canCreateGrafanaRules ? RuleFormType.grafana : canCreateCloudRules ? RuleFormType.cloudAlerting : undefined, // viewers can't create prom alerts + group: '', + + // grafana + folder: undefined, + queries: [], + recordingRulesQueries: [], + condition: '', + noDataState: GrafanaAlertStateDecision.NoData, + execErrState: GrafanaAlertStateDecision.Error, + evaluateFor: DEFAULT_GROUP_EVALUATION_INTERVAL, + evaluateEvery: DEFAULT_GROUP_EVALUATION_INTERVAL, + manualRouting: getDefautManualRouting(), // we default to true if the feature toggle is enabled and the user hasn't set local storage to false + contactPoints: {}, + overrideGrouping: false, + overrideTimings: false, + muteTimeIntervals: [], + editorSettings: getDefaultEditorSettings(), + + // cortex / loki + namespace: '', + expression: '', + forTime: 1, + forTimeUnit: 'm', + }); +}; + +export const getDefautManualRouting = () => { + // first check if feature toggle for simplified routing is enabled + const simplifiedRoutingToggleEnabled = config.featureToggles.alertingSimplifiedRouting ?? false; + if (!simplifiedRoutingToggleEnabled) { + return false; + } + //then, check in local storage if the user has enabled simplified routing + // if it's not set, we'll default to true + const manualRouting = localStorage.getItem(MANUAL_ROUTING_KEY); + return manualRouting !== 'false'; +}; + +function getDefaultEditorSettings() { + const editorSettingsEnabled = config.featureToggles.alertingQueryAndExpressionsStepMode ?? false; + if (!editorSettingsEnabled) { + return undefined; + } + //then, check in local storage if the user has saved last rule with sections simplified + const queryEditorSettings = localStorage.getItem(SIMPLIFIED_QUERY_EDITOR_KEY); + const notificationStepSettings = localStorage.getItem(MANUAL_ROUTING_KEY); + return { + simplifiedQueryEditor: queryEditorSettings !== 'false', + simplifiedNotificationEditor: notificationStepSettings !== 'false', + }; +} + +export function formValuesFromQueryParams(ruleDefinition: string, type: RuleFormType): RuleFormValues { + let ruleFromQueryParams: Partial; + + try { + ruleFromQueryParams = JSON.parse(ruleDefinition); + } catch (err) { + return { + ...getDefaultFormValues(), + queries: getDefaultQueries(), + }; + } + + return setQueryEditorSettings( + setInstantOrRange( + revealHiddenQueries({ + ...getDefaultFormValues(), + ...ruleFromQueryParams, + annotations: normalizeDefaultAnnotations(ruleFromQueryParams.annotations ?? []), + queries: ruleFromQueryParams.queries ?? getDefaultQueries(), + type: type || RuleFormType.grafana, + evaluateEvery: DEFAULT_GROUP_EVALUATION_INTERVAL, + }) + ) + ); +} + +export function formValuesFromPrefill(rule: Partial): RuleFormValues { + return revealHiddenQueries({ + ...getDefaultFormValues(), + ...rule, + }); +} + +export function formValuesFromExistingRule(rule: RuleWithLocation) { + return revealHiddenQueries(rulerRuleToFormValues(rule)); +} + +export function defaultFormValuesForRuleType(ruleType: RuleFormType): RuleFormValues { + return { + ...getDefaultFormValues(), + condition: 'C', + queries: getDefaultQueries(isGrafanaRecordingRuleByType(ruleType)), + type: ruleType, + evaluateEvery: DEFAULT_GROUP_EVALUATION_INTERVAL, + }; +} + +// TODO This function is not 100% valid. There is no support for cloud form type because +// it's not valid from the path param point of view. +export function translateRouteParamToRuleType(param = ''): RuleFormType { + if (param === 'recording') { + return RuleFormType.cloudRecording; + } + + if (param === 'grafana-recording') { + return RuleFormType.grafanaRecording; + } + + return RuleFormType.grafana; +} diff --git a/public/app/features/alerting/unified/rule-editor/formProcessing.ts b/public/app/features/alerting/unified/rule-editor/formProcessing.ts new file mode 100644 index 00000000000..c9fa1951417 --- /dev/null +++ b/public/app/features/alerting/unified/rule-editor/formProcessing.ts @@ -0,0 +1,150 @@ +import { omit } from 'lodash'; + +import { config } from '@grafana/runtime'; +import { isExpressionQuery } from 'app/features/expressions/guards'; +import { ExpressionQuery, ExpressionQueryType, ReducerMode } from 'app/features/expressions/types'; +import { AlertDataQuery, AlertQuery } from 'app/types/unified-alerting-dto'; + +import { SimpleConditionIdentifier } from '../components/rule-editor/query-and-alert-condition/SimpleCondition'; +import { KVObject, RuleFormValues } from '../types/rule-form'; +import { defaultAnnotations } from '../utils/constants'; +import { DataSourceType } from '../utils/datasource'; + +export function setQueryEditorSettings(values: RuleFormValues): RuleFormValues { + const isQuerySwitchModeEnabled = config.featureToggles.alertingQueryAndExpressionsStepMode ?? false; + + if (!isQuerySwitchModeEnabled) { + return { + ...values, + editorSettings: { + simplifiedQueryEditor: false, + simplifiedNotificationEditor: true, // actually it doesn't matter in this case + }, + }; + } + + // data queries only + const dataQueries = values.queries.filter((query) => !isExpressionQuery(query.model)); + + // expression queries only + const expressionQueries = values.queries.filter((query) => isExpressionQueryInAlert(query)); + + const queryParamsAreTransformable = areQueriesTransformableToSimpleCondition(dataQueries, expressionQueries); + return { + ...values, + editorSettings: { + simplifiedQueryEditor: queryParamsAreTransformable, + simplifiedNotificationEditor: true, + }, + }; +} + +export function setInstantOrRange(values: RuleFormValues): RuleFormValues { + return { + ...values, + queries: values.queries?.map((query) => { + if (isExpressionQuery(query.model)) { + return query; + } + // data query + const defaultToInstant = + query.model.datasource?.type === DataSourceType.Loki || + query.model.datasource?.type === DataSourceType.Prometheus; + const isInstant = + 'instant' in query.model && query.model.instant !== undefined ? query.model.instant : defaultToInstant; + return { + ...query, + model: { + ...query.model, + instant: isInstant, + range: !isInstant, // we cannot have both instant and range queries in alerting + }, + }; + }), + }; +} + +export function areQueriesTransformableToSimpleCondition( + dataQueries: Array>, + expressionQueries: Array> +) { + if (dataQueries.length !== 1) { + return false; + } + const singleReduceExpressionInInstantQuery = + 'instant' in dataQueries[0].model && dataQueries[0].model.instant && expressionQueries.length === 1; + + if (expressionQueries.length !== 2 && !singleReduceExpressionInInstantQuery) { + return false; + } + + const query = dataQueries[0]; + + if (query.refId !== SimpleConditionIdentifier.queryId) { + return false; + } + + const reduceExpressionIndex = expressionQueries.findIndex( + (query) => query.model.type === ExpressionQueryType.reduce && query.refId === SimpleConditionIdentifier.reducerId + ); + const reduceExpression = expressionQueries.at(reduceExpressionIndex); + const reduceOk = + reduceExpression && + reduceExpressionIndex === 0 && + (reduceExpression.model.settings?.mode === ReducerMode.Strict || + reduceExpression.model.settings?.mode === undefined); + + const thresholdExpressionIndex = expressionQueries.findIndex( + (query) => + query.model.type === ExpressionQueryType.threshold && query.refId === SimpleConditionIdentifier.thresholdId + ); + const thresholdExpression = expressionQueries.at(thresholdExpressionIndex); + const conditions = thresholdExpression?.model.conditions ?? []; + const thresholdIndexOk = singleReduceExpressionInInstantQuery + ? thresholdExpressionIndex === 0 + : thresholdExpressionIndex === 1; + const thresholdOk = thresholdExpression && thresholdIndexOk && conditions[0]?.unloadEvaluator === undefined; + return (Boolean(reduceOk) || Boolean(singleReduceExpressionInInstantQuery)) && Boolean(thresholdOk); +} + +export function isExpressionQueryInAlert( + query: AlertQuery +): query is AlertQuery { + return isExpressionQuery(query.model); +} + +export function isAlertQueryOfAlertData( + query: AlertQuery +): query is AlertQuery { + return !isExpressionQuery(query.model); +} + +// the backend will always execute "hidden" queries, so we have no choice but to remove the property in the front-end +// to avoid confusion. The query editor shows them as "disabled" and that's a different semantic meaning. +// furthermore the "AlertingQueryRunner" calls `filterQuery` on each data source and those will skip running queries that are "hidden"." +// It seems like we have no choice but to act like "hidden" queries don't exist in alerting. +export const revealHiddenQueries = (ruleDefinition: RuleFormValues): RuleFormValues => { + return { + ...ruleDefinition, + queries: ruleDefinition.queries?.map((query) => omit(query, 'model.hide')), + }; +}; + +export function normalizeDefaultAnnotations(annotations: KVObject[]) { + const orderedAnnotations = [...annotations]; + const defaultAnnotationKeys = defaultAnnotations.map((annotation) => annotation.key); + + defaultAnnotationKeys.forEach((defaultAnnotationKey, index) => { + const fieldIndex = orderedAnnotations.findIndex((field) => field.key === defaultAnnotationKey); + + if (fieldIndex === -1) { + //add the default annotation if abstent + const emptyValue = { key: defaultAnnotationKey, value: '' }; + orderedAnnotations.splice(index, 0, emptyValue); + } else if (fieldIndex !== index) { + //move it to the correct position if present + orderedAnnotations.splice(index, 0, orderedAnnotations.splice(fieldIndex, 1)[0]); + } + }); + return orderedAnnotations; +} diff --git a/public/app/features/alerting/unified/rule-list/FilterView.test.tsx b/public/app/features/alerting/unified/rule-list/FilterView.test.tsx index b436e84941a..de51e6dde8b 100644 --- a/public/app/features/alerting/unified/rule-list/FilterView.test.tsx +++ b/public/app/features/alerting/unified/rule-list/FilterView.test.tsx @@ -19,9 +19,9 @@ grantUserPermissions([AccessControlAction.AlertingRuleExternalRead]); setupMswServer(); -const mimirGroups = alertingFactory.group.buildList(5000, { file: 'test-mimir-namespace' }); -alertingFactory.group.rewindSequence(); -const prometheusGroups = alertingFactory.group.buildList(200, { file: 'test-prometheus-namespace' }); +const mimirGroups = alertingFactory.prometheus.group.buildList(5000, { file: 'test-mimir-namespace' }); +alertingFactory.prometheus.group.rewindSequence(); +const prometheusGroups = alertingFactory.prometheus.group.buildList(200, { file: 'test-prometheus-namespace' }); const mimirDs = alertingFactory.dataSource.build({ name: 'Mimir', uid: 'mimir' }); const prometheusDs = alertingFactory.dataSource.build({ name: 'Prometheus', uid: 'prometheus' }); diff --git a/public/app/features/alerting/unified/rule-list/GroupedView.test.tsx b/public/app/features/alerting/unified/rule-list/GroupedView.test.tsx index adb8b83c077..953bce50ead 100644 --- a/public/app/features/alerting/unified/rule-list/GroupedView.test.tsx +++ b/public/app/features/alerting/unified/rule-list/GroupedView.test.tsx @@ -19,9 +19,9 @@ grantUserPermissions([AccessControlAction.AlertingRuleExternalRead]); setupMswServer(); -const mimirGroups = alertingFactory.group.buildList(500, { file: 'test-mimir-namespace' }); -alertingFactory.group.rewindSequence(); -const prometheusGroups = alertingFactory.group.buildList(130, { file: 'test-prometheus-namespace' }); +const mimirGroups = alertingFactory.prometheus.group.buildList(500, { file: 'test-mimir-namespace' }); +alertingFactory.prometheus.group.rewindSequence(); +const prometheusGroups = alertingFactory.prometheus.group.buildList(130, { file: 'test-prometheus-namespace' }); const mimirDs = alertingFactory.dataSource.build({ name: 'Mimir', uid: 'mimir' }); const prometheusDs = alertingFactory.dataSource.build({ name: 'Prometheus', uid: 'prometheus' }); diff --git a/public/app/features/alerting/unified/test/test-utils.ts b/public/app/features/alerting/unified/test/test-utils.ts index 88de7e27984..66598dea083 100644 --- a/public/app/features/alerting/unified/test/test-utils.ts +++ b/public/app/features/alerting/unified/test/test-utils.ts @@ -2,6 +2,10 @@ import { act } from '@testing-library/react'; import { FeatureToggles } from '@grafana/data'; import { config } from '@grafana/runtime'; +import { AccessControlAction } from 'app/types'; + +import { grantUserPermissions } from '../mocks'; +import { setFolderAccessControl } from '../mocks/server/configure'; /** * Flushes out microtasks so we don't get warnings from `@floating-ui/react` @@ -47,3 +51,13 @@ export const testWithLicenseFeatures = (features: string[]) => { config.licenseInfo.enabledFeatures = originalFeatures; }); }; + +/** + * "Grants" permissions via contextSrv mock, and additionally sets folder access control + * API response to match + */ +export const grantPermissionsHelper = (permissions: AccessControlAction[]) => { + const permissionsHash = permissions.reduce((hash, permission) => ({ ...hash, [permission]: true }), {}); + grantUserPermissions(permissions); + setFolderAccessControl(permissionsHash); +}; diff --git a/public/app/features/alerting/unified/utils/rule-form.test.ts b/public/app/features/alerting/unified/utils/rule-form.test.ts index bfc7f86ea5c..80442e87984 100644 --- a/public/app/features/alerting/unified/utils/rule-form.test.ts +++ b/public/app/features/alerting/unified/utils/rule-form.test.ts @@ -1,20 +1,17 @@ import { PromQuery } from '@grafana/prometheus'; -import { config } from '@grafana/runtime'; import { GrafanaAlertStateDecision, GrafanaRuleDefinition, RulerAlertingRuleDTO } from 'app/types/unified-alerting-dto'; +import { getDefaultFormValues } from '../rule-editor/formDefaults'; import { AlertManagerManualRouting, RuleFormType, RuleFormValues } from '../types/rule-form'; import { GRAFANA_RULES_SOURCE_NAME } from './datasource'; import { - MANUAL_ROUTING_KEY, alertingRulerRuleToRuleForm, cleanAnnotations, cleanLabels, formValuesToRulerGrafanaRuleDTO, formValuesToRulerRuleDTO, getContactPointsFromDTO, - getDefaultFormValues, - getDefautManualRouting, getNotificationSettingsForDTO, } from './rule-form'; @@ -227,36 +224,6 @@ describe('getNotificationSettingsForDTO', () => { }); }); -describe('getDefautManualRouting', () => { - afterEach(() => { - window.localStorage.clear(); - }); - - it('returns false if the feature toggle is not enabled', () => { - config.featureToggles.alertingSimplifiedRouting = false; - expect(getDefautManualRouting()).toBe(false); - }); - - it('returns true if the feature toggle is enabled and localStorage is not set', () => { - config.featureToggles.alertingSimplifiedRouting = true; - expect(getDefautManualRouting()).toBe(true); - }); - - it('returns false if the feature toggle is enabled and localStorage is set to "false"', () => { - config.featureToggles.alertingSimplifiedRouting = true; - localStorage.setItem(MANUAL_ROUTING_KEY, 'false'); - expect(getDefautManualRouting()).toBe(false); - }); - - it('returns true if the feature toggle is enabled and localStorage is set to any value other than "false"', () => { - config.featureToggles.alertingSimplifiedRouting = true; - localStorage.setItem(MANUAL_ROUTING_KEY, 'true'); - expect(getDefautManualRouting()).toBe(true); - localStorage.removeItem(MANUAL_ROUTING_KEY); - expect(getDefautManualRouting()).toBe(true); - }); -}); - describe('cleanAnnotations', () => { it('should remove falsy KVs', () => { const output = cleanAnnotations([{ key: '', value: '' }]); diff --git a/public/app/features/alerting/unified/utils/rule-form.ts b/public/app/features/alerting/unified/utils/rule-form.ts index de5ee071296..ecea384f9e7 100644 --- a/public/app/features/alerting/unified/utils/rule-form.ts +++ b/public/app/features/alerting/unified/utils/rule-form.ts @@ -1,5 +1,3 @@ -import { clamp, omit } from 'lodash'; - import { DataQuery, DataSourceInstanceSettings, @@ -31,7 +29,6 @@ import { AlertDataQuery, AlertQuery, Annotations, - GrafanaAlertStateDecision, GrafanaNotificationSettings, GrafanaRuleDefinition, Labels, @@ -42,6 +39,8 @@ import { } from 'app/types/unified-alerting-dto'; import { EvalFunction } from '../../state/alertDef'; +import { getDefaultFormValues } from '../rule-editor/formDefaults'; +import { normalizeDefaultAnnotations } from '../rule-editor/formProcessing'; import { AlertManagerManualRouting, ContactPoint, @@ -51,8 +50,7 @@ import { SimplifiedEditor, } from '../types/rule-form'; -import { getRulesAccess } from './access-control'; -import { Annotation, defaultAnnotations } from './constants'; +import { Annotation } from './constants'; import { DataSourceType, GRAFANA_RULES_SOURCE_NAME, @@ -68,84 +66,13 @@ import { isGrafanaRulerRule, isRecordingRulerRule, } from './rules'; -import { formatPrometheusDuration, parseInterval, safeParsePrometheusDuration } from './time'; +import { parseInterval } from './time'; export type PromOrLokiQuery = PromQuery | LokiQuery; export const MANUAL_ROUTING_KEY = 'grafana.alerting.manualRouting'; export const SIMPLIFIED_QUERY_EDITOR_KEY = 'grafana.alerting.simplifiedQueryEditor'; -// even if the min interval is < 1m we should default to 1m, but allow arbitrary values for minInterval > 1m -const GROUP_EVALUATION_MIN_INTERVAL_MS = safeParsePrometheusDuration(config.unifiedAlerting?.minInterval ?? '10s'); -const GROUP_EVALUATION_INTERVAL_LOWER_BOUND = safeParsePrometheusDuration('1m'); -const GROUP_EVALUATION_INTERVAL_UPPER_BOUND = Infinity; - -export const DEFAULT_GROUP_EVALUATION_INTERVAL = formatPrometheusDuration( - clamp(GROUP_EVALUATION_MIN_INTERVAL_MS, GROUP_EVALUATION_INTERVAL_LOWER_BOUND, GROUP_EVALUATION_INTERVAL_UPPER_BOUND) -); - -export const getDefaultFormValues = (): RuleFormValues => { - const { canCreateGrafanaRules, canCreateCloudRules } = getRulesAccess(); - - return Object.freeze({ - name: '', - uid: '', - labels: [{ key: '', value: '' }], - annotations: defaultAnnotations, - dataSourceName: GRAFANA_RULES_SOURCE_NAME, // let's use Grafana-managed alert rule by default - type: canCreateGrafanaRules ? RuleFormType.grafana : canCreateCloudRules ? RuleFormType.cloudAlerting : undefined, // viewers can't create prom alerts - group: '', - - // grafana - folder: undefined, - queries: [], - recordingRulesQueries: [], - condition: '', - noDataState: GrafanaAlertStateDecision.NoData, - execErrState: GrafanaAlertStateDecision.Error, - evaluateFor: DEFAULT_GROUP_EVALUATION_INTERVAL, - evaluateEvery: DEFAULT_GROUP_EVALUATION_INTERVAL, - manualRouting: getDefautManualRouting(), // we default to true if the feature toggle is enabled and the user hasn't set local storage to false - contactPoints: {}, - overrideGrouping: false, - overrideTimings: false, - muteTimeIntervals: [], - editorSettings: getDefaultEditorSettings(), - - // cortex / loki - namespace: '', - expression: '', - forTime: 1, - forTimeUnit: 'm', - }); -}; - -export const getDefautManualRouting = () => { - // first check if feature toggle for simplified routing is enabled - const simplifiedRoutingToggleEnabled = config.featureToggles.alertingSimplifiedRouting ?? false; - if (!simplifiedRoutingToggleEnabled) { - return false; - } - //then, check in local storage if the user has enabled simplified routing - // if it's not set, we'll default to true - const manualRouting = localStorage.getItem(MANUAL_ROUTING_KEY); - return manualRouting !== 'false'; -}; - -function getDefaultEditorSettings() { - const editorSettingsEnabled = config.featureToggles.alertingQueryAndExpressionsStepMode ?? false; - if (!editorSettingsEnabled) { - return undefined; - } - //then, check in local storage if the user has saved last rule with sections simplified - const queryEditorSettings = localStorage.getItem(SIMPLIFIED_QUERY_EDITOR_KEY); - const notificationStepSettings = localStorage.getItem(MANUAL_ROUTING_KEY); - return { - simplifiedQueryEditor: queryEditorSettings !== 'false', - simplifiedNotificationEditor: notificationStepSettings !== 'false', - }; -} - export function formValuesToRulerRuleDTO(values: RuleFormValues): RulerRuleDTO { const { name, expression, forTime, forTimeUnit, keepFiringForTime, keepFiringForTimeUnit, type } = values; @@ -184,26 +111,6 @@ export function listifyLabelsOrAnnotations(item: Labels | Annotations | undefine return list; } -//make sure default annotations are always shown in order even if empty -export function normalizeDefaultAnnotations(annotations: KVObject[]) { - const orderedAnnotations = [...annotations]; - const defaultAnnotationKeys = defaultAnnotations.map((annotation) => annotation.key); - - defaultAnnotationKeys.forEach((defaultAnnotationKey, index) => { - const fieldIndex = orderedAnnotations.findIndex((field) => field.key === defaultAnnotationKey); - - if (fieldIndex === -1) { - //add the default annotation if abstent - const emptyValue = { key: defaultAnnotationKey, value: '' }; - orderedAnnotations.splice(index, 0, emptyValue); - } else if (fieldIndex !== index) { - //move it to the correct position if present - orderedAnnotations.splice(index, 0, orderedAnnotations.splice(fieldIndex, 1)[0]); - } - }); - return orderedAnnotations; -} - export function getNotificationSettingsForDTO( manualRouting: boolean, contactPoints?: AlertManagerManualRouting @@ -902,21 +809,6 @@ export function isPromOrLokiQuery(model: AlertDataQuery): model is PromOrLokiQue return 'expr' in model; } -// the backend will always execute "hidden" queries, so we have no choice but to remove the property in the front-end -// to avoid confusion. The query editor shows them as "disabled" and that's a different semantic meaning. -// furthermore the "AlertingQueryRunner" calls `filterQuery` on each data source and those will skip running queries that are "hidden"." -// It seems like we have no choice but to act like "hidden" queries don't exist in alerting. -export const ignoreHiddenQueries = (ruleDefinition: RuleFormValues): RuleFormValues => { - return { - ...ruleDefinition, - queries: ruleDefinition.queries?.map((query) => omit(query, 'model.hide')), - }; -}; - -export function formValuesFromExistingRule(rule: RuleWithLocation) { - return ignoreHiddenQueries(rulerRuleToFormValues(rule)); -} - export function getInstantFromDataQuery(model: AlertDataQuery, type: string): boolean | undefined { // if the datasource is not prometheus or loki, instant is defined in the model or defaults to undefined if (type !== DataSourceType.Prometheus && type !== DataSourceType.Loki) { diff --git a/public/test/helpers/alertingRuleEditor.tsx b/public/test/helpers/alertingRuleEditor.tsx index 1f1dc3552f2..4b50dce768e 100644 --- a/public/test/helpers/alertingRuleEditor.tsx +++ b/public/test/helpers/alertingRuleEditor.tsx @@ -4,7 +4,8 @@ import { byRole, byTestId, byText } from 'testing-library-selector'; import { selectors } from '@grafana/e2e-selectors'; import { AppNotificationList } from 'app/core/components/AppNotifications/AppNotificationList'; -import RuleEditor from 'app/features/alerting/unified/RuleEditor'; +import RuleEditor from 'app/features/alerting/unified/rule-editor/RuleEditor'; + export enum GrafanaRuleFormStep { Query = 2, Notification = 5, From 52a6c27d99e5d984032cc36802e7cc4dcd2bf79d Mon Sep 17 00:00:00 2001 From: Kristina Date: Tue, 28 Jan 2025 08:00:15 -0600 Subject: [PATCH 24/39] VizTooltip: Check useragent to perform consistently on all mobile devices (#99655) * Add useragent check to be comprehensive * move check outside function and add hopefully clarifying comment --- .../src/components/uPlot/plugins/TooltipPlugin2.tsx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/grafana-ui/src/components/uPlot/plugins/TooltipPlugin2.tsx b/packages/grafana-ui/src/components/uPlot/plugins/TooltipPlugin2.tsx index cb0527a9961..b2e142d6a64 100644 --- a/packages/grafana-ui/src/components/uPlot/plugins/TooltipPlugin2.tsx +++ b/packages/grafana-ui/src/components/uPlot/plugins/TooltipPlugin2.tsx @@ -108,6 +108,8 @@ const maybeZoomAction = (e?: MouseEvent | null) => e != null && !e.ctrlKey && !e const getDataLinksFallback: GetDataLinksCallback = () => []; +const userAgentIsMobile = /Android|iPhone|iPad/i.test(navigator.userAgent); + /** * @alpha */ @@ -666,8 +668,9 @@ export const TooltipPlugin2 = ({ // if not viaSync, re-dispatch real event if (event != null) { - // we expect to re-dispatch mousemove, but on mobile we'll get mouseup or click - const isMobile = event.type !== 'mousemove'; + // we expect to re-dispatch mousemove, but may have a different event type, so create a mousemove event and fire that instead + // this doesn't work for every mobile device, so fall back to checking the useragent as well + const isMobile = event.type !== 'mousemove' || userAgentIsMobile; if (isMobile) { event = new MouseEvent('mousemove', { From 058d3946b7642f8450e07ffe6c7ba56d32d60bc0 Mon Sep 17 00:00:00 2001 From: Ivana Huckova <30407135+ivanahuckova@users.noreply.github.com> Date: Tue, 28 Jan 2025 15:11:10 +0100 Subject: [PATCH 25/39] Zipkin: Remove frontend query running code (#99557) * Zipkin: Remove frontend query running code * Fix lint --- .betterer.results | 3 +- .../feature-toggles/index.md | 1 - .../src/types/featureToggles.gen.ts | 1 - pkg/services/featuremgmt/registry.go | 7 -- pkg/services/featuremgmt/toggles_gen.csv | 1 - pkg/services/featuremgmt/toggles_gen.go | 4 - pkg/services/featuremgmt/toggles_gen.json | 1 + .../datasource/zipkin/datasource.test.ts | 77 ++++++++----------- .../plugins/datasource/zipkin/datasource.ts | 66 +++------------- 9 files changed, 48 insertions(+), 113 deletions(-) diff --git a/.betterer.results b/.betterer.results index c08d07d5459..01d1a68627b 100644 --- a/.betterer.results +++ b/.betterer.results @@ -7550,8 +7550,7 @@ exports[`better eslint`] = { [0, 0, 0, "Unexpected any. Specify a different type.", "2"] ], "public/app/plugins/datasource/zipkin/datasource.ts:5381": [ - [0, 0, 0, "Do not use any type assertions.", "0"], - [0, 0, 0, "Unexpected any. Specify a different type.", "1"] + [0, 0, 0, "Do not use any type assertions.", "0"] ], "public/app/plugins/datasource/zipkin/utils/transforms.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"] diff --git a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md index 3038e1624b3..fecc19478b7 100644 --- a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md +++ b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md @@ -81,7 +81,6 @@ Most [generally available](https://grafana.com/docs/release-life-cycle/#general- | `userStorageAPI` | Enables the user storage API | Yes | | `azureMonitorDisableLogLimit` | Disables the log limit restriction for Azure Monitor when true. The limit is enabled by default. | | | `preinstallAutoUpdate` | Enables automatic updates for pre-installed plugins | Yes | -| `zipkinBackendMigration` | Enables querying Zipkin data source without the proxy | Yes | | `reportingUseRawTimeRange` | Uses the original report or dashboard time range instead of making an absolute transformation | Yes | | `alertingUIOptimizeReducer` | Enables removing the reducer from the alerting UI when creating a new alert rule and using instant query | Yes | | `azureMonitorEnableUserAuth` | Enables user auth for Azure Monitor datasource only | Yes | diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index 04d84ee202e..0a6775edac1 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -229,7 +229,6 @@ export interface FeatureToggles { exploreMetricsRelatedLogs?: boolean; prometheusSpecialCharsInLabelValues?: boolean; enableExtensionsAdminPage?: boolean; - zipkinBackendMigration?: boolean; enableSCIM?: boolean; crashDetection?: boolean; jaegerBackendMigration?: boolean; diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index a717b3bb707..a4baa2652bf 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -1587,13 +1587,6 @@ var ( Owner: grafanaPluginsPlatformSquad, RequiresRestart: true, }, - { - Name: "zipkinBackendMigration", - Description: "Enables querying Zipkin data source without the proxy", - Stage: FeatureStageGeneralAvailability, - Owner: grafanaOSSBigTent, - Expression: "true", // enabled by default - }, { Name: "enableSCIM", Description: "Enables SCIM support for user and group management", diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv index 5a23134e827..34f40c2585d 100644 --- a/pkg/services/featuremgmt/toggles_gen.csv +++ b/pkg/services/featuremgmt/toggles_gen.csv @@ -210,7 +210,6 @@ passwordlessMagicLinkAuthentication,experimental,@grafana/identity-access-team,f exploreMetricsRelatedLogs,experimental,@grafana/observability-metrics,false,false,true prometheusSpecialCharsInLabelValues,experimental,@grafana/oss-big-tent,false,false,true enableExtensionsAdminPage,experimental,@grafana/plugins-platform-backend,false,true,false -zipkinBackendMigration,GA,@grafana/oss-big-tent,false,false,false enableSCIM,experimental,@grafana/identity-access-team,false,false,false crashDetection,experimental,@grafana/observability-traces-and-profiling,false,false,true jaegerBackendMigration,experimental,@grafana/oss-big-tent,false,false,false diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go index 6ca1cd23fef..06bc4b00564 100644 --- a/pkg/services/featuremgmt/toggles_gen.go +++ b/pkg/services/featuremgmt/toggles_gen.go @@ -851,10 +851,6 @@ const ( // Enables the extension admin page regardless of development mode FlagEnableExtensionsAdminPage = "enableExtensionsAdminPage" - // FlagZipkinBackendMigration - // Enables querying Zipkin data source without the proxy - FlagZipkinBackendMigration = "zipkinBackendMigration" - // FlagEnableSCIM // Enables SCIM support for user and group management FlagEnableSCIM = "enableSCIM" diff --git a/pkg/services/featuremgmt/toggles_gen.json b/pkg/services/featuremgmt/toggles_gen.json index 0ac61ba717c..7ef236e5194 100644 --- a/pkg/services/featuremgmt/toggles_gen.json +++ b/pkg/services/featuremgmt/toggles_gen.json @@ -4062,6 +4062,7 @@ "name": "zipkinBackendMigration", "resourceVersion": "1733846643829", "creationTimestamp": "2024-11-07T09:35:53Z", + "deletionTimestamp": "2025-01-27T11:47:54Z", "annotations": { "grafana.app/updatedTimestamp": "2024-12-10 16:04:03.82919 +0000 UTC" } diff --git a/public/app/plugins/datasource/zipkin/datasource.test.ts b/public/app/plugins/datasource/zipkin/datasource.test.ts index 4123de7960d..5b1ab000004 100644 --- a/public/app/plugins/datasource/zipkin/datasource.test.ts +++ b/public/app/plugins/datasource/zipkin/datasource.test.ts @@ -1,5 +1,4 @@ import { lastValueFrom, of } from 'rxjs'; -import { createFetchResponse } from 'test/helpers/createFetchResponse'; import { DataFrameView, @@ -8,52 +7,49 @@ import { DataSourcePluginMeta, FieldType, } from '@grafana/data'; -import { BackendSrv, TemplateSrv } from '@grafana/runtime'; +import { BackendSrv, getBackendSrv, setBackendSrv, TemplateSrv } from '@grafana/runtime'; import { addNodeGraphFramesToResponse, ZipkinDatasource } from './datasource'; import mockJson from './mocks/mockJsonResponse.json'; import { mockTraceDataFrame } from './mocks/mockTraceDataFrame'; -import { ZipkinQuery, ZipkinSpan } from './types'; -import { traceFrameFields, zipkinResponse } from './utils/testData'; +import { ZipkinQuery } from './types'; -export const backendSrv = { fetch: jest.fn() } as unknown as BackendSrv; - -jest.mock('@grafana/runtime', () => ({ - ...jest.requireActual('@grafana/runtime'), - getBackendSrv: () => backendSrv, -})); +const templateSrv: TemplateSrv = { + replace: jest.fn().mockImplementation((value) => value), + getVariables: jest.fn(), + containsTemplate: jest.fn(), + updateTimeRange: jest.fn(), +}; describe('ZipkinDatasource', () => { + let origBackendSrv: BackendSrv; + let ds: ZipkinDatasource; + beforeEach(() => { + origBackendSrv = getBackendSrv(); + setBackendSrv({ ...origBackendSrv, fetch: jest.fn().mockReturnValue(of({ data: {} })) }); + ds = new ZipkinDatasource(defaultSettings, templateSrv); + }); + + afterEach(() => { + setBackendSrv(origBackendSrv); + jest.clearAllMocks(); + }); describe('query', () => { - const templateSrv: TemplateSrv = { - replace: jest.fn(), - getVariables: jest.fn(), - containsTemplate: jest.fn(), - updateTimeRange: jest.fn(), - }; - it('runs query', async () => { - setupBackendSrv(zipkinResponse); - const ds = new ZipkinDatasource(defaultSettings, templateSrv); - await expect(ds.query({ targets: [{ query: '12345' }] } as DataQueryRequest)).toEmitValuesWith( - (val) => { - expect(val[0].data[0].fields).toMatchObject(traceFrameFields); - } - ); - }); + const origBackendSrv = getBackendSrv(); + setBackendSrv({ + ...origBackendSrv, + fetch: jest.fn().mockReturnValue(of({ data: [] })), + }); - it('runs query with traceId that includes special characters', async () => { - setupBackendSrv(zipkinResponse); - const ds = new ZipkinDatasource(defaultSettings, templateSrv); - await expect(ds.query({ targets: [{ query: 'a/b' }] } as DataQueryRequest)).toEmitValuesWith( - (val) => { - expect(val[0].data[0].fields).toMatchObject(traceFrameFields); - } - ); + const response = await lastValueFrom(ds.query({ targets: [{ query: 'test' }] } as DataQueryRequest)); + expect(response).toEqual({ + state: 'Done', + data: [], + }); }); it('should handle json file upload', async () => { - const ds = new ZipkinDatasource(defaultSettings); ds.uploadedJson = JSON.stringify(mockJson); const response = await lastValueFrom( ds.query({ @@ -67,7 +63,6 @@ describe('ZipkinDatasource', () => { }); it('should fail on invalid json file upload', async () => { - const ds = new ZipkinDatasource(defaultSettings); ds.uploadedJson = JSON.stringify({ key: 'value', arr: [] }); const response = await lastValueFrom( ds.query({ @@ -81,7 +76,10 @@ describe('ZipkinDatasource', () => { describe('metadataRequest', () => { it('runs query', async () => { - setupBackendSrv(['service 1', 'service 2'] as unknown as ZipkinSpan[]); + setBackendSrv({ + ...origBackendSrv, + fetch: jest.fn().mockReturnValue(of({ data: ['service 1', 'service 2'] })), + }); const ds = new ZipkinDatasource(defaultSettings); const response = await ds.metadataRequest('services'); expect(response).toEqual(['service 1', 'service 2']); @@ -118,13 +116,6 @@ describe('addNodeGraphFramesToResponse', () => { }); }); -function setupBackendSrv(response: ZipkinSpan[]) { - const defaultMock = () => of(createFetchResponse(response)); - - const fetchMock = jest.spyOn(backendSrv, 'fetch'); - fetchMock.mockImplementation(defaultMock); -} - const defaultSettings: DataSourceInstanceSettings = { id: 1, uid: '1', diff --git a/public/app/plugins/datasource/zipkin/datasource.ts b/public/app/plugins/datasource/zipkin/datasource.ts index 54ff15dafb1..7b49e7a0c1c 100644 --- a/public/app/plugins/datasource/zipkin/datasource.ts +++ b/public/app/plugins/datasource/zipkin/datasource.ts @@ -1,4 +1,4 @@ -import { lastValueFrom, Observable, of } from 'rxjs'; +import { Observable, of } from 'rxjs'; import { map } from 'rxjs/operators'; import { @@ -9,26 +9,15 @@ import { FieldType, createDataFrame, ScopedVars, - urlUtil, toDataFrame, } from '@grafana/data'; import { createNodeGraphFrames, NodeGraphOptions, SpanBarOptions } from '@grafana/o11y-ds-frontend'; -import { - BackendSrvRequest, - config, - DataSourceWithBackend, - FetchResponse, - getBackendSrv, - getTemplateSrv, - TemplateSrv, -} from '@grafana/runtime'; +import { DataSourceWithBackend, getTemplateSrv, TemplateSrv } from '@grafana/runtime'; import { ZipkinQuery, ZipkinSpan } from './types'; import { createGraphFrames } from './utils/graphTransform'; import { transformResponse } from './utils/transforms'; -const apiPrefix = '/api/v2'; - export interface ZipkinJsonData extends DataSourceJsonData { nodeGraph?: NodeGraphOptions; } @@ -38,7 +27,7 @@ export class ZipkinDatasource extends DataSourceWithBackend, + instanceSettings: DataSourceInstanceSettings, private readonly templateSrv: TemplateSrv = getTemplateSrv() ) { super(instanceSettings); @@ -61,40 +50,24 @@ export class ZipkinDatasource extends DataSourceWithBackend { - if (this.nodeGraph?.enabled) { - return addNodeGraphFramesToResponse(response); - } - return response; - }) - ); - } - const query = this.applyTemplateVariables(target, options.scopedVars); - return this.request(`${apiPrefix}/trace/${encodeURIComponent(query.query)}`).pipe( - map((res) => responseToDataQueryResponse(res, this.nodeGraph?.enabled)) + return super.query(options).pipe( + map((response) => { + if (this.nodeGraph?.enabled) { + return addNodeGraphFramesToResponse(response); + } + return response; + }) ); } return of(emptyDataQueryResponse); } async metadataRequest(url: string, params?: Record) { - if (config.featureToggles.zipkinBackendMigration) { - return await this.getResource(url, params); - } - const urlWithPrefix = `${apiPrefix}/${url}`; - const res = await lastValueFrom(this.request(urlWithPrefix, params, { hideFromInspector: true })); - return res.data; + return await this.getResource(url, params); } async testDatasource(): Promise<{ status: string; message: string }> { - if (config.featureToggles.zipkinBackendMigration) { - return await super.testDatasource(); - } - - await this.metadataRequest('services'); - return { status: 'success', message: 'Data source is working' }; + return await super.testDatasource(); } getQueryDisplayText(query: ZipkinQuery): string { @@ -123,21 +96,6 @@ export class ZipkinDatasource extends DataSourceWithBackend( - apiUrl: string, - data?: unknown, - options?: Partial - ): Observable> { - const params = data ? urlUtil.serializeParams(data) : ''; - const url = `${this.instanceSettings.url}${apiUrl}${params.length ? `?${params}` : ''}`; - const req = { - ...options, - url, - }; - - return getBackendSrv().fetch(req); - } } function responseToDataQueryResponse(response: { data: ZipkinSpan[] }, nodeGraph = false): DataQueryResponse { From 05905a50692ae90a973ed6e8f019188ae703797b Mon Sep 17 00:00:00 2001 From: Ashley Harrison Date: Tue, 28 Jan 2025 14:16:36 +0000 Subject: [PATCH 26/39] Chore: bracket properly to ensure name is set correctly (#99660) bracket properly to ensure name is set --- packages/grafana-data/src/themes/createTheme.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/grafana-data/src/themes/createTheme.ts b/packages/grafana-data/src/themes/createTheme.ts index a224f1be11b..554e567c7c4 100644 --- a/packages/grafana-data/src/themes/createTheme.ts +++ b/packages/grafana-data/src/themes/createTheme.ts @@ -41,7 +41,7 @@ export function createTheme(options: NewThemeOptions = {}): GrafanaTheme2 { const visualization = createVisualizationColors(colors); const theme = { - name: (name ?? colors.mode === 'dark') ? 'Dark' : 'Light', + name: name ?? (colors.mode === 'dark' ? 'Dark' : 'Light'), isDark: colors.mode === 'dark', isLight: colors.mode === 'light', colors, From 0cef2b9ae7cc2f2ac82cdd4dd4768c18f95160f6 Mon Sep 17 00:00:00 2001 From: Stephanie Hingtgen Date: Tue, 28 Jan 2025 07:17:52 -0700 Subject: [PATCH 27/39] Dashboard Versions: Make compatible with app platform (#99327) --- pkg/api/dashboard.go | 34 ++- pkg/api/dashboard_test.go | 12 +- pkg/api/dtos/dashboard.go | 4 +- pkg/components/dashdiffs/compare.go | 2 +- .../dashboard/legacy/query_dashboards.sql | 6 +- .../apis/dashboard/legacy/sql_dashboards.go | 26 +- pkg/registry/apis/dashboard/legacy/storage.go | 2 +- ...uery_dashboards-history_uid_at_version.sql | 4 +- ...uery_dashboards-history_uid_at_version.sql | 4 +- ...uery_dashboards-history_uid_at_version.sql | 4 +- pkg/registry/apis/dashboard/legacy/utils.go | 9 - .../apis/dashboard/legacy/utils_test.go | 13 - pkg/services/apiserver/client/client.go | 37 ++- pkg/services/apiserver/client/client_mock.go | 13 +- pkg/services/apiserver/client/client_test.go | 34 +++ .../dashboards/service/dashboard_service.go | 41 +-- .../service/dashboard_service_test.go | 67 +++-- pkg/services/dashboardversion/dashver.go | 2 +- .../dashboardversion/dashverimpl/dashver.go | 187 ++++++++++++- .../dashverimpl/dashver_test.go | 114 ++++++-- .../dashverimpl/store_test.go | 4 +- .../dashboardversion/dashvertest/fake.go | 8 +- pkg/services/dashboardversion/model.go | 24 +- pkg/storage/unified/resource/cdk_backend.go | 5 + pkg/storage/unified/resource/server.go | 10 + pkg/storage/unified/sql/backend.go | 4 + .../unified/sql/data/resource_history_get.sql | 2 +- .../unified/sql/test/integration_test.go | 77 ++++++ ...rce_history_get-read trash second page.sql | 2 +- ...rce_history_get-read trash second page.sql | 2 +- ...rce_history_get-read trash second page.sql | 2 +- .../dashboard-scene/scene/DashboardScene.tsx | 8 +- .../settings/VersionsEditView.test.tsx | 79 +++--- .../settings/VersionsEditView.tsx | 25 +- .../version-history/HistorySrv.test.ts | 4 +- .../settings/version-history/HistorySrv.ts | 1 + .../__mocks__/dashboardHistoryMocks.ts | 97 +++---- .../VersionsSettings.test.tsx | 41 ++- .../DashboardSettings/VersionsSettings.tsx | 25 +- .../DashboardSettings/__mocks__/versions.ts | 249 +++++++++--------- .../VersionHistory/RevertDashboardModal.tsx | 5 +- .../VersionHistoryComparison.tsx | 1 + .../VersionHistory/VersionHistoryTable.tsx | 1 + .../VersionHistory/useDashboardRestore.tsx | 9 +- 44 files changed, 887 insertions(+), 413 deletions(-) delete mode 100644 pkg/registry/apis/dashboard/legacy/utils.go delete mode 100644 pkg/registry/apis/dashboard/legacy/utils_test.go create mode 100644 pkg/services/apiserver/client/client_test.go diff --git a/pkg/api/dashboard.go b/pkg/api/dashboard.go index 486911d626c..5079d7d9877 100644 --- a/pkg/api/dashboard.go +++ b/pkg/api/dashboard.go @@ -24,6 +24,7 @@ import ( contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/dashboards" dashver "github.com/grafana/grafana/pkg/services/dashboardversion" + "github.com/grafana/grafana/pkg/services/dashboardversion/dashverimpl" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/folder" "github.com/grafana/grafana/pkg/services/guardian" @@ -828,21 +829,22 @@ func (hs *HTTPServer) GetDashboardVersions(c *contextmodel.ReqContext) response. } query := dashver.ListDashboardVersionsQuery{ - OrgID: c.SignedInUser.GetOrgID(), - DashboardID: dash.ID, - DashboardUID: dash.UID, - Limit: c.QueryInt("limit"), - Start: c.QueryInt("start"), + OrgID: c.SignedInUser.GetOrgID(), + DashboardID: dash.ID, + DashboardUID: dash.UID, + Limit: c.QueryInt("limit"), + Start: c.QueryInt("start"), + ContinueToken: c.Query("continueToken"), } - versions, err := hs.dashboardVersionService.List(c.Req.Context(), &query) + resp, err := hs.dashboardVersionService.List(c.Req.Context(), &query) if err != nil { return response.Error(http.StatusNotFound, fmt.Sprintf("No versions found for dashboardId %d", dash.ID), err) } - loginMem := make(map[int64]string, len(versions)) - res := make([]dashver.DashboardVersionMeta, 0, len(versions)) - for _, version := range versions { + loginMem := make(map[int64]string, len(resp.Versions)) + res := make([]dashver.DashboardVersionMeta, 0, len(resp.Versions)) + for _, version := range resp.Versions { msg := version.Message if version.RestoredFrom == version.Version { msg = "Initial save (created by migration)" @@ -883,7 +885,10 @@ func (hs *HTTPServer) GetDashboardVersions(c *contextmodel.ReqContext) response. }) } - return response.JSON(http.StatusOK, res) + return response.JSON(http.StatusOK, dashver.DashboardVersionResponseMeta{ + Versions: res, + ContinueToken: resp.ContinueToken, + }) } // swagger:route GET /dashboards/id/{DashboardID}/versions/{DashboardVersionID} dashboard_versions getDashboardVersionByID @@ -943,12 +948,15 @@ func (hs *HTTPServer) GetDashboardVersion(c *contextmodel.ReqContext) response.R return dashboardGuardianResponse(err) } - version, _ := strconv.ParseInt(web.Params(c.Req)[":id"], 10, 32) + version, err := strconv.ParseInt(web.Params(c.Req)[":id"], 10, 64) + if err != nil { + return response.Err(err) + } query := dashver.GetDashboardVersionQuery{ OrgID: c.SignedInUser.GetOrgID(), DashboardID: dash.ID, DashboardUID: dash.UID, - Version: int(version), + Version: version, } res, err := hs.dashboardVersionService.Get(c.Req.Context(), &query) @@ -1158,7 +1166,7 @@ func (hs *HTTPServer) RestoreDashboardVersion(c *contextmodel.ReqContext) respon saveCmd.Dashboard = version.Data saveCmd.Dashboard.Set("version", dash.Version) saveCmd.Dashboard.Set("uid", dash.UID) - saveCmd.Message = fmt.Sprintf("Restored from version %d", version.Version) + saveCmd.Message = dashverimpl.DashboardRestoreMessage(version.Version) // nolint:staticcheck saveCmd.FolderID = dash.FolderID metrics.MFolderIDsAPICount.WithLabelValues(metrics.RestoreDashboardVersion).Inc() diff --git a/pkg/api/dashboard_test.go b/pkg/api/dashboard_test.go index 34c6c2c4dbe..a08deefb4bb 100644 --- a/pkg/api/dashboard_test.go +++ b/pkg/api/dashboard_test.go @@ -743,10 +743,10 @@ func TestDashboardVersionsAPIEndpoint(t *testing.T) { }).callGetDashboardVersions(sc) assert.Equal(t, http.StatusOK, sc.resp.Code) - var versions []dashver.DashboardVersionMeta + var versions dashver.DashboardVersionResponseMeta err := json.NewDecoder(sc.resp.Body).Decode(&versions) require.NoError(t, err) - for _, v := range versions { + for _, v := range versions.Versions { assert.Equal(t, "test-user", v.CreatedBy) } }, mockSQLStore) @@ -769,10 +769,10 @@ func TestDashboardVersionsAPIEndpoint(t *testing.T) { }).callGetDashboardVersions(sc) assert.Equal(t, http.StatusOK, sc.resp.Code) - var versions []dashver.DashboardVersionMeta + var versions dashver.DashboardVersionResponseMeta err := json.NewDecoder(sc.resp.Body).Decode(&versions) require.NoError(t, err) - for _, v := range versions { + for _, v := range versions.Versions { assert.Equal(t, anonString, v.CreatedBy) } }, mockSQLStore) @@ -795,10 +795,10 @@ func TestDashboardVersionsAPIEndpoint(t *testing.T) { }).callGetDashboardVersions(sc) assert.Equal(t, http.StatusOK, sc.resp.Code) - var versions []dashver.DashboardVersionMeta + var versions dashver.DashboardVersionResponseMeta err := json.NewDecoder(sc.resp.Body).Decode(&versions) require.NoError(t, err) - for _, v := range versions { + for _, v := range versions.Versions { assert.Equal(t, anonString, v.CreatedBy) } }, mockSQLStore) diff --git a/pkg/api/dtos/dashboard.go b/pkg/api/dtos/dashboard.go index 9d8f96f8c7a..73c3d7cae6f 100644 --- a/pkg/api/dtos/dashboard.go +++ b/pkg/api/dtos/dashboard.go @@ -54,10 +54,10 @@ type CalculateDiffOptions struct { type CalculateDiffTarget struct { DashboardId int64 `json:"dashboardId"` - Version int `json:"version"` + Version int64 `json:"version"` UnsavedDashboard *simplejson.Json `json:"unsavedDashboard"` } type RestoreDashboardVersionCommand struct { - Version int `json:"version" binding:"Required"` + Version int64 `json:"version" binding:"Required"` } diff --git a/pkg/components/dashdiffs/compare.go b/pkg/components/dashdiffs/compare.go index e5c6e841901..7bb06a92ff2 100644 --- a/pkg/components/dashdiffs/compare.go +++ b/pkg/components/dashdiffs/compare.go @@ -34,7 +34,7 @@ type Options struct { type DiffTarget struct { DashboardId int64 - Version int + Version int64 UnsavedDashboard *simplejson.Json } diff --git a/pkg/registry/apis/dashboard/legacy/query_dashboards.sql b/pkg/registry/apis/dashboard/legacy/query_dashboards.sql index e5d532ffc87..c4901404f25 100644 --- a/pkg/registry/apis/dashboard/legacy/query_dashboards.sql +++ b/pkg/registry/apis/dashboard/legacy/query_dashboards.sql @@ -30,11 +30,11 @@ WHERE dashboard.is_folder = {{ .Arg .Query.GetFolders }} {{ if .Query.Version }} AND dashboard_version.version = {{ .Arg .Query.Version }} {{ else if .Query.LastID }} - AND dashboard_version.version < {{ .Arg .Query.LastID }} + AND dashboard_version.version <= {{ .Arg .Query.LastID }} {{ end }} ORDER BY - dashboard_version.created ASC, - dashboard_version.version ASC, + dashboard_version.created DESC, + dashboard_version.version DESC, dashboard.uid ASC {{ else }} {{ if .Query.UID }} diff --git a/pkg/registry/apis/dashboard/legacy/sql_dashboards.go b/pkg/registry/apis/dashboard/legacy/sql_dashboards.go index 999586b13a1..f3b86f8b639 100644 --- a/pkg/registry/apis/dashboard/legacy/sql_dashboards.go +++ b/pkg/registry/apis/dashboard/legacy/sql_dashboards.go @@ -115,8 +115,9 @@ func (a *dashboardSqlAccess) getRows(ctx context.Context, sql *legacysql.LegacyD rows = nil } return &rowsWrapper{ - rows: rows, - a: a, + rows: rows, + a: a, + history: query.GetHistory, // This looks up rules from the permissions on a user canReadDashboard: func(scopes ...string) bool { return true // ??? @@ -128,8 +129,9 @@ func (a *dashboardSqlAccess) getRows(ctx context.Context, sql *legacysql.LegacyD var _ resource.ListIterator = (*rowsWrapper)(nil) type rowsWrapper struct { - a *dashboardSqlAccess - rows *sql.Rows + a *dashboardSqlAccess + rows *sql.Rows + history bool canReadDashboard func(scopes ...string) bool @@ -157,7 +159,7 @@ func (r *rowsWrapper) Next() bool { // breaks after first readable value for r.rows.Next() { - r.row, err = r.a.scanRow(r.rows) + r.row, err = r.a.scanRow(r.rows, r.history) if err != nil { r.err = err return false @@ -187,6 +189,11 @@ func (r *rowsWrapper) ContinueToken() string { return r.row.token.String() } +// ContinueTokenWithCurrentRV implements resource.ListIterator. +func (r *rowsWrapper) ContinueTokenWithCurrentRV() string { + return r.row.token.String() +} + // Error implements resource.ListIterator. func (r *rowsWrapper) Error() error { return r.err @@ -218,7 +225,7 @@ func (r *rowsWrapper) Value() []byte { return b } -func (a *dashboardSqlAccess) scanRow(rows *sql.Rows) (*dashboardRow, error) { +func (a *dashboardSqlAccess) scanRow(rows *sql.Rows, history bool) (*dashboardRow, error) { dash := &dashboard.Dashboard{ TypeMeta: dashboard.DashboardResourceInfo.TypeMeta(), ObjectMeta: metav1.ObjectMeta{Annotations: make(map[string]string)}, @@ -255,8 +262,12 @@ func (a *dashboardSqlAccess) scanRow(rows *sql.Rows) (*dashboardRow, error) { ) row.token = &continueToken{orgId: orgId, id: dashboard_id} + // when listing from the history table, we want to use the version as the ID to continue from + if history { + row.token.id = version + } if err == nil { - row.RV = getResourceVersion(dashboard_id, version) + row.RV = version dash.ResourceVersion = fmt.Sprintf("%d", row.RV) dash.Namespace = a.namespacer(orgId) dash.UID = gapiutil.CalculateClusterWideUID(dash) @@ -405,6 +416,7 @@ func (a *dashboardSqlAccess) SaveDashboard(ctx context.Context, orgId int64, das } out, err := a.dashStore.SaveDashboard(ctx, dashboards.SaveDashboardCommand{ OrgID: orgId, + Message: meta.GetMessage(), PluginID: service.GetPluginIDFromMeta(meta), Dashboard: simplejson.NewFromAny(dash.Spec.UnstructuredContent()), FolderUID: meta.GetFolder(), diff --git a/pkg/registry/apis/dashboard/legacy/storage.go b/pkg/registry/apis/dashboard/legacy/storage.go index e5f51bf78f9..7064d1d9994 100644 --- a/pkg/registry/apis/dashboard/legacy/storage.go +++ b/pkg/registry/apis/dashboard/legacy/storage.go @@ -138,7 +138,7 @@ func (a *dashboardSqlAccess) ReadResource(ctx context.Context, req *resource.Rea } version := int64(0) if req.ResourceVersion > 0 { - version = getVersionFromRV(req.ResourceVersion) + version = req.ResourceVersion } dash, rv, err := a.GetDashboard(ctx, info.OrgID, req.Key.Name, version) diff --git a/pkg/registry/apis/dashboard/legacy/testdata/mysql--query_dashboards-history_uid_at_version.sql b/pkg/registry/apis/dashboard/legacy/testdata/mysql--query_dashboards-history_uid_at_version.sql index d85d3bbd8c8..728e9a8e72b 100755 --- a/pkg/registry/apis/dashboard/legacy/testdata/mysql--query_dashboards-history_uid_at_version.sql +++ b/pkg/registry/apis/dashboard/legacy/testdata/mysql--query_dashboards-history_uid_at_version.sql @@ -19,6 +19,6 @@ WHERE dashboard.is_folder = FALSE AND dashboard.uid = 'UUU' AND dashboard_version.version = 3 ORDER BY - dashboard_version.created ASC, - dashboard_version.version ASC, + dashboard_version.created DESC, + dashboard_version.version DESC, dashboard.uid ASC diff --git a/pkg/registry/apis/dashboard/legacy/testdata/postgres--query_dashboards-history_uid_at_version.sql b/pkg/registry/apis/dashboard/legacy/testdata/postgres--query_dashboards-history_uid_at_version.sql index 5fe3708643a..b149e393780 100755 --- a/pkg/registry/apis/dashboard/legacy/testdata/postgres--query_dashboards-history_uid_at_version.sql +++ b/pkg/registry/apis/dashboard/legacy/testdata/postgres--query_dashboards-history_uid_at_version.sql @@ -19,6 +19,6 @@ WHERE dashboard.is_folder = FALSE AND dashboard.uid = 'UUU' AND dashboard_version.version = 3 ORDER BY - dashboard_version.created ASC, - dashboard_version.version ASC, + dashboard_version.created DESC, + dashboard_version.version DESC, dashboard.uid ASC diff --git a/pkg/registry/apis/dashboard/legacy/testdata/sqlite--query_dashboards-history_uid_at_version.sql b/pkg/registry/apis/dashboard/legacy/testdata/sqlite--query_dashboards-history_uid_at_version.sql index 5fe3708643a..b149e393780 100755 --- a/pkg/registry/apis/dashboard/legacy/testdata/sqlite--query_dashboards-history_uid_at_version.sql +++ b/pkg/registry/apis/dashboard/legacy/testdata/sqlite--query_dashboards-history_uid_at_version.sql @@ -19,6 +19,6 @@ WHERE dashboard.is_folder = FALSE AND dashboard.uid = 'UUU' AND dashboard_version.version = 3 ORDER BY - dashboard_version.created ASC, - dashboard_version.version ASC, + dashboard_version.created DESC, + dashboard_version.version DESC, dashboard.uid ASC diff --git a/pkg/registry/apis/dashboard/legacy/utils.go b/pkg/registry/apis/dashboard/legacy/utils.go deleted file mode 100644 index c61db27788a..00000000000 --- a/pkg/registry/apis/dashboard/legacy/utils.go +++ /dev/null @@ -1,9 +0,0 @@ -package legacy - -func getResourceVersion(id int64, version int64) int64 { - return version + (id * 10000000) -} - -func getVersionFromRV(rv int64) int64 { - return rv % 10000000 -} diff --git a/pkg/registry/apis/dashboard/legacy/utils_test.go b/pkg/registry/apis/dashboard/legacy/utils_test.go deleted file mode 100644 index a058dae612a..00000000000 --- a/pkg/registry/apis/dashboard/legacy/utils_test.go +++ /dev/null @@ -1,13 +0,0 @@ -package legacy - -import ( - "testing" - - "github.com/stretchr/testify/require" -) - -func TestVersionHacks(t *testing.T) { - rv := getResourceVersion(123, 456) - require.Equal(t, int64(1230000456), rv) - require.Equal(t, int64(456), getVersionFromRV(rv)) -} diff --git a/pkg/services/apiserver/client/client.go b/pkg/services/apiserver/client/client.go index 2e25ca07ee2..2f45e426bc3 100644 --- a/pkg/services/apiserver/client/client.go +++ b/pkg/services/apiserver/client/client.go @@ -2,7 +2,10 @@ package client import ( "context" + "errors" "fmt" + "strconv" + "strings" "time" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -16,6 +19,7 @@ import ( "github.com/grafana/grafana/pkg/services/apiserver" "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request" "github.com/grafana/grafana/pkg/services/dashboards" + "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/storage/unified/resource" k8sUser "k8s.io/apiserver/pkg/authentication/user" @@ -24,7 +28,7 @@ import ( type K8sHandler interface { GetNamespace(orgID int64) string - Get(ctx context.Context, name string, orgID int64, subresource ...string) (*unstructured.Unstructured, error) + Get(ctx context.Context, name string, orgID int64, options v1.GetOptions, subresource ...string) (*unstructured.Unstructured, error) Create(ctx context.Context, obj *unstructured.Unstructured, orgID int64) (*unstructured.Unstructured, error) Update(ctx context.Context, obj *unstructured.Unstructured, orgID int64) (*unstructured.Unstructured, error) Delete(ctx context.Context, name string, orgID int64, options v1.DeleteOptions) error @@ -32,6 +36,7 @@ type K8sHandler interface { List(ctx context.Context, orgID int64, options v1.ListOptions) (*unstructured.UnstructuredList, error) Search(ctx context.Context, orgID int64, in *resource.ResourceSearchRequest) (*resource.ResourceSearchResponse, error) GetStats(ctx context.Context, orgID int64) (*resource.ResourceStatsResponse, error) + GetUserFromMeta(ctx context.Context, userMeta string) (*user.User, error) } var _ K8sHandler = (*k8sHandler)(nil) @@ -41,9 +46,10 @@ type k8sHandler struct { gvr schema.GroupVersionResource restConfigProvider apiserver.RestConfigProvider searcher resource.ResourceIndexClient + userService user.Service } -func NewK8sHandler(cfg *setting.Cfg, namespacer request.NamespaceMapper, gvr schema.GroupVersionResource, restConfigProvider apiserver.RestConfigProvider, searcher resource.ResourceIndexClient, dashStore dashboards.Store) K8sHandler { +func NewK8sHandler(cfg *setting.Cfg, namespacer request.NamespaceMapper, gvr schema.GroupVersionResource, restConfigProvider apiserver.RestConfigProvider, searcher resource.ResourceIndexClient, dashStore dashboards.Store, userSvc user.Service) K8sHandler { legacySearcher := legacysearcher.NewDashboardSearchClient(dashStore) searchClient := resource.NewSearchClient(cfg, setting.UnifiedStorageConfigKeyDashboard, searcher, legacySearcher) return &k8sHandler{ @@ -51,6 +57,7 @@ func NewK8sHandler(cfg *setting.Cfg, namespacer request.NamespaceMapper, gvr sch gvr: gvr, restConfigProvider: restConfigProvider, searcher: searchClient, + userService: userSvc, } } @@ -58,7 +65,7 @@ func (h *k8sHandler) GetNamespace(orgID int64) string { return h.namespacer(orgID) } -func (h *k8sHandler) Get(ctx context.Context, name string, orgID int64, subresource ...string) (*unstructured.Unstructured, error) { +func (h *k8sHandler) Get(ctx context.Context, name string, orgID int64, options v1.GetOptions, subresource ...string) (*unstructured.Unstructured, error) { // create a new context - prevents issues when the request stems from the k8s api itself // otherwise the context goes through the handlers twice and causes issues newCtx, cancel, err := h.getK8sContext(ctx) @@ -73,7 +80,7 @@ func (h *k8sHandler) Get(ctx context.Context, name string, orgID int64, subresou return nil, nil } - return client.Get(newCtx, name, v1.GetOptions{}, subresource...) + return client.Get(newCtx, name, options, subresource...) } func (h *k8sHandler) Create(ctx context.Context, obj *unstructured.Unstructured, orgID int64) (*unstructured.Unstructured, error) { @@ -191,6 +198,28 @@ func (h *k8sHandler) GetStats(ctx context.Context, orgID int64) (*resource.Resou }) } +// GetUserFromMeta takes what meta accessor gives you from `GetCreatedBy` or `GetUpdatedBy` and returns the user +func (h *k8sHandler) GetUserFromMeta(ctx context.Context, userMeta string) (*user.User, error) { + parts := strings.Split(userMeta, ":") + if len(parts) < 2 { + return &user.User{}, nil + } + meta := parts[1] + + userId, err := strconv.ParseInt(meta, 10, 64) + var u *user.User + if err == nil { + u, err = h.userService.GetByID(ctx, &user.GetUserByIDQuery{ID: userId}) + } else { + u, err = h.userService.GetByUID(ctx, &user.GetUserByUIDQuery{UID: meta}) + } + + if err != nil && errors.Is(err, user.ErrUserNotFound) { + return &user.User{}, nil + } + return u, err +} + func (h *k8sHandler) getClient(ctx context.Context, orgID int64) (dynamic.ResourceInterface, bool) { cfg := h.restConfigProvider.GetRestConfig(ctx) if cfg == nil { diff --git a/pkg/services/apiserver/client/client_mock.go b/pkg/services/apiserver/client/client_mock.go index f3a4e347e2f..b5e2fc2eecb 100644 --- a/pkg/services/apiserver/client/client_mock.go +++ b/pkg/services/apiserver/client/client_mock.go @@ -5,6 +5,7 @@ import ( "github.com/stretchr/testify/mock" + "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/storage/unified/resource" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" @@ -21,8 +22,8 @@ func (m *MockK8sHandler) GetNamespace(orgID int64) string { return args.String(0) } -func (m *MockK8sHandler) Get(ctx context.Context, name string, orgID int64, subresource ...string) (*unstructured.Unstructured, error) { - args := m.Called(ctx, name, orgID, subresource) +func (m *MockK8sHandler) Get(ctx context.Context, name string, orgID int64, options v1.GetOptions, subresource ...string) (*unstructured.Unstructured, error) { + args := m.Called(ctx, name, orgID, options, subresource) if args.Get(0) == nil { return nil, args.Error(1) } @@ -79,3 +80,11 @@ func (m *MockK8sHandler) GetStats(ctx context.Context, orgID int64) (*resource.R } return args.Get(0).(*resource.ResourceStatsResponse), args.Error(1) } + +func (m *MockK8sHandler) GetUserFromMeta(ctx context.Context, userMeta string) (*user.User, error) { + args := m.Called(ctx, userMeta) + if args.Get(0) == nil { + return nil, args.Error(1) + } + return args.Get(0).(*user.User), args.Error(1) +} diff --git a/pkg/services/apiserver/client/client_test.go b/pkg/services/apiserver/client/client_test.go new file mode 100644 index 00000000000..c97c5f0a31d --- /dev/null +++ b/pkg/services/apiserver/client/client_test.go @@ -0,0 +1,34 @@ +package client + +import ( + "context" + "testing" + + "github.com/grafana/grafana/pkg/services/user" + "github.com/grafana/grafana/pkg/services/user/usertest" + "github.com/stretchr/testify/require" +) + +func TestGetUserFromMeta(t *testing.T) { + userSvcTest := usertest.NewUserServiceFake() + userSvcTest.ExpectedUser = &user.User{ + ID: 1, + UID: "uid-value", + } + client := &k8sHandler{ + userService: userSvcTest, + } + t.Run("returns user with valid UID", func(t *testing.T) { + result, err := client.GetUserFromMeta(context.Background(), "user:uid-value") + require.NoError(t, err) + require.Equal(t, "uid-value", result.UID) + require.Equal(t, int64(1), result.ID) + }) + + t.Run("returns user when id is passed in", func(t *testing.T) { + result, err := client.GetUserFromMeta(context.Background(), "user:1") + require.NoError(t, err) + require.Equal(t, "uid-value", result.UID) + require.Equal(t, int64(1), result.ID) + }) +} diff --git a/pkg/services/dashboards/service/dashboard_service.go b/pkg/services/dashboards/service/dashboard_service.go index dd8d95348ad..ad78d14952f 100644 --- a/pkg/services/dashboards/service/dashboard_service.go +++ b/pkg/services/dashboards/service/dashboard_service.go @@ -68,7 +68,6 @@ type DashboardServiceImpl struct { dashboardStore dashboards.Store folderStore folder.FolderStore folderService folder.Service - userService user.Service orgService org.Service features featuremgmt.FeatureToggles folderPermissions accesscontrol.FolderPermissionsService @@ -91,7 +90,7 @@ func ProvideDashboardServiceImpl( restConfigProvider apiserver.RestConfigProvider, userService user.Service, unified resource.ResourceClient, quotaService quota.Service, orgService org.Service, publicDashboardService publicdashboards.ServiceWrapper, ) (*DashboardServiceImpl, error) { - k8sHandler := client.NewK8sHandler(cfg, request.GetNamespaceMapper(cfg), v0alpha1.DashboardResourceInfo.GroupVersionResource(), restConfigProvider, unified, dashboardStore) + k8sHandler := client.NewK8sHandler(cfg, request.GetNamespaceMapper(cfg), v0alpha1.DashboardResourceInfo.GroupVersionResource(), restConfigProvider, unified, dashboardStore, userService) dashSvc := &DashboardServiceImpl{ cfg: cfg, @@ -103,7 +102,6 @@ func ProvideDashboardServiceImpl( folderStore: folderStore, folderService: folderSvc, orgService: orgService, - userService: userService, k8sclient: k8sHandler, metrics: newDashboardsMetrics(r), dashboardPermissionsReady: make(chan struct{}), @@ -1489,7 +1487,7 @@ func (dr *DashboardServiceImpl) getDashboardThroughK8s(ctx context.Context, quer query.UID = result.UID } - out, err := dr.k8sclient.Get(ctx, query.UID, query.OrgID, subresource) + out, err := dr.k8sclient.Get(ctx, query.UID, query.OrgID, v1.GetOptions{}, subresource) if err != nil && !apierrors.IsNotFound(err) { return nil, err } else if err != nil || out == nil { @@ -1553,7 +1551,7 @@ func (dr *DashboardServiceImpl) saveDashboardThroughK8s(ctx context.Context, cmd func (dr *DashboardServiceImpl) createOrUpdateDash(ctx context.Context, obj unstructured.Unstructured, orgID int64) (*dashboards.Dashboard, error) { var out *unstructured.Unstructured - current, err := dr.k8sclient.Get(ctx, obj.GetName(), orgID) + current, err := dr.k8sclient.Get(ctx, obj.GetName(), orgID, v1.GetOptions{}) if current == nil || err != nil { out, err = dr.k8sclient.Create(ctx, &obj, orgID) if err != nil { @@ -1751,7 +1749,7 @@ func (dr *DashboardServiceImpl) searchProvisionedDashboardsThroughK8s(ctx contex for _, h := range searchResults.Hits { func(hit v0alpha1.DashboardHit) { g.Go(func() error { - out, err := dr.k8sclient.Get(ctx, hit.Name, query.OrgId) + out, err := dr.k8sclient.Get(ctx, hit.Name, query.OrgId, v1.GetOptions{}) if err != nil { return err } else if out == nil { @@ -1863,13 +1861,13 @@ func (dr *DashboardServiceImpl) UnstructuredToLegacyDashboard(ctx context.Contex out.PluginID = GetPluginIDFromMeta(obj) - creator, err := dr.getUserFromMeta(ctx, obj.GetCreatedBy()) + creator, err := dr.k8sclient.GetUserFromMeta(ctx, obj.GetCreatedBy()) if err != nil { return nil, err } out.CreatedBy = creator.ID - updater, err := dr.getUserFromMeta(ctx, obj.GetUpdatedBy()) + updater, err := dr.k8sclient.GetUserFromMeta(ctx, obj.GetUpdatedBy()) if err != nil { return nil, err } @@ -1906,25 +1904,6 @@ func (dr *DashboardServiceImpl) UnstructuredToLegacyDashboard(ctx context.Contex return &out, nil } -func (dr *DashboardServiceImpl) getUserFromMeta(ctx context.Context, userMeta string) (*user.User, error) { - if userMeta == "" || toUID(userMeta) == "" { - return &user.User{}, nil - } - usr, err := dr.getUser(ctx, toUID(userMeta)) - if err != nil && errors.Is(err, user.ErrUserNotFound) { - return &user.User{}, nil - } - return usr, err -} - -func (dr *DashboardServiceImpl) getUser(ctx context.Context, uid string) (*user.User, error) { - userId, err := strconv.ParseInt(uid, 10, 64) - if err == nil { - return dr.userService.GetByID(ctx, &user.GetUserByIDQuery{ID: userId}) - } - return dr.userService.GetByUID(ctx, &user.GetUserByUIDQuery{UID: uid}) -} - var pluginIDRepoName = "plugin" var fileProvisionedRepoPrefix = "file:" @@ -2010,11 +1989,3 @@ func LegacySaveCommandToUnstructured(cmd *dashboards.SaveDashboardCommand, names return finalObj, nil } - -func toUID(rawIdentifier string) string { - parts := strings.Split(rawIdentifier, ":") - if len(parts) < 2 { - return "" - } - return parts[1] -} diff --git a/pkg/services/dashboards/service/dashboard_service_test.go b/pkg/services/dashboards/service/dashboard_service_test.go index fbe99cf0fb4..d7adcd9ad9a 100644 --- a/pkg/services/dashboards/service/dashboard_service_test.go +++ b/pkg/services/dashboards/service/dashboard_service_test.go @@ -26,7 +26,6 @@ import ( "github.com/grafana/grafana/pkg/services/quota" "github.com/grafana/grafana/pkg/services/search/model" "github.com/grafana/grafana/pkg/services/user" - "github.com/grafana/grafana/pkg/services/user/usertest" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/storage/unified/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -315,7 +314,8 @@ func TestGetDashboard(t *testing.T) { Version: 1, Data: simplejson.NewFromAny(map[string]any{"test": "test", "title": "testing slugify", "uid": "uid", "version": int64(1)}), } - k8sCliMock.On("Get", mock.Anything, query.UID, mock.Anything, mock.Anything).Return(&dashboardUnstructured, nil).Once() + k8sCliMock.On("Get", mock.Anything, query.UID, mock.Anything, mock.Anything, mock.Anything).Return(&dashboardUnstructured, nil).Once() + k8sCliMock.On("GetUserFromMeta", mock.Anything, mock.Anything).Return(&user.User{}, nil) dashboard, err := service.GetDashboard(ctx, query) require.NoError(t, err) @@ -351,7 +351,8 @@ func TestGetDashboard(t *testing.T) { Version: 1, Data: simplejson.NewFromAny(map[string]any{"test": "test", "title": "testing slugify", "uid": "uid", "version": int64(1)}), } - k8sCliMock.On("Get", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&dashboardUnstructured, nil).Once() + k8sCliMock.On("Get", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&dashboardUnstructured, nil).Once() + k8sCliMock.On("GetUserFromMeta", mock.Anything, mock.Anything).Return(&user.User{}, nil) k8sCliMock.On("Search", mock.Anything, mock.Anything, mock.Anything).Return(&resource.ResourceSearchResponse{ Results: &resource.ResourceTable{ Columns: []*resource.ResourceTableColumnDefinition{ @@ -391,7 +392,7 @@ func TestGetDashboard(t *testing.T) { t.Run("Should return error when Kubernetes client fails", func(t *testing.T) { ctx, k8sCliMock := setupK8sDashboardTests(service) - k8sCliMock.On("Get", mock.Anything, query.UID, mock.Anything, mock.Anything).Return(nil, assert.AnError).Once() + k8sCliMock.On("Get", mock.Anything, query.UID, mock.Anything, mock.Anything, mock.Anything).Return(nil, assert.AnError).Once() dashboard, err := service.GetDashboard(ctx, query) require.Error(t, err) @@ -401,7 +402,7 @@ func TestGetDashboard(t *testing.T) { t.Run("Should return dashboard not found if Kubernetes client returns nil", func(t *testing.T) { ctx, k8sCliMock := setupK8sDashboardTests(service) - k8sCliMock.On("Get", mock.Anything, query.UID, mock.Anything, mock.Anything).Return(nil, nil).Once() + k8sCliMock.On("Get", mock.Anything, query.UID, mock.Anything, mock.Anything, mock.Anything).Return(nil, nil).Once() dashboard, err := service.GetDashboard(ctx, query) require.Error(t, err) require.Equal(t, dashboards.ErrDashboardNotFound, err) @@ -450,6 +451,7 @@ func TestGetAllDashboards(t *testing.T) { Data: simplejson.NewFromAny(map[string]any{"test": "test", "title": "testing slugify", "uid": "uid", "version": int64(1)}), } + k8sCliMock.On("GetUserFromMeta", mock.Anything, mock.Anything).Return(&user.User{}, nil) k8sCliMock.On("List", mock.Anything, mock.Anything, mock.Anything).Return(&unstructured.UnstructuredList{Items: []unstructured.Unstructured{dashboardUnstructured}}, nil).Once() dashes, err := service.GetAllDashboards(ctx) @@ -501,6 +503,7 @@ func TestGetAllDashboardsByOrgId(t *testing.T) { Data: simplejson.NewFromAny(map[string]any{"test": "test", "title": "testing slugify", "uid": "uid", "version": int64(1)}), } + k8sCliMock.On("GetUserFromMeta", mock.Anything, mock.Anything).Return(&user.User{}, nil) k8sCliMock.On("List", mock.Anything, mock.Anything, mock.Anything).Return(&unstructured.UnstructuredList{Items: []unstructured.Unstructured{dashboardUnstructured}}, nil).Once() dashes, err := service.GetAllDashboardsByOrgId(ctx, 1) @@ -534,7 +537,7 @@ func TestGetProvisionedDashboardData(t *testing.T) { t.Run("Should use Kubernetes client if feature flags are enabled and get from relevant org", func(t *testing.T) { ctx, k8sCliMock := setupK8sDashboardTests(service) - k8sCliMock.On("Get", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&unstructured.Unstructured{Object: map[string]any{ + k8sCliMock.On("Get", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&unstructured.Unstructured{Object: map[string]any{ "metadata": map[string]any{ "name": "uid", "labels": map[string]any{ @@ -632,7 +635,7 @@ func TestGetProvisionedDashboardDataByDashboardID(t *testing.T) { t.Run("Should use Kubernetes client if feature flags are enabled and get from whatever org it is in", func(t *testing.T) { ctx, k8sCliMock := setupK8sDashboardTests(service) - k8sCliMock.On("Get", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&unstructured.Unstructured{Object: map[string]any{ + k8sCliMock.On("Get", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&unstructured.Unstructured{Object: map[string]any{ "metadata": map[string]any{ "name": "uid", "labels": map[string]any{ @@ -721,7 +724,7 @@ func TestGetProvisionedDashboardDataByDashboardUID(t *testing.T) { t.Run("Should use Kubernetes client if feature flags are enabled", func(t *testing.T) { ctx, k8sCliMock := setupK8sDashboardTests(service) - k8sCliMock.On("Get", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&unstructured.Unstructured{Object: map[string]any{ + k8sCliMock.On("Get", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&unstructured.Unstructured{Object: map[string]any{ "metadata": map[string]any{ "name": "uid", "labels": map[string]any{ @@ -812,7 +815,7 @@ func TestDeleteOrphanedProvisionedDashboards(t *testing.T) { fakeStore.On("CleanupAfterDelete", mock.Anything, &dashboards.DeleteDashboardCommand{UID: "uid", OrgID: 1}).Return(nil).Once() fakeStore.On("CleanupAfterDelete", mock.Anything, &dashboards.DeleteDashboardCommand{UID: "uid3", OrgID: 2}).Return(nil).Once() fakePublicDashboardService.On("DeleteByDashboardUIDs", mock.Anything, mock.Anything, mock.Anything).Return(nil) - k8sCliMock.On("Get", mock.Anything, "uid", mock.Anything, mock.Anything).Return(&unstructured.Unstructured{Object: map[string]any{ + k8sCliMock.On("Get", mock.Anything, "uid", mock.Anything, mock.Anything, mock.Anything).Return(&unstructured.Unstructured{Object: map[string]any{ "metadata": map[string]any{ "name": "uid", "annotations": map[string]any{ @@ -825,7 +828,7 @@ func TestDeleteOrphanedProvisionedDashboards(t *testing.T) { "spec": map[string]any{}, }}, nil).Once() // should not delete this one, because it does not start with "file:" - k8sCliMock.On("Get", mock.Anything, "uid2", mock.Anything, mock.Anything).Return(&unstructured.Unstructured{Object: map[string]any{ + k8sCliMock.On("Get", mock.Anything, "uid2", mock.Anything, mock.Anything, mock.Anything).Return(&unstructured.Unstructured{Object: map[string]any{ "metadata": map[string]any{ "name": "uid2", "annotations": map[string]any{ @@ -836,7 +839,7 @@ func TestDeleteOrphanedProvisionedDashboards(t *testing.T) { "spec": map[string]any{}, }}, nil).Once() - k8sCliMock.On("Get", mock.Anything, "uid3", mock.Anything, mock.Anything).Return(&unstructured.Unstructured{Object: map[string]any{ + k8sCliMock.On("Get", mock.Anything, "uid3", mock.Anything, mock.Anything, mock.Anything).Return(&unstructured.Unstructured{Object: map[string]any{ "metadata": map[string]any{ "name": "uid3", "annotations": map[string]any{ @@ -958,7 +961,7 @@ func TestUnprovisionDashboard(t *testing.T) { }, "spec": map[string]any{}, }} - k8sCliMock.On("Get", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(dash, nil) + k8sCliMock.On("Get", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(dash, nil) dashWithoutAnnotations := &unstructured.Unstructured{Object: map[string]any{ "apiVersion": "dashboard.grafana.app/v0alpha1", "kind": "Dashboard", @@ -975,6 +978,7 @@ func TestUnprovisionDashboard(t *testing.T) { // should update it to be without annotations k8sCliMock.On("Update", mock.Anything, dashWithoutAnnotations, mock.Anything, mock.Anything).Return(dashWithoutAnnotations, nil) k8sCliMock.On("GetNamespace", mock.Anything).Return("default") + k8sCliMock.On("GetUserFromMeta", mock.Anything, mock.Anything).Return(&user.User{}, nil) k8sCliMock.On("Search", mock.Anything, mock.Anything, mock.Anything).Return(&resource.ResourceSearchResponse{ Results: &resource.ResourceTable{ Columns: []*resource.ResourceTableColumnDefinition{ @@ -1039,7 +1043,8 @@ func TestGetDashboardsByPluginID(t *testing.T) { t.Run("Should use Kubernetes client if feature flags are enabled", func(t *testing.T) { ctx, k8sCliMock := setupK8sDashboardTests(service) - k8sCliMock.On("Get", mock.Anything, "uid", mock.Anything, mock.Anything).Return(uidUnstructured, nil) + k8sCliMock.On("Get", mock.Anything, "uid", mock.Anything, mock.Anything, mock.Anything).Return(uidUnstructured, nil) + k8sCliMock.On("GetUserFromMeta", mock.Anything, mock.Anything).Return(&user.User{}, nil) k8sCliMock.On("Search", mock.Anything, mock.Anything, mock.MatchedBy(func(req *resource.ResourceSearchRequest) bool { return req.Options.Fields[0].Key == "repo.name" && req.Options.Fields[0].Values[0] == "plugin" && req.Options.Fields[1].Key == "repo.path" && req.Options.Fields[1].Values[0] == "testing" @@ -1127,7 +1132,8 @@ func TestSaveProvisionedDashboard(t *testing.T) { t.Run("Should use Kubernetes create if feature flags are enabled", func(t *testing.T) { ctx, k8sCliMock := setupK8sDashboardTests(service) fakeStore.On("SaveProvisionedDashboard", mock.Anything, mock.Anything, mock.Anything).Return(nil) - k8sCliMock.On("Get", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil, nil) + k8sCliMock.On("Get", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil, nil) + k8sCliMock.On("GetUserFromMeta", mock.Anything, mock.Anything).Return(&user.User{}, nil) k8sCliMock.On("Create", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&dashboardUnstructured, nil) k8sCliMock.On("GetNamespace", mock.Anything).Return("default") @@ -1188,7 +1194,8 @@ func TestSaveDashboard(t *testing.T) { t.Run("Should use Kubernetes create if feature flags are enabled and dashboard doesn't exist", func(t *testing.T) { ctx, k8sCliMock := setupK8sDashboardTests(service) - k8sCliMock.On("Get", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil, nil) + k8sCliMock.On("Get", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil, nil) + k8sCliMock.On("GetUserFromMeta", mock.Anything, mock.Anything).Return(&user.User{}, nil) k8sCliMock.On("GetNamespace", mock.Anything).Return("default") k8sCliMock.On("Create", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&dashboardUnstructured, nil) @@ -1199,7 +1206,8 @@ func TestSaveDashboard(t *testing.T) { t.Run("Should use Kubernetes update if feature flags are enabled and dashboard exists", func(t *testing.T) { ctx, k8sCliMock := setupK8sDashboardTests(service) - k8sCliMock.On("Get", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&dashboardUnstructured, nil) + k8sCliMock.On("Get", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&dashboardUnstructured, nil) + k8sCliMock.On("GetUserFromMeta", mock.Anything, mock.Anything).Return(&user.User{}, nil) k8sCliMock.On("GetNamespace", mock.Anything).Return("default") k8sCliMock.On("Update", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&dashboardUnstructured, nil) @@ -1210,7 +1218,7 @@ func TestSaveDashboard(t *testing.T) { t.Run("Should return an error if uid is invalid", func(t *testing.T) { ctx, k8sCliMock := setupK8sDashboardTests(service) - k8sCliMock.On("Get", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil, nil) + k8sCliMock.On("Get", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil, nil) k8sCliMock.On("GetNamespace", mock.Anything).Return("default") k8sCliMock.On("Create", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&dashboardUnstructured, nil) @@ -1495,8 +1503,9 @@ func TestGetDashboards(t *testing.T) { t.Run("Should use Kubernetes client if feature flags are enabled", func(t *testing.T) { ctx, k8sCliMock := setupK8sDashboardTests(service) - k8sCliMock.On("Get", mock.Anything, "uid1", mock.Anything, mock.Anything).Return(uid1Unstructured, nil) - k8sCliMock.On("Get", mock.Anything, "uid2", mock.Anything, mock.Anything).Return(uid2Unstructured, nil) + k8sCliMock.On("Get", mock.Anything, "uid1", mock.Anything, mock.Anything, mock.Anything).Return(uid1Unstructured, nil) + k8sCliMock.On("Get", mock.Anything, "uid2", mock.Anything, mock.Anything, mock.Anything).Return(uid2Unstructured, nil) + k8sCliMock.On("GetUserFromMeta", mock.Anything, mock.Anything).Return(&user.User{}, nil) k8sCliMock.On("Search", mock.Anything, mock.Anything, mock.Anything).Return(&resource.ResourceSearchResponse{ Results: &resource.ResourceTable{ Columns: []*resource.ResourceTableColumnDefinition{ @@ -1611,10 +1620,10 @@ func TestGetDashboardUIDByID(t *testing.T) { } func TestUnstructuredToLegacyDashboard(t *testing.T) { - fake := usertest.NewUserServiceFake() - fake.ExpectedUser = &user.User{ID: 10, UID: "useruid"} + k8sCliMock := new(client.MockK8sHandler) + k8sCliMock.On("GetUserFromMeta", mock.Anything, mock.Anything).Return(&user.User{ID: 10, UID: "useruid"}, nil) dr := &DashboardServiceImpl{ - userService: fake, + k8sclient: k8sCliMock, } t.Run("successfully converts unstructured to legacy dashboard", func(t *testing.T) { uid := "36b7c825-79cc-435e-acf6-c78bd96a4510" @@ -1912,17 +1921,3 @@ func TestLegacySaveCommandToUnstructured(t *testing.T) { assert.Equal(t, result.GetAnnotations(), map[string]string(nil)) }) } - -func TestToUID(t *testing.T) { - t.Run("parses valid UID", func(t *testing.T) { - rawIdentifier := "user:uid-value" - result := toUID(rawIdentifier) - assert.Equal(t, "uid-value", result) - }) - - t.Run("returns empty string for invalid identifier", func(t *testing.T) { - rawIdentifier := "invalid-uid" - result := toUID(rawIdentifier) - assert.Equal(t, "", result) - }) -} diff --git a/pkg/services/dashboardversion/dashver.go b/pkg/services/dashboardversion/dashver.go index 8609e238d55..9d4dcb60264 100644 --- a/pkg/services/dashboardversion/dashver.go +++ b/pkg/services/dashboardversion/dashver.go @@ -7,5 +7,5 @@ import ( type Service interface { Get(context.Context, *GetDashboardVersionQuery) (*DashboardVersionDTO, error) DeleteExpired(context.Context, *DeleteExpiredVersionsCommand) error - List(context.Context, *ListDashboardVersionsQuery) ([]*DashboardVersionDTO, error) + List(context.Context, *ListDashboardVersionsQuery) (*DashboardVersionResponse, error) } diff --git a/pkg/services/dashboardversion/dashverimpl/dashver.go b/pkg/services/dashboardversion/dashverimpl/dashver.go index 04fed84c9bd..02bed95b96e 100644 --- a/pkg/services/dashboardversion/dashverimpl/dashver.go +++ b/pkg/services/dashboardversion/dashverimpl/dashver.go @@ -3,12 +3,26 @@ package dashverimpl import ( "context" "errors" + "fmt" + "strconv" + "strings" + "github.com/grafana/grafana/pkg/apimachinery/utils" + "github.com/grafana/grafana/pkg/apis/dashboard/v0alpha1" + "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/services/apiserver" + "github.com/grafana/grafana/pkg/services/apiserver/client" + "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request" "github.com/grafana/grafana/pkg/services/dashboards" dashver "github.com/grafana/grafana/pkg/services/dashboardversion" + "github.com/grafana/grafana/pkg/services/featuremgmt" + "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/setting" + "github.com/grafana/grafana/pkg/storage/unified/resource" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" ) const ( @@ -17,19 +31,32 @@ const ( ) type Service struct { - cfg *setting.Cfg - store store - dashSvc dashboards.DashboardService - log log.Logger + cfg *setting.Cfg + store store + dashSvc dashboards.DashboardService + k8sclient client.K8sHandler + features featuremgmt.FeatureToggles + log log.Logger } -func ProvideService(cfg *setting.Cfg, db db.DB, dashboardService dashboards.DashboardService) dashver.Service { +func ProvideService(cfg *setting.Cfg, db db.DB, dashboardService dashboards.DashboardService, dashboardStore dashboards.Store, features featuremgmt.FeatureToggles, + restConfigProvider apiserver.RestConfigProvider, userService user.Service, unified resource.ResourceClient) dashver.Service { return &Service{ cfg: cfg, store: &sqlStore{ db: db, dialect: db.GetDialect(), }, + features: features, + k8sclient: client.NewK8sHandler( + cfg, + request.GetNamespaceMapper(cfg), + v0alpha1.DashboardResourceInfo.GroupVersionResource(), + restConfigProvider, + unified, + dashboardStore, + userService, + ), dashSvc: dashboardService, log: log.New("dashboard-version"), } @@ -49,13 +76,21 @@ func (s *Service) Get(ctx context.Context, query *dashver.GetDashboardVersionQue // versions table, at time of this writing), so get the DashboardID if it // was not populated. if query.DashboardID == 0 { - id, err := s.getDashIDMaybeEmpty(ctx, query.DashboardUID) + id, err := s.getDashIDMaybeEmpty(ctx, query.DashboardUID, query.OrgID) if err != nil { return nil, err } query.DashboardID = id } + if s.features.IsEnabledGlobally(featuremgmt.FlagKubernetesCliDashboards) { + version, err := s.getHistoryThroughK8s(ctx, query.OrgID, query.DashboardUID, query.Version) + if err != nil { + return nil, err + } + return version, nil + } + version, err := s.store.Get(ctx, query) if err != nil { return nil, err @@ -95,7 +130,7 @@ func (s *Service) DeleteExpired(ctx context.Context, cmd *dashver.DeleteExpiredV } // List all dashboard versions for the given dashboard ID. -func (s *Service) List(ctx context.Context, query *dashver.ListDashboardVersionsQuery) ([]*dashver.DashboardVersionDTO, error) { +func (s *Service) List(ctx context.Context, query *dashver.ListDashboardVersionsQuery) (*dashver.DashboardVersionResponse, error) { // Get the DashboardUID if not populated if query.DashboardUID == "" { u, err := s.getDashUIDMaybeEmpty(ctx, query.DashboardID) @@ -109,7 +144,7 @@ func (s *Service) List(ctx context.Context, query *dashver.ListDashboardVersions // versions table, at time of this writing), so get the DashboardID if it // was not populated. if query.DashboardID == 0 { - id, err := s.getDashIDMaybeEmpty(ctx, query.DashboardUID) + id, err := s.getDashIDMaybeEmpty(ctx, query.DashboardUID, query.OrgID) if err != nil { return nil, err } @@ -118,6 +153,21 @@ func (s *Service) List(ctx context.Context, query *dashver.ListDashboardVersions if query.Limit == 0 { query.Limit = 1000 } + + if s.features.IsEnabledGlobally(featuremgmt.FlagKubernetesCliDashboards) { + versions, err := s.listHistoryThroughK8s( + ctx, + query.OrgID, + query.DashboardUID, + int64(query.Limit), + query.ContinueToken, + ) + if err != nil { + return nil, err + } + return versions, nil + } + dvs, err := s.store.List(ctx, query) if err != nil { return nil, err @@ -126,7 +176,9 @@ func (s *Service) List(ctx context.Context, query *dashver.ListDashboardVersions for i, v := range dvs { dtos[i] = v.ToDTO(query.DashboardUID) } - return dtos, nil + return &dashver.DashboardVersionResponse{ + Versions: dtos, + }, nil } // getDashUIDMaybeEmpty is a helper function which takes a dashboardID and @@ -149,8 +201,8 @@ func (s *Service) getDashUIDMaybeEmpty(ctx context.Context, id int64) (string, e // getDashIDMaybeEmpty is a helper function which takes a dashboardUID and // returns the ID. If the dashboard is not found, it will return -1. -func (s *Service) getDashIDMaybeEmpty(ctx context.Context, uid string) (int64, error) { - q := dashboards.GetDashboardQuery{UID: uid} +func (s *Service) getDashIDMaybeEmpty(ctx context.Context, uid string, orgID int64) (int64, error) { + q := dashboards.GetDashboardQuery{UID: uid, OrgID: orgID} result, err := s.dashSvc.GetDashboard(ctx, &q) if err != nil { if errors.Is(err, dashboards.ErrDashboardNotFound) { @@ -163,3 +215,116 @@ func (s *Service) getDashIDMaybeEmpty(ctx context.Context, uid string) (int64, e } return result.ID, nil } + +func (s *Service) getHistoryThroughK8s(ctx context.Context, orgID int64, dashboardUID string, rv int64) (*dashver.DashboardVersionDTO, error) { + out, err := s.k8sclient.Get(ctx, dashboardUID, orgID, v1.GetOptions{ResourceVersion: strconv.FormatInt(rv, 10)}) + if err != nil { + return nil, err + } else if out == nil { + return nil, dashboards.ErrDashboardNotFound + } + + dash, err := s.UnstructuredToLegacyDashboardVersion(ctx, out, orgID) + if err != nil { + return nil, err + } + + return dash, nil +} + +func (s *Service) listHistoryThroughK8s(ctx context.Context, orgID int64, dashboardUID string, limit int64, continueToken string) (*dashver.DashboardVersionResponse, error) { + out, err := s.k8sclient.List(ctx, orgID, v1.ListOptions{ + LabelSelector: utils.LabelKeyGetHistory + "=" + dashboardUID, + Limit: limit, + Continue: continueToken, + }) + if err != nil { + return nil, err + } else if out == nil { + return nil, dashboards.ErrDashboardNotFound + } + + dashboards := make([]*dashver.DashboardVersionDTO, len(out.Items)) + for i, item := range out.Items { + dash, err := s.UnstructuredToLegacyDashboardVersion(ctx, &item, orgID) + if err != nil { + return nil, err + } + dashboards[i] = dash + } + + return &dashver.DashboardVersionResponse{ + ContinueToken: out.GetContinue(), + Versions: dashboards, + }, nil +} + +func (s *Service) UnstructuredToLegacyDashboardVersion(ctx context.Context, item *unstructured.Unstructured, orgID int64) (*dashver.DashboardVersionDTO, error) { + spec, ok := item.Object["spec"].(map[string]any) + if !ok { + return nil, errors.New("error parsing dashboard from k8s response") + } + obj, err := utils.MetaAccessor(item) + if err != nil { + return nil, err + } + uid := obj.GetName() + spec["uid"] = uid + + dashVersion := 0 + parentVersion := 0 + if version, ok := spec["version"].(int64); ok { + dashVersion = int(version) + parentVersion = dashVersion - 1 + } + + createdBy, err := s.k8sclient.GetUserFromMeta(ctx, obj.GetCreatedBy()) + if err != nil { + return nil, err + } + + id, err := obj.GetResourceVersionInt64() + if err != nil { + return nil, err + } + + restoreVer, err := getRestoreVersion(obj.GetMessage()) + if err != nil { + return nil, err + } + + out := dashver.DashboardVersionDTO{ + ID: id, + DashboardID: obj.GetDeprecatedInternalID(), // nolint:staticcheck + DashboardUID: uid, + Created: obj.GetCreationTimestamp().Time, + CreatedBy: createdBy.ID, + Message: obj.GetMessage(), + RestoredFrom: restoreVer, + Version: dashVersion, + ParentVersion: parentVersion, + Data: simplejson.NewFromAny(spec), + } + + return &out, nil +} + +var restoreMsg = "Restored from version " + +func DashboardRestoreMessage(version int) string { + return fmt.Sprintf("%s%d", restoreMsg, version) +} + +func getRestoreVersion(msg string) (int, error) { + parts := strings.Split(msg, restoreMsg) + if len(parts) < 2 { + return 0, nil + } + + ver, err := strconv.ParseInt(parts[1], 10, 64) + if err != nil { + return 0, err + } + + return int(ver), nil +} diff --git a/pkg/services/dashboardversion/dashverimpl/dashver_test.go b/pkg/services/dashboardversion/dashverimpl/dashver_test.go index 7ec8fc4ef64..ed58c27bb7f 100644 --- a/pkg/services/dashboardversion/dashverimpl/dashver_test.go +++ b/pkg/services/dashboardversion/dashverimpl/dashver_test.go @@ -7,18 +7,24 @@ import ( "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "github.com/grafana/grafana/pkg/apimachinery/utils" "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/services/apiserver/client" "github.com/grafana/grafana/pkg/services/dashboards" dashver "github.com/grafana/grafana/pkg/services/dashboardversion" + "github.com/grafana/grafana/pkg/services/featuremgmt" + "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/setting" ) func TestDashboardVersionService(t *testing.T) { dashboardVersionStore := newDashboardVersionStoreFake() dashboardService := dashboards.NewFakeDashboardService(t) - dashboardVersionService := Service{store: dashboardVersionStore, dashSvc: dashboardService} + dashboardVersionService := Service{store: dashboardVersionStore, dashSvc: dashboardService, features: featuremgmt.WithFeatures()} t.Run("Get dashboard version", func(t *testing.T) { dashboard := &dashver.DashboardVersion{ @@ -32,6 +38,44 @@ func TestDashboardVersionService(t *testing.T) { require.NoError(t, err) require.Equal(t, dashboard.ToDTO("uid"), dashboardVersion) }) + + t.Run("Get dashboard version through k8s", func(t *testing.T) { + dashboardService := dashboards.NewFakeDashboardService(t) + dashboardVersionService := Service{dashSvc: dashboardService, features: featuremgmt.WithFeatures()} + mockCli := new(client.MockK8sHandler) + dashboardVersionService.k8sclient = mockCli + dashboardVersionService.features = featuremgmt.WithFeatures(featuremgmt.FlagKubernetesCliDashboards) + dashboardService.On("GetDashboardUIDByID", mock.Anything, mock.AnythingOfType("*dashboards.GetDashboardRefByIDQuery")).Return(&dashboards.DashboardRef{UID: "uid"}, nil) + + mockCli.On("GetUserFromMeta", mock.Anything, mock.Anything).Return(&user.User{}, nil) + mockCli.On("Get", mock.Anything, "uid", int64(1), v1.GetOptions{ResourceVersion: "10"}, mock.Anything).Return(&unstructured.Unstructured{ + Object: map[string]any{ + "metadata": map[string]any{ + "name": "uid", + "resourceVersion": "12", + "labels": map[string]any{ + utils.LabelKeyDeprecatedInternalID: "42", // nolint:staticcheck + }, + }, + "spec": map[string]any{ + "version": int64(10), + }, + }}, nil).Once() + res, err := dashboardVersionService.Get(context.Background(), &dashver.GetDashboardVersionQuery{ + DashboardID: 42, + OrgID: 1, + Version: 10, + }) + require.Nil(t, err) + require.Equal(t, res, &dashver.DashboardVersionDTO{ + ID: 12, // RV should be used + Version: 10, + ParentVersion: 9, + DashboardID: 42, + DashboardUID: "uid", + Data: simplejson.NewFromAny(map[string]any{"uid": "uid", "version": int64(10)}), + }) + }) } func TestDeleteExpiredVersions(t *testing.T) { @@ -42,7 +86,7 @@ func TestDeleteExpiredVersions(t *testing.T) { dashboardVersionStore := newDashboardVersionStoreFake() dashboardService := dashboards.NewFakeDashboardService(t) dashboardVersionService := Service{ - cfg: cfg, store: dashboardVersionStore, dashSvc: dashboardService} + cfg: cfg, store: dashboardVersionStore, dashSvc: dashboardService, features: featuremgmt.WithFeatures()} t.Run("Don't delete anything if there are no expired versions", func(t *testing.T) { err := dashboardVersionService.DeleteExpired(context.Background(), &dashver.DeleteExpiredVersionsCommand{DeletedRows: 4}) @@ -67,7 +111,7 @@ func TestListDashboardVersions(t *testing.T) { t.Run("List all versions for a given Dashboard ID", func(t *testing.T) { dashboardVersionStore := newDashboardVersionStoreFake() dashboardService := dashboards.NewFakeDashboardService(t) - dashboardVersionService := Service{store: dashboardVersionStore, dashSvc: dashboardService} + dashboardVersionService := Service{store: dashboardVersionStore, dashSvc: dashboardService, features: featuremgmt.WithFeatures()} dashboardVersionStore.ExpectedListVersions = []*dashver.DashboardVersion{ {ID: 1, DashboardID: 42}, } @@ -78,15 +122,15 @@ func TestListDashboardVersions(t *testing.T) { query := dashver.ListDashboardVersionsQuery{DashboardID: 42} res, err := dashboardVersionService.List(context.Background(), &query) require.Nil(t, err) - require.Equal(t, 1, len(res)) + require.Equal(t, 1, len(res.Versions)) // validate that the UID was populated - require.EqualValues(t, []*dashver.DashboardVersionDTO{{ID: 1, DashboardID: 42, DashboardUID: "uid"}}, res) + require.EqualValues(t, &dashver.DashboardVersionResponse{Versions: []*dashver.DashboardVersionDTO{{ID: 1, DashboardID: 42, DashboardUID: "uid"}}}, res) }) t.Run("List all versions for a non-existent DashboardID", func(t *testing.T) { dashboardVersionStore := newDashboardVersionStoreFake() dashboardService := dashboards.NewFakeDashboardService(t) - dashboardVersionService := Service{store: dashboardVersionStore, dashSvc: dashboardService, log: log.NewNopLogger()} + dashboardVersionService := Service{store: dashboardVersionStore, dashSvc: dashboardService, log: log.NewNopLogger(), features: featuremgmt.WithFeatures()} dashboardVersionStore.ExpectedListVersions = []*dashver.DashboardVersion{ {ID: 1, DashboardID: 42}, } @@ -96,15 +140,15 @@ func TestListDashboardVersions(t *testing.T) { query := dashver.ListDashboardVersionsQuery{DashboardID: 42} res, err := dashboardVersionService.List(context.Background(), &query) require.Nil(t, err) - require.Equal(t, 1, len(res)) + require.Equal(t, 1, len(res.Versions)) // The DashboardID remains populated with the given value, even though the dash was not found - require.EqualValues(t, []*dashver.DashboardVersionDTO{{ID: 1, DashboardID: 42}}, res) + require.EqualValues(t, &dashver.DashboardVersionResponse{Versions: []*dashver.DashboardVersionDTO{{ID: 1, DashboardID: 42}}}, res) }) t.Run("List all versions for a given DashboardUID", func(t *testing.T) { dashboardVersionStore := newDashboardVersionStoreFake() dashboardService := dashboards.NewFakeDashboardService(t) - dashboardVersionService := Service{store: dashboardVersionStore, dashSvc: dashboardService, log: log.NewNopLogger()} + dashboardVersionService := Service{store: dashboardVersionStore, dashSvc: dashboardService, log: log.NewNopLogger(), features: featuremgmt.WithFeatures()} dashboardVersionStore.ExpectedListVersions = []*dashver.DashboardVersion{{DashboardID: 42, ID: 1}} dashboardService.On("GetDashboard", mock.Anything, mock.AnythingOfType("*dashboards.GetDashboardQuery")). Return(&dashboards.Dashboard{ID: 42}, nil) @@ -112,15 +156,15 @@ func TestListDashboardVersions(t *testing.T) { query := dashver.ListDashboardVersionsQuery{DashboardUID: "uid"} res, err := dashboardVersionService.List(context.Background(), &query) require.Nil(t, err) - require.Equal(t, 1, len(res)) + require.Equal(t, 1, len(res.Versions)) // validate that the dashboardID was populated from the GetDashboard method call. - require.EqualValues(t, []*dashver.DashboardVersionDTO{{ID: 1, DashboardID: 42, DashboardUID: "uid"}}, res) + require.EqualValues(t, &dashver.DashboardVersionResponse{Versions: []*dashver.DashboardVersionDTO{{ID: 1, DashboardID: 42, DashboardUID: "uid"}}}, res) }) t.Run("List all versions for a given non-existent DashboardUID", func(t *testing.T) { dashboardVersionStore := newDashboardVersionStoreFake() dashboardService := dashboards.NewFakeDashboardService(t) - dashboardVersionService := Service{store: dashboardVersionStore, dashSvc: dashboardService, log: log.NewNopLogger()} + dashboardVersionService := Service{store: dashboardVersionStore, dashSvc: dashboardService, log: log.NewNopLogger(), features: featuremgmt.WithFeatures()} dashboardVersionStore.ExpectedListVersions = []*dashver.DashboardVersion{{DashboardID: 42, ID: 1}} dashboardService.On("GetDashboard", mock.Anything, mock.AnythingOfType("*dashboards.GetDashboardQuery")). Return(nil, dashboards.ErrDashboardNotFound) @@ -128,15 +172,15 @@ func TestListDashboardVersions(t *testing.T) { query := dashver.ListDashboardVersionsQuery{DashboardUID: "uid"} res, err := dashboardVersionService.List(context.Background(), &query) require.Nil(t, err) - require.Equal(t, 1, len(res)) + require.Equal(t, 1, len(res.Versions)) // validate that the dashboardUID & ID are populated, even though the dash was not found - require.EqualValues(t, []*dashver.DashboardVersionDTO{{ID: 1, DashboardID: 42, DashboardUID: "uid"}}, res) + require.EqualValues(t, &dashver.DashboardVersionResponse{Versions: []*dashver.DashboardVersionDTO{{ID: 1, DashboardID: 42, DashboardUID: "uid"}}}, res) }) t.Run("List Dashboard versions - error from store", func(t *testing.T) { dashboardVersionStore := newDashboardVersionStoreFake() dashboardService := dashboards.NewFakeDashboardService(t) - dashboardVersionService := Service{store: dashboardVersionStore, dashSvc: dashboardService, log: log.NewNopLogger()} + dashboardVersionService := Service{store: dashboardVersionStore, dashSvc: dashboardService, log: log.NewNopLogger(), features: featuremgmt.WithFeatures()} dashboardVersionStore.ExpectedError = dashver.ErrDashboardVersionNotFound query := dashver.ListDashboardVersionsQuery{DashboardID: 42, DashboardUID: "42"} @@ -144,6 +188,46 @@ func TestListDashboardVersions(t *testing.T) { require.Nil(t, res) require.ErrorIs(t, err, dashver.ErrDashboardVersionNotFound) }) + + t.Run("List all versions for a given Dashboard ID through k8s", func(t *testing.T) { + dashboardService := dashboards.NewFakeDashboardService(t) + dashboardVersionService := Service{dashSvc: dashboardService, features: featuremgmt.WithFeatures()} + mockCli := new(client.MockK8sHandler) + dashboardVersionService.k8sclient = mockCli + dashboardVersionService.features = featuremgmt.WithFeatures(featuremgmt.FlagKubernetesCliDashboards) + + dashboardService.On("GetDashboardUIDByID", mock.Anything, + mock.AnythingOfType("*dashboards.GetDashboardRefByIDQuery")). + Return(&dashboards.DashboardRef{UID: "uid"}, nil) + + query := dashver.ListDashboardVersionsQuery{DashboardID: 42} + mockCli.On("GetUserFromMeta", mock.Anything, mock.Anything).Return(&user.User{}, nil) + mockCli.On("List", mock.Anything, mock.Anything, mock.Anything).Return(&unstructured.UnstructuredList{ + Items: []unstructured.Unstructured{{Object: map[string]any{ + "metadata": map[string]any{ + "name": "uid", + "resourceVersion": "12", + "labels": map[string]any{ + utils.LabelKeyDeprecatedInternalID: "42", // nolint:staticcheck + }, + }, + "spec": map[string]any{ + "version": int64(5), + }, + }}}}, nil).Once() + res, err := dashboardVersionService.List(context.Background(), &query) + require.Nil(t, err) + require.Equal(t, 1, len(res.Versions)) + require.EqualValues(t, &dashver.DashboardVersionResponse{ + Versions: []*dashver.DashboardVersionDTO{{ + ID: 12, // should take rv + DashboardID: 42, + ParentVersion: 4, + Version: 5, // should take from spec + DashboardUID: "uid", + Data: simplejson.NewFromAny(map[string]any{"uid": "uid", "version": int64(5)}), + }}}, res) + }) } type FakeDashboardVersionStore struct { diff --git a/pkg/services/dashboardversion/dashverimpl/store_test.go b/pkg/services/dashboardversion/dashverimpl/store_test.go index cccd1d220ce..a8c26b1d102 100644 --- a/pkg/services/dashboardversion/dashverimpl/store_test.go +++ b/pkg/services/dashboardversion/dashverimpl/store_test.go @@ -34,7 +34,7 @@ func testIntegrationGetDashboardVersion(t *testing.T, fn getStore) { query := dashver.GetDashboardVersionQuery{ DashboardID: savedDash.ID, - Version: savedDash.Version, + Version: int64(savedDash.Version), OrgID: 1, } @@ -60,7 +60,7 @@ func testIntegrationGetDashboardVersion(t *testing.T, fn getStore) { t.Run("Attempt to get a version that doesn't exist", func(t *testing.T) { query := dashver.GetDashboardVersionQuery{ DashboardID: int64(999), - Version: 123, + Version: int64(123), OrgID: 1, } diff --git a/pkg/services/dashboardversion/dashvertest/fake.go b/pkg/services/dashboardversion/dashvertest/fake.go index 1d45c63aa3c..acef53263bc 100644 --- a/pkg/services/dashboardversion/dashvertest/fake.go +++ b/pkg/services/dashboardversion/dashvertest/fake.go @@ -10,6 +10,7 @@ type FakeDashboardVersionService struct { ExpectedDashboardVersion *dashver.DashboardVersionDTO ExpectedDashboardVersions []*dashver.DashboardVersionDTO ExpectedListDashboarVersions []*dashver.DashboardVersionDTO + ExpectedContinueToken string counter int ExpectedError error } @@ -30,6 +31,9 @@ func (f *FakeDashboardVersionService) DeleteExpired(ctx context.Context, cmd *da return f.ExpectedError } -func (f *FakeDashboardVersionService) List(ctx context.Context, query *dashver.ListDashboardVersionsQuery) ([]*dashver.DashboardVersionDTO, error) { - return f.ExpectedListDashboarVersions, f.ExpectedError +func (f *FakeDashboardVersionService) List(ctx context.Context, query *dashver.ListDashboardVersionsQuery) (*dashver.DashboardVersionResponse, error) { + return &dashver.DashboardVersionResponse{ + ContinueToken: f.ExpectedContinueToken, + Versions: f.ExpectedListDashboarVersions, + }, f.ExpectedError } diff --git a/pkg/services/dashboardversion/model.go b/pkg/services/dashboardversion/model.go index 435fc794311..c4f585dc366 100644 --- a/pkg/services/dashboardversion/model.go +++ b/pkg/services/dashboardversion/model.go @@ -52,7 +52,7 @@ type GetDashboardVersionQuery struct { DashboardID int64 DashboardUID string OrgID int64 - Version int + Version int64 } type DeleteExpiredVersionsCommand struct { @@ -60,12 +60,19 @@ type DeleteExpiredVersionsCommand struct { } type ListDashboardVersionsQuery struct { - DashboardID int64 - DashboardUID string - OrgID int64 - Limit int - Start int + DashboardID int64 + DashboardUID string + OrgID int64 + Limit int + Start int + ContinueToken string } + +type DashboardVersionResponse struct { + ContinueToken string `json:"continueToken"` + Versions []*DashboardVersionDTO `json:"versions"` +} + type DashboardVersionDTO struct { ID int64 `json:"id"` DashboardID int64 `json:"dashboardId"` @@ -94,3 +101,8 @@ type DashboardVersionMeta struct { Data *simplejson.Json `json:"data"` CreatedBy string `json:"createdBy"` } + +type DashboardVersionResponseMeta struct { + ContinueToken string `json:"continueToken"` + Versions []DashboardVersionMeta `json:"versions"` +} diff --git a/pkg/storage/unified/resource/cdk_backend.go b/pkg/storage/unified/resource/cdk_backend.go index ded2d34d7aa..eefba09d78f 100644 --- a/pkg/storage/unified/resource/cdk_backend.go +++ b/pkg/storage/unified/resource/cdk_backend.go @@ -321,6 +321,11 @@ func (c *cdkListIterator) ContinueToken() string { return fmt.Sprintf("index:%d/key:%s", c.index, c.currentKey) } +// ContinueTokenWithCurrentRV implements ListIterator. +func (c *cdkListIterator) ContinueTokenWithCurrentRV() string { + return fmt.Sprintf("index:%d/key:%s", c.index, c.currentKey) +} + // Name implements ListIterator. func (c *cdkListIterator) Name() string { return c.currentKey // TODO (parse name from key) diff --git a/pkg/storage/unified/resource/server.go b/pkg/storage/unified/resource/server.go index fab1e585486..58d555bd429 100644 --- a/pkg/storage/unified/resource/server.go +++ b/pkg/storage/unified/resource/server.go @@ -41,6 +41,9 @@ type ListIterator interface { // The token that can be used to start iterating *after* this item ContinueToken() string + // The token that can be used to start iterating *before* this item + ContinueTokenWithCurrentRV() string + // ResourceVersion of the current item ResourceVersion() int64 @@ -756,9 +759,16 @@ func (s *server) List(ctx context.Context, req *ListRequest) (*ListResponse, err rsp.Items = append(rsp.Items, item) if len(rsp.Items) >= int(req.Limit) || pageBytes >= maxPageBytes { t := iter.ContinueToken() + if req.Source == ListRequest_HISTORY { + // history lists in desc order, so the continue token takes the + // final RV in the list, and then will start from there in the next page, + // rather than the lists first RV + t = iter.ContinueTokenWithCurrentRV() + } if iter.Next() { rsp.NextPageToken = t } + break } } diff --git a/pkg/storage/unified/sql/backend.go b/pkg/storage/unified/sql/backend.go index d56cd87b2e1..0401c77a25c 100644 --- a/pkg/storage/unified/sql/backend.go +++ b/pkg/storage/unified/sql/backend.go @@ -519,6 +519,10 @@ func (l *listIter) ContinueToken() string { return ContinueToken{ResourceVersion: l.listRV, StartOffset: l.offset}.String() } +func (l *listIter) ContinueTokenWithCurrentRV() string { + return ContinueToken{ResourceVersion: l.rv, StartOffset: l.offset}.String() +} + func (l *listIter) Error() error { return l.err } diff --git a/pkg/storage/unified/sql/data/resource_history_get.sql b/pkg/storage/unified/sql/data/resource_history_get.sql index cbb2786e534..5ab25eb2e0c 100644 --- a/pkg/storage/unified/sql/data/resource_history_get.sql +++ b/pkg/storage/unified/sql/data/resource_history_get.sql @@ -16,6 +16,6 @@ WHERE 1 = 1 AND {{ .Ident "action" }} = 3 {{ end }} {{ if (gt .StartRV 0) }} - AND {{ .Ident "resource_version" }} > {{ .Arg .StartRV }} + AND {{ .Ident "resource_version" }} < {{ .Arg .StartRV }} {{ end }} ORDER BY resource_version DESC diff --git a/pkg/storage/unified/sql/test/integration_test.go b/pkg/storage/unified/sql/test/integration_test.go index 5883610bd54..cc5cacc524b 100644 --- a/pkg/storage/unified/sql/test/integration_test.go +++ b/pkg/storage/unified/sql/test/integration_test.go @@ -351,6 +351,83 @@ func TestIntegrationBackendList(t *testing.T) { require.Equal(t, rv8, continueToken.ResourceVersion) require.Equal(t, int64(4), continueToken.StartOffset) }) + + // add 5 events for item1 - should be saved to history + rvHistory1, err := writeEvent(ctx, backend, "item1", resource.WatchEvent_MODIFIED) + require.NoError(t, err) + require.Greater(t, rvHistory1, rv1) + rvHistory2, err := writeEvent(ctx, backend, "item1", resource.WatchEvent_MODIFIED) + require.NoError(t, err) + require.Greater(t, rvHistory2, rvHistory1) + rvHistory3, err := writeEvent(ctx, backend, "item1", resource.WatchEvent_MODIFIED) + require.NoError(t, err) + require.Greater(t, rvHistory3, rvHistory2) + rvHistory4, err := writeEvent(ctx, backend, "item1", resource.WatchEvent_MODIFIED) + require.NoError(t, err) + require.Greater(t, rvHistory4, rvHistory3) + rvHistory5, err := writeEvent(ctx, backend, "item1", resource.WatchEvent_MODIFIED) + require.NoError(t, err) + require.Greater(t, rvHistory5, rvHistory4) + + t.Run("fetch first history page at revision with limit", func(t *testing.T) { + res, err := server.List(ctx, &resource.ListRequest{ + Limit: 3, + Source: resource.ListRequest_HISTORY, + Options: &resource.ListOptions{ + Key: &resource.ResourceKey{ + Namespace: "namespace", + Group: "group", + Resource: "resource", + Name: "item1", + }, + }, + }) + require.NoError(t, err) + require.NoError(t, err) + require.Nil(t, res.Error) + require.Len(t, res.Items, 3) + t.Log(res.Items) + // should be in desc order, so the newest RVs are returned first + require.Equal(t, "item1 MODIFIED", string(res.Items[0].Value)) + require.Equal(t, rvHistory5, res.Items[0].ResourceVersion) + require.Equal(t, "item1 MODIFIED", string(res.Items[1].Value)) + require.Equal(t, rvHistory4, res.Items[1].ResourceVersion) + require.Equal(t, "item1 MODIFIED", string(res.Items[2].Value)) + require.Equal(t, rvHistory3, res.Items[2].ResourceVersion) + + continueToken, err := sql.GetContinueToken(res.NextPageToken) + require.NoError(t, err) + // should return the furthest back RV as the next page token + require.Equal(t, rvHistory3, continueToken.ResourceVersion) + }) + + t.Run("fetch second page of history at revision", func(t *testing.T) { + continueToken := &sql.ContinueToken{ + ResourceVersion: rvHistory3, + StartOffset: 2, + } + res, err := server.List(ctx, &resource.ListRequest{ + NextPageToken: continueToken.String(), + Limit: 2, + Source: resource.ListRequest_HISTORY, + Options: &resource.ListOptions{ + Key: &resource.ResourceKey{ + Namespace: "namespace", + Group: "group", + Resource: "resource", + Name: "item1", + }, + }, + }) + require.NoError(t, err) + require.Nil(t, res.Error) + require.Len(t, res.Items, 2) + t.Log(res.Items) + require.Equal(t, "item1 MODIFIED", string(res.Items[0].Value)) + require.Equal(t, rvHistory2, res.Items[0].ResourceVersion) + require.Equal(t, "item1 MODIFIED", string(res.Items[1].Value)) + require.Equal(t, rvHistory1, res.Items[1].ResourceVersion) + }) } func TestIntegrationBlobSupport(t *testing.T) { diff --git a/pkg/storage/unified/sql/testdata/mysql--resource_history_get-read trash second page.sql b/pkg/storage/unified/sql/testdata/mysql--resource_history_get-read trash second page.sql index 16359886766..cd026c1dfef 100755 --- a/pkg/storage/unified/sql/testdata/mysql--resource_history_get-read trash second page.sql +++ b/pkg/storage/unified/sql/testdata/mysql--resource_history_get-read trash second page.sql @@ -10,5 +10,5 @@ WHERE 1 = 1 AND `group` = 'gg' AND `resource` = 'rr' AND `action` = 3 - AND `resource_version` > 123456 + AND `resource_version` < 123456 ORDER BY resource_version DESC diff --git a/pkg/storage/unified/sql/testdata/postgres--resource_history_get-read trash second page.sql b/pkg/storage/unified/sql/testdata/postgres--resource_history_get-read trash second page.sql index 0bf9c65a25a..b5df5de6315 100755 --- a/pkg/storage/unified/sql/testdata/postgres--resource_history_get-read trash second page.sql +++ b/pkg/storage/unified/sql/testdata/postgres--resource_history_get-read trash second page.sql @@ -10,5 +10,5 @@ WHERE 1 = 1 AND "group" = 'gg' AND "resource" = 'rr' AND "action" = 3 - AND "resource_version" > 123456 + AND "resource_version" < 123456 ORDER BY resource_version DESC diff --git a/pkg/storage/unified/sql/testdata/sqlite--resource_history_get-read trash second page.sql b/pkg/storage/unified/sql/testdata/sqlite--resource_history_get-read trash second page.sql index 0bf9c65a25a..b5df5de6315 100755 --- a/pkg/storage/unified/sql/testdata/sqlite--resource_history_get-read trash second page.sql +++ b/pkg/storage/unified/sql/testdata/sqlite--resource_history_get-read trash second page.sql @@ -10,5 +10,5 @@ WHERE 1 = 1 AND "group" = 'gg' AND "resource" = 'rr' AND "action" = 3 - AND "resource_version" > 123456 + AND "resource_version" < 123456 ORDER BY resource_version DESC diff --git a/public/app/features/dashboard-scene/scene/DashboardScene.tsx b/public/app/features/dashboard-scene/scene/DashboardScene.tsx index e7f9a6e7f07..3bef71caf21 100644 --- a/public/app/features/dashboard-scene/scene/DashboardScene.tsx +++ b/public/app/features/dashboard-scene/scene/DashboardScene.tsx @@ -374,7 +374,13 @@ export class DashboardScene extends SceneObjectBase { } public onRestore = async (version: DecoratedRevisionModel): Promise => { - const versionRsp = await historySrv.restoreDashboard(version.uid, version.version); + let versionRsp; + if (config.featureToggles.kubernetesCliDashboards) { + // the id here is the resource version in k8s, use this instead to get the specific version + versionRsp = await historySrv.restoreDashboard(version.uid, version.id); + } else { + versionRsp = await historySrv.restoreDashboard(version.uid, version.version); + } if (!Number.isInteger(versionRsp.version)) { return false; diff --git a/public/app/features/dashboard-scene/settings/VersionsEditView.test.tsx b/public/app/features/dashboard-scene/settings/VersionsEditView.test.tsx index 6d2b37219f6..e6449d0d3f6 100644 --- a/public/app/features/dashboard-scene/settings/VersionsEditView.test.tsx +++ b/public/app/features/dashboard-scene/settings/VersionsEditView.test.tsx @@ -112,44 +112,47 @@ describe('VersionsEditView', () => { }); function getVersions() { - return [ - { - id: 4, - dashboardId: 1, - dashboardUID: '_U4zObQMz', - parentVersion: 3, - restoredFrom: 0, - version: 4, - created: '2017-02-22T17:43:01-08:00', - createdBy: 'admin', - message: '', - checked: false, - }, - { - id: 3, - dashboardId: 1, - dashboardUID: '_U4zObQMz', - parentVersion: 1, - restoredFrom: 1, - version: 3, - created: '2017-02-22T17:43:01-08:00', - createdBy: 'admin', - message: '', - checked: false, - }, - { - id: 2, - dashboardId: 1, - dashboardUID: '_U4zObQMz', - parentVersion: 1, - restoredFrom: 1, - version: 2, - created: '2017-02-23T17:43:01-08:00', - createdBy: 'admin', - message: '', - checked: false, - }, - ]; + return { + continueToken: '', + versions: [ + { + id: 4, + dashboardId: 1, + dashboardUID: '_U4zObQMz', + parentVersion: 3, + restoredFrom: 0, + version: 4, + created: '2017-02-22T17:43:01-08:00', + createdBy: 'admin', + message: '', + checked: false, + }, + { + id: 3, + dashboardId: 1, + dashboardUID: '_U4zObQMz', + parentVersion: 1, + restoredFrom: 1, + version: 3, + created: '2017-02-22T17:43:01-08:00', + createdBy: 'admin', + message: '', + checked: false, + }, + { + id: 2, + dashboardId: 1, + dashboardUID: '_U4zObQMz', + parentVersion: 1, + restoredFrom: 1, + version: 2, + created: '2017-02-23T17:43:01-08:00', + createdBy: 'admin', + message: '', + checked: false, + }, + ], + }; } async function buildTestScene() { diff --git a/public/app/features/dashboard-scene/settings/VersionsEditView.tsx b/public/app/features/dashboard-scene/settings/VersionsEditView.tsx index c2acbb9b7af..17256382d0b 100644 --- a/public/app/features/dashboard-scene/settings/VersionsEditView.tsx +++ b/public/app/features/dashboard-scene/settings/VersionsEditView.tsx @@ -1,6 +1,7 @@ import * as React from 'react'; import { PageLayoutType, dateTimeFormat, dateTimeFormatTimeAgo } from '@grafana/data'; +import { config } from '@grafana/runtime'; import { SceneComponentProps, SceneObjectBase, sceneGraph } from '@grafana/scenes'; import { Spinner, Stack } from '@grafana/ui'; import { Page } from 'app/core/components/Page/Page'; @@ -41,6 +42,7 @@ export class VersionsEditView extends SceneObjectBase imp public static Component = VersionsEditorSettingsListView; private _limit: number = VERSIONS_FETCH_LIMIT; private _start = 0; + private _continueToken = ''; constructor(state: VersionsEditViewState) { super({ @@ -102,14 +104,20 @@ export class VersionsEditView extends SceneObjectBase imp this.setState({ isAppending: append }); + const requestOptions = this._continueToken + ? { limit: this._limit, start: this._start, continueToken: this._continueToken } + : { limit: this._limit, start: this._start }; + historySrv - .getHistoryList(uid, { limit: this._limit, start: this._start }) + .getHistoryList(uid, requestOptions) .then((result) => { this.setState({ isLoading: false, - versions: [...(this.state.versions ?? []), ...this.decorateVersions(result)], + versions: [...(this.state.versions ?? []), ...this.decorateVersions(result.versions)], }); this._start += this._limit; + // Update the continueToken for the next request, if available + this._continueToken = result.continueToken ?? ''; }) .catch((err) => console.log(err)) .finally(() => this.setState({ isAppending: false })); @@ -127,9 +135,15 @@ export class VersionsEditView extends SceneObjectBase imp if (!this._dashboard.state.uid) { return; } - - const lhs = await historySrv.getDashboardVersion(this._dashboard.state.uid, baseInfo.version); - const rhs = await historySrv.getDashboardVersion(this._dashboard.state.uid, newInfo.version); + let lhs, rhs; + if (config.featureToggles.kubernetesCliDashboards) { + // the id here is the resource version in k8s, use this instead to get the specific version + lhs = await historySrv.getDashboardVersion(this._dashboard.state.uid, baseInfo.id); + rhs = await historySrv.getDashboardVersion(this._dashboard.state.uid, newInfo.id); + } else { + lhs = await historySrv.getDashboardVersion(this._dashboard.state.uid, baseInfo.version); + rhs = await historySrv.getDashboardVersion(this._dashboard.state.uid, newInfo.version); + } this.setState({ baseInfo, @@ -145,6 +159,7 @@ export class VersionsEditView extends SceneObjectBase imp }; public reset = () => { + this._continueToken = ''; this.setState({ baseInfo: undefined, diffData: { diff --git a/public/app/features/dashboard-scene/settings/version-history/HistorySrv.test.ts b/public/app/features/dashboard-scene/settings/version-history/HistorySrv.test.ts index c2caee31a0e..84a737d5a1d 100644 --- a/public/app/features/dashboard-scene/settings/version-history/HistorySrv.test.ts +++ b/public/app/features/dashboard-scene/settings/version-history/HistorySrv.test.ts @@ -62,11 +62,11 @@ describe('historySrv', () => { describe('getDashboardVersion', () => { it('should return a version object for the given dashboard id and version', () => { - getMock.mockImplementation(() => Promise.resolve(versionsResponse[0])); + getMock.mockImplementation(() => Promise.resolve(versionsResponse.versions[0])); historySrv = new HistorySrv(); return historySrv.getDashboardVersion(dash.uid, 4).then((version) => { - expect(version).toEqual(versionsResponse[0]); + expect(version).toEqual(versionsResponse.versions[0]); }); }); diff --git a/public/app/features/dashboard-scene/settings/version-history/HistorySrv.ts b/public/app/features/dashboard-scene/settings/version-history/HistorySrv.ts index 61312b8ad45..95cd468ec63 100644 --- a/public/app/features/dashboard-scene/settings/version-history/HistorySrv.ts +++ b/public/app/features/dashboard-scene/settings/version-history/HistorySrv.ts @@ -4,6 +4,7 @@ import { Dashboard } from '@grafana/schema'; export interface HistoryListOpts { limit: number; start: number; + continueToken?: string; } export interface RevisionsModel { diff --git a/public/app/features/dashboard-scene/settings/version-history/__mocks__/dashboardHistoryMocks.ts b/public/app/features/dashboard-scene/settings/version-history/__mocks__/dashboardHistoryMocks.ts index 0de97c47a95..3fbefb31c92 100644 --- a/public/app/features/dashboard-scene/settings/version-history/__mocks__/dashboardHistoryMocks.ts +++ b/public/app/features/dashboard-scene/settings/version-history/__mocks__/dashboardHistoryMocks.ts @@ -1,51 +1,54 @@ export function versions() { - return [ - { - id: 4, - dashboardId: 1, - dashboardUID: '_U4zObQMz', - parentVersion: 3, - restoredFrom: 0, - version: 4, - created: '2017-02-22T17:43:01-08:00', - createdBy: 'admin', - message: '', - }, - { - id: 3, - dashboardId: 1, - dashboardUID: '_U4zObQMz', - parentVersion: 1, - restoredFrom: 1, - version: 3, - created: '2017-02-22T17:43:01-08:00', - createdBy: 'admin', - message: '', - }, - { - id: 2, - dashboardId: 1, - dashboardUID: '_U4zObQMz', - parentVersion: 0, - restoredFrom: -1, - version: 2, - created: '2017-02-22T17:29:52-08:00', - createdBy: 'admin', - message: '', - }, - { - id: 1, - dashboardId: 1, - dashboardUID: '_U4zObQMz', - parentVersion: 0, - restoredFrom: -1, - slug: 'history-dashboard', - version: 1, - created: '2017-02-22T17:06:37-08:00', - createdBy: 'admin', - message: '', - }, - ]; + return { + continueToken: '', + versions: [ + { + id: 4, + dashboardId: 1, + dashboardUID: '_U4zObQMz', + parentVersion: 3, + restoredFrom: 0, + version: 4, + created: '2017-02-22T17:43:01-08:00', + createdBy: 'admin', + message: '', + }, + { + id: 3, + dashboardId: 1, + dashboardUID: '_U4zObQMz', + parentVersion: 1, + restoredFrom: 1, + version: 3, + created: '2017-02-22T17:43:01-08:00', + createdBy: 'admin', + message: '', + }, + { + id: 2, + dashboardId: 1, + dashboardUID: '_U4zObQMz', + parentVersion: 0, + restoredFrom: -1, + version: 2, + created: '2017-02-22T17:29:52-08:00', + createdBy: 'admin', + message: '', + }, + { + id: 1, + dashboardId: 1, + dashboardUID: '_U4zObQMz', + parentVersion: 0, + restoredFrom: -1, + slug: 'history-dashboard', + version: 1, + created: '2017-02-22T17:06:37-08:00', + createdBy: 'admin', + message: '', + }, + ], + }; } export function restore(version: number, restoredFrom?: number) { diff --git a/public/app/features/dashboard/components/DashboardSettings/VersionsSettings.test.tsx b/public/app/features/dashboard/components/DashboardSettings/VersionsSettings.test.tsx index 4b0b91ef6e6..16f8d84f237 100644 --- a/public/app/features/dashboard/components/DashboardSettings/VersionsSettings.test.tsx +++ b/public/app/features/dashboard/components/DashboardSettings/VersionsSettings.test.tsx @@ -66,7 +66,7 @@ describe('VersionSettings', () => { await waitFor(() => expect(screen.getByRole('table')).toBeInTheDocument()); const tableBodyRows = within(screen.getAllByRole('rowgroup')[1]).getAllByRole('row'); - expect(tableBodyRows.length).toBe(versions.length); + expect(tableBodyRows.length).toBe(versions.versions.length); const firstRow = within(screen.getAllByRole('rowgroup')[1]).getAllByRole('row')[0]; @@ -76,7 +76,11 @@ describe('VersionSettings', () => { test('does not render buttons if versions === 1', async () => { // @ts-ignore - historySrv.getHistoryList.mockResolvedValue(versions.slice(0, 1)); + historySrv.getHistoryList.mockResolvedValue({ + continueToken: versions.continueToken, + versions: versions.versions.slice(0, 1), + }); + setup(); expect(screen.queryByRole('button', { name: /show more versions/i })).not.toBeInTheDocument(); @@ -90,7 +94,11 @@ describe('VersionSettings', () => { test('does not render show more button if versions < VERSIONS_FETCH_LIMIT', async () => { // @ts-ignore - historySrv.getHistoryList.mockResolvedValue(versions.slice(0, VERSIONS_FETCH_LIMIT - 5)); + historySrv.getHistoryList.mockResolvedValue({ + continueToken: versions.continueToken, + versions: versions.versions.slice(0, VERSIONS_FETCH_LIMIT - 5), + }); + setup(); expect(screen.queryByRole('button', { name: /show more versions/i })).not.toBeInTheDocument(); @@ -104,7 +112,11 @@ describe('VersionSettings', () => { test('renders buttons if versions >= VERSIONS_FETCH_LIMIT', async () => { // @ts-ignore - historySrv.getHistoryList.mockResolvedValue(versions.slice(0, VERSIONS_FETCH_LIMIT)); + historySrv.getHistoryList.mockResolvedValue({ + continueToken: versions.continueToken, + versions: versions.versions.slice(0, VERSIONS_FETCH_LIMIT), + }); + setup(); expect(screen.queryByRole('button', { name: /show more versions/i })).not.toBeInTheDocument(); @@ -124,9 +136,17 @@ describe('VersionSettings', () => { test('clicking show more appends results to the table', async () => { historySrv.getHistoryList // @ts-ignore - .mockImplementationOnce(() => Promise.resolve(versions.slice(0, VERSIONS_FETCH_LIMIT))) - .mockImplementationOnce( - () => new Promise((resolve) => setTimeout(() => resolve(versions.slice(VERSIONS_FETCH_LIMIT)), 1000)) + .mockImplementationOnce(() => + Promise.resolve({ + continueToken: versions.continueToken, + versions: versions.versions.slice(0, VERSIONS_FETCH_LIMIT), + }) + ) + .mockImplementationOnce(() => + Promise.resolve({ + continueToken: versions.continueToken, + versions: versions.versions.slice(VERSIONS_FETCH_LIMIT), + }) ); setup(); @@ -146,13 +166,16 @@ describe('VersionSettings', () => { await waitFor(() => { expect(screen.queryByText(/Fetching more entries/i)).not.toBeInTheDocument(); - expect(within(screen.getAllByRole('rowgroup')[1]).getAllByRole('row').length).toBe(versions.length); + expect(within(screen.getAllByRole('rowgroup')[1]).getAllByRole('row').length).toBe(versions.versions.length); }); }); test('selecting two versions and clicking compare button should render compare view', async () => { // @ts-ignore - historySrv.getHistoryList.mockResolvedValue(versions.slice(0, VERSIONS_FETCH_LIMIT)); + historySrv.getHistoryList.mockResolvedValue({ + continueToken: versions.continueToken, + versions: versions.versions.slice(0, VERSIONS_FETCH_LIMIT), + }); historySrv.getDashboardVersion // @ts-ignore .mockImplementationOnce(() => Promise.resolve(diffs.lhs)) diff --git a/public/app/features/dashboard/components/DashboardSettings/VersionsSettings.tsx b/public/app/features/dashboard/components/DashboardSettings/VersionsSettings.tsx index c22f31c9437..30ccbe39708 100644 --- a/public/app/features/dashboard/components/DashboardSettings/VersionsSettings.tsx +++ b/public/app/features/dashboard/components/DashboardSettings/VersionsSettings.tsx @@ -1,6 +1,7 @@ import { PureComponent } from 'react'; import * as React from 'react'; +import { config } from '@grafana/runtime'; import { Spinner, HorizontalGroup } from '@grafana/ui'; import { Page } from 'app/core/components/Page/Page'; import { @@ -38,11 +39,13 @@ export const VERSIONS_FETCH_LIMIT = 10; export class VersionsSettings extends PureComponent { limit: number; start: number; + continueToken: string; constructor(props: Props) { super(props); this.limit = VERSIONS_FETCH_LIMIT; this.start = 0; + this.continueToken = ''; this.state = { isAppending: true, isLoading: true, @@ -62,14 +65,20 @@ export class VersionsSettings extends PureComponent { getVersions = (append = false) => { this.setState({ isAppending: append }); + const requestOptions = this.continueToken + ? { limit: this.limit, start: this.start, continueToken: this.continueToken } + : { limit: this.limit, start: this.start }; + historySrv - .getHistoryList(this.props.dashboard.uid, { limit: this.limit, start: this.start }) + .getHistoryList(this.props.dashboard.uid, requestOptions) .then((res) => { this.setState({ isLoading: false, - versions: [...this.state.versions, ...this.decorateVersions(res)], + versions: [...(this.state.versions ?? []), ...this.decorateVersions(res.versions)], }); this.start += this.limit; + // Update the continueToken for the next request, if available + this.continueToken = res.continueToken ?? ''; }) .catch((err) => console.log(err)) .finally(() => this.setState({ isAppending: false })); @@ -84,8 +93,15 @@ export class VersionsSettings extends PureComponent { isLoading: true, }); - const lhs = await historySrv.getDashboardVersion(this.props.dashboard.uid, baseInfo.version); - const rhs = await historySrv.getDashboardVersion(this.props.dashboard.uid, newInfo.version); + let lhs, rhs; + if (config.featureToggles.kubernetesCliDashboards) { + // the id here is the resource version in k8s, use this instead to get the specific version + lhs = await historySrv.getDashboardVersion(this.props.dashboard.uid, baseInfo.id); + rhs = await historySrv.getDashboardVersion(this.props.dashboard.uid, newInfo.id); + } else { + lhs = await historySrv.getDashboardVersion(this.props.dashboard.uid, baseInfo.version); + rhs = await historySrv.getDashboardVersion(this.props.dashboard.uid, newInfo.version); + } this.setState({ baseInfo, @@ -121,6 +137,7 @@ export class VersionsSettings extends PureComponent { }; reset = () => { + this.continueToken = ''; this.setState({ baseInfo: undefined, diffData: { diff --git a/public/app/features/dashboard/components/DashboardSettings/__mocks__/versions.ts b/public/app/features/dashboard/components/DashboardSettings/__mocks__/versions.ts index 62f3cf4d876..3b3dc5629d8 100644 --- a/public/app/features/dashboard/components/DashboardSettings/__mocks__/versions.ts +++ b/public/app/features/dashboard/components/DashboardSettings/__mocks__/versions.ts @@ -1,126 +1,129 @@ -export const versions = [ - { - id: 249, - dashboardId: 74, - dashboardUID: '_U4zObQMz', - parentVersion: 10, - restoredFrom: 0, - version: 11, - created: '2021-01-15T14:44:44+01:00', - createdBy: 'admin', - message: 'testing changes...', - }, - { - id: 247, - dashboardId: 74, - dashboardUID: '_U4zObQMz', - parentVersion: 9, - restoredFrom: 0, - version: 10, - created: '2021-01-15T10:19:17+01:00', - createdBy: 'admin', - message: '', - }, - { - id: 246, - dashboardId: 74, - dashboardUID: '_U4zObQMz', - parentVersion: 8, - restoredFrom: 0, - version: 9, - created: '2021-01-15T10:18:12+01:00', - createdBy: 'admin', - message: '', - }, - { - id: 245, - dashboardId: 74, - dashboardUID: '_U4zObQMz', - parentVersion: 7, - restoredFrom: 0, - version: 8, - created: '2021-01-15T10:11:16+01:00', - createdBy: 'admin', - message: '', - }, - { - id: 239, - dashboardId: 74, - dashboardUID: '_U4zObQMz', - parentVersion: 6, - restoredFrom: 0, - version: 7, - created: '2021-01-14T15:14:25+01:00', - createdBy: 'admin', - message: '', - }, - { - id: 237, - dashboardId: 74, - dashboardUID: '_U4zObQMz', - parentVersion: 5, - restoredFrom: 0, - version: 6, - created: '2021-01-14T14:55:29+01:00', - createdBy: 'admin', - message: '', - }, - { - id: 236, - dashboardId: 74, - dashboardUID: '_U4zObQMz', - parentVersion: 4, - restoredFrom: 0, - version: 5, - created: '2021-01-14T14:28:01+01:00', - createdBy: 'admin', - message: '', - }, - { - id: 218, - dashboardId: 74, - dashboardUID: '_U4zObQMz', - parentVersion: 3, - restoredFrom: 0, - version: 4, - created: '2021-01-08T10:45:33+01:00', - createdBy: 'admin', - message: '', - }, - { - id: 217, - dashboardId: 74, - dashboardUID: '_U4zObQMz', - parentVersion: 2, - restoredFrom: 0, - version: 3, - created: '2021-01-05T15:41:33+01:00', - createdBy: 'admin', - message: '', - }, - { - id: 216, - dashboardId: 74, - dashboardUID: '_U4zObQMz', - parentVersion: 1, - restoredFrom: 0, - version: 2, - created: '2021-01-05T15:01:50+01:00', - createdBy: 'admin', - message: '', - }, - { - id: 215, - dashboardId: 74, - dashboardUID: '_U4zObQMz', - parentVersion: 1, - restoredFrom: 0, - version: 1, - created: '2021-01-05T14:59:15+01:00', - createdBy: 'admin', - message: '', - }, -]; +export const versions = { + continueToken: '', + versions: [ + { + id: 249, + dashboardId: 74, + dashboardUID: '_U4zObQMz', + parentVersion: 10, + restoredFrom: 0, + version: 11, + created: '2021-01-15T14:44:44+01:00', + createdBy: 'admin', + message: 'testing changes...', + }, + { + id: 247, + dashboardId: 74, + dashboardUID: '_U4zObQMz', + parentVersion: 9, + restoredFrom: 0, + version: 10, + created: '2021-01-15T10:19:17+01:00', + createdBy: 'admin', + message: '', + }, + { + id: 246, + dashboardId: 74, + dashboardUID: '_U4zObQMz', + parentVersion: 8, + restoredFrom: 0, + version: 9, + created: '2021-01-15T10:18:12+01:00', + createdBy: 'admin', + message: '', + }, + { + id: 245, + dashboardId: 74, + dashboardUID: '_U4zObQMz', + parentVersion: 7, + restoredFrom: 0, + version: 8, + created: '2021-01-15T10:11:16+01:00', + createdBy: 'admin', + message: '', + }, + { + id: 239, + dashboardId: 74, + dashboardUID: '_U4zObQMz', + parentVersion: 6, + restoredFrom: 0, + version: 7, + created: '2021-01-14T15:14:25+01:00', + createdBy: 'admin', + message: '', + }, + { + id: 237, + dashboardId: 74, + dashboardUID: '_U4zObQMz', + parentVersion: 5, + restoredFrom: 0, + version: 6, + created: '2021-01-14T14:55:29+01:00', + createdBy: 'admin', + message: '', + }, + { + id: 236, + dashboardId: 74, + dashboardUID: '_U4zObQMz', + parentVersion: 4, + restoredFrom: 0, + version: 5, + created: '2021-01-14T14:28:01+01:00', + createdBy: 'admin', + message: '', + }, + { + id: 218, + dashboardId: 74, + dashboardUID: '_U4zObQMz', + parentVersion: 3, + restoredFrom: 0, + version: 4, + created: '2021-01-08T10:45:33+01:00', + createdBy: 'admin', + message: '', + }, + { + id: 217, + dashboardId: 74, + dashboardUID: '_U4zObQMz', + parentVersion: 2, + restoredFrom: 0, + version: 3, + created: '2021-01-05T15:41:33+01:00', + createdBy: 'admin', + message: '', + }, + { + id: 216, + dashboardId: 74, + dashboardUID: '_U4zObQMz', + parentVersion: 1, + restoredFrom: 0, + version: 2, + created: '2021-01-05T15:01:50+01:00', + createdBy: 'admin', + message: '', + }, + { + id: 215, + dashboardId: 74, + dashboardUID: '_U4zObQMz', + parentVersion: 1, + restoredFrom: 0, + version: 1, + created: '2021-01-05T14:59:15+01:00', + createdBy: 'admin', + message: '', + }, + ], +}; export const diffs = { lhs: { diff --git a/public/app/features/dashboard/components/VersionHistory/RevertDashboardModal.tsx b/public/app/features/dashboard/components/VersionHistory/RevertDashboardModal.tsx index 5a4451b4596..955d45388c4 100644 --- a/public/app/features/dashboard/components/VersionHistory/RevertDashboardModal.tsx +++ b/public/app/features/dashboard/components/VersionHistory/RevertDashboardModal.tsx @@ -5,12 +5,13 @@ import { ConfirmModal } from '@grafana/ui'; import { useDashboardRestore } from './useDashboardRestore'; export interface RevertDashboardModalProps { hideModal: () => void; + id: number; version: number; } -export const RevertDashboardModal = ({ hideModal, version }: RevertDashboardModalProps) => { +export const RevertDashboardModal = ({ hideModal, id, version }: RevertDashboardModalProps) => { // TODO: how should state.error be handled? - const { state, onRestoreDashboard } = useDashboardRestore(version); + const { state, onRestoreDashboard } = useDashboardRestore(id, version); useEffect(() => { if (!state.loading && state.value) { diff --git a/public/app/features/dashboard/components/VersionHistory/VersionHistoryComparison.tsx b/public/app/features/dashboard/components/VersionHistory/VersionHistoryComparison.tsx index 2f23cd32e17..72294e80941 100644 --- a/public/app/features/dashboard/components/VersionHistory/VersionHistoryComparison.tsx +++ b/public/app/features/dashboard/components/VersionHistory/VersionHistoryComparison.tsx @@ -43,6 +43,7 @@ export const VersionHistoryComparison = ({ baseInfo, newInfo, diffData, isNewLat icon="history" onClick={() => { showModal(RevertDashboardModal, { + id: baseInfo.id, version: baseInfo.version, hideModal, }); diff --git a/public/app/features/dashboard/components/VersionHistory/VersionHistoryTable.tsx b/public/app/features/dashboard/components/VersionHistory/VersionHistoryTable.tsx index 2cd8514992a..5edd119e490 100644 --- a/public/app/features/dashboard/components/VersionHistory/VersionHistoryTable.tsx +++ b/public/app/features/dashboard/components/VersionHistory/VersionHistoryTable.tsx @@ -60,6 +60,7 @@ export const VersionHistoryTable = ({ versions, canCompare, onCheck }: VersionsT icon="history" onClick={() => { showModal(RevertDashboardModal, { + id: version.id, version: version.version, hideModal, }); diff --git a/public/app/features/dashboard/components/VersionHistory/useDashboardRestore.tsx b/public/app/features/dashboard/components/VersionHistory/useDashboardRestore.tsx index 4ee340a8110..91fd0964aca 100644 --- a/public/app/features/dashboard/components/VersionHistory/useDashboardRestore.tsx +++ b/public/app/features/dashboard/components/VersionHistory/useDashboardRestore.tsx @@ -2,7 +2,7 @@ import { useEffect } from 'react'; import { useAsyncFn } from 'react-use'; import { locationUtil } from '@grafana/data'; -import { locationService } from '@grafana/runtime'; +import { config, locationService } from '@grafana/runtime'; import { useAppNotification } from 'app/core/copy/appNotification'; import { historySrv } from 'app/features/dashboard-scene/settings/version-history'; import { useSelector } from 'app/types'; @@ -16,9 +16,12 @@ const restoreDashboard = async (version: number, dashboard: DashboardModel) => { return await historySrv.restoreDashboard(dashboard.uid, version); }; -export const useDashboardRestore = (version: number) => { +export const useDashboardRestore = (id: number, version: number) => { const dashboard = useSelector((state) => state.dashboard.getModel()); - const [state, onRestoreDashboard] = useAsyncFn(async () => await restoreDashboard(version, dashboard!), []); + const [state, onRestoreDashboard] = useAsyncFn( + async () => await restoreDashboard(config.featureToggles.kubernetesCliDashboards ? id : version, dashboard!), + [] + ); const notifyApp = useAppNotification(); useEffect(() => { From fe49b6279adbc3d4037a54db2f8ef2b8dc0a0df1 Mon Sep 17 00:00:00 2001 From: Mitch Seaman Date: Tue, 28 Jan 2025 15:38:17 +0100 Subject: [PATCH 28/39] docs: update enterprise docs with license token renewal details (#99662) --- .../administration/enterprise-licensing/_index.md | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/docs/sources/administration/enterprise-licensing/_index.md b/docs/sources/administration/enterprise-licensing/_index.md index e42696f97ec..b532bcf74db 100644 --- a/docs/sources/administration/enterprise-licensing/_index.md +++ b/docs/sources/administration/enterprise-licensing/_index.md @@ -246,6 +246,14 @@ Your license is controlled by the following rules: As the license expiration date approaches, you will see a banner in Grafana that encourages you to renew. To learn about how to renew your license and what happens in Grafana when a license expires, refer to [License expiration]({{< relref "#license-expiration" >}}). +**License token expiration:** Your license must contain a valid token, which renews periodically. + +A license token is a digital key that activates your license. By default, license tokens renew every 7 days by calling the Grafana.com API. Short-lived license tokens enable more frequent validation that licenses are compliant, and allow for more frequent license updates - for example, adding users or invalidating a compromised license. + +To view the details of your license token, sign in to Grafana Enterprise as a Server Administrator and visit **Administration** > **General** > **Statistics and licensing**. Token details are in the Token section under License Details. + +License token renewal requires internet access, and requires that the `auto_refresh_license` [configuration setting](https://grafana.com/docs/grafana/latest/setup-grafana/configure-grafana/enterprise-configuration/#auto_refresh_license) be set to `true`. If your Grafana Enterprise instance cannot connect to the internet, contact your Grafana Labs account team for additional options for token renewal and license audit. + **Grafana License URL:** Your license does not work with an instance of Grafana with a different root URL. The License URL is the complete URL of your Grafana instance, for example `https://grafana.your-company.com/`. It is defined in the [root_url]({{< relref "../../setup-grafana/configure-grafana/#root_url" >}}) configuration setting. @@ -264,7 +272,7 @@ For instructions about how to activate your license after it is updated, refer t ## Usage billing -Standard Grafana Enterprise licenses include a certain number of seats that can be used, and prevent more users logging into Grafana than have been licensed. This makes sense if you prefer a predictable bill. It can however be a problem if you anticipate uneven usage patterns over time or when it's critical that no user ever be prevented from logging into Grafana due to capacity constraints. +Standard Grafana Enterprise licenses include a certain number of seats that can be used, and prevent more users logging into Grafana than have been licensed. This makes sense if you prefer a predictable bill. It can however be a problem if you anticipate uneven usage patterns over time or when it's critical that no user ever be prevented from logging in to Grafana due to capacity constraints. For those use-cases we support usage-based billing, where your license includes a certain number of included users and you are billed on a monthly basis for any excess active users during the month. From 0a805a23144cc4250c71543db819dad37129687a Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 28 Jan 2025 16:40:21 +0200 Subject: [PATCH 29/39] Update dependency moment-timezone to v0.5.47 (#99657) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package.json | 2 +- packages/grafana-data/package.json | 2 +- packages/grafana-prometheus/package.json | 2 +- yarn.lock | 15 ++++++++++++--- 4 files changed, 15 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index 92651a8dbd1..f0a249ff633 100644 --- a/package.json +++ b/package.json @@ -349,7 +349,7 @@ "ml-regression-polynomial": "^3.0.0", "ml-regression-simple-linear": "^3.0.0", "moment": "2.30.1", - "moment-timezone": "0.5.46", + "moment-timezone": "0.5.47", "monaco-editor": "0.34.1", "moveable": "0.53.0", "nanoid": "^5.0.4", diff --git a/packages/grafana-data/package.json b/packages/grafana-data/package.json index ff3a9928802..2d5576fa453 100644 --- a/packages/grafana-data/package.json +++ b/packages/grafana-data/package.json @@ -49,7 +49,7 @@ "marked": "15.0.6", "marked-mangle": "1.1.10", "moment": "2.30.1", - "moment-timezone": "0.5.46", + "moment-timezone": "0.5.47", "ol": "7.4.0", "papaparse": "5.5.2", "react-use": "17.6.0", diff --git a/packages/grafana-prometheus/package.json b/packages/grafana-prometheus/package.json index 3f3060e08cc..695108bb59f 100644 --- a/packages/grafana-prometheus/package.json +++ b/packages/grafana-prometheus/package.json @@ -61,7 +61,7 @@ "marked": "15.0.6", "marked-mangle": "1.1.10", "moment": "2.30.1", - "moment-timezone": "0.5.46", + "moment-timezone": "0.5.47", "monaco-promql": "1.7.4", "pluralize": "8.0.0", "prismjs": "1.29.0", diff --git a/yarn.lock b/yarn.lock index 2714fa57eea..5bab6cc7238 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3223,7 +3223,7 @@ __metadata: marked: "npm:15.0.6" marked-mangle: "npm:1.1.10" moment: "npm:2.30.1" - moment-timezone: "npm:0.5.46" + moment-timezone: "npm:0.5.47" ol: "npm:7.4.0" papaparse: "npm:5.5.2" react: "npm:18.3.1" @@ -3666,7 +3666,7 @@ __metadata: marked: "npm:15.0.6" marked-mangle: "npm:1.1.10" moment: "npm:2.30.1" - moment-timezone: "npm:0.5.46" + moment-timezone: "npm:0.5.47" monaco-promql: "npm:1.7.4" pluralize: "npm:8.0.0" prettier: "npm:3.4.2" @@ -17959,7 +17959,7 @@ __metadata: ml-regression-polynomial: "npm:^3.0.0" ml-regression-simple-linear: "npm:^3.0.0" moment: "npm:2.30.1" - moment-timezone: "npm:0.5.46" + moment-timezone: "npm:0.5.47" monaco-editor: "npm:0.34.1" moveable: "npm:0.53.0" msw: "npm:2.7.0" @@ -22433,6 +22433,15 @@ __metadata: languageName: node linkType: hard +"moment-timezone@npm:0.5.47": + version: 0.5.47 + resolution: "moment-timezone@npm:0.5.47" + dependencies: + moment: "npm:^2.29.4" + checksum: 10/b2ad32e6b7ea4ce99756c9f571f8eb1bdd6592ecdc2bf3f7fb988ae980a49a7e87b986776090571b9608c8576205f839b736dc7589c180ce36c82fad4b8416c6 + languageName: node + linkType: hard + "moment@npm:2.30.1, moment@npm:2.x, moment@npm:^2.20.1, moment@npm:^2.29.4, moment@npm:^2.30.1": version: 2.30.1 resolution: "moment@npm:2.30.1" From 78848095ca64eaca228144b6d539e79b8bcf9728 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 28 Jan 2025 17:01:32 +0200 Subject: [PATCH 30/39] Update dependency swagger-ui-react to v5.18.3 (#99669) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package.json | 2 +- yarn.lock | 222 ++++++++++++++++++++++++++++++--------------------- 2 files changed, 133 insertions(+), 91 deletions(-) diff --git a/package.json b/package.json index f0a249ff633..257f9e62616 100644 --- a/package.json +++ b/package.json @@ -400,7 +400,7 @@ "slate": "0.47.9", "slate-plain-serializer": "0.7.13", "slate-react": "0.22.10", - "swagger-ui-react": "5.18.2", + "swagger-ui-react": "5.18.3", "symbol-observable": "4.0.0", "systemjs": "6.15.1", "systemjs-cjs-extra": "0.2.1", diff --git a/yarn.lock b/yarn.lock index 5bab6cc7238..c0a3fe7677e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7977,57 +7977,57 @@ __metadata: languageName: node linkType: hard -"@swagger-api/apidom-ast@npm:^1.0.0-beta.5": - version: 1.0.0-beta.5 - resolution: "@swagger-api/apidom-ast@npm:1.0.0-beta.5" +"@swagger-api/apidom-ast@npm:^1.0.0-beta.11, @swagger-api/apidom-ast@npm:^1.0.0-beta.5": + version: 1.0.0-beta.11 + resolution: "@swagger-api/apidom-ast@npm:1.0.0-beta.11" dependencies: "@babel/runtime-corejs3": "npm:^7.20.7" - "@swagger-api/apidom-error": "npm:^1.0.0-beta.5" + "@swagger-api/apidom-error": "npm:^1.0.0-beta.11" "@types/ramda": "npm:~0.30.0" ramda: "npm:~0.30.0" ramda-adjunct: "npm:^5.0.0" unraw: "npm:^3.0.0" - checksum: 10/b2ea32b8ed589a3aff122e9209d5f0c873364bb34b234d13796422d4fce6b9f52fab599ef47956f655316256cf6af821c233117c0ca96a677b867e075b70cb5d + checksum: 10/efe7caf37735f6b1a9b56ca780ce59134540c00c50ef00606607a458905142b7794466cacbffdcd9afe11ece9f5a99651f2f62d24b05f71727d86718e2dea91c languageName: node linkType: hard -"@swagger-api/apidom-core@npm:>=1.0.0-beta.3 <1.0.0-rc.0, @swagger-api/apidom-core@npm:^1.0.0-beta.5": - version: 1.0.0-beta.5 - resolution: "@swagger-api/apidom-core@npm:1.0.0-beta.5" +"@swagger-api/apidom-core@npm:>=1.0.0-beta.11 <1.0.0-rc.0, @swagger-api/apidom-core@npm:^1.0.0-beta.11, @swagger-api/apidom-core@npm:^1.0.0-beta.5": + version: 1.0.0-beta.11 + resolution: "@swagger-api/apidom-core@npm:1.0.0-beta.11" dependencies: "@babel/runtime-corejs3": "npm:^7.20.7" - "@swagger-api/apidom-ast": "npm:^1.0.0-beta.5" - "@swagger-api/apidom-error": "npm:^1.0.0-beta.5" + "@swagger-api/apidom-ast": "npm:^1.0.0-beta.11" + "@swagger-api/apidom-error": "npm:^1.0.0-beta.11" "@types/ramda": "npm:~0.30.0" minim: "npm:~0.23.8" ramda: "npm:~0.30.0" ramda-adjunct: "npm:^5.0.0" short-unique-id: "npm:^5.0.2" ts-mixer: "npm:^6.0.3" - checksum: 10/c034ef286738b2b5aab525b068fd22e1b54145e3024477abcafde926f3783c280c69e4de23cc28d9cc568a62fb02719a0a89e6fd2011136cf447f42ed66fca55 + checksum: 10/bb81c1ef603b80985c8e7ad8808a12eaaf2daa3de2a41ac2f9f8c041c7a7f26bcbfa9713738248db198af1a6b424127b10abd5ac7e6fcad95d852cea8fb8a526 languageName: node linkType: hard -"@swagger-api/apidom-error@npm:>=1.0.0-beta.3 <1.0.0-rc.0, @swagger-api/apidom-error@npm:^1.0.0-beta.3 <1.0.0-rc.0, @swagger-api/apidom-error@npm:^1.0.0-beta.5": - version: 1.0.0-beta.5 - resolution: "@swagger-api/apidom-error@npm:1.0.0-beta.5" +"@swagger-api/apidom-error@npm:>=1.0.0-beta.11 <1.0.0-rc.0, @swagger-api/apidom-error@npm:^1.0.0-beta.11, @swagger-api/apidom-error@npm:^1.0.0-beta.3 <1.0.0-rc.0, @swagger-api/apidom-error@npm:^1.0.0-beta.5": + version: 1.0.0-beta.11 + resolution: "@swagger-api/apidom-error@npm:1.0.0-beta.11" dependencies: "@babel/runtime-corejs3": "npm:^7.20.7" - checksum: 10/defb3ba3775be8a511ff01be4ed7d2eca66faf5ab478f65a39845c8981510a0286e622268240de56613fd3ee37de906a7c6947a82aceb214fdd89d0988b972bb + checksum: 10/d63ec68067a17c10fa51b2929c18d18f397d91ab45961e398ad99341b45e87d60a0ec6f634ac3883733ea7f4a5155e9929207dd44572f65754fa0f5b7d004466 languageName: node linkType: hard -"@swagger-api/apidom-json-pointer@npm:>=1.0.0-beta.3 <1.0.0-rc.0, @swagger-api/apidom-json-pointer@npm:^1.0.0-beta.3 <1.0.0-rc.0, @swagger-api/apidom-json-pointer@npm:^1.0.0-beta.5": - version: 1.0.0-beta.5 - resolution: "@swagger-api/apidom-json-pointer@npm:1.0.0-beta.5" +"@swagger-api/apidom-json-pointer@npm:>=1.0.0-beta.11 <1.0.0-rc.0, @swagger-api/apidom-json-pointer@npm:^1.0.0-beta.11, @swagger-api/apidom-json-pointer@npm:^1.0.0-beta.3 <1.0.0-rc.0": + version: 1.0.0-beta.11 + resolution: "@swagger-api/apidom-json-pointer@npm:1.0.0-beta.11" dependencies: "@babel/runtime-corejs3": "npm:^7.20.7" - "@swagger-api/apidom-core": "npm:^1.0.0-beta.5" - "@swagger-api/apidom-error": "npm:^1.0.0-beta.5" + "@swagger-api/apidom-core": "npm:^1.0.0-beta.11" + "@swagger-api/apidom-error": "npm:^1.0.0-beta.11" "@types/ramda": "npm:~0.30.0" ramda: "npm:~0.30.0" ramda-adjunct: "npm:^5.0.0" - checksum: 10/68b3b196de1d2cb86663350ba70f885599a12c21804cd9bc1be1777ee5e99e9f570623599d476d241bda298e0890c34d2c9a516f7a47cf060992107c31e9d9f5 + checksum: 10/dee858ee4b4a0f93ab082451a703c8b9e0250f7b486d8d9ef243b77f92acc49741cfdd1e10a9cc44d946b64e469aa4016a1380c7a28d660fe048a2897098afc7 languageName: node linkType: hard @@ -8062,50 +8062,82 @@ __metadata: languageName: node linkType: hard -"@swagger-api/apidom-ns-json-schema-draft-4@npm:^1.0.0-beta.5": - version: 1.0.0-beta.5 - resolution: "@swagger-api/apidom-ns-json-schema-draft-4@npm:1.0.0-beta.5" +"@swagger-api/apidom-ns-json-schema-2019-09@npm:^1.0.0-beta.11": + version: 1.0.0-beta.11 + resolution: "@swagger-api/apidom-ns-json-schema-2019-09@npm:1.0.0-beta.11" dependencies: "@babel/runtime-corejs3": "npm:^7.20.7" - "@swagger-api/apidom-ast": "npm:^1.0.0-beta.5" - "@swagger-api/apidom-core": "npm:^1.0.0-beta.5" + "@swagger-api/apidom-core": "npm:^1.0.0-beta.11" + "@swagger-api/apidom-error": "npm:^1.0.0-beta.11" + "@swagger-api/apidom-ns-json-schema-draft-7": "npm:^1.0.0-beta.11" "@types/ramda": "npm:~0.30.0" ramda: "npm:~0.30.0" ramda-adjunct: "npm:^5.0.0" ts-mixer: "npm:^6.0.4" - checksum: 10/2518b3aa9b7387a90553565132101616dc24c6d1f8aee581f3562bac87403f87887217fdb3cf17b113f685e051980fc66ec25d9cab7e4a0bf4922b4bcd3bc502 + checksum: 10/b91f90e7376922f1112752520bcac265c20245c1d2d682cd5ba9344a3eb7287962f18e3430f501af4d4db43397a2977267b15ef02a3bcd6e8ba384901e195672 languageName: node linkType: hard -"@swagger-api/apidom-ns-json-schema-draft-6@npm:^1.0.0-beta.5": - version: 1.0.0-beta.5 - resolution: "@swagger-api/apidom-ns-json-schema-draft-6@npm:1.0.0-beta.5" +"@swagger-api/apidom-ns-json-schema-2020-12@npm:^1.0.0-beta.11": + version: 1.0.0-beta.11 + resolution: "@swagger-api/apidom-ns-json-schema-2020-12@npm:1.0.0-beta.11" dependencies: "@babel/runtime-corejs3": "npm:^7.20.7" - "@swagger-api/apidom-core": "npm:^1.0.0-beta.5" - "@swagger-api/apidom-error": "npm:^1.0.0-beta.5" - "@swagger-api/apidom-ns-json-schema-draft-4": "npm:^1.0.0-beta.5" + "@swagger-api/apidom-core": "npm:^1.0.0-beta.11" + "@swagger-api/apidom-error": "npm:^1.0.0-beta.11" + "@swagger-api/apidom-ns-json-schema-2019-09": "npm:^1.0.0-beta.11" "@types/ramda": "npm:~0.30.0" ramda: "npm:~0.30.0" ramda-adjunct: "npm:^5.0.0" ts-mixer: "npm:^6.0.4" - checksum: 10/c439c3679dc6ea1807affa0a2913deea52ff8d63b48722c741458da147e849be9acc0c13955757a7b6904a4e7148cdb7845291da758a085083c61610a96fc36f + checksum: 10/847103b0923fa7a0fcff124f5359f735f56e38a88666051e5959403e0060e33bea63b30098dec99d36bcda6619b2a3d13ce61c26e6ca03c508ea8518e871c123 languageName: node linkType: hard -"@swagger-api/apidom-ns-json-schema-draft-7@npm:^1.0.0-beta.5": - version: 1.0.0-beta.5 - resolution: "@swagger-api/apidom-ns-json-schema-draft-7@npm:1.0.0-beta.5" +"@swagger-api/apidom-ns-json-schema-draft-4@npm:^1.0.0-beta.11, @swagger-api/apidom-ns-json-schema-draft-4@npm:^1.0.0-beta.5": + version: 1.0.0-beta.11 + resolution: "@swagger-api/apidom-ns-json-schema-draft-4@npm:1.0.0-beta.11" dependencies: "@babel/runtime-corejs3": "npm:^7.20.7" - "@swagger-api/apidom-core": "npm:^1.0.0-beta.5" - "@swagger-api/apidom-error": "npm:^1.0.0-beta.5" - "@swagger-api/apidom-ns-json-schema-draft-6": "npm:^1.0.0-beta.5" + "@swagger-api/apidom-ast": "npm:^1.0.0-beta.11" + "@swagger-api/apidom-core": "npm:^1.0.0-beta.11" "@types/ramda": "npm:~0.30.0" ramda: "npm:~0.30.0" ramda-adjunct: "npm:^5.0.0" ts-mixer: "npm:^6.0.4" - checksum: 10/28c8f989e26a453f2ca6c655d6822d63f7bc57137375c460c707caa964a5097566969031f322d772b49de0ce2924aeb69e2c5a6ff66191b457844e68a40f013e + checksum: 10/9e0ba9e29849915c0067227a82d8d9a741221b3ac73592b73e23d3cfe0c57548b21cbbb66cdeba914218c5f675f4ec497232d465880013f8aec8f486e4c3d91c + languageName: node + linkType: hard + +"@swagger-api/apidom-ns-json-schema-draft-6@npm:^1.0.0-beta.11": + version: 1.0.0-beta.11 + resolution: "@swagger-api/apidom-ns-json-schema-draft-6@npm:1.0.0-beta.11" + dependencies: + "@babel/runtime-corejs3": "npm:^7.20.7" + "@swagger-api/apidom-core": "npm:^1.0.0-beta.11" + "@swagger-api/apidom-error": "npm:^1.0.0-beta.11" + "@swagger-api/apidom-ns-json-schema-draft-4": "npm:^1.0.0-beta.11" + "@types/ramda": "npm:~0.30.0" + ramda: "npm:~0.30.0" + ramda-adjunct: "npm:^5.0.0" + ts-mixer: "npm:^6.0.4" + checksum: 10/0df53880202cdb8dbe1e534314842c2c10fe4cd03f6eecd56d67174492807df03134f360e1c0724610fc4531520c49bb41d81e6fd7373118031ad883e0101338 + languageName: node + linkType: hard + +"@swagger-api/apidom-ns-json-schema-draft-7@npm:^1.0.0-beta.11, @swagger-api/apidom-ns-json-schema-draft-7@npm:^1.0.0-beta.5": + version: 1.0.0-beta.11 + resolution: "@swagger-api/apidom-ns-json-schema-draft-7@npm:1.0.0-beta.11" + dependencies: + "@babel/runtime-corejs3": "npm:^7.20.7" + "@swagger-api/apidom-core": "npm:^1.0.0-beta.11" + "@swagger-api/apidom-error": "npm:^1.0.0-beta.11" + "@swagger-api/apidom-ns-json-schema-draft-6": "npm:^1.0.0-beta.11" + "@types/ramda": "npm:~0.30.0" + ramda: "npm:~0.30.0" + ramda-adjunct: "npm:^5.0.0" + ts-mixer: "npm:^6.0.4" + checksum: 10/9ef5261e58b2e6798a3c22ad01940c613475a6515d2cd79d2f150b02152118036ef472bb75a271c0d1c3b617d51ccb2262680469824335f27150600f695eb6d8 languageName: node linkType: hard @@ -8125,36 +8157,37 @@ __metadata: languageName: node linkType: hard -"@swagger-api/apidom-ns-openapi-3-0@npm:^1.0.0-beta.3 <1.0.0-rc.0, @swagger-api/apidom-ns-openapi-3-0@npm:^1.0.0-beta.5": - version: 1.0.0-beta.5 - resolution: "@swagger-api/apidom-ns-openapi-3-0@npm:1.0.0-beta.5" +"@swagger-api/apidom-ns-openapi-3-0@npm:^1.0.0-beta.11, @swagger-api/apidom-ns-openapi-3-0@npm:^1.0.0-beta.3 <1.0.0-rc.0, @swagger-api/apidom-ns-openapi-3-0@npm:^1.0.0-beta.5": + version: 1.0.0-beta.11 + resolution: "@swagger-api/apidom-ns-openapi-3-0@npm:1.0.0-beta.11" dependencies: "@babel/runtime-corejs3": "npm:^7.20.7" - "@swagger-api/apidom-core": "npm:^1.0.0-beta.5" - "@swagger-api/apidom-error": "npm:^1.0.0-beta.5" - "@swagger-api/apidom-ns-json-schema-draft-4": "npm:^1.0.0-beta.5" + "@swagger-api/apidom-core": "npm:^1.0.0-beta.11" + "@swagger-api/apidom-error": "npm:^1.0.0-beta.11" + "@swagger-api/apidom-ns-json-schema-draft-4": "npm:^1.0.0-beta.11" "@types/ramda": "npm:~0.30.0" ramda: "npm:~0.30.0" ramda-adjunct: "npm:^5.0.0" ts-mixer: "npm:^6.0.3" - checksum: 10/5bb9c191a3d79d9a69aa04deb5dd6ce2eaa09700210f5a89fdeb0668ec76eb3f2d4a26950405f001459e59ad199ddd4632251992b9fc1b9fa03955e1ad600810 + checksum: 10/44eabda02fb8ad965b7756931bba7ac2ae9f49ef720f401179efb9eafbf94ff6b3c8cb8fd260c9ddb1affe35c3876f780cef4cb090b755736847adb7eba699c8 languageName: node linkType: hard -"@swagger-api/apidom-ns-openapi-3-1@npm:>=1.0.0-beta.3 <1.0.0-rc.0, @swagger-api/apidom-ns-openapi-3-1@npm:^1.0.0-beta.3 <1.0.0-rc.0, @swagger-api/apidom-ns-openapi-3-1@npm:^1.0.0-beta.5": - version: 1.0.0-beta.5 - resolution: "@swagger-api/apidom-ns-openapi-3-1@npm:1.0.0-beta.5" +"@swagger-api/apidom-ns-openapi-3-1@npm:>=1.0.0-beta.11 <1.0.0-rc.0, @swagger-api/apidom-ns-openapi-3-1@npm:^1.0.0-beta.3 <1.0.0-rc.0, @swagger-api/apidom-ns-openapi-3-1@npm:^1.0.0-beta.5": + version: 1.0.0-beta.11 + resolution: "@swagger-api/apidom-ns-openapi-3-1@npm:1.0.0-beta.11" dependencies: "@babel/runtime-corejs3": "npm:^7.20.7" - "@swagger-api/apidom-ast": "npm:^1.0.0-beta.5" - "@swagger-api/apidom-core": "npm:^1.0.0-beta.5" - "@swagger-api/apidom-json-pointer": "npm:^1.0.0-beta.5" - "@swagger-api/apidom-ns-openapi-3-0": "npm:^1.0.0-beta.5" + "@swagger-api/apidom-ast": "npm:^1.0.0-beta.11" + "@swagger-api/apidom-core": "npm:^1.0.0-beta.11" + "@swagger-api/apidom-json-pointer": "npm:^1.0.0-beta.11" + "@swagger-api/apidom-ns-json-schema-2020-12": "npm:^1.0.0-beta.11" + "@swagger-api/apidom-ns-openapi-3-0": "npm:^1.0.0-beta.11" "@types/ramda": "npm:~0.30.0" ramda: "npm:~0.30.0" ramda-adjunct: "npm:^5.0.0" ts-mixer: "npm:^6.0.3" - checksum: 10/69018147465c78a25efc5e7bc4439561c2d620c65c9cf86bdc503dbad9b40e52db77bb0cb11800a4c34bdc88d462126e3dc355bad290cdfe7c9fbeda391405c8 + checksum: 10/6ed3a3d7bc231c8f3273c60dcd459cdb117246e372ed9d0d2483601a966a3109b9617a4d8f327f494a31013d5bcf2bc372c5f3297bbf953c61ff4770c87414a5 languageName: node linkType: hard @@ -8391,12 +8424,12 @@ __metadata: languageName: node linkType: hard -"@swagger-api/apidom-reference@npm:>=1.0.0-beta.3 <1.0.0-rc.0": - version: 1.0.0-beta.5 - resolution: "@swagger-api/apidom-reference@npm:1.0.0-beta.5" +"@swagger-api/apidom-reference@npm:>=1.0.0-beta.11 <1.0.0-rc.0": + version: 1.0.0-beta.11 + resolution: "@swagger-api/apidom-reference@npm:1.0.0-beta.11" dependencies: "@babel/runtime-corejs3": "npm:^7.20.7" - "@swagger-api/apidom-core": "npm:^1.0.0-beta.5" + "@swagger-api/apidom-core": "npm:^1.0.0-beta.11" "@swagger-api/apidom-error": "npm:^1.0.0-beta.3 <1.0.0-rc.0" "@swagger-api/apidom-json-pointer": "npm:^1.0.0-beta.3 <1.0.0-rc.0" "@swagger-api/apidom-ns-asyncapi-2": "npm:^1.0.0-beta.3 <1.0.0-rc.0" @@ -8467,7 +8500,16 @@ __metadata: optional: true "@swagger-api/apidom-parser-adapter-yaml-1-2": optional: true - checksum: 10/e118278a4a460545de4e393aecaef16f92828cc0af02f4b9b3e8773b9f61c495e0afbce97e68ebd901858dc4b73a178aec2ec18fc1cec2a54d3ccbd3f6bc5331 + checksum: 10/1ae856fd2d13884f8f063421c010f166da1602075ae3b08c041c98199c724615b389110672e2c1b129b7b551a21b8738d32df87c066cc105669a03b6b21897e6 + languageName: node + linkType: hard + +"@swaggerexpert/cookie@npm:^1.4.1": + version: 1.4.1 + resolution: "@swaggerexpert/cookie@npm:1.4.1" + dependencies: + apg-lite: "npm:^1.0.3" + checksum: 10/936590cb70fb7af4ec988e7ee7c5e965b66d09046d3fd2b52c140f9777cda1452f2f24d80eb64ff9678b79f566dd0ef51699050aea9c9a3fd5345dcc73a3fa7a languageName: node linkType: hard @@ -11269,7 +11311,7 @@ __metadata: languageName: node linkType: hard -"apg-lite@npm:^1.0.3": +"apg-lite@npm:^1.0.3, apg-lite@npm:^1.0.4": version: 1.0.4 resolution: "apg-lite@npm:1.0.4" checksum: 10/9c5eb431497415b738e332e5805f836c64ac9b75a399afaec0354859f3f44c95e203fd5c7b8fee9f28fe1e184dd30f6073ed9df062849c3df03c51624901a8a6 @@ -18032,7 +18074,7 @@ __metadata: style-loader: "npm:4.0.0" stylelint: "npm:16.13.2" stylelint-config-sass-guidelines: "npm:12.1.0" - swagger-ui-react: "npm:5.18.2" + swagger-ui-react: "npm:5.18.3" symbol-observable: "npm:4.0.0" systemjs: "npm:6.15.1" systemjs-cjs-extra: "npm:0.2.1" @@ -23443,21 +23485,21 @@ __metadata: languageName: node linkType: hard -"openapi-path-templating@npm:^1.5.1": - version: 1.6.0 - resolution: "openapi-path-templating@npm:1.6.0" +"openapi-path-templating@npm:^2.0.1": + version: 2.1.0 + resolution: "openapi-path-templating@npm:2.1.0" dependencies: - apg-lite: "npm:^1.0.3" - checksum: 10/35353a0ce712dd79b7a60beca75d6a67fb6ad4bbdac4ceef87a6ab78d916f88dd6b426bd81e8ecf12c51798221fb15e63da8f9cadb1bcab4bc7aace8e8a266c1 + apg-lite: "npm:^1.0.4" + checksum: 10/de3ba30a19cc4bed5ace5dad0314bea66e09689001bd3510224a441f7ced53d854655daea846f884ed024d90a8f2af2d4bd0f28256dbda0c49b337684a802da1 languageName: node linkType: hard -"openapi-server-url-templating@npm:^1.0.0": - version: 1.1.0 - resolution: "openapi-server-url-templating@npm:1.1.0" +"openapi-server-url-templating@npm:^1.2.0": + version: 1.3.0 + resolution: "openapi-server-url-templating@npm:1.3.0" dependencies: - apg-lite: "npm:^1.0.3" - checksum: 10/932f08f390269506e1ea1d24208319f0f2a90023368562f1ce167a30d4199f0e39236e17f250749359b4c7b64d86c510e68e8eb9c7f6168da35f0475534ef3ed + apg-lite: "npm:^1.0.4" + checksum: 10/4a98f67cedc0958d3a30cc6db91f35970b0aeae113d0403639adbe3f5e123ff02abebc0468c10639bd0d6db165a9bb0f21c4e367836e984c2345a5a4213620a9 languageName: node linkType: hard @@ -29373,35 +29415,35 @@ __metadata: languageName: node linkType: hard -"swagger-client@npm:^3.31.0": - version: 3.32.2 - resolution: "swagger-client@npm:3.32.2" +"swagger-client@npm:^3.34.0": + version: 3.34.0 + resolution: "swagger-client@npm:3.34.0" dependencies: "@babel/runtime-corejs3": "npm:^7.22.15" "@scarf/scarf": "npm:=1.4.0" - "@swagger-api/apidom-core": "npm:>=1.0.0-beta.3 <1.0.0-rc.0" - "@swagger-api/apidom-error": "npm:>=1.0.0-beta.3 <1.0.0-rc.0" - "@swagger-api/apidom-json-pointer": "npm:>=1.0.0-beta.3 <1.0.0-rc.0" - "@swagger-api/apidom-ns-openapi-3-1": "npm:>=1.0.0-beta.3 <1.0.0-rc.0" - "@swagger-api/apidom-reference": "npm:>=1.0.0-beta.3 <1.0.0-rc.0" - cookie: "npm:~0.7.2" + "@swagger-api/apidom-core": "npm:>=1.0.0-beta.11 <1.0.0-rc.0" + "@swagger-api/apidom-error": "npm:>=1.0.0-beta.11 <1.0.0-rc.0" + "@swagger-api/apidom-json-pointer": "npm:>=1.0.0-beta.11 <1.0.0-rc.0" + "@swagger-api/apidom-ns-openapi-3-1": "npm:>=1.0.0-beta.11 <1.0.0-rc.0" + "@swagger-api/apidom-reference": "npm:>=1.0.0-beta.11 <1.0.0-rc.0" + "@swaggerexpert/cookie": "npm:^1.4.1" deepmerge: "npm:~4.3.0" fast-json-patch: "npm:^3.0.0-1" js-yaml: "npm:^4.1.0" neotraverse: "npm:=0.6.18" node-abort-controller: "npm:^3.1.1" node-fetch-commonjs: "npm:^3.3.2" - openapi-path-templating: "npm:^1.5.1" - openapi-server-url-templating: "npm:^1.0.0" + openapi-path-templating: "npm:^2.0.1" + openapi-server-url-templating: "npm:^1.2.0" ramda: "npm:^0.30.1" ramda-adjunct: "npm:^5.0.0" - checksum: 10/651f8e6446b57c42de61684440e854f146c9e6a0d9eab28892d07a00bc0dd9e742205c285d4db2dea85d657c62da3a81a44cf417db2383753a75d69377877253 + checksum: 10/ed89c44ba172abb9cd6a36a189cf2810ef9b1983cb71657772ede94ea804105b07549b90ba36bbead87e4ac71e609b51f0977d9f9c90112952756c7f4f8bde82 languageName: node linkType: hard -"swagger-ui-react@npm:5.18.2": - version: 5.18.2 - resolution: "swagger-ui-react@npm:5.18.2" +"swagger-ui-react@npm:5.18.3": + version: 5.18.3 + resolution: "swagger-ui-react@npm:5.18.3" dependencies: "@babel/runtime-corejs3": "npm:^7.24.7" "@braintree/sanitize-url": "npm:=7.0.4" @@ -29432,7 +29474,7 @@ __metadata: reselect: "npm:^5.1.1" serialize-error: "npm:^8.1.0" sha.js: "npm:^2.4.11" - swagger-client: "npm:^3.31.0" + swagger-client: "npm:^3.34.0" url-parse: "npm:^1.5.10" xml: "npm:=1.0.1" xml-but-prettier: "npm:^1.0.1" @@ -29440,7 +29482,7 @@ __metadata: peerDependencies: react: ">=16.8.0 <19" react-dom: ">=16.8.0 <19" - checksum: 10/04d4bd8e67ac19472d7915ed779933108b660babd14740c74efa33c76a6bd17e94ebcabd4449e365da3527937f3c60f53c1c392c3e13e5ac5f2e984e6cdcee42 + checksum: 10/4437fc3fd9a869ab3ef357c78137e20fc9d96dbc71332e9ac53b45decd41382c9467d610ebf0c15a628485c67475a8f89c37882ca548305d633e2dd4b632db5f languageName: node linkType: hard From 1fb1f8846f04856b85584b81a28a05880ca19dec Mon Sep 17 00:00:00 2001 From: Jacob Valdez Date: Tue, 28 Jan 2025 09:16:42 -0600 Subject: [PATCH 31/39] Docs: What's new & Upgrade guide v11.5 (#99341) Co-authored-by: Jack Baldry Co-authored-by: Robby Milo Co-authored-by: Isabel Matwawana <76437239+imatwawana@users.noreply.github.com> Co-authored-by: Isabel Matwawana --- .../upgrade-guide/upgrade-v11.5/index.md | 22 +++++++ docs/sources/whatsnew/_index.md | 1 + docs/sources/whatsnew/whats-new-in-v11-5.md | 62 +++++++++++++++++++ 3 files changed, 85 insertions(+) create mode 100644 docs/sources/upgrade-guide/upgrade-v11.5/index.md create mode 100644 docs/sources/whatsnew/whats-new-in-v11-5.md diff --git a/docs/sources/upgrade-guide/upgrade-v11.5/index.md b/docs/sources/upgrade-guide/upgrade-v11.5/index.md new file mode 100644 index 00000000000..633dfa3b7e0 --- /dev/null +++ b/docs/sources/upgrade-guide/upgrade-v11.5/index.md @@ -0,0 +1,22 @@ +--- +description: Guide for upgrading to Grafana v11.5 +keywords: + - grafana + - configuration + - documentation + - upgrade + - '11.5' +title: Upgrade to Grafana v11.5 +menuTitle: Upgrade to v11.5 +weight: 700 +--- + +# Upgrade to Grafana v11.5 + +{{< docs/shared lookup="upgrade/intro.md" source="grafana" version="" >}} + +{{< docs/shared lookup="back-up/back-up-grafana.md" source="grafana" version="" leveloffset="+1" >}} + +{{< docs/shared lookup="upgrade/upgrade-common-tasks.md" source="grafana" version="" >}} + +## Technical notes diff --git a/docs/sources/whatsnew/_index.md b/docs/sources/whatsnew/_index.md index 0de15d17301..db397d8a537 100644 --- a/docs/sources/whatsnew/_index.md +++ b/docs/sources/whatsnew/_index.md @@ -76,6 +76,7 @@ For a complete list of every change, with links to pull requests and related iss ## Grafana 11 +- [What's new in 11.5](https://grafana.com/docs/grafana//whatsnew/whats-new-in-v11-5/) - [What's new in 11.4](https://grafana.com/docs/grafana//whatsnew/whats-new-in-v11-4/) - [What's new in 11.3](https://grafana.com/docs/grafana//whatsnew/whats-new-in-v11-3/) - [What's new in 11.2](https://grafana.com/docs/grafana//whatsnew/whats-new-in-v11-2/) diff --git a/docs/sources/whatsnew/whats-new-in-v11-5.md b/docs/sources/whatsnew/whats-new-in-v11-5.md new file mode 100644 index 00000000000..f532c4a82cb --- /dev/null +++ b/docs/sources/whatsnew/whats-new-in-v11-5.md @@ -0,0 +1,62 @@ +--- +description: Feature and improvement highlights for Grafana v11.5 +keywords: + - grafana + - new + - documentation + - '11.5' + - release notes +labels: +products: + - cloud + - enterprise + - oss +title: What's new in Grafana v11.5 +posts: + - title: Cloud Migration Assistant + items: + - docs/grafana-cloud/whats-new/2025-01-10-grafana-cloud-migration-assistant-supports-all-plugins-and-grafana-alerting.md + - title: Dashboards and visualizations + items: + - docs/grafana-cloud/whats-new/2024-10-16-redesigned-ad-hoc-filters-for-dashboards.md + - docs/grafana-cloud/whats-new/2024-11-19-new-regular-expression-option-for-extract-fields-transformation.md + - docs/grafana-cloud/whats-new/2024-09-04-sharing-drawer.md + - docs/grafana-cloud/whats-new/2024-12-16-customizable-shareable-dashboard-panel-images.md + - title: Reporting + items: + - docs/grafana-cloud/whats-new/2024-10-21-theme-options-for-reporting.md + - docs/grafana-cloud/whats-new/2024-12-02-pdf-export-improvements-in-ga.md + - title: Alerting + items: + - docs/grafana-cloud/whats-new/2025-01-22-rbac-for-alerting-notifications.md + - docs/grafana-cloud/whats-new/2025-01-22-rbac-for-notification-policies.md + - title: Data sources + items: + - docs/grafana-cloud/whats-new/2025-01-09-elasticsearch-cross-cluster-search-support.md + - docs/grafana-cloud/whats-new/2024-11-12-open-search-datasource-now-supports-private-datasource-connect.md + - docs/grafana-cloud/whats-new/2024-12-04-time-series-macro-support-in-visual-query-builder-for-sql-data-sources.md + - title: Authentication and authorization + items: + - docs/grafana-cloud/whats-new/2025-01-07-oauth-and-saml-session-handling-improvements.md + - title: Plugins + items: + - docs/grafana-cloud/whats-new/2025-01-10-plugin-frontend-sandbox.md + - title: Public dashboards + items: + - docs/grafana-cloud/whats-new/2024-09-09-public-dashboards-are-now-shared-dashboards.md +whats_new_grafana_version: 11.5 +weight: -47 +--- + +# What’s new in Grafana v11.5 + +Welcome to Grafana 11.5! +Read on to learn about new sharing, reporting, and export options, cross-cluster search for Elasticsearch, PDC support for several new data sources, and more. +The Grafana Cloud Migration Assistant is in public preview and now supports all plugins and Grafana Alerts, in addition to dashboards, folders, and data sources. +We've also made it more secure to run third-party apps and data sources, and improved user session handling for OAuth 2.0 and SAML. + +{{< youtube id="RGiktzfhRd0" >}} + +For even more detail about all the changes in this release, refer to the [changelog](https://github.com/grafana/grafana/blob/main/CHANGELOG.md). For the specific steps we recommend when you upgrade to v11.5, check out our [Upgrade Guide](https://grafana.com/docs/grafana//upgrade-guide/upgrade-v11.5/). + +{{< docs/whats-new >}} From 6e8e320acec2e427a75a790b9298c2f6eef0ccb3 Mon Sep 17 00:00:00 2001 From: Ashley Harrison Date: Tue, 28 Jan 2025 16:00:54 +0000 Subject: [PATCH 32/39] Chore: emit event whenever the theme changes (#99672) emit event whenever the theme changes --- .../core/components/SharedPreferences/SharedPreferences.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/public/app/core/components/SharedPreferences/SharedPreferences.tsx b/public/app/core/components/SharedPreferences/SharedPreferences.tsx index 462e38863a5..a24bd4b027b 100644 --- a/public/app/core/components/SharedPreferences/SharedPreferences.tsx +++ b/public/app/core/components/SharedPreferences/SharedPreferences.tsx @@ -120,6 +120,10 @@ export class SharedPreferences extends PureComponent { onThemeChanged = (value: ComboboxOption) => { this.setState({ theme: value.value }); + reportInteraction('grafana_preferences_theme_changed', { + toTheme: value.value, + preferenceType: this.props.preferenceType, + }); if (value.value) { changeTheme(value.value, true); From d81b1bf803afb71be0cf0ef09c671a97a765ccf9 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 28 Jan 2025 18:37:22 +0200 Subject: [PATCH 33/39] Update scenes to v5.41.0 (#99684) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package.json | 4 ++-- yarn.lock | 22 +++++++++++----------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/package.json b/package.json index 257f9e62616..ec79d22d1e5 100644 --- a/package.json +++ b/package.json @@ -271,8 +271,8 @@ "@grafana/prometheus": "workspace:*", "@grafana/runtime": "workspace:*", "@grafana/saga-icons": "workspace:*", - "@grafana/scenes": "5.38.0", - "@grafana/scenes-react": "5.38.0", + "@grafana/scenes": "5.41.0", + "@grafana/scenes-react": "5.41.0", "@grafana/schema": "workspace:*", "@grafana/sql": "workspace:*", "@grafana/ui": "workspace:*", diff --git a/yarn.lock b/yarn.lock index c0a3fe7677e..62be189a3a7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3799,11 +3799,11 @@ __metadata: languageName: unknown linkType: soft -"@grafana/scenes-react@npm:5.38.0": - version: 5.38.0 - resolution: "@grafana/scenes-react@npm:5.38.0" +"@grafana/scenes-react@npm:5.41.0": + version: 5.41.0 + resolution: "@grafana/scenes-react@npm:5.41.0" dependencies: - "@grafana/scenes": "npm:5.38.0" + "@grafana/scenes": "npm:5.41.0" lru-cache: "npm:^10.2.2" react-use: "npm:^17.4.0" peerDependencies: @@ -3814,13 +3814,13 @@ __metadata: "@grafana/ui": ^11.0.0 react: ^18.0.0 react-dom: ^18.0.0 - checksum: 10/f130dabfd82abeb232f5b9d485102d03d19a7b0a91924899f6df25ee79e093d216738c9c9b12cd0ae0378eeae91dc1a0b06dadc3c9bda80ab7a2f855fb1cdac9 + checksum: 10/80d5b51a190ed962e17468c0646e9b48a52133c3af026f8bc599c5e8e1150bcd0210f7973dd13e28f96a300798a1c2a1ec41c1bcedc580d8da393591d1e69eb4 languageName: node linkType: hard -"@grafana/scenes@npm:5.38.0": - version: 5.38.0 - resolution: "@grafana/scenes@npm:5.38.0" +"@grafana/scenes@npm:5.41.0": + version: 5.41.0 + resolution: "@grafana/scenes@npm:5.41.0" dependencies: "@floating-ui/react": "npm:^0.26.16" "@leeoniya/ufuzzy": "npm:^1.0.16" @@ -3837,7 +3837,7 @@ __metadata: "@grafana/ui": ">=10.4" react: ^18.0.0 react-dom: ^18.0.0 - checksum: 10/ed6f67e1d54ec49df77cf5fce55e13402c84755dc2f2ef09650fa44c3783dd7f18d08de26721dd4ba5f51ac02187f2c35d27023d75deb9d71c8599490f05eaf8 + checksum: 10/a77b57688b5ac518fa1f3842a871ad21e2daf840d670abda6b69facce74bb6761195c43f25507a3dff87c8551625a979c4240debe62832834473b67145b5d589 languageName: node linkType: hard @@ -17794,8 +17794,8 @@ __metadata: "@grafana/prometheus": "workspace:*" "@grafana/runtime": "workspace:*" "@grafana/saga-icons": "workspace:*" - "@grafana/scenes": "npm:5.38.0" - "@grafana/scenes-react": "npm:5.38.0" + "@grafana/scenes": "npm:5.41.0" + "@grafana/scenes-react": "npm:5.41.0" "@grafana/schema": "workspace:*" "@grafana/sql": "workspace:*" "@grafana/tsconfig": "npm:^2.0.0" From 94a844977e9fa19a6d9c1cff13bef5ea68045d0e Mon Sep 17 00:00:00 2001 From: "Arati R." <33031346+suntala@users.noreply.github.com> Date: Tue, 28 Jan 2025 17:46:07 +0100 Subject: [PATCH 34/39] Folders/K8s: Fix createdBy and updatedBy fields in response (#99569) --- pkg/registry/apis/folders/conversions.go | 49 --------- pkg/services/folder/folderimpl/conversions.go | 104 ++++++++++++++++++ .../folder/folderimpl}/conversions_test.go | 18 ++- pkg/services/folder/folderimpl/folder.go | 4 +- .../folderimpl/folder_unifiedstorage_test.go | 7 +- .../folder/folderimpl/unifiedstore.go | 33 +++--- 6 files changed, 143 insertions(+), 72 deletions(-) create mode 100644 pkg/services/folder/folderimpl/conversions.go rename pkg/{registry/apis/folders => services/folder/folderimpl}/conversions_test.go (76%) diff --git a/pkg/registry/apis/folders/conversions.go b/pkg/registry/apis/folders/conversions.go index d5e2f50901d..e3af9a28d9a 100644 --- a/pkg/registry/apis/folders/conversions.go +++ b/pkg/registry/apis/folders/conversions.go @@ -6,13 +6,10 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - authlib "github.com/grafana/authlib/types" "github.com/grafana/grafana/pkg/apimachinery/utils" "github.com/grafana/grafana/pkg/apis/folder/v0alpha1" - "github.com/grafana/grafana/pkg/infra/slugify" "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request" gapiutil "github.com/grafana/grafana/pkg/services/apiserver/utils" - "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/folder" "github.com/grafana/grafana/pkg/util" ) @@ -41,52 +38,6 @@ func LegacyCreateCommandToUnstructured(cmd *folder.CreateFolderCommand) (*unstru return obj, nil } -func UnstructuredToLegacyFolder(item *unstructured.Unstructured) (*folder.Folder, error) { - meta, err := utils.MetaAccessor(item) - if err != nil { - return nil, err - } - - info, _ := authlib.ParseNamespace(meta.GetNamespace()) - if info.OrgID < 0 { - info.OrgID = 1 // This resolves all test cases that assume org 1 - } - - title, _, _ := unstructured.NestedString(item.Object, "spec", "title") - description, _, _ := unstructured.NestedString(item.Object, "spec", "description") - - uid := meta.GetName() - url := "" - if uid != folder.RootFolder.UID { - slug := slugify.Slugify(title) - url = dashboards.GetFolderURL(uid, slug) - } - - created := meta.GetCreationTimestamp().Time.UTC() - updated, _ := meta.GetUpdatedTimestamp() - if updated == nil { - updated = &created - } else { - tmp := updated.UTC() - updated = &tmp - } - - return &folder.Folder{ - UID: uid, - Title: title, - Description: description, - ID: meta.GetDeprecatedInternalID(), // nolint:staticcheck - ParentUID: meta.GetFolder(), - Version: int(meta.GetGeneration()), - Repository: meta.GetRepositoryName(), - - URL: url, - Created: created, - Updated: *updated, - OrgID: info.OrgID, - }, nil -} - func LegacyFolderToUnstructured(v *folder.Folder, namespacer request.NamespaceMapper) (*v0alpha1.Folder, error) { return convertToK8sResource(v, namespacer) } diff --git a/pkg/services/folder/folderimpl/conversions.go b/pkg/services/folder/folderimpl/conversions.go new file mode 100644 index 00000000000..29ef06d14fc --- /dev/null +++ b/pkg/services/folder/folderimpl/conversions.go @@ -0,0 +1,104 @@ +package folderimpl + +import ( + "context" + "errors" + "strconv" + "strings" + + authlib "github.com/grafana/authlib/types" + "github.com/grafana/grafana/pkg/apimachinery/utils" + "github.com/grafana/grafana/pkg/infra/slugify" + "github.com/grafana/grafana/pkg/services/dashboards" + "github.com/grafana/grafana/pkg/services/folder" + "github.com/grafana/grafana/pkg/services/user" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" +) + +func (ss *FolderUnifiedStoreImpl) UnstructuredToLegacyFolder(ctx context.Context, item *unstructured.Unstructured) (*folder.Folder, error) { + meta, err := utils.MetaAccessor(item) + if err != nil { + return nil, err + } + + info, _ := authlib.ParseNamespace(meta.GetNamespace()) + if info.OrgID < 0 { + info.OrgID = 1 // This resolves all test cases that assume org 1 + } + + title, _, _ := unstructured.NestedString(item.Object, "spec", "title") + description, _, _ := unstructured.NestedString(item.Object, "spec", "description") + + uid := meta.GetName() + url := "" + if uid != folder.RootFolder.UID { + slug := slugify.Slugify(title) + url = dashboards.GetFolderURL(uid, slug) + } + + created := meta.GetCreationTimestamp().Time.UTC() + updated, _ := meta.GetUpdatedTimestamp() + if updated == nil { + updated = &created + } else { + tmp := updated.UTC() + updated = &tmp + } + + creator, err := ss.getUserFromMeta(ctx, meta.GetCreatedBy()) + if err != nil { + return nil, err + } + + updater, err := ss.getUserFromMeta(ctx, meta.GetUpdatedBy()) + if err != nil { + return nil, err + } + if updater.UID == "" { + updater = creator + } + + return &folder.Folder{ + UID: uid, + Title: title, + Description: description, + ID: meta.GetDeprecatedInternalID(), // nolint:staticcheck + ParentUID: meta.GetFolder(), + Version: int(meta.GetGeneration()), + Repository: meta.GetRepositoryName(), + + URL: url, + Created: created, + Updated: *updated, + OrgID: info.OrgID, + CreatedBy: creator.ID, + UpdatedBy: updater.ID, + }, nil +} + +func (ss *FolderUnifiedStoreImpl) getUserFromMeta(ctx context.Context, userMeta string) (*user.User, error) { + if userMeta == "" || toUID(userMeta) == "" { + return &user.User{}, nil + } + usr, err := ss.getUser(ctx, toUID(userMeta)) + if err != nil && errors.Is(err, user.ErrUserNotFound) { + return &user.User{}, nil + } + return usr, err +} + +func (ss *FolderUnifiedStoreImpl) getUser(ctx context.Context, uid string) (*user.User, error) { + userID, err := strconv.ParseInt(uid, 10, 64) + if err == nil { + return ss.userService.GetByID(ctx, &user.GetUserByIDQuery{ID: userID}) + } + return ss.userService.GetByUID(ctx, &user.GetUserByUIDQuery{UID: uid}) +} + +func toUID(rawIdentifier string) string { + parts := strings.Split(rawIdentifier, ":") + if len(parts) < 2 { + return "" + } + return parts[1] +} diff --git a/pkg/registry/apis/folders/conversions_test.go b/pkg/services/folder/folderimpl/conversions_test.go similarity index 76% rename from pkg/registry/apis/folders/conversions_test.go rename to pkg/services/folder/folderimpl/conversions_test.go index f7bef8ba015..a6120c7c23d 100644 --- a/pkg/registry/apis/folders/conversions_test.go +++ b/pkg/services/folder/folderimpl/conversions_test.go @@ -1,6 +1,7 @@ -package folders +package folderimpl import ( + "context" "testing" "time" @@ -8,6 +9,8 @@ import ( "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "github.com/grafana/grafana/pkg/services/folder" + "github.com/grafana/grafana/pkg/services/user" + "github.com/grafana/grafana/pkg/services/user/usertest" ) func TestFolderConversions(t *testing.T) { @@ -29,8 +32,8 @@ func TestFolderConversions(t *testing.T) { "grafana.app/folder": "parent-folder-name", "grafana.app/updatedTimestamp": "2022-12-02T07:02:02Z", "grafana.app/repoName": "example-repo", - "grafana.app/createdBy": "user:abc", - "grafana.app/updatedBy": "service:xyz" + "grafana.app/createdBy": "user:useruid", + "grafana.app/updatedBy": "user:useruid" } }, "spec": { @@ -44,7 +47,12 @@ func TestFolderConversions(t *testing.T) { created = created.UTC() require.NoError(t, err) - converted, err := UnstructuredToLegacyFolder(input) + fake := usertest.NewUserServiceFake() + fake.ExpectedUser = &user.User{ID: 10, UID: "useruid"} + + fs := ProvideUnifiedStore(nil, fake) + + converted, err := fs.UnstructuredToLegacyFolder(context.Background(), input) require.NoError(t, err) require.Equal(t, folder.Folder{ ID: 234, @@ -58,5 +66,7 @@ func TestFolderConversions(t *testing.T) { Repository: "example-repo", Created: created, Updated: created.Add(time.Hour * 5), + CreatedBy: 10, + UpdatedBy: 10, }, *converted) } diff --git a/pkg/services/folder/folderimpl/folder.go b/pkg/services/folder/folderimpl/folder.go index 9c918f9d360..cafa0e38c91 100644 --- a/pkg/services/folder/folderimpl/folder.go +++ b/pkg/services/folder/folderimpl/folder.go @@ -53,7 +53,6 @@ type Service struct { log *slog.Logger dashboardStore dashboards.Store dashboardFolderStore folder.FolderStore - userService user.Service features featuremgmt.FeatureToggles accessControl accesscontrol.AccessControl k8sclient folderK8sHandler @@ -88,7 +87,6 @@ func ProvideService( dashboardStore: dashboardStore, dashboardFolderStore: folderStore, store: store, - userService: userService, features: features, accessControl: ac, bus: bus, @@ -114,7 +112,7 @@ func ProvideService( recourceClientProvider: unified.GetResourceClient, } - unifiedStore := ProvideUnifiedStore(k8sHandler) + unifiedStore := ProvideUnifiedStore(k8sHandler, userService) srv.unifiedStore = unifiedStore srv.k8sclient = k8sHandler diff --git a/pkg/services/folder/folderimpl/folder_unifiedstorage_test.go b/pkg/services/folder/folderimpl/folder_unifiedstorage_test.go index c9e9f536d17..3d5a84a6aad 100644 --- a/pkg/services/folder/folderimpl/folder_unifiedstorage_test.go +++ b/pkg/services/folder/folderimpl/folder_unifiedstorage_test.go @@ -35,6 +35,7 @@ import ( "github.com/grafana/grafana/pkg/services/publicdashboards" "github.com/grafana/grafana/pkg/services/sqlstore" "github.com/grafana/grafana/pkg/services/user" + "github.com/grafana/grafana/pkg/services/user/usertest" "github.com/grafana/grafana/pkg/storage/unified/resource" ) @@ -181,7 +182,11 @@ func TestIntegrationFolderServiceViaUnifiedStorage(t *testing.T) { recourceClientProvider: f, } - unifiedStore := ProvideUnifiedStore(k8sHandler) + userService := &usertest.FakeUserService{ + ExpectedUser: &user.User{}, + } + + unifiedStore := ProvideUnifiedStore(k8sHandler, userService) ctx := context.Background() usr := &user.SignedInUser{UserID: 1, OrgID: 1, Permissions: map[int64]map[string][]string{ diff --git a/pkg/services/folder/folderimpl/unifiedstore.go b/pkg/services/folder/folderimpl/unifiedstore.go index fe835c16f7a..18c40751185 100644 --- a/pkg/services/folder/folderimpl/unifiedstore.go +++ b/pkg/services/folder/folderimpl/unifiedstore.go @@ -20,21 +20,24 @@ import ( internalfolders "github.com/grafana/grafana/pkg/registry/apis/folders" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/folder" + "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/util" ) type FolderUnifiedStoreImpl struct { - log log.Logger - k8sclient folderK8sHandler + log log.Logger + k8sclient folderK8sHandler + userService user.Service } // sqlStore implements the store interface. var _ folder.Store = (*FolderUnifiedStoreImpl)(nil) -func ProvideUnifiedStore(k8sHandler *foldk8sHandler) *FolderUnifiedStoreImpl { +func ProvideUnifiedStore(k8sHandler *foldk8sHandler, userService user.Service) *FolderUnifiedStoreImpl { return &FolderUnifiedStoreImpl{ - k8sclient: k8sHandler, - log: log.New("folder-store"), + k8sclient: k8sHandler, + log: log.New("folder-store"), + userService: userService, } } @@ -60,7 +63,7 @@ func (ss *FolderUnifiedStoreImpl) Create(ctx context.Context, cmd folder.CreateF return nil, err } - folder, err := internalfolders.UnstructuredToLegacyFolder(out) + folder, err := ss.UnstructuredToLegacyFolder(ctx, out) if err != nil { return nil, err } @@ -135,7 +138,7 @@ func (ss *FolderUnifiedStoreImpl) Update(ctx context.Context, cmd folder.UpdateF return nil, err } - return internalfolders.UnstructuredToLegacyFolder(out) + return ss.UnstructuredToLegacyFolder(ctx, out) } // If WithFullpath is true it computes also the full path of a folder. @@ -178,7 +181,7 @@ func (ss *FolderUnifiedStoreImpl) Get(ctx context.Context, q folder.GetFolderQue return nil, dashboards.ErrFolderNotFound } - return internalfolders.UnstructuredToLegacyFolder(out) + return ss.UnstructuredToLegacyFolder(ctx, out) } func (ss *FolderUnifiedStoreImpl) GetParents(ctx context.Context, q folder.GetParentsQuery) ([]*folder.Folder, error) { @@ -206,7 +209,7 @@ func (ss *FolderUnifiedStoreImpl) GetParents(ctx context.Context, q folder.GetPa return nil, err } - folder, err := internalfolders.UnstructuredToLegacyFolder(out) + folder, err := ss.UnstructuredToLegacyFolder(ctx, out) if err != nil { return nil, err } @@ -245,9 +248,9 @@ func (ss *FolderUnifiedStoreImpl) GetChildren(ctx context.Context, q folder.GetC hits := make([]*folder.Folder, 0) for _, item := range out.Items { // convert item to legacy folder format - f, err := internalfolders.UnstructuredToLegacyFolder(&item) + f, err := ss.UnstructuredToLegacyFolder(ctx, &item) if f == nil { - return nil, fmt.Errorf("unable covert unstructured item to legacy folder %w", err) + return nil, fmt.Errorf("unable to convert unstructured item to legacy folder %w", err) } // it we are at root level, skip subfolder @@ -345,9 +348,9 @@ func (ss *FolderUnifiedStoreImpl) GetFolders(ctx context.Context, q folder.GetFo m := map[string]*folder.Folder{} for _, item := range out.Items { // convert item to legacy folder format - f, err := internalfolders.UnstructuredToLegacyFolder(&item) + f, err := ss.UnstructuredToLegacyFolder(ctx, &item) if f == nil { - return nil, fmt.Errorf("unable covert unstructured item to legacy folder %w", err) + return nil, fmt.Errorf("unable to convert unstructured item to legacy folder %w", err) } m[f.UID] = f @@ -405,9 +408,9 @@ func (ss *FolderUnifiedStoreImpl) GetDescendants(ctx context.Context, orgID int6 nodes := map[string]*folder.Folder{} for _, item := range out.Items { // convert item to legacy folder format - f, err := internalfolders.UnstructuredToLegacyFolder(&item) + f, err := ss.UnstructuredToLegacyFolder(ctx, &item) if f == nil { - return nil, fmt.Errorf("unable covert unstructured item to legacy folder %w", err) + return nil, fmt.Errorf("unable to convert unstructured item to legacy folder %w", err) } nodes[f.UID] = f From abac53bd0a8df19626524e319e6ebf3091bb56c6 Mon Sep 17 00:00:00 2001 From: Ivan Ortega Alba Date: Tue, 28 Jan 2025 17:53:19 +0100 Subject: [PATCH 35/39] Revert "Revert "LibraryPanel: Fallback to panel title if library panel title is not set"" (#99678) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Revert "Revert "LibraryPanel: Fallback to panel title if library panel title …" This reverts commit 6e705ee67c1286df82eb97d44f887f9d38258a74. --- .../dashboard-scene/inspect/InspectJsonTab.test.tsx | 2 +- .../dashboard-scene/panel-edit/PanelEditor.test.ts | 4 +--- .../dashboard-scene/panel-edit/PanelOptions.test.tsx | 1 - .../scene/AddLibraryPanelDrawer.test.tsx | 2 +- .../scene/DashboardDatasourceBehaviour.test.tsx | 3 --- .../dashboard-scene/scene/DashboardScene.test.tsx | 10 +++++----- .../scene/LibraryPanelBehavior.test.tsx | 2 +- .../dashboard-scene/scene/LibraryPanelBehavior.tsx | 6 ++---- .../serialization/transformSceneToSaveModel.test.ts | 6 ++---- .../serialization/transformSceneToSaveModel.ts | 2 +- .../utils/PanelModelCompatibilityWrapper.test.ts | 1 - 11 files changed, 14 insertions(+), 25 deletions(-) diff --git a/public/app/features/dashboard-scene/inspect/InspectJsonTab.test.tsx b/public/app/features/dashboard-scene/inspect/InspectJsonTab.test.tsx index 7d93518167e..582e0855008 100644 --- a/public/app/features/dashboard-scene/inspect/InspectJsonTab.test.tsx +++ b/public/app/features/dashboard-scene/inspect/InspectJsonTab.test.tsx @@ -223,7 +223,7 @@ async function buildTestSceneWithLibraryPanel() { title: 'Panel A', pluginId: 'table', key: 'panel-12', - $behaviors: [new LibraryPanelBehavior({ title: 'LibraryPanel A title', name: 'LibraryPanel A', uid: '111' })], + $behaviors: [new LibraryPanelBehavior({ name: 'LibraryPanel A', uid: '111' })], titleItems: [new VizPanelLinks({ menu: new VizPanelLinksMenu({}) })], $data: new SceneDataTransformer({ transformations: [ diff --git a/public/app/features/dashboard-scene/panel-edit/PanelEditor.test.ts b/public/app/features/dashboard-scene/panel-edit/PanelEditor.test.ts index fa309c8bd21..f204eae7138 100644 --- a/public/app/features/dashboard-scene/panel-edit/PanelEditor.test.ts +++ b/public/app/features/dashboard-scene/panel-edit/PanelEditor.test.ts @@ -200,7 +200,6 @@ describe('PanelEditor', () => { const libPanelBehavior = new LibraryPanelBehavior({ isLoaded: true, - title: libraryPanelModel.title, uid: libraryPanelModel.uid, name: libraryPanelModel.name, _loadedPanel: libraryPanelModel, @@ -239,7 +238,7 @@ describe('PanelEditor', () => { // Wait for mock api to return and update the library panel expect(libPanelBehavior.state._loadedPanel?.version).toBe(2); expect(libPanelBehavior.state.name).toBe('changed name'); - expect(libPanelBehavior.state.title).toBe('changed title'); + expect(panel.state.title).toBe('changed title'); expect((gridItem.state.body as VizPanel).state.title).toBe('changed title'); }); @@ -258,7 +257,6 @@ describe('PanelEditor', () => { const libPanelBehavior = new LibraryPanelBehavior({ isLoaded: true, - title: libraryPanelModel.title, uid: libraryPanelModel.uid, name: libraryPanelModel.name, _loadedPanel: libraryPanelModel, diff --git a/public/app/features/dashboard-scene/panel-edit/PanelOptions.test.tsx b/public/app/features/dashboard-scene/panel-edit/PanelOptions.test.tsx index 264c053de17..3f54a519b70 100644 --- a/public/app/features/dashboard-scene/panel-edit/PanelOptions.test.tsx +++ b/public/app/features/dashboard-scene/panel-edit/PanelOptions.test.tsx @@ -177,7 +177,6 @@ describe('PanelOptions', () => { const libraryPanel = new LibraryPanelBehavior({ isLoaded: true, - title: libraryPanelModel.title, uid: libraryPanelModel.uid, name: libraryPanelModel.name, _loadedPanel: libraryPanelModel, diff --git a/public/app/features/dashboard-scene/scene/AddLibraryPanelDrawer.test.tsx b/public/app/features/dashboard-scene/scene/AddLibraryPanelDrawer.test.tsx index f298e7b19f9..e8a0054a731 100644 --- a/public/app/features/dashboard-scene/scene/AddLibraryPanelDrawer.test.tsx +++ b/public/app/features/dashboard-scene/scene/AddLibraryPanelDrawer.test.tsx @@ -96,7 +96,7 @@ describe('AddLibraryPanelWidget', () => { title: 'Panel Title', pluginId: 'table', key: 'panel-1', - $behaviors: [new LibraryPanelBehavior({ title: 'LibraryPanel A title', name: 'LibraryPanel A', uid: 'uid' })], + $behaviors: [new LibraryPanelBehavior({ name: 'LibraryPanel A', uid: 'uid' })], }); addLibPanelDrawer = new AddLibraryPanelDrawer({ panelToReplaceRef: libPanel.getRef() }); diff --git a/public/app/features/dashboard-scene/scene/DashboardDatasourceBehaviour.test.tsx b/public/app/features/dashboard-scene/scene/DashboardDatasourceBehaviour.test.tsx index 393f3cfaea7..1b183577b1f 100644 --- a/public/app/features/dashboard-scene/scene/DashboardDatasourceBehaviour.test.tsx +++ b/public/app/features/dashboard-scene/scene/DashboardDatasourceBehaviour.test.tsx @@ -408,7 +408,6 @@ describe('DashboardDatasourceBehaviour', () => { it('should re-run queries when library panel re-runs query', async () => { const libPanelBehavior = new LibraryPanelBehavior({ isLoaded: false, - title: 'Panel title', uid: 'fdcvggvfy2qdca', name: 'My Library Panel', _loadedPanel: undefined, @@ -469,7 +468,6 @@ describe('DashboardDatasourceBehaviour', () => { jest.spyOn(console, 'error').mockImplementation(); const libPanelBehavior = new LibraryPanelBehavior({ isLoaded: false, - title: 'Panel title', uid: 'fdcvggvfy2qdca', name: 'My Library Panel', _loadedPanel: undefined, @@ -519,7 +517,6 @@ describe('DashboardDatasourceBehaviour', () => { // Simulate library panel being loaded libPanelBehavior.setState({ isLoaded: true, - title: 'Panel title', uid: 'fdcvggvfy2qdca', name: 'My Library Panel', _loadedPanel: undefined, diff --git a/public/app/features/dashboard-scene/scene/DashboardScene.test.tsx b/public/app/features/dashboard-scene/scene/DashboardScene.test.tsx index 2c54e174bea..80d4b19fa3a 100644 --- a/public/app/features/dashboard-scene/scene/DashboardScene.test.tsx +++ b/public/app/features/dashboard-scene/scene/DashboardScene.test.tsx @@ -468,7 +468,7 @@ describe('DashboardScene', () => { title: 'Library Panel', pluginId: 'table', key: 'panel-4', - $behaviors: [new LibraryPanelBehavior({ title: 'Library Panel', name: 'libraryPanel', uid: 'uid' })], + $behaviors: [new LibraryPanelBehavior({ name: 'libraryPanel', uid: 'uid' })], }); scene.copyPanel(libVizPanel); @@ -523,7 +523,7 @@ describe('DashboardScene', () => { title: 'Library Panel', pluginId: 'table', key: 'panel-4', - $behaviors: [new LibraryPanelBehavior({ title: 'Library Panel', name: 'libraryPanel', uid: 'uid' })], + $behaviors: [new LibraryPanelBehavior({ name: 'libraryPanel', uid: 'uid' })], }), }) ); @@ -542,7 +542,7 @@ describe('DashboardScene', () => { const libPanel = new VizPanel({ title: 'Panel B', pluginId: 'table', - $behaviors: [new LibraryPanelBehavior({ title: 'title', name: 'lib panel', uid: 'abc', isLoaded: true })], + $behaviors: [new LibraryPanelBehavior({ name: 'lib panel', uid: 'abc', isLoaded: true })], }); const scene = buildTestScene({ @@ -907,7 +907,7 @@ function buildTestScene(overrides?: Partial) { title: 'Library Panel', pluginId: 'table', key: 'panel-5', - $behaviors: [new LibraryPanelBehavior({ title: 'Library Panel', name: 'libraryPanel', uid: 'uid' })], + $behaviors: [new LibraryPanelBehavior({ name: 'libraryPanel', uid: 'uid' })], }), }), ], @@ -925,7 +925,7 @@ function buildTestScene(overrides?: Partial) { title: 'Library Panel', pluginId: 'table', key: 'panel-6', - $behaviors: [new LibraryPanelBehavior({ title: 'Library Panel', name: 'libraryPanel', uid: 'uid' })], + $behaviors: [new LibraryPanelBehavior({ name: 'libraryPanel', uid: 'uid' })], }), }), ], diff --git a/public/app/features/dashboard-scene/scene/LibraryPanelBehavior.test.tsx b/public/app/features/dashboard-scene/scene/LibraryPanelBehavior.test.tsx index 02a363d5d3a..b6eb6872566 100644 --- a/public/app/features/dashboard-scene/scene/LibraryPanelBehavior.test.tsx +++ b/public/app/features/dashboard-scene/scene/LibraryPanelBehavior.test.tsx @@ -167,7 +167,7 @@ describe('LibraryPanelBehavior', () => { }); async function buildTestSceneWithLibraryPanel() { - const behavior = new LibraryPanelBehavior({ title: 'LibraryPanel A title', name: 'LibraryPanel A', uid: '111' }); + const behavior = new LibraryPanelBehavior({ name: 'LibraryPanel A', uid: '111' }); const vizPanel = new VizPanel({ title: 'Panel A', diff --git a/public/app/features/dashboard-scene/scene/LibraryPanelBehavior.tsx b/public/app/features/dashboard-scene/scene/LibraryPanelBehavior.tsx index 423cc2c34ee..44a1c0de5a8 100644 --- a/public/app/features/dashboard-scene/scene/LibraryPanelBehavior.tsx +++ b/public/app/features/dashboard-scene/scene/LibraryPanelBehavior.tsx @@ -17,8 +17,6 @@ import { AngularDeprecation } from './angular/AngularDeprecation'; import { DashboardGridItem } from './layout-default/DashboardGridItem'; export interface LibraryPanelBehaviorState extends SceneObjectState { - // Library panels use title from dashboard JSON's panel model, not from library panel definition, hence we pass it. - title?: string; uid: string; name: string; isLoaded?: boolean; @@ -66,7 +64,7 @@ export class LibraryPanelBehavior extends SceneObjectBase { $behaviors: [ new LibraryPanelBehavior({ name: 'Some lib panel panel', - title: 'A panel', uid: 'lib-panel-uid', }), ], @@ -399,7 +398,7 @@ describe('transformSceneToSaveModel', () => { x: 0, y: 0, }); - expect(result.title).toBe('A panel'); + expect(result.title).toBe('Panel blahh blah'); expect(result.transformations).toBeUndefined(); expect(result.fieldConfig).toBeUndefined(); expect(result.options).toBeUndefined(); @@ -851,7 +850,6 @@ describe('transformSceneToSaveModel', () => { $behaviors: [ new LibraryPanelBehavior({ name: 'Some lib panel panel', - title: 'A panel', uid: 'lib-panel-uid', }), ], @@ -865,7 +863,7 @@ describe('transformSceneToSaveModel', () => { expect(result[0]).toMatchObject({ id: 4, - title: 'A panel', + title: 'Panel blahh blah', libraryPanel: { name: 'Some lib panel panel', uid: 'lib-panel-uid', diff --git a/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.ts b/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.ts index 4c8368fc1fd..c1b6c96ec07 100644 --- a/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.ts +++ b/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.ts @@ -188,7 +188,7 @@ export function vizPanelToPanel( panel = { id: getPanelIdForVizPanel(vizPanel), - title: libPanel!.state.title, + title: vizPanel.state.title, gridPos: gridPos, libraryPanel: { name: libPanel!.state.name, diff --git a/public/app/features/dashboard-scene/utils/PanelModelCompatibilityWrapper.test.ts b/public/app/features/dashboard-scene/utils/PanelModelCompatibilityWrapper.test.ts index e6e44c7a583..02dcc897936 100644 --- a/public/app/features/dashboard-scene/utils/PanelModelCompatibilityWrapper.test.ts +++ b/public/app/features/dashboard-scene/utils/PanelModelCompatibilityWrapper.test.ts @@ -17,7 +17,6 @@ describe('PanelModelCompatibilityWrapper', () => { const libPanel = new LibraryPanelBehavior({ uid: 'a', name: 'aa', - title: 'a', }); panel.setState({ From 92d5e82a334e046c3408c97a83fe668fd6d528dd Mon Sep 17 00:00:00 2001 From: Ivan Ortega Alba Date: Tue, 28 Jan 2025 18:19:33 +0100 Subject: [PATCH 36/39] LibraryPanels: Respect model title when adding a library panel (#99687) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Revert "Revert "LibraryPanel: Fallback to panel title if library panel title …" This reverts commit 6e705ee67c1286df82eb97d44f887f9d38258a74. * LibraryPanels: Respect model title when adding a library panel to a dashboard * remove debugger --------- Co-authored-by: Haris Rozajac --- .../scene/AddLibraryPanelDrawer.test.tsx | 29 ++++++++++++++++++- .../scene/AddLibraryPanelDrawer.tsx | 3 ++ 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/public/app/features/dashboard-scene/scene/AddLibraryPanelDrawer.test.tsx b/public/app/features/dashboard-scene/scene/AddLibraryPanelDrawer.test.tsx index e8a0054a731..cba0d0f9e01 100644 --- a/public/app/features/dashboard-scene/scene/AddLibraryPanelDrawer.test.tsx +++ b/public/app/features/dashboard-scene/scene/AddLibraryPanelDrawer.test.tsx @@ -32,6 +32,7 @@ describe('AddLibraryPanelWidget', () => { const panelInfo: LibraryPanel = { uid: 'uid', model: { + title: 'model title', type: 'timeseries', }, name: 'name', @@ -47,6 +48,8 @@ describe('AddLibraryPanelWidget', () => { expect(panels.length).toBe(1); expect(panel.state.$behaviors![0]).toBeInstanceOf(LibraryPanelBehavior); expect(panel.state.key).toBe('panel-1'); + expect(panel.state.title).toBe('model title'); + expect(panel.state.hoverHeader).toBe(false); }); it('should add library panel from menu and enter edit mode in a dashboard that is not already in edit mode', async () => { @@ -69,6 +72,7 @@ describe('AddLibraryPanelWidget', () => { const panelInfo: LibraryPanel = { uid: 'uid', model: { + title: 'model title', type: 'timeseries', }, name: 'name', @@ -88,12 +92,13 @@ describe('AddLibraryPanelWidget', () => { expect(panels.length).toBe(1); expect(panel.state.$behaviors![0]).toBeInstanceOf(LibraryPanelBehavior); expect(panel.state.key).toBe('panel-1'); + expect(panel.state.title).toBe('model title'); expect(dashboard.state.isEditing).toBe(true); }); it('should replace grid item when grid item state is passed', async () => { const libPanel = new VizPanel({ - title: 'Panel Title', + title: 'Some panel title', pluginId: 'table', key: 'panel-1', $behaviors: [new LibraryPanelBehavior({ name: 'LibraryPanel A', uid: 'uid' })], @@ -115,6 +120,7 @@ describe('AddLibraryPanelWidget', () => { const panelInfo: LibraryPanel = { uid: 'new_uid', model: { + title: 'model title', type: 'timeseries', }, name: 'new_name', @@ -132,6 +138,27 @@ describe('AddLibraryPanelWidget', () => { expect(behavior).toBeInstanceOf(LibraryPanelBehavior); expect(behavior.state.uid).toBe('new_uid'); expect(behavior.state.name).toBe('new_name'); + expect(panels[0].state.title).toBe('model title'); + }); + + it('should set hoverHeader to true if the library panel title is empty', () => { + const panelInfo: LibraryPanel = { + uid: 'uid', + model: { + title: '', + type: 'timeseries', + }, + name: 'name', + version: 1, + type: 'timeseries', + }; + + addLibPanelDrawer.onAddLibraryPanel(panelInfo); + + const panels = dashboard.state.body.getVizPanels(); + const panel = panels[0]; + expect(panel.state.title).toBe(''); + expect(panel.state.hoverHeader).toBe(true); }); }); diff --git a/public/app/features/dashboard-scene/scene/AddLibraryPanelDrawer.tsx b/public/app/features/dashboard-scene/scene/AddLibraryPanelDrawer.tsx index e8e462005eb..1d4c4b198a5 100644 --- a/public/app/features/dashboard-scene/scene/AddLibraryPanelDrawer.tsx +++ b/public/app/features/dashboard-scene/scene/AddLibraryPanelDrawer.tsx @@ -26,6 +26,9 @@ export class AddLibraryPanelDrawer extends SceneObjectBase Date: Tue, 28 Jan 2025 18:36:10 +0100 Subject: [PATCH 37/39] Dashboards: Monitor dashboard loading performance (#99629) * WIP benchmark dashboard rendering * Script * Benchmark with variable and a panel * Add one more benchmark * Explicitely enable profiling * Playwright tests * update scenes * Report measurement to faro when config set * Let user enable metrics reporting in UI * Fix logging * Change how performance metrics is enabled per dashboard, now in config file only * add benchmark run option * Fix benchmark runs * fix description for performance config * remove console.log * update codeowners * add back crashDetection init that was lost in merge * fix yarn.lock * restore custom.ini * fix import * Make sure we have the echoSrv * fix config type * Try to limit changes to e2e runs * remove benchmark * Fix lint issue * fix codeowners --------- Co-authored-by: Dominik Prokop Co-authored-by: Sergej-Vlasov --- .gitignore | 1 - conf/defaults.ini | 3 ++ package.json | 2 + packages/grafana-data/src/types/config.ts | 1 + packages/grafana-runtime/src/config.ts | 1 + packages/grafana-runtime/src/index.ts | 2 +- packages/grafana-runtime/src/utils/logging.ts | 12 +++-- pkg/api/dtos/frontend_settings.go | 2 + pkg/api/frontendsettings.go | 1 + pkg/setting/setting.go | 8 ++-- .../pages/DashboardScenePageStateManager.ts | 4 ++ .../transformSaveModelToScene.ts | 46 ++++++++++++++++++- 12 files changed, 71 insertions(+), 12 deletions(-) diff --git a/.gitignore b/.gitignore index ac5ad6f5f7d..201697fce4d 100644 --- a/.gitignore +++ b/.gitignore @@ -176,7 +176,6 @@ compilation-stats.json /blob-report/ /playwright/.cache/ /playwright/.auth/ - # grafana server /scripts/grafana-server/server.log diff --git a/conf/defaults.ini b/conf/defaults.ini index 1ed0bb7a24f..55a89dfa205 100644 --- a/conf/defaults.ini +++ b/conf/defaults.ini @@ -452,6 +452,9 @@ min_refresh_interval = 5s # Path to the default home dashboard. If this value is empty, then Grafana uses StaticRootPath + "dashboards/home.json" default_home_dashboard_path = +# Dashboards UIDs to report performance metrics for. * can be used to report metrics for all dashboards +dashboard_performance_metrics = + ################################### Data sources ######################### [datasources] # Upper limit of data sources that Grafana will return. This limit is a temporary configuration and it will be deprecated when pagination will be introduced on the list data sources API. diff --git a/package.json b/package.json index ec79d22d1e5..12f8eb5769b 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,8 @@ "e2e:enterprise": "./e2e/start-and-run-suite enterprise", "e2e:enterprise:dev": "./e2e/start-and-run-suite enterprise dev", "e2e:enterprise:debug": "./e2e/start-and-run-suite enterprise debug", + "build-benchmark": "NODE_ENV=dev nx exec -- webpack --config scripts/webpack/webpack.dev.js --env benchmark=1", + "e2e:playwright:benchmark": "yarn build-benchmark && ./e2e/plugin-e2e/start-and-benchmark", "e2e:playwright": "yarn playwright test", "e2e:playwright:server": "yarn e2e:plugin:build && ./e2e/plugin-e2e/start-and-run-suite", "e2e:storybook": "PORT=9001 ./e2e/run-suite storybook true", diff --git a/packages/grafana-data/src/types/config.ts b/packages/grafana-data/src/types/config.ts index b1f6ca4b28a..560f4ba672f 100644 --- a/packages/grafana-data/src/types/config.ts +++ b/packages/grafana-data/src/types/config.ts @@ -228,6 +228,7 @@ export interface GrafanaConfig { rudderstackConfigUrl: string | undefined; rudderstackIntegrationsUrl: string | undefined; analyticsConsoleReporting: boolean; + dashboardPerformanceMetrics: string[]; sqlConnectionLimits: SqlConnectionLimits; sharedWithMeFolderUID?: string; rootFolderUID?: string; diff --git a/packages/grafana-runtime/src/config.ts b/packages/grafana-runtime/src/config.ts index f8b087b1bc6..6b29cdc0dee 100644 --- a/packages/grafana-runtime/src/config.ts +++ b/packages/grafana-runtime/src/config.ts @@ -185,6 +185,7 @@ export class GrafanaBootConfig implements GrafanaConfig { rudderstackConfigUrl: undefined; rudderstackIntegrationsUrl: undefined; analyticsConsoleReporting = false; + dashboardPerformanceMetrics: string[] = []; sqlConnectionLimits = { maxOpenConns: 100, maxIdleConns: 100, diff --git a/packages/grafana-runtime/src/index.ts b/packages/grafana-runtime/src/index.ts index 0c64456428b..a28fab8050a 100644 --- a/packages/grafana-runtime/src/index.ts +++ b/packages/grafana-runtime/src/index.ts @@ -9,7 +9,7 @@ export * from './analytics/types'; export { loadPluginCss, type PluginCssOptions, setPluginImportUtils, getPluginImportUtils } from './utils/plugin'; export { reportMetaAnalytics, reportInteraction, reportPageview, reportExperimentView } from './analytics/utils'; export { featureEnabled } from './utils/licensing'; -export { logInfo, logDebug, logWarning, logError, createMonitoringLogger } from './utils/logging'; +export { logInfo, logDebug, logWarning, logError, createMonitoringLogger, logMeasurement } from './utils/logging'; export { DataSourceWithBackend, HealthCheckError, diff --git a/packages/grafana-runtime/src/utils/logging.ts b/packages/grafana-runtime/src/utils/logging.ts index d7c94da53d8..98eb7241361 100644 --- a/packages/grafana-runtime/src/utils/logging.ts +++ b/packages/grafana-runtime/src/utils/logging.ts @@ -66,11 +66,13 @@ export function logError(err: Error, contexts?: LogContext) { export type MeasurementValues = Record; export function logMeasurement(type: string, values: MeasurementValues, context?: LogContext) { if (config.grafanaJavascriptAgent.enabled) { - faro.api.pushMeasurement({ - type, - values, - context, - }); + faro.api.pushMeasurement( + { + type, + values, + }, + { context: context } + ); } } diff --git a/pkg/api/dtos/frontend_settings.go b/pkg/api/dtos/frontend_settings.go index d7094de9d90..6827525be04 100644 --- a/pkg/api/dtos/frontend_settings.go +++ b/pkg/api/dtos/frontend_settings.go @@ -190,6 +190,8 @@ type FrontendSettingsDTO struct { AnalyticsConsoleReporting bool `json:"analyticsConsoleReporting"` + DashboardPerformanceMetrics []string `json:"dashboardPerformanceMetrics"` + FeedbackLinksEnabled bool `json:"feedbackLinksEnabled"` ApplicationInsightsConnectionString string `json:"applicationInsightsConnectionString"` ApplicationInsightsEndpointUrl string `json:"applicationInsightsEndpointUrl"` diff --git a/pkg/api/frontendsettings.go b/pkg/api/frontendsettings.go index f11114b299a..8ec544337b0 100644 --- a/pkg/api/frontendsettings.go +++ b/pkg/api/frontendsettings.go @@ -215,6 +215,7 @@ func (hs *HTTPServer) getFrontendSettings(c *contextmodel.ReqContext) (*dtos.Fro RudderstackConfigUrl: hs.Cfg.RudderstackConfigURL, RudderstackIntegrationsUrl: hs.Cfg.RudderstackIntegrationsURL, AnalyticsConsoleReporting: hs.Cfg.FrontendAnalyticsConsoleReporting, + DashboardPerformanceMetrics: hs.Cfg.DashboardPerformanceMetrics, FeedbackLinksEnabled: hs.Cfg.FeedbackLinksEnabled, ApplicationInsightsConnectionString: hs.Cfg.ApplicationInsightsConnectionString, ApplicationInsightsEndpointUrl: hs.Cfg.ApplicationInsightsEndpointUrl, diff --git a/pkg/setting/setting.go b/pkg/setting/setting.go index c78e455fa24..6bb86697f10 100644 --- a/pkg/setting/setting.go +++ b/pkg/setting/setting.go @@ -218,9 +218,10 @@ type Cfg struct { MetricsGrafanaEnvironmentInfo map[string]string // Dashboards - DashboardVersionsToKeep int - MinRefreshInterval string - DefaultHomeDashboardPath string + DashboardVersionsToKeep int + MinRefreshInterval string + DefaultHomeDashboardPath string + DashboardPerformanceMetrics []string // Auth LoginCookieName string @@ -1133,6 +1134,7 @@ func (cfg *Cfg) parseINIFile(iniFile *ini.File) error { cfg.DashboardVersionsToKeep = dashboards.Key("versions_to_keep").MustInt(20) cfg.MinRefreshInterval = valueAsString(dashboards, "min_refresh_interval", "5s") cfg.DefaultHomeDashboardPath = dashboards.Key("default_home_dashboard_path").MustString("") + cfg.DashboardPerformanceMetrics = util.SplitString(dashboards.Key("dashboard_performance_metrics").MustString("")) if err := readUserSettings(iniFile, cfg); err != nil { return err diff --git a/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.ts b/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.ts index d5dcf3d533b..8989681da8d 100644 --- a/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.ts +++ b/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.ts @@ -2,6 +2,7 @@ import { isEqual } from 'lodash'; import { locationUtil, UrlQueryMap } from '@grafana/data'; import { config, getBackendSrv, isFetchError, locationService } from '@grafana/runtime'; +import { sceneGraph } from '@grafana/scenes'; import { DashboardV2Spec } from '@grafana/schema/dist/esm/schema/dashboard/v2alpha0'; import { StateManagerBase } from 'app/core/services/StateManagerBase'; import { getMessageFromError, getMessageIdFromError, getStatusFromError } from 'app/core/utils/errors'; @@ -135,7 +136,10 @@ abstract class DashboardScenePageStateManagerBase this.setState({ dashboard: dashboard, isLoading: false, options }); const measure = stopMeasure(LOAD_SCENE_MEASUREMENT); + const queryController = sceneGraph.getQueryController(dashboard); + trackDashboardSceneLoaded(dashboard, measure?.duration); + queryController?.startProfile(dashboard); if (options.route !== DashboardRoutes.New) { emitDashboardViewEvent({ diff --git a/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts b/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts index c3b82b69b66..e483b3bfe19 100644 --- a/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts +++ b/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts @@ -1,7 +1,7 @@ import { uniqueId } from 'lodash'; import { DataFrameDTO, DataFrameJSON } from '@grafana/data'; -import { config } from '@grafana/runtime'; +import { config, logMeasurement, reportInteraction } from '@grafana/runtime'; import { VizPanel, SceneTimePicker, @@ -19,6 +19,7 @@ import { SceneDataLayerProvider, SceneDataLayerControls, UserActionEvent, + SceneInteractionProfileEvent, SceneObjectState, } from '@grafana/scenes'; import { contextSrv } from 'app/core/core'; @@ -229,7 +230,11 @@ export function createDashboardSceneFromDashboardModel(oldModel: DashboardModel, new behaviors.CursorSync({ sync: oldModel.graphTooltip, }), - new behaviors.SceneQueryController(), + new behaviors.SceneQueryController({ + enableProfiling: + config.dashboardPerformanceMetrics.findIndex((uid) => uid === '*' || uid === oldModel.uid) !== -1, + onProfileComplete: getDashboardInteractionCallback(oldModel.uid, oldModel.title), + }), registerDashboardMacro, registerPanelInteractionsReporter, new behaviors.LiveNowTimer({ enabled: oldModel.liveNow }), @@ -434,3 +439,40 @@ function trackIfEmpty(grid: SceneGridLayout) { sub.unsubscribe(); }; } + +function getDashboardInteractionCallback(uid: string, title: string) { + return (e: SceneInteractionProfileEvent) => { + let interactionType = ''; + + if (e.origin === 'SceneTimeRange') { + interactionType = 'time-range-change'; + } else if (e.origin === 'SceneRefreshPicker') { + interactionType = 'refresh'; + } else if (e.origin === 'DashboardScene') { + interactionType = 'view'; + } else if (e.origin.indexOf('Variable') > -1) { + interactionType = 'variable-change'; + } + reportInteraction('dashboard-render', { + interactionType, + duration: e.duration, + networkDuration: e.networkDuration, + totalJSHeapSize: e.totalJSHeapSize, + usedJSHeapSize: e.usedJSHeapSize, + jsHeapSizeLimit: e.jsHeapSizeLimit, + }); + + logMeasurement( + `dashboard.${interactionType}`, + { + duration: e.duration, + networkDuration: e.networkDuration, + totalJSHeapSize: e.totalJSHeapSize, + usedJSHeapSize: e.usedJSHeapSize, + jsHeapSizeLimit: e.jsHeapSizeLimit, + timeSinceBoot: performance.measure('time_since_boot', 'frontend_boot_js_done_time_seconds').duration, + }, + { dashboard: uid, title: title } + ); + }; +} From 3228ae727eb2c0711b3a0f96dbbf8416bbb0754e Mon Sep 17 00:00:00 2001 From: owensmallwood Date: Tue, 28 Jan 2025 11:36:26 -0600 Subject: [PATCH 38/39] wires up dashboards page to be able to sort by usage stats (sprinkles) (#99479) * wires up dashboards page to be able to sort by usage stats (sprinkles) * dont mutate field * use better type for field * adds tests. Had to export some types and put the field type back to object. * frontend asks for sort field in response if needed * adds some unit tests for getSortOptions * use Record instead of object * prettier * adds ternaries, another unit test --- pkg/storage/unified/search/bleve.go | 4 ++ pkg/storage/unified/search/bleve_test.go | 30 +++++++++ .../features/search/service/unified.test.ts | 65 +++++++++++++++++++ public/app/features/search/service/unified.ts | 26 ++++++-- 4 files changed, 120 insertions(+), 5 deletions(-) create mode 100644 public/app/features/search/service/unified.test.ts diff --git a/pkg/storage/unified/search/bleve.go b/pkg/storage/unified/search/bleve.go index 79a5149efc7..d0c8b0f4ca7 100644 --- a/pkg/storage/unified/search/bleve.go +++ b/pkg/storage/unified/search/bleve.go @@ -655,6 +655,10 @@ func getSortFields(req *resource.ResourceSearchRequest) []string { input = field } + if slices.Contains(DashboardFields(), input) { + input = "fields." + input + } + if sort.Desc { input = "-" + input } diff --git a/pkg/storage/unified/search/bleve_test.go b/pkg/storage/unified/search/bleve_test.go index effc77bdd57..4535b276c0a 100644 --- a/pkg/storage/unified/search/bleve_test.go +++ b/pkg/storage/unified/search/bleve_test.go @@ -529,6 +529,36 @@ func TestBleveBackend(t *testing.T) { }) } +func TestGetSortFields(t *testing.T) { + t.Run("will prepend 'fields.' to sort fields when they are dashboard fields", func(t *testing.T) { + searchReq := &resource.ResourceSearchRequest{ + SortBy: []*resource.ResourceSearchRequest_Sort{ + {Field: "views_total", Desc: false}, + }, + } + sortFields := getSortFields(searchReq) + assert.Equal(t, []string{"fields.views_total"}, sortFields) + }) + t.Run("will prepend sort fields with a '-' when sort is Desc", func(t *testing.T) { + searchReq := &resource.ResourceSearchRequest{ + SortBy: []*resource.ResourceSearchRequest_Sort{ + {Field: "views_total", Desc: true}, + }, + } + sortFields := getSortFields(searchReq) + assert.Equal(t, []string{"-fields.views_total"}, sortFields) + }) + t.Run("will not prepend 'fields.' to common fields", func(t *testing.T) { + searchReq := &resource.ResourceSearchRequest{ + SortBy: []*resource.ResourceSearchRequest_Sort{ + {Field: "description", Desc: false}, + }, + } + sortFields := getSortFields(searchReq) + assert.Equal(t, []string{"description"}, sortFields) + }) +} + func asTimePointer(milli int64) *time.Time { if milli > 0 { t := time.UnixMilli(milli) diff --git a/public/app/features/search/service/unified.test.ts b/public/app/features/search/service/unified.test.ts new file mode 100644 index 00000000000..4d50e25193b --- /dev/null +++ b/public/app/features/search/service/unified.test.ts @@ -0,0 +1,65 @@ +import { toDashboardResults, SearchHit, SearchAPIResponse } from './unified'; + +describe('Unified Storage Searcher', () => { + it('can create dashboard search results and set meta sortBy so column is added for sprinkles sort field', () => { + const mockHits: SearchHit[] = [ + { + resource: 'dashboard', + name: 'Main Dashboard', + title: 'Main Dashboard Title', + location: '/dashboards/1', + folder: 'General', + tags: ['monitoring', 'performance'], + field: { errors_today: 1 }, + url: '/dashboards/1', + }, + { + resource: 'dashboard', + name: 'Main Dashboard', + title: 'Main Dashboard Title', + location: '/dashboards/1', + folder: 'General', + tags: ['monitoring', 'performance'], + field: { errors_today: 2 }, + url: '/dashboards/1', + }, + ]; + + const mockResponse: SearchAPIResponse = { + totalHits: 2, + hits: mockHits, + facets: {}, + }; + const results = toDashboardResults(mockResponse, 'errors_today'); + + expect(results.length).toBe(2); + const sprinklesField = results.fields[10]; + expect(sprinklesField.name).toBe('errors_today'); + expect(sprinklesField.values).toEqual([1, 2]); // this also tests the hits original order is preserved + expect(results.meta?.custom?.sortBy).toBe('errors_today'); + }); + + it('will trim "-" from the sort field name', () => { + const mockHits: SearchHit[] = [ + { + resource: 'dashboard', + name: 'Main Dashboard', + title: 'Main Dashboard Title', + location: '/dashboards/1', + folder: 'General', + tags: ['monitoring', 'performance'], + field: { errors_today: 1 }, + url: '/dashboards/1', + }, + ]; + + const mockResponse: SearchAPIResponse = { + totalHits: 0, + hits: mockHits, + facets: {}, + }; + const results = toDashboardResults(mockResponse, '-errors_today'); + + expect(results.meta?.custom?.sortBy).toBe('errors_today'); + }); +}); diff --git a/public/app/features/search/service/unified.ts b/public/app/features/search/service/unified.ts index 9e379873cc3..3ed44146c35 100644 --- a/public/app/features/search/service/unified.ts +++ b/public/app/features/search/service/unified.ts @@ -20,7 +20,7 @@ const loadingFrameName = 'Loading'; const searchURI = `apis/dashboard.grafana.app/v0alpha1/namespaces/${config.namespace}/search`; -type SearchHit = { +export type SearchHit = { resource: string; // dashboards | folders name: string; title: string; @@ -28,11 +28,13 @@ type SearchHit = { folder: string; tags: string[]; + field: Record; // extra fields from the backend - sort fields included here as well + // calculated in the frontend url: string; }; -type SearchAPIResponse = { +export type SearchAPIResponse = { totalHits: number; hits: SearchHit[]; facets?: { @@ -110,7 +112,7 @@ export class UnifiedSearcher implements GrafanaSearcher { const uri = await this.newRequest(query); const rsp = await getBackendSrv().get(uri); - const first = toDashboardResults(rsp); + const first = toDashboardResults(rsp, query.sort ?? ''); if (first.name === loadingFrameName) { return this.fallbackSearcher.search(query); } @@ -145,7 +147,7 @@ export class UnifiedSearcher implements GrafanaSearcher { } const nextPageUrl = `${uri}&offset=${offset}`; const resp = await getBackendSrv().get(nextPageUrl); - const frame = toDashboardResults(resp); + const frame = toDashboardResults(resp, query.sort ?? ''); if (!frame) { console.log('no results', frame); return; @@ -217,6 +219,9 @@ export class UnifiedSearcher implements GrafanaSearcher { if (query.sort) { const sort = query.sort.replace('_sort', '').replace('name', 'title'); uri += `&sort=${sort}`; + const sortField = sort.startsWith('-') ? sort.substring(1) : sort; + + uri += `&field=${sortField}`; // we want to the sort field to be included in the response } if (query.name?.length) { @@ -279,7 +284,7 @@ function getSortFieldDisplayName(name: string) { return name; } -function toDashboardResults(rsp: SearchAPIResponse): DataFrame { +export function toDashboardResults(rsp: SearchAPIResponse, sort: string): DataFrame { const hits = rsp.hits; if (hits.length < 1) { return { fields: [], length: 0 }; @@ -290,6 +295,11 @@ function toDashboardResults(rsp: SearchAPIResponse): DataFrame { location = 'general'; } + // display null field values as "-" + const field = Object.fromEntries( + Object.entries(hit.field ?? {}).map(([key, value]) => [key, value == null ? '-' : value]) + ); + return { ...hit, uid: hit.name, @@ -299,6 +309,7 @@ function toDashboardResults(rsp: SearchAPIResponse): DataFrame { location, name: hit.title, // 🤯 FIXME hit.name is k8s name, eg grafana dashboards UID kind: hit.resource.substring(0, hit.resource.length - 1), // dashboard "kind" is not plural + ...field, }; }); const frame = toDataFrame(dashboardHits); @@ -308,6 +319,11 @@ function toDashboardResults(rsp: SearchAPIResponse): DataFrame { max_score: 1, }, }; + if (sort && frame.meta.custom) { + // trim the "-" from sort if it exists + frame.meta.custom.sortBy = sort.startsWith('-') ? sort.substring(1) : sort; + } + for (const field of frame.fields) { field.display = getDisplayProcessor({ field, theme: config.theme2 }); } From 04c84e06e538a0482a1406ba6bf315a259b215af Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 28 Jan 2025 18:01:00 +0000 Subject: [PATCH 39/39] Update dependency @types/swagger-ui-react to v4.19.0 (#99674) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package.json | 2 +- yarn.lock | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index 12f8eb5769b..9b22a995e00 100644 --- a/package.json +++ b/package.json @@ -147,7 +147,7 @@ "@types/slate": "0.47.11", "@types/slate-plain-serializer": "0.7.5", "@types/slate-react": "0.22.9", - "@types/swagger-ui-react": "4.18.3", + "@types/swagger-ui-react": "4.19.0", "@types/systemjs": "6.15.1", "@types/tinycolor2": "1.4.6", "@types/uuid": "10.0.0", diff --git a/yarn.lock b/yarn.lock index 62be189a3a7..53483ac0007 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10204,12 +10204,12 @@ __metadata: languageName: node linkType: hard -"@types/swagger-ui-react@npm:4.18.3": - version: 4.18.3 - resolution: "@types/swagger-ui-react@npm:4.18.3" +"@types/swagger-ui-react@npm:4.19.0": + version: 4.19.0 + resolution: "@types/swagger-ui-react@npm:4.19.0" dependencies: "@types/react": "npm:*" - checksum: 10/4927314f1b0d68edf200ef15bca7555f12ec1bb8cc699fa397ef2f4e1e1873d17880b663bbc6d70e30149e50c3e7055178c1e0aad110520dfb31d781ab326dcb + checksum: 10/c4a7ae85ca081bf079164b3e11ecc0c91dec1ed10da0f44fc67f8cf93a7d92d5bfa681035eb8b9d3327a0361d201861c02571b4b18f361253510f3ce19ce073f languageName: node linkType: hard @@ -17886,7 +17886,7 @@ __metadata: "@types/slate": "npm:0.47.11" "@types/slate-plain-serializer": "npm:0.7.5" "@types/slate-react": "npm:0.22.9" - "@types/swagger-ui-react": "npm:4.18.3" + "@types/swagger-ui-react": "npm:4.19.0" "@types/systemjs": "npm:6.15.1" "@types/tinycolor2": "npm:1.4.6" "@types/uuid": "npm:10.0.0"