From f9b21083c6234017321b4b9642b3f3a151efd740 Mon Sep 17 00:00:00 2001 From: Kat Yang <69819079+yangkb09@users.noreply.github.com> Date: Tue, 31 Oct 2023 13:02:53 -0400 Subject: [PATCH 001/869] Chore: Deprecate FolderID in LibraryElement struct (#77377) * Chore: Deprecate FolderID in LibraryElement struct * chore: format deprecated comment * chore: add remaining nolint comments --- pkg/services/libraryelements/database.go | 10 +++++----- pkg/services/libraryelements/model/model.go | 5 +++-- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/pkg/services/libraryelements/database.go b/pkg/services/libraryelements/database.go index 5a157fdda3812..d3bbd45066f41 100644 --- a/pkg/services/libraryelements/database.go +++ b/pkg/services/libraryelements/database.go @@ -147,7 +147,7 @@ func (l *LibraryElementService) createLibraryElement(c context.Context, signedIn element := model.LibraryElement{ OrgID: signedInUser.GetOrgID(), - FolderID: cmd.FolderID, + FolderID: cmd.FolderID, // nolint:staticcheck UID: createUID, Name: cmd.Name, Model: updatedModel, @@ -191,7 +191,7 @@ func (l *LibraryElementService) createLibraryElement(c context.Context, signedIn dto := model.LibraryElementDTO{ ID: element.ID, OrgID: element.OrgID, - FolderID: element.FolderID, + FolderID: element.FolderID, // nolint:staticcheck UID: element.UID, Name: element.Name, Kind: element.Kind, @@ -524,7 +524,7 @@ func (l *LibraryElementService) handleFolderIDPatches(ctx context.Context, eleme if err := l.requireEditPermissionsOnFolder(ctx, user, fromFolderID); err != nil { return err } - + // nolint:staticcheck elementToPatch.FolderID = toFolderID return nil @@ -574,7 +574,7 @@ func (l *LibraryElementService) patchLibraryElement(c context.Context, signedInU var libraryElement = model.LibraryElement{ ID: elementInDB.ID, OrgID: signedInUser.GetOrgID(), - FolderID: cmd.FolderID, + FolderID: cmd.FolderID, // nolint:staticcheck UID: updateUID, Name: cmd.Name, Kind: elementInDB.Kind, @@ -613,7 +613,7 @@ func (l *LibraryElementService) patchLibraryElement(c context.Context, signedInU dto = model.LibraryElementDTO{ ID: libraryElement.ID, OrgID: libraryElement.OrgID, - FolderID: libraryElement.FolderID, + FolderID: libraryElement.FolderID, // nolint:staticcheck UID: libraryElement.UID, Name: libraryElement.Name, Kind: libraryElement.Kind, diff --git a/pkg/services/libraryelements/model/model.go b/pkg/services/libraryelements/model/model.go index aa48ba3af4f1b..ea73ab59929d0 100644 --- a/pkg/services/libraryelements/model/model.go +++ b/pkg/services/libraryelements/model/model.go @@ -20,8 +20,9 @@ const ( // LibraryElement is the model for library element definitions. type LibraryElement struct { - ID int64 `xorm:"pk autoincr 'id'"` - OrgID int64 `xorm:"org_id"` + ID int64 `xorm:"pk autoincr 'id'"` + OrgID int64 `xorm:"org_id"` + // Deprecated: use FolderUID instead FolderID int64 `xorm:"folder_id"` UID string `xorm:"uid"` Name string From 899b3e2b0c3c5b8b612adde19df08eae854aa649 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 31 Oct 2023 18:03:00 +0100 Subject: [PATCH 002/869] PanelChrome: Fixes z-index issue with status message when hover header is true (#77443) --- packages/grafana-ui/src/components/PanelChrome/PanelChrome.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/grafana-ui/src/components/PanelChrome/PanelChrome.tsx b/packages/grafana-ui/src/components/PanelChrome/PanelChrome.tsx index 474ace6c3efb0..4b2bd1c1c1722 100644 --- a/packages/grafana-ui/src/components/PanelChrome/PanelChrome.tsx +++ b/packages/grafana-ui/src/components/PanelChrome/PanelChrome.tsx @@ -465,7 +465,7 @@ const getStyles = (theme: GrafanaTheme2) => { position: 'absolute', left: 0, top: 0, - zIndex: theme.zIndex.tooltip, + zIndex: 1, }), rightActions: css({ display: 'flex', From 2b9c9293152f660009896b95de52643f71df3611 Mon Sep 17 00:00:00 2001 From: Adela Almasan <88068998+adela-almasan@users.noreply.github.com> Date: Tue, 31 Oct 2023 12:07:52 -0500 Subject: [PATCH 003/869] Tooltips: Components update (#77410) --- .../src/components/VizTooltip/SeriesList.tsx | 69 +++++++++++++++++++ .../VizTooltip/VizTooltipColorIndicator.tsx | 64 +++++++++++++++++ .../VizTooltip/VizTooltipHeaderLabelValue.tsx | 47 ++----------- .../src/components/VizTooltip/utils.ts | 4 +- 4 files changed, 140 insertions(+), 44 deletions(-) create mode 100644 packages/grafana-ui/src/components/VizTooltip/SeriesList.tsx create mode 100644 packages/grafana-ui/src/components/VizTooltip/VizTooltipColorIndicator.tsx diff --git a/packages/grafana-ui/src/components/VizTooltip/SeriesList.tsx b/packages/grafana-ui/src/components/VizTooltip/SeriesList.tsx new file mode 100644 index 0000000000000..fb5e1e28965a9 --- /dev/null +++ b/packages/grafana-ui/src/components/VizTooltip/SeriesList.tsx @@ -0,0 +1,69 @@ +import { css, cx } from '@emotion/css'; +import React from 'react'; + +import { GrafanaTheme2, GraphSeriesValue } from '@grafana/data'; + +import { useStyles2 } from '../../themes'; +import { HorizontalGroup } from '../Layout/Layout'; + +import { VizTooltipColorIndicator } from './VizTooltipColorIndicator'; +import { ColorIndicator } from './types'; + +export interface SeriesListProps { + series: SingleSeriesProps[]; +} + +// Based on SeriesTable, with new styling +export const SeriesList = ({ series }: SeriesListProps) => { + return ( + <> + {series.map((series, index) => { + return ( + + ); + })} + + ); +}; + +export interface SingleSeriesProps { + color?: string; + label?: React.ReactNode; + value?: string | GraphSeriesValue; + isActive?: boolean; + colorIndicator?: ColorIndicator; +} + +const SingleSeries = ({ label, value, color, colorIndicator = ColorIndicator.series, isActive }: SingleSeriesProps) => { + const styles = useStyles2(getStyles); + + return ( + + <> + {color && } + {label &&
{label}
} + + {value &&
{value}
} +
+ ); +}; + +const getStyles = (theme: GrafanaTheme2) => ({ + hgContainer: css({ + flexGrow: 1, + }), + activeSeries: css({ + fontWeight: theme.typography.fontWeightBold, + color: theme.colors.text.maxContrast, + }), + label: css({ + color: theme.colors.text.secondary, + fontWeight: 400, + }), +}); diff --git a/packages/grafana-ui/src/components/VizTooltip/VizTooltipColorIndicator.tsx b/packages/grafana-ui/src/components/VizTooltip/VizTooltipColorIndicator.tsx new file mode 100644 index 0000000000000..139c10d42e9a1 --- /dev/null +++ b/packages/grafana-ui/src/components/VizTooltip/VizTooltipColorIndicator.tsx @@ -0,0 +1,64 @@ +import { css, cx } from '@emotion/css'; +import React from 'react'; + +import { GrafanaTheme2 } from '@grafana/data'; + +import { useStyles2 } from '../../themes'; + +import { ColorIndicator } from './types'; +import { getColorIndicatorClass } from './utils'; + +interface Props { + color: string; + colorIndicator: ColorIndicator; +} + +export type ColorIndicatorStyles = ReturnType; + +export const VizTooltipColorIndicator = ({ color, colorIndicator = ColorIndicator.value }: Props) => { + const styles = useStyles2(getStyles); + + return ( + + ); +}; + +// @TODO Update classes/add svgs +const getStyles = (theme: GrafanaTheme2) => ({ + colorIndicator: css({ + marginRight: theme.spacing(0.5), + }), + series: css({ + width: '14px', + height: '4px', + borderRadius: theme.shape.radius.pill, + }), + value: css({ + width: '12px', + height: '12px', + borderRadius: theme.shape.radius.default, + fontWeight: 500, + }), + hexagon: css({}), + pie_1_4: css({}), + pie_2_4: css({}), + pie_3_4: css({}), + marker_sm: css({ + width: '4px', + height: '4px', + borderRadius: theme.shape.radius.circle, + }), + marker_md: css({ + width: '8px', + height: '8px', + borderRadius: theme.shape.radius.circle, + }), + marker_lg: css({ + width: '12px', + height: '12px', + borderRadius: theme.shape.radius.circle, + }), +}); diff --git a/packages/grafana-ui/src/components/VizTooltip/VizTooltipHeaderLabelValue.tsx b/packages/grafana-ui/src/components/VizTooltip/VizTooltipHeaderLabelValue.tsx index 13d8db31a8cc9..909b60ef7a37e 100644 --- a/packages/grafana-ui/src/components/VizTooltip/VizTooltipHeaderLabelValue.tsx +++ b/packages/grafana-ui/src/components/VizTooltip/VizTooltipHeaderLabelValue.tsx @@ -1,4 +1,4 @@ -import { css, cx } from '@emotion/css'; +import { css } from '@emotion/css'; import React from 'react'; import { GrafanaTheme2 } from '@grafana/data'; @@ -6,15 +6,13 @@ import { GrafanaTheme2 } from '@grafana/data'; import { HorizontalGroup } from '..'; import { useStyles2 } from '../../themes'; +import { VizTooltipColorIndicator } from './VizTooltipColorIndicator'; import { LabelValue } from './types'; -import { getColorIndicatorClass } from './utils'; interface Props { keyValuePairs?: LabelValue[]; } -export type HeaderLabelValueStyles = ReturnType; - export const VizTooltipHeaderLabelValue = ({ keyValuePairs }: Props) => { const styles = useStyles2(getStyles); @@ -25,10 +23,9 @@ export const VizTooltipHeaderLabelValue = ({ keyValuePairs }: Props) => {
{keyValuePair.label}
<> - + {keyValuePair.color && ( + + )} {keyValuePair.value}
@@ -38,46 +35,12 @@ export const VizTooltipHeaderLabelValue = ({ keyValuePairs }: Props) => { ); }; -// @TODO Update classes/add svgs? const getStyles = (theme: GrafanaTheme2) => ({ hgContainer: css({ flexGrow: 1, }), - colorIndicator: css({ - marginRight: theme.spacing(0.5), - }), label: css({ color: theme.colors.text.secondary, fontWeight: 400, }), - series: css({ - width: '14px', - height: '4px', - borderRadius: theme.shape.radius.pill, - }), - value: css({ - width: '12px', - height: '12px', - borderRadius: theme.shape.radius.default, - fontWeight: 500, - }), - hexagon: css({}), - pie_1_4: css({}), - pie_2_4: css({}), - pie_3_4: css({}), - marker_sm: css({ - width: '4px', - height: '4px', - borderRadius: theme.shape.radius.circle, - }), - marker_md: css({ - width: '8px', - height: '8px', - borderRadius: theme.shape.radius.circle, - }), - marker_lg: css({ - width: '12px', - height: '12px', - borderRadius: theme.shape.radius.circle, - }), }); diff --git a/packages/grafana-ui/src/components/VizTooltip/utils.ts b/packages/grafana-ui/src/components/VizTooltip/utils.ts index 44f65d28ffabe..6a229a3f8c33e 100644 --- a/packages/grafana-ui/src/components/VizTooltip/utils.ts +++ b/packages/grafana-ui/src/components/VizTooltip/utils.ts @@ -1,4 +1,4 @@ -import { HeaderLabelValueStyles } from './VizTooltipHeaderLabelValue'; +import { ColorIndicatorStyles } from './VizTooltipColorIndicator'; import { ColorIndicator } from './types'; export const calculateTooltipPosition = ( @@ -42,7 +42,7 @@ export const calculateTooltipPosition = ( return { x, y }; }; -export const getColorIndicatorClass = (colorIndicator: string, styles: HeaderLabelValueStyles) => { +export const getColorIndicatorClass = (colorIndicator: string, styles: ColorIndicatorStyles) => { switch (colorIndicator) { case ColorIndicator.value: return styles.value; From 5f6d15d912c015a143382ffbd8af8d61e2990bf4 Mon Sep 17 00:00:00 2001 From: Ashley Harrison Date: Tue, 31 Oct 2023 17:17:22 +0000 Subject: [PATCH 004/869] Chore: Update `detect-breaking-changes` workflow to use node 20 (#77459) * attempt to fix levitate workflow * add comment to force levitate to run * update node version in levitate workflow * change to test levitate * remove dummy comment --- .github/workflows/detect-breaking-changes-build.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/detect-breaking-changes-build.yml b/.github/workflows/detect-breaking-changes-build.yml index be0af37f76bca..27d119d638e13 100644 --- a/.github/workflows/detect-breaking-changes-build.yml +++ b/.github/workflows/detect-breaking-changes-build.yml @@ -1,9 +1,9 @@ -# Only runs if anything under the packages/ directory changes. +# Only runs if anything under the packages/ directory changes. # (Otherwise detect-breaking-changes-build-skip.yml takes over) name: Levitate / Detect breaking changes -on: +on: pull_request: paths: - 'packages/**' @@ -24,7 +24,7 @@ jobs: path: './pr' - uses: actions/setup-node@v4 with: - node-version: 16.16.0 + node-version: 20.9.0 - name: Get yarn cache directory path id: yarn-cache-dir-path @@ -72,7 +72,7 @@ jobs: - uses: actions/setup-node@v4 with: - node-version: 16.16.0 + node-version: 20.9.0 - name: Get yarn cache directory path id: yarn-cache-dir-path From 254648b96bd022aed872eb7ff86b371035e46b79 Mon Sep 17 00:00:00 2001 From: Kat Yang <69819079+yangkb09@users.noreply.github.com> Date: Tue, 31 Oct 2023 13:24:16 -0400 Subject: [PATCH 005/869] Chore: Deprecate FolderID in CreateLibraryElementComand (#77403) * Chore: Deprecate FolderID in CreateLibraryElementComand * chore: add remaining nolint comments * chore: regen specs to include deprecation notice --- pkg/services/folder/folderimpl/folder_test.go | 7 ++++++- pkg/services/libraryelements/api.go | 2 ++ pkg/services/libraryelements/database.go | 1 + pkg/services/libraryelements/libraryelements_test.go | 2 +- pkg/services/libraryelements/model/model.go | 2 ++ pkg/services/librarypanels/librarypanels.go | 2 +- pkg/services/librarypanels/librarypanels_test.go | 6 +++--- public/api-merged.json | 2 +- public/openapi3.json | 2 +- 9 files changed, 18 insertions(+), 8 deletions(-) diff --git a/pkg/services/folder/folderimpl/folder_test.go b/pkg/services/folder/folderimpl/folder_test.go index 14d01a34489e1..996ccd5351c12 100644 --- a/pkg/services/folder/folderimpl/folder_test.go +++ b/pkg/services/folder/folderimpl/folder_test.go @@ -421,9 +421,11 @@ func TestIntegrationNestedFolderService(t *testing.T) { _ = createRule(t, alertStore, parent.UID, "parent alert") _ = createRule(t, alertStore, subfolder.UID, "sub alert") + // nolint:staticcheck libraryElementCmd.FolderID = parent.ID _, err = lps.LibraryElementService.CreateElement(context.Background(), &signedInUser, libraryElementCmd) require.NoError(t, err) + // nolint:staticcheck libraryElementCmd.FolderID = subfolder.ID _, err = lps.LibraryElementService.CreateElement(context.Background(), &signedInUser, libraryElementCmd) require.NoError(t, err) @@ -496,9 +498,11 @@ func TestIntegrationNestedFolderService(t *testing.T) { _ = createRule(t, alertStore, parent.UID, "parent alert") _ = createRule(t, alertStore, subfolder.UID, "sub alert") + // nolint:staticcheck libraryElementCmd.FolderID = parent.ID _, err = lps.LibraryElementService.CreateElement(context.Background(), &signedInUser, libraryElementCmd) require.NoError(t, err) + // nolint:staticcheck libraryElementCmd.FolderID = subfolder.ID _, err = lps.LibraryElementService.CreateElement(context.Background(), &signedInUser, libraryElementCmd) require.NoError(t, err) @@ -631,11 +635,12 @@ func TestIntegrationNestedFolderService(t *testing.T) { subfolder, err = serviceWithFlagOn.dashboardFolderStore.GetFolderByUID(context.Background(), orgID, ancestorUIDs[1]) require.NoError(t, err) _ = createRule(t, alertStore, subfolder.UID, "sub alert") + // nolint:staticcheck libraryElementCmd.FolderID = subfolder.ID subPanel, err = lps.LibraryElementService.CreateElement(context.Background(), &signedInUser, libraryElementCmd) require.NoError(t, err) } - + // nolint:staticcheck libraryElementCmd.FolderID = parent.ID parentPanel, err := lps.LibraryElementService.CreateElement(context.Background(), &signedInUser, libraryElementCmd) require.NoError(t, err) diff --git a/pkg/services/libraryelements/api.go b/pkg/services/libraryelements/api.go index d3113c48526b8..213722fd0b08a 100644 --- a/pkg/services/libraryelements/api.go +++ b/pkg/services/libraryelements/api.go @@ -62,6 +62,7 @@ func (l *LibraryElementService) createHandler(c *contextmodel.ReqContext) respon if cmd.FolderUID != nil { if *cmd.FolderUID == "" { + // nolint:staticcheck cmd.FolderID = 0 generalFolderUID := ac.GeneralFolderUID cmd.FolderUID = &generalFolderUID @@ -70,6 +71,7 @@ func (l *LibraryElementService) createHandler(c *contextmodel.ReqContext) respon if err != nil || folder == nil { return response.ErrOrFallback(http.StatusBadRequest, "failed to get folder", err) } + // nolint:staticcheck cmd.FolderID = folder.ID } } diff --git a/pkg/services/libraryelements/database.go b/pkg/services/libraryelements/database.go index d3bbd45066f41..ef6c44464f15d 100644 --- a/pkg/services/libraryelements/database.go +++ b/pkg/services/libraryelements/database.go @@ -175,6 +175,7 @@ func (l *LibraryElementService) createLibraryElement(c context.Context, signedIn return err } } else { + // nolint:staticcheck if err := l.requireEditPermissionsOnFolder(c, signedInUser, cmd.FolderID); err != nil { return err } diff --git a/pkg/services/libraryelements/libraryelements_test.go b/pkg/services/libraryelements/libraryelements_test.go index 38e40479abb9f..af64578a92043 100644 --- a/pkg/services/libraryelements/libraryelements_test.go +++ b/pkg/services/libraryelements/libraryelements_test.go @@ -252,7 +252,7 @@ func getCreateVariableCommand(folderID int64, name string) model.CreateLibraryEl func getCreateCommandWithModel(folderID int64, name string, kind model.LibraryElementKind, byteModel []byte) model.CreateLibraryElementCommand { command := model.CreateLibraryElementCommand{ - FolderID: folderID, + FolderID: folderID, // nolint:staticcheck Name: name, Model: byteModel, Kind: int64(kind), diff --git a/pkg/services/libraryelements/model/model.go b/pkg/services/libraryelements/model/model.go index ea73ab59929d0..5d8804911e421 100644 --- a/pkg/services/libraryelements/model/model.go +++ b/pkg/services/libraryelements/model/model.go @@ -192,6 +192,8 @@ var ( // swagger:model type CreateLibraryElementCommand struct { // ID of the folder where the library element is stored. + // + // Deprecated: use FolderUID instead FolderID int64 `json:"folderId"` // UID of the folder where the library element is stored. FolderUID *string `json:"folderUid"` diff --git a/pkg/services/librarypanels/librarypanels.go b/pkg/services/librarypanels/librarypanels.go index 163bcf9153e2a..22de54daadc12 100644 --- a/pkg/services/librarypanels/librarypanels.go +++ b/pkg/services/librarypanels/librarypanels.go @@ -164,7 +164,7 @@ func importLibraryPanelsRecursively(c context.Context, service libraryelements.S } var cmd = model.CreateLibraryElementCommand{ - FolderID: folderID, + FolderID: folderID, // nolint:staticcheck Name: name, Model: Model, Kind: int64(model.PanelElement), diff --git a/pkg/services/librarypanels/librarypanels_test.go b/pkg/services/librarypanels/librarypanels_test.go index 2b3bae99b2aec..8e61676c9f871 100644 --- a/pkg/services/librarypanels/librarypanels_test.go +++ b/pkg/services/librarypanels/librarypanels_test.go @@ -94,7 +94,7 @@ func TestConnectLibraryPanelsForDashboard(t *testing.T) { scenarioWithLibraryPanel(t, "When an admin tries to store a dashboard with library panels inside and outside of rows, it should connect all", func(t *testing.T, sc scenarioContext) { cmd := model.CreateLibraryElementCommand{ - FolderID: sc.initialResult.Result.FolderID, + FolderID: sc.initialResult.Result.FolderID, // nolint:staticcheck Name: "Outside row", Model: []byte(` { @@ -233,7 +233,7 @@ func TestConnectLibraryPanelsForDashboard(t *testing.T) { scenarioWithLibraryPanel(t, "When an admin tries to store a dashboard with unused/removed library panels, it should disconnect unused/removed library panels", func(t *testing.T, sc scenarioContext) { unused, err := sc.elementService.CreateElement(sc.ctx, sc.user, model.CreateLibraryElementCommand{ - FolderID: sc.folder.ID, + FolderID: sc.folder.ID, // nolint:staticcheck Name: "Unused Libray Panel", Model: []byte(` { @@ -762,7 +762,7 @@ func scenarioWithLibraryPanel(t *testing.T, desc string, fn func(t *testing.T, s testScenario(t, desc, func(t *testing.T, sc scenarioContext) { command := model.CreateLibraryElementCommand{ - FolderID: sc.folder.ID, + FolderID: sc.folder.ID, // nolint:staticcheck Name: "Text - Library Panel", Model: []byte(` { diff --git a/public/api-merged.json b/public/api-merged.json index 25324ec089fd5..eb2274d65ac89 100644 --- a/public/api-merged.json +++ b/public/api-merged.json @@ -13073,7 +13073,7 @@ "type": "object", "properties": { "folderId": { - "description": "ID of the folder where the library element is stored.", + "description": "ID of the folder where the library element is stored.\n\nDeprecated: use FolderUID instead", "type": "integer", "format": "int64" }, diff --git a/public/openapi3.json b/public/openapi3.json index 361cfd8926ea5..ff42e78e8edd7 100644 --- a/public/openapi3.json +++ b/public/openapi3.json @@ -3962,7 +3962,7 @@ "description": "CreateLibraryElementCommand is the command for adding a LibraryElement", "properties": { "folderId": { - "description": "ID of the folder where the library element is stored.", + "description": "ID of the folder where the library element is stored.\n\nDeprecated: use FolderUID instead", "format": "int64", "type": "integer" }, From dd773e74f120ba908cafd596a6875c2d7fa199bf Mon Sep 17 00:00:00 2001 From: Ryan McKinley Date: Tue, 31 Oct 2023 10:26:39 -0700 Subject: [PATCH 006/869] K8s: Implement playlist api with k8s client (#77405) --- .../feature-toggles/index.md | 1 + .../src/types/featureToggles.gen.ts | 1 + pkg/api/api.go | 9 +- pkg/api/http_server.go | 7 +- pkg/api/playlist.go | 132 +++++++++++++++++- pkg/apis/playlist/v0alpha1/conversions.go | 57 ++++++++ .../playlist/v0alpha1/conversions_test.go | 10 +- pkg/kinds/general.go | 38 +++-- pkg/services/featuremgmt/registry.go | 7 + pkg/services/featuremgmt/toggles_gen.csv | 1 + pkg/services/featuremgmt/toggles_gen.go | 4 + pkg/services/grafana-apiserver/service.go | 75 ++++++---- pkg/services/grafana-apiserver/wireset.go | 1 + pkg/services/playlist/model.go | 3 + .../playlist/playlistimpl/playlist.go | 1 + 15 files changed, 294 insertions(+), 53 deletions(-) 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 461521bb069d2..36cec2c1fe74e 100644 --- a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md +++ b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md @@ -147,6 +147,7 @@ Experimental features might be changed or removed without prior notice. | `formatString` | Enable format string transformer | | `transformationsVariableSupport` | Allows using variables in transformations | | `kubernetesPlaylists` | Use the kubernetes API in the frontend for playlists | +| `kubernetesPlaylistsAPI` | Route /api/playlist API to k8s handlers | | `navAdminSubsections` | Splits the administration section of the nav tree into subsections | | `recoveryThreshold` | Enables feature recovery threshold (aka hysteresis) for threshold server-side expression | | `teamHttpHeaders` | Enables datasources to apply team headers to the client requests | diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index 16ffae92325d5..ef42c20708622 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -139,6 +139,7 @@ export interface FeatureToggles { formatString?: boolean; transformationsVariableSupport?: boolean; kubernetesPlaylists?: boolean; + kubernetesPlaylistsAPI?: boolean; cloudWatchBatchQueries?: boolean; navAdminSubsections?: boolean; recoveryThreshold?: boolean; diff --git a/pkg/api/api.go b/pkg/api/api.go index da3814fc882e6..15bee759b6def 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -499,14 +499,7 @@ func (hs *HTTPServer) registerRoutes() { }) // Playlist - apiRoute.Group("/playlists", func(playlistRoute routing.RouteRegister) { - playlistRoute.Get("/", routing.Wrap(hs.SearchPlaylists)) - playlistRoute.Get("/:uid", hs.ValidateOrgPlaylist, routing.Wrap(hs.GetPlaylist)) - playlistRoute.Get("/:uid/items", hs.ValidateOrgPlaylist, routing.Wrap(hs.GetPlaylistItems)) - playlistRoute.Delete("/:uid", reqEditorRole, hs.ValidateOrgPlaylist, routing.Wrap(hs.DeletePlaylist)) - playlistRoute.Put("/:uid", reqEditorRole, hs.ValidateOrgPlaylist, routing.Wrap(hs.UpdatePlaylist)) - playlistRoute.Post("/", reqEditorRole, routing.Wrap(hs.CreatePlaylist)) - }) + hs.registerPlaylistAPI(apiRoute) // Search apiRoute.Get("/search/sorting", routing.Wrap(hs.ListSortOptions)) diff --git a/pkg/api/http_server.go b/pkg/api/http_server.go index 4f3a9c04ef5a2..412facb7cfb8b 100644 --- a/pkg/api/http_server.go +++ b/pkg/api/http_server.go @@ -16,6 +16,8 @@ import ( "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promhttp" + grafanaapiserver "github.com/grafana/grafana/pkg/services/grafana-apiserver" + "github.com/grafana/grafana/pkg/api/avatar" "github.com/grafana/grafana/pkg/api/routing" httpstatic "github.com/grafana/grafana/pkg/api/static" @@ -205,6 +207,7 @@ type HTTPServer struct { authnService authn.Service starApi *starApi.API promRegister prometheus.Registerer + clientConfigProvider grafanaapiserver.DirectRestConfigProvider } type ServerOptions struct { @@ -246,8 +249,7 @@ func ProvideHTTPServer(opts ServerOptions, cfg *setting.Cfg, routeRegister routi accesscontrolService accesscontrol.Service, navTreeService navtree.Service, annotationRepo annotations.Repository, tagService tag.Service, searchv2HTTPService searchV2.SearchHTTPService, oauthTokenService oauthtoken.OAuthTokenService, statsService stats.Service, authnService authn.Service, pluginsCDNService *pluginscdn.Service, - starApi *starApi.API, promRegister prometheus.Registerer, - + starApi *starApi.API, promRegister prometheus.Registerer, clientConfigProvider grafanaapiserver.DirectRestConfigProvider, ) (*HTTPServer, error) { web.Env = cfg.Env m := web.New() @@ -348,6 +350,7 @@ func ProvideHTTPServer(opts ServerOptions, cfg *setting.Cfg, routeRegister routi pluginsCDNService: pluginsCDNService, starApi: starApi, promRegister: promRegister, + clientConfigProvider: clientConfigProvider, } if hs.Listener != nil { hs.log.Debug("Using provided listener") diff --git a/pkg/api/playlist.go b/pkg/api/playlist.go index e8e6f4cdc8c69..65c1605a13fa2 100644 --- a/pkg/api/playlist.go +++ b/pkg/api/playlist.go @@ -2,15 +2,145 @@ package api import ( "net/http" + "strings" + + "k8s.io/apimachinery/pkg/api/errors" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/client-go/dynamic" "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/api/response" + "github.com/grafana/grafana/pkg/api/routing" + "github.com/grafana/grafana/pkg/apis/playlist/v0alpha1" + "github.com/grafana/grafana/pkg/middleware" contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" + "github.com/grafana/grafana/pkg/services/featuremgmt" + "github.com/grafana/grafana/pkg/services/grafana-apiserver/endpoints/request" "github.com/grafana/grafana/pkg/services/playlist" + "github.com/grafana/grafana/pkg/util/errutil/errhttp" "github.com/grafana/grafana/pkg/web" ) -func (hs *HTTPServer) ValidateOrgPlaylist(c *contextmodel.ReqContext) { +type playlistAPIHandler struct { + SearchPlaylists []web.Handler + GetPlaylist []web.Handler + GetPlaylistItems []web.Handler + DeletePlaylist []web.Handler + UpdatePlaylist []web.Handler + CreatePlaylist []web.Handler +} + +func chainHandlers(h ...web.Handler) []web.Handler { + return h +} + +func (hs *HTTPServer) registerPlaylistAPI(apiRoute routing.RouteRegister) { + handler := playlistAPIHandler{ + SearchPlaylists: chainHandlers(routing.Wrap(hs.SearchPlaylists)), + GetPlaylist: chainHandlers(hs.validateOrgPlaylist, routing.Wrap(hs.GetPlaylist)), + GetPlaylistItems: chainHandlers(hs.validateOrgPlaylist, routing.Wrap(hs.GetPlaylistItems)), + DeletePlaylist: chainHandlers(middleware.ReqEditorRole, hs.validateOrgPlaylist, routing.Wrap(hs.DeletePlaylist)), + UpdatePlaylist: chainHandlers(middleware.ReqEditorRole, hs.validateOrgPlaylist, routing.Wrap(hs.UpdatePlaylist)), + CreatePlaylist: chainHandlers(middleware.ReqEditorRole, routing.Wrap(hs.CreatePlaylist)), + } + + // Alternative implementations for k8s + if hs.Features.IsEnabled(featuremgmt.FlagKubernetesPlaylistsAPI) { + namespacer := request.GetNamespaceMapper(hs.Cfg) + gvr := schema.GroupVersionResource{ + Group: v0alpha1.GroupName, + Version: v0alpha1.VersionID, + Resource: "playlists", + } + + clientGetter := func(c *contextmodel.ReqContext) (dynamic.ResourceInterface, bool) { + dyn, err := dynamic.NewForConfig(hs.clientConfigProvider.GetDirectRestConfig(c)) + if err != nil { + c.JsonApiErr(500, "client", err) + return nil, false + } + return dyn.Resource(gvr).Namespace(namespacer(c.OrgID)), true + } + + errorWriter := func(c *contextmodel.ReqContext, err error) { + //nolint:errorlint + statusError, ok := err.(*errors.StatusError) + if ok { + c.JsonApiErr(int(statusError.Status().Code), + statusError.Status().Message, err) + return + } + errhttp.Write(c.Req.Context(), err, c.Resp) + } + + handler.SearchPlaylists = []web.Handler{func(c *contextmodel.ReqContext) { + client, ok := clientGetter(c) + if !ok { + return // error is already sent + } + out, err := client.List(c.Req.Context(), v1.ListOptions{}) + if err != nil { + errorWriter(c, err) + return + } + + query := strings.ToUpper(c.Query("query")) + playlists := []playlist.Playlist{} + for _, item := range out.Items { + p := v0alpha1.UnstructuredToLegacyPlaylist(item) + if p == nil { + continue + } + if query != "" && !strings.Contains(strings.ToUpper(p.Name), query) { + continue // query filter + } + playlists = append(playlists, *p) + } + c.JSON(http.StatusOK, playlists) + }} + + handler.GetPlaylist = []web.Handler{func(c *contextmodel.ReqContext) { + client, ok := clientGetter(c) + if !ok { + return // error is already sent + } + uid := web.Params(c.Req)[":uid"] + out, err := client.Get(c.Req.Context(), uid, v1.GetOptions{}) + if err != nil { + errorWriter(c, err) + return + } + c.JSON(http.StatusOK, v0alpha1.UnstructuredToLegacyPlaylistDTO(*out)) + }} + + handler.GetPlaylistItems = []web.Handler{func(c *contextmodel.ReqContext) { + client, ok := clientGetter(c) + if !ok { + return // error is already sent + } + uid := web.Params(c.Req)[":uid"] + out, err := client.Get(c.Req.Context(), uid, v1.GetOptions{}) + if err != nil { + errorWriter(c, err) + return + } + c.JSON(http.StatusOK, v0alpha1.UnstructuredToLegacyPlaylistDTO(*out).Items) + }} + } + + // Register the actual handlers + apiRoute.Group("/playlists", func(playlistRoute routing.RouteRegister) { + playlistRoute.Get("/", handler.SearchPlaylists...) + playlistRoute.Get("/:uid", handler.GetPlaylist...) + playlistRoute.Get("/:uid/items", handler.GetPlaylistItems...) + playlistRoute.Delete("/:uid", handler.DeletePlaylist...) + playlistRoute.Put("/:uid", handler.UpdatePlaylist...) + playlistRoute.Post("/", handler.CreatePlaylist...) + }) +} + +func (hs *HTTPServer) validateOrgPlaylist(c *contextmodel.ReqContext) { uid := web.Params(c.Req)[":uid"] query := playlist.GetPlaylistByUidQuery{UID: uid, OrgId: c.SignedInUser.GetOrgID()} p, err := hs.playlistService.GetWithoutItems(c.Req.Context(), &query) diff --git a/pkg/apis/playlist/v0alpha1/conversions.go b/pkg/apis/playlist/v0alpha1/conversions.go index b512f2072949a..121eb491458ba 100644 --- a/pkg/apis/playlist/v0alpha1/conversions.go +++ b/pkg/apis/playlist/v0alpha1/conversions.go @@ -1,16 +1,48 @@ package v0alpha1 import ( + "encoding/json" "fmt" + "strconv" "time" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/types" + "github.com/grafana/grafana/pkg/kinds" "github.com/grafana/grafana/pkg/services/grafana-apiserver/endpoints/request" "github.com/grafana/grafana/pkg/services/playlist" ) +func UnstructuredToLegacyPlaylist(item unstructured.Unstructured) *playlist.Playlist { + spec := item.Object["spec"].(map[string]any) + return &playlist.Playlist{ + UID: item.GetName(), + Name: spec["title"].(string), + Interval: spec["interval"].(string), + Id: getLegacyID(&item), + } +} + +func UnstructuredToLegacyPlaylistDTO(item unstructured.Unstructured) *playlist.PlaylistDTO { + spec := item.Object["spec"].(map[string]any) + dto := &playlist.PlaylistDTO{ + Uid: item.GetName(), + Name: spec["title"].(string), + Interval: spec["interval"].(string), + Id: getLegacyID(&item), + } + items := spec["items"] + if items != nil { + b, err := json.Marshal(items) + if err == nil { + _ = json.Unmarshal(b, &dto.Items) + } + } + return dto +} + func convertToK8sResource(v *playlist.PlaylistDTO, namespacer request.NamespaceMapper) *Playlist { spec := Spec{ Title: v.Name, @@ -22,6 +54,15 @@ func convertToK8sResource(v *playlist.PlaylistDTO, namespacer request.NamespaceM Value: item.Value, }) } + + meta := kinds.GrafanaResourceMetadata{} + meta.SetUpdatedTimestampMillis(v.UpdatedAt) + if v.Id > 0 { + meta.SetOriginInfo(&kinds.ResourceOriginInfo{ + Name: "SQL", + Key: fmt.Sprintf("%d", v.Id), + }) + } return &Playlist{ ObjectMeta: metav1.ObjectMeta{ Name: v.Uid, @@ -29,7 +70,23 @@ func convertToK8sResource(v *playlist.PlaylistDTO, namespacer request.NamespaceM ResourceVersion: fmt.Sprintf("%d", v.UpdatedAt), CreationTimestamp: metav1.NewTime(time.UnixMilli(v.CreatedAt)), Namespace: namespacer(v.OrgID), + Annotations: meta.Annotations, }, Spec: spec, } } + +// Read legacy ID from metadata annotations +func getLegacyID(item *unstructured.Unstructured) int64 { + meta := kinds.GrafanaResourceMetadata{ + Annotations: item.GetAnnotations(), + } + info := meta.GetOriginInfo() + if info != nil && info.Name == "SQL" { + i, err := strconv.ParseInt(info.Key, 10, 64) + if err == nil { + return i + } + } + return 0 +} diff --git a/pkg/apis/playlist/v0alpha1/conversions_test.go b/pkg/apis/playlist/v0alpha1/conversions_test.go index ce13fdd2ea0d7..1c64a1ec08256 100644 --- a/pkg/apis/playlist/v0alpha1/conversions_test.go +++ b/pkg/apis/playlist/v0alpha1/conversions_test.go @@ -12,6 +12,7 @@ import ( func TestPlaylistConversion(t *testing.T) { src := &playlist.PlaylistDTO{ + Id: 123, OrgID: 3, Uid: "abc", // becomes k8s name Name: "MyPlaylists", // becomes title @@ -32,14 +33,19 @@ func TestPlaylistConversion(t *testing.T) { out, err := json.MarshalIndent(dst, "", " ") require.NoError(t, err) - //fmt.Printf("%s", string(out)) + // fmt.Printf("%s", string(out)) require.JSONEq(t, `{ "metadata": { "name": "abc", "namespace": "org-3", "uid": "abc", "resourceVersion": "54321", - "creationTimestamp": "1970-01-01T00:00:12Z" + "creationTimestamp": "1970-01-01T00:00:12Z", + "annotations": { + "grafana.app/originKey": "123", + "grafana.app/originName": "SQL", + "grafana.app/updatedTimestamp": "1970-01-01T00:00:54Z" + } }, "spec": { "title": "MyPlaylists", diff --git a/pkg/kinds/general.go b/pkg/kinds/general.go index c044f3de7e2c8..691e7cd252862 100644 --- a/pkg/kinds/general.go +++ b/pkg/kinds/general.go @@ -60,10 +60,15 @@ const annoKeyOriginTimestamp = "grafana.app/originTimestamp" func (m *GrafanaResourceMetadata) set(key string, val string) { if val == "" { - delete(m.Annotations, key) - } else { - m.Annotations[key] = val + if m.Annotations != nil { + delete(m.Annotations, key) + } + return + } + if m.Annotations == nil { + m.Annotations = make(map[string]string) } + m.Annotations[key] = val } func (m *GrafanaResourceMetadata) GetUpdatedTimestamp() *time.Time { @@ -77,12 +82,21 @@ func (m *GrafanaResourceMetadata) GetUpdatedTimestamp() *time.Time { return nil } -func (m *GrafanaResourceMetadata) SetUpdatedTimestamp(v *time.Time) { - if v == nil { - delete(m.Annotations, annoKeyUpdatedTimestamp) +func (m *GrafanaResourceMetadata) SetUpdatedTimestampMillis(v int64) { + if v > 0 { + t := time.UnixMilli(v) + m.SetUpdatedTimestamp(&t) } else { - m.Annotations[annoKeyUpdatedTimestamp] = v.Format(time.RFC3339) + m.SetUpdatedTimestamp(nil) + } +} + +func (m *GrafanaResourceMetadata) SetUpdatedTimestamp(v *time.Time) { + txt := "" + if v != nil { + txt = v.UTC().Format(time.RFC3339) } + m.set(annoKeyUpdatedTimestamp, txt) } func (m *GrafanaResourceMetadata) GetCreatedBy() string { @@ -123,13 +137,9 @@ func (m *GrafanaResourceMetadata) SetOriginInfo(info *ResourceOriginInfo) { delete(m.Annotations, annoKeyOriginKey) delete(m.Annotations, annoKeyOriginTimestamp) if info != nil || info.Name != "" { - m.Annotations[annoKeyOriginName] = info.Name - if info.Path != "" { - m.Annotations[annoKeyOriginPath] = info.Path - } - if info.Key != "" { - m.Annotations[annoKeyOriginKey] = info.Key - } + m.set(annoKeyOriginName, info.Name) + m.set(annoKeyOriginKey, info.Key) + m.set(annoKeyOriginPath, info.Path) if info.Timestamp != nil { m.Annotations[annoKeyOriginTimestamp] = info.Timestamp.Format(time.RFC3339) } diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index be7fbdf1bbe64..62d3c7f948a67 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -850,6 +850,13 @@ var ( Stage: FeatureStageExperimental, Owner: grafanaAppPlatformSquad, }, + { + Name: "kubernetesPlaylistsAPI", + Description: "Route /api/playlist API to k8s handlers", + Stage: FeatureStageExperimental, + Owner: grafanaAppPlatformSquad, + RequiresRestart: true, // changes the API routing + }, { Name: "cloudWatchBatchQueries", Description: "Runs CloudWatch metrics queries as separate batches", diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv index b12ba83ea18ff..9deb2bc15f986 100644 --- a/pkg/services/featuremgmt/toggles_gen.csv +++ b/pkg/services/featuremgmt/toggles_gen.csv @@ -120,6 +120,7 @@ enableNativeHTTPHistogram,experimental,@grafana/hosted-grafana-team,false,false, formatString,experimental,@grafana/grafana-bi-squad,false,false,false,true transformationsVariableSupport,experimental,@grafana/grafana-bi-squad,false,false,false,true kubernetesPlaylists,experimental,@grafana/grafana-app-platform-squad,false,false,false,true +kubernetesPlaylistsAPI,experimental,@grafana/grafana-app-platform-squad,false,false,true,false cloudWatchBatchQueries,preview,@grafana/aws-datasources,false,false,false,false navAdminSubsections,experimental,@grafana/grafana-frontend-platform,false,false,false,false recoveryThreshold,experimental,@grafana/alerting-squad,false,false,true,false diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go index 621f20d1046c5..7ca935ff87465 100644 --- a/pkg/services/featuremgmt/toggles_gen.go +++ b/pkg/services/featuremgmt/toggles_gen.go @@ -491,6 +491,10 @@ const ( // Use the kubernetes API in the frontend for playlists FlagKubernetesPlaylists = "kubernetesPlaylists" + // FlagKubernetesPlaylistsAPI + // Route /api/playlist API to k8s handlers + FlagKubernetesPlaylistsAPI = "kubernetesPlaylistsAPI" + // FlagCloudWatchBatchQueries // Runs CloudWatch metrics queries as separate batches FlagCloudWatchBatchQueries = "cloudWatchBatchQueries" diff --git a/pkg/services/grafana-apiserver/service.go b/pkg/services/grafana-apiserver/service.go index 3eadba4b15c6e..5193b9c89ef04 100644 --- a/pkg/services/grafana-apiserver/service.go +++ b/pkg/services/grafana-apiserver/service.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "net/http" + "net/http/httptest" "path" "strconv" @@ -35,7 +36,6 @@ import ( contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" filestorage "github.com/grafana/grafana/pkg/services/grafana-apiserver/storage/file" "github.com/grafana/grafana/pkg/setting" - "github.com/grafana/grafana/pkg/web" ) type StorageType string @@ -86,6 +86,13 @@ type RestConfigProvider interface { GetRestConfig() *clientrest.Config } +type DirectRestConfigProvider interface { + // GetDirectRestConfig returns a k8s client configuration that will use the same + // logged logged in user as the current request context. This is useful when + // creating clients that map legacy API handlers to k8s backed services + GetDirectRestConfig(c *contextmodel.ReqContext) *clientrest.Config +} + type service struct { *services.BasicService @@ -96,7 +103,7 @@ type service struct { stoppedCh chan error rr routing.RouteRegister - handler web.Handler + handler http.Handler builders []APIGroupBuilder tracing *tracing.TracingService @@ -133,10 +140,24 @@ func ProvideService( return } - if handle, ok := s.handler.(func(c *contextmodel.ReqContext)); ok { - handle(c) - return + req := c.Req + if req.URL.Path == "" { + req.URL.Path = "/" } + + //TODO: add support for the existing MetricsEndpointBasicAuth config option + if req.URL.Path == "/apiserver-metrics" { + req.URL.Path = "/metrics" + } + + ctx := req.Context() + signedInUser := appcontext.MustUser(ctx) + + req.Header.Set("X-Remote-User", strconv.FormatInt(signedInUser.UserID, 10)) + req.Header.Set("X-Remote-Group", "grafana") + + resp := responsewriter.WrapForHTTP1Or2(c.Resp) + s.handler.ServeHTTP(resp, req) } k8sRoute.Any("/", middleware.ReqSignedIn, handler) k8sRoute.Any("/*", middleware.ReqSignedIn, handler) @@ -301,27 +322,8 @@ func (s *service) start(ctx context.Context) error { } } - // TODO: this is a hack. see note in ProvideService - s.handler = func(c *contextmodel.ReqContext) { - req := c.Req - if req.URL.Path == "" { - req.URL.Path = "/" - } - - //TODO: add support for the existing MetricsEndpointBasicAuth config option - if req.URL.Path == "/apiserver-metrics" { - req.URL.Path = "/metrics" - } - - ctx := req.Context() - signedInUser := appcontext.MustUser(ctx) - - req.Header.Set("X-Remote-User", strconv.FormatInt(signedInUser.UserID, 10)) - req.Header.Set("X-Remote-Group", "grafana") - - resp := responsewriter.WrapForHTTP1Or2(c.Resp) - server.Handler.ServeHTTP(resp, req) - } + // Used by the proxy wrapper registered in ProvideService + s.handler = server.Handler // skip starting the server in prod mode if !s.config.devMode { @@ -335,6 +337,19 @@ func (s *service) start(ctx context.Context) error { return nil } +func (s *service) GetDirectRestConfig(c *contextmodel.ReqContext) *clientrest.Config { + return &clientrest.Config{ + Transport: &roundTripperFunc{ + fn: func(req *http.Request) (*http.Response, error) { + ctx := appcontext.WithUser(req.Context(), c.SignedInUser) + w := httptest.NewRecorder() + s.handler.ServeHTTP(w, req.WithContext(ctx)) + return w.Result(), nil + }, + }, + } +} + func (s *service) running(ctx context.Context) error { // skip waiting for the server in prod mode if !s.config.devMode { @@ -383,3 +398,11 @@ func (s *service) ensureKubeConfig() error { return clientcmd.WriteToFile(clientConfig, path.Join(s.config.dataPath, "grafana.kubeconfig")) } + +type roundTripperFunc struct { + fn func(req *http.Request) (*http.Response, error) +} + +func (f *roundTripperFunc) RoundTrip(req *http.Request) (*http.Response, error) { + return f.fn(req) +} diff --git a/pkg/services/grafana-apiserver/wireset.go b/pkg/services/grafana-apiserver/wireset.go index f4986e59313be..aa01caa503f14 100644 --- a/pkg/services/grafana-apiserver/wireset.go +++ b/pkg/services/grafana-apiserver/wireset.go @@ -11,5 +11,6 @@ var WireSet = wire.NewSet( wire.Bind(new(RestConfigProvider), new(*service)), wire.Bind(new(Service), new(*service)), wire.Bind(new(APIRegistrar), new(*service)), + wire.Bind(new(DirectRestConfigProvider), new(*service)), authorizer.WireSet, ) diff --git a/pkg/services/playlist/model.go b/pkg/services/playlist/model.go index 178bd7c83f803..ffd6938b117e2 100644 --- a/pkg/services/playlist/model.go +++ b/pkg/services/playlist/model.go @@ -47,6 +47,9 @@ type PlaylistDTO struct { // Returned for k8s OrgID int64 `json:"-"` + + // Returned for k8s and added as an annotation + Id int64 `json:"-"` } type PlaylistItemDTO struct { diff --git a/pkg/services/playlist/playlistimpl/playlist.go b/pkg/services/playlist/playlistimpl/playlist.go index 65dbdd359871b..90aa4b0b1f53f 100644 --- a/pkg/services/playlist/playlistimpl/playlist.go +++ b/pkg/services/playlist/playlistimpl/playlist.go @@ -68,6 +68,7 @@ func (s *Service) Get(ctx context.Context, q *playlist.GetPlaylistByUidQuery) (* } } return &playlist.PlaylistDTO{ + Id: v.Id, Uid: v.UID, Name: v.Name, Interval: v.Interval, From 714aec2275f8df1e517a45e50ecf16bd8ca0e9c3 Mon Sep 17 00:00:00 2001 From: Nathan Marrs Date: Tue, 31 Oct 2023 13:52:46 -0600 Subject: [PATCH 007/869] Auto-generate: Update generation character limits, improve generation history UX (#76849) --- .../GenAI/GenAIDashDescriptionButton.tsx | 4 +++- .../components/GenAI/GenAIDashTitleButton.tsx | 4 +++- .../components/GenAI/GenAIHistory.tsx | 3 +-- .../GenAI/GenAIPanelDescriptionButton.tsx | 6 ++++-- .../GenAI/GenAIPanelTitleButton.tsx | 21 +++++++------------ .../GenAI/MinimalisticPagination.tsx | 1 + .../dashboard/components/GenAI/utils.ts | 15 +++++++++++++ 7 files changed, 34 insertions(+), 20 deletions(-) diff --git a/public/app/features/dashboard/components/GenAI/GenAIDashDescriptionButton.tsx b/public/app/features/dashboard/components/GenAI/GenAIDashDescriptionButton.tsx index b039735c06ea6..4f9cc429ed070 100644 --- a/public/app/features/dashboard/components/GenAI/GenAIDashDescriptionButton.tsx +++ b/public/app/features/dashboard/components/GenAI/GenAIDashDescriptionButton.tsx @@ -11,6 +11,8 @@ interface GenAIDashDescriptionButtonProps { dashboard: DashboardModel; } +const DASHBOARD_DESCRIPTION_CHAR_LIMIT = 300; + const DESCRIPTION_GENERATION_STANDARD_PROMPT = 'You are an expert in creating Grafana Dashboards.\n' + 'Your goal is to write a descriptive and concise dashboard description.\n' + @@ -19,7 +21,7 @@ const DESCRIPTION_GENERATION_STANDARD_PROMPT = 'If the dashboard has no panels, the description should be "Empty dashboard"\n' + 'There should be no numbers in the description except where they are important.\n' + 'The dashboard description should not have the dashboard title or any quotation marks in it.\n' + - 'The description should be, at most, 140 characters.\n' + + `The description should be, at most, ${DASHBOARD_DESCRIPTION_CHAR_LIMIT} characters.\n` + 'Respond with only the description of the dashboard.'; export const GenAIDashDescriptionButton = ({ onGenerate, dashboard }: GenAIDashDescriptionButtonProps) => { diff --git a/public/app/features/dashboard/components/GenAI/GenAIDashTitleButton.tsx b/public/app/features/dashboard/components/GenAI/GenAIDashTitleButton.tsx index f39e7ce37bd5d..cebd5cb947848 100644 --- a/public/app/features/dashboard/components/GenAI/GenAIDashTitleButton.tsx +++ b/public/app/features/dashboard/components/GenAI/GenAIDashTitleButton.tsx @@ -11,6 +11,8 @@ interface GenAIDashTitleButtonProps { onGenerate: (description: string) => void; } +const DASH_TITLE_CHAR_LIMIT = 50; + const TITLE_GENERATION_STANDARD_PROMPT = 'You are an expert in creating Grafana Dashboards.\n' + 'Your goal is to write a concise dashboard title.\n' + @@ -19,7 +21,7 @@ const TITLE_GENERATION_STANDARD_PROMPT = 'If the dashboard has no panels, the title should be "Empty dashboard"\n' + 'There should be no numbers in the title.\n' + 'The dashboard title should not have quotation marks in it.\n' + - 'The title should be, at most, 50 characters.\n' + + `The title should be, at most, ${DASH_TITLE_CHAR_LIMIT} characters.\n` + 'Respond with only the title of the dashboard.'; export const GenAIDashTitleButton = ({ onGenerate, dashboard }: GenAIDashTitleButtonProps) => { diff --git a/public/app/features/dashboard/components/GenAI/GenAIHistory.tsx b/public/app/features/dashboard/components/GenAI/GenAIHistory.tsx index 51a2d1072e390..71006a897d068 100644 --- a/public/app/features/dashboard/components/GenAI/GenAIHistory.tsx +++ b/public/app/features/dashboard/components/GenAI/GenAIHistory.tsx @@ -16,12 +16,11 @@ import { VerticalGroup, } from '@grafana/ui'; -import { getFeedbackMessage } from './GenAIPanelTitleButton'; import { GenerationHistoryCarousel } from './GenerationHistoryCarousel'; import { QuickFeedback } from './QuickFeedback'; import { StreamStatus, useOpenAIStream } from './hooks'; import { AutoGenerateItem, EventTrackingSrc, reportAutoGenerateInteraction } from './tracking'; -import { Message, DEFAULT_OAI_MODEL, QuickFeedbackType, sanitizeReply } from './utils'; +import { getFeedbackMessage, Message, DEFAULT_OAI_MODEL, QuickFeedbackType, sanitizeReply } from './utils'; export interface GenAIHistoryProps { history: string[]; diff --git a/public/app/features/dashboard/components/GenAI/GenAIPanelDescriptionButton.tsx b/public/app/features/dashboard/components/GenAI/GenAIPanelDescriptionButton.tsx index 570443e8759dc..fa42e40c3362a 100644 --- a/public/app/features/dashboard/components/GenAI/GenAIPanelDescriptionButton.tsx +++ b/public/app/features/dashboard/components/GenAI/GenAIPanelDescriptionButton.tsx @@ -12,6 +12,8 @@ interface GenAIPanelDescriptionButtonProps { panel: PanelModel; } +const PANEL_DESCRIPTION_CHAR_LIMIT = 200; + const DESCRIPTION_GENERATION_STANDARD_PROMPT = 'You are an expert in creating Grafana Panels.\n' + 'You will be given the title and description of the dashboard the panel is in as well as the JSON for the panel.\n' + @@ -19,7 +21,7 @@ const DESCRIPTION_GENERATION_STANDARD_PROMPT = 'The panel description is meant to explain the purpose of the panel, not just its attributes.\n' + 'Do not refer to the panel; simply describe its purpose.\n' + 'There should be no numbers in the description except for thresholds.\n' + - 'The description should be, at most, 140 characters.'; + `The description should be, at most, ${PANEL_DESCRIPTION_CHAR_LIMIT} characters.`; export const GenAIPanelDescriptionButton = ({ onGenerate, panel }: GenAIPanelDescriptionButtonProps) => { const messages = React.useMemo(() => getMessages(panel), [panel]); @@ -48,7 +50,7 @@ function getMessages(panel: PanelModel): Message[] { role: Role.system, }, { - content: `The panel is part of a dashboard with the description: ${dashboard.title}`, + content: `The panel is part of a dashboard with the description: ${dashboard.description}`, role: Role.system, }, { diff --git a/public/app/features/dashboard/components/GenAI/GenAIPanelTitleButton.tsx b/public/app/features/dashboard/components/GenAI/GenAIPanelTitleButton.tsx index 8e58103d8267f..3cb314623aca5 100644 --- a/public/app/features/dashboard/components/GenAI/GenAIPanelTitleButton.tsx +++ b/public/app/features/dashboard/components/GenAI/GenAIPanelTitleButton.tsx @@ -5,17 +5,19 @@ import { PanelModel } from '../../state'; import { GenAIButton } from './GenAIButton'; import { EventTrackingSrc } from './tracking'; -import { Message, QuickFeedbackType, Role } from './utils'; +import { Message, Role } from './utils'; interface GenAIPanelTitleButtonProps { onGenerate: (title: string) => void; panel: PanelModel; } +const PANEL_TITLE_CHAR_LIMIT = 50; + const TITLE_GENERATION_STANDARD_PROMPT = 'You are an expert in creating Grafana Panels.' + - 'Your goal is to write short, descriptive, and concise panel title for a panel.' + - 'The title should be shorter than 50 characters.'; + 'Your goal is to write short, descriptive, and concise panel title.' + + `The title should be shorter than ${PANEL_TITLE_CHAR_LIMIT} characters.`; export const GenAIPanelTitleButton = ({ onGenerate, panel }: GenAIPanelTitleButtonProps) => { const messages = React.useMemo(() => getMessages(panel), [panel]); @@ -44,21 +46,12 @@ function getMessages(panel: PanelModel): Message[] { role: Role.system, }, { - content: `The panel is part of a dashboard with the description: ${dashboard.title}`, + content: `The panel is part of a dashboard with the description: ${dashboard.description}`, role: Role.system, }, { content: `Use this JSON object which defines the panel: ${JSON.stringify(panel.getSaveModel())}`, - role: Role.user, - }, - ]; -} - -export const getFeedbackMessage = (previousResponse: string, feedback: string | QuickFeedbackType): Message[] => { - return [ - { role: Role.system, - content: `Your previous response was: ${previousResponse}. The user has provided the following feedback: ${feedback}. Re-generate your response according to the provided feedback.`, }, ]; -}; +} diff --git a/public/app/features/dashboard/components/GenAI/MinimalisticPagination.tsx b/public/app/features/dashboard/components/GenAI/MinimalisticPagination.tsx index 623532656eeaf..ce59b2a69de66 100644 --- a/public/app/features/dashboard/components/GenAI/MinimalisticPagination.tsx +++ b/public/app/features/dashboard/components/GenAI/MinimalisticPagination.tsx @@ -51,5 +51,6 @@ const getStyles = (theme: GrafanaTheme2) => ({ display: 'flex', flexDirection: 'row', gap: 16, + userSelect: 'none', }), }); diff --git a/public/app/features/dashboard/components/GenAI/utils.ts b/public/app/features/dashboard/components/GenAI/utils.ts index 3d695ba05386b..ca5607c4d90f6 100644 --- a/public/app/features/dashboard/components/GenAI/utils.ts +++ b/public/app/features/dashboard/components/GenAI/utils.ts @@ -65,6 +65,21 @@ export async function isLLMPluginEnabled() { return llms.openai.enabled().then((response) => response.ok); } +/** + * Get the message to be sent to OpenAI to generate a new response. + * @param previousResponse + * @param feedback + * @returns Message[] to be sent to OpenAI to generate a new response + */ +export const getFeedbackMessage = (previousResponse: string, feedback: string | QuickFeedbackType): Message[] => { + return [ + { + role: Role.system, + content: `Your previous response was: ${previousResponse}. The user has provided the following feedback: ${feedback}. Re-generate your response according to the provided feedback.`, + }, + ]; +}; + /** * * @param dashboard Dashboard to generate a title or description for From 8a5d4c4c6e323529bbfea3a2774a38b91062eeeb Mon Sep 17 00:00:00 2001 From: Kevin Minehart Date: Tue, 31 Oct 2023 20:52:09 +0000 Subject: [PATCH 008/869] CI: Update RGM steps to use the artifacts command (#77470) * update rgm steps to use artifacts subcmd * format-drone * make drone --- .drone.yml | 102 ++++++++++++++++------------- scripts/drone/pipelines/build.star | 15 +++-- scripts/drone/rgm.star | 2 + scripts/drone/steps/rgm.star | 44 +++++++------ scripts/drone/utils/images.star | 2 +- 5 files changed, 96 insertions(+), 69 deletions(-) diff --git a/.drone.yml b/.drone.yml index aa50476196c7e..4fe665bf56815 100644 --- a/.drone.yml +++ b/.drone.yml @@ -17,7 +17,7 @@ services: [] steps: - commands: - echo $DRONE_RUNNER_NAME - image: alpine:3.18.3 + image: alpine:3.18.4 name: identify-runner - commands: - go build -o ./bin/build -ldflags '-extldflags -static' ./pkg/build/cmd @@ -67,7 +67,7 @@ services: [] steps: - commands: - echo $DRONE_RUNNER_NAME - image: alpine:3.18.3 + image: alpine:3.18.4 name: identify-runner - commands: - go build -o ./bin/build -ldflags '-extldflags -static' ./pkg/build/cmd @@ -117,7 +117,7 @@ services: [] steps: - commands: - echo $DRONE_RUNNER_NAME - image: alpine:3.18.3 + image: alpine:3.18.4 name: identify-runner - commands: - yarn install --immutable @@ -217,7 +217,7 @@ steps: name: clone-enterprise - commands: - echo $DRONE_RUNNER_NAME - image: alpine:3.18.3 + image: alpine:3.18.4 name: identify-runner - commands: - yarn install --immutable @@ -306,7 +306,7 @@ steps: name: clone-enterprise - commands: - echo $DRONE_RUNNER_NAME - image: alpine:3.18.3 + image: alpine:3.18.4 name: identify-runner - commands: - '# It is required that code generated from Thema/CUE be committed and in sync @@ -390,7 +390,7 @@ services: [] steps: - commands: - echo $DRONE_RUNNER_NAME - image: alpine:3.18.3 + image: alpine:3.18.4 name: identify-runner - commands: - go build -o ./bin/build -ldflags '-extldflags -static' ./pkg/build/cmd @@ -485,7 +485,7 @@ services: [] steps: - commands: - echo $DRONE_RUNNER_NAME - image: alpine:3.18.3 + image: alpine:3.18.4 name: identify-runner - commands: - mkdir -p bin @@ -556,9 +556,9 @@ steps: token: from_secret: drone_token - commands: - - /src/grafana-build package --distro=linux/amd64,linux/arm64 --go-version=1.20.10 - --yarn-cache=$$YARN_CACHE_FOLDER --build-id=$$DRONE_BUILD_NUMBER --grafana-dir=$$PWD - > packages.txt + - /src/grafana-build artifacts -a targz:grafana:linux/amd64 -a targz:grafana:linux/arm64 + --go-version=1.20.10 --yarn-cache=$$YARN_CACHE_FOLDER --build-id=$$DRONE_BUILD_NUMBER + --grafana-dir=$$PWD > packages.txt depends_on: - yarn-install image: grafana/grafana-build:main @@ -579,7 +579,7 @@ steps: GF_APP_MODE: development GF_SERVER_HTTP_PORT: "3001" GF_SERVER_ROUTER_LOGGING: "1" - image: alpine:3.18.3 + image: alpine:3.18.4 name: grafana-server - commands: - ./bin/build e2e-tests --port 3001 --suite dashboards-suite @@ -699,13 +699,15 @@ steps: name: test-a11y-frontend - commands: - docker run --privileged --rm tonistiigi/binfmt --install all - - /src/grafana-build docker $(cat packages.txt | grep tar.gz | grep -v docker | - grep -v sha256 | awk '{print "--package=" $0}') --ubuntu-base=ubuntu:22.04 --alpine-base=alpine:3.18.3 - --tag-format='{{ .version_base }}-{{ .buildID }}-{{ .arch }}' --ubuntu-tag-format='{{ - .version_base }}-{{ .buildID }}-ubuntu-{{ .arch }}' > docker.txt + - /src/grafana-build artifacts -a docker:grafana:linux/amd64 -a docker:grafana:linux/amd64:ubuntu + -a docker:grafana:linux/arm64 -a docker:grafana:linux/arm64:ubuntu --yarn-cache=$$YARN_CACHE_FOLDER + --build-id=$$DRONE_BUILD_NUMBER --ubuntu-base=ubuntu:22.04 --alpine-base=alpine:3.18.4 + --tag-format='{{ .version_base }}-{{ .buildID }}-{{ .arch }}' --grafana-dir=$$PWD + --ubuntu-tag-format='{{ .version_base }}-{{ .buildID }}-ubuntu-{{ .arch }}' > + docker.txt - find ./dist -name '*docker*.tar.gz' -type f | xargs -n1 docker load -i depends_on: - - rgm-package + - yarn-install image: grafana/grafana-build:main name: rgm-build-docker pull: always @@ -844,7 +846,7 @@ steps: name: compile-build-cmd - commands: - echo $DRONE_RUNNER_NAME - image: alpine:3.18.3 + image: alpine:3.18.4 name: identify-runner - commands: - '# It is required that code generated from Thema/CUE be committed and in sync @@ -1030,7 +1032,7 @@ services: [] steps: - commands: - echo $DRONE_RUNNER_NAME - image: alpine:3.18.3 + image: alpine:3.18.4 name: identify-runner - commands: - yarn install --immutable @@ -1385,7 +1387,7 @@ services: [] steps: - commands: - echo $DRONE_RUNNER_NAME - image: alpine:3.18.3 + image: alpine:3.18.4 name: identify-runner - commands: - yarn install --immutable @@ -1460,7 +1462,7 @@ services: [] steps: - commands: - echo $DRONE_RUNNER_NAME - image: alpine:3.18.3 + image: alpine:3.18.4 name: identify-runner - commands: - yarn install --immutable @@ -1517,7 +1519,7 @@ services: [] steps: - commands: - echo $DRONE_RUNNER_NAME - image: alpine:3.18.3 + image: alpine:3.18.4 name: identify-runner - commands: - yarn install --immutable @@ -1584,7 +1586,7 @@ services: [] steps: - commands: - echo $DRONE_RUNNER_NAME - image: alpine:3.18.3 + image: alpine:3.18.4 name: identify-runner - commands: - '# It is required that code generated from Thema/CUE be committed and in sync @@ -1663,7 +1665,7 @@ services: [] steps: - commands: - echo $DRONE_RUNNER_NAME - image: alpine:3.18.3 + image: alpine:3.18.4 name: identify-runner - commands: - go build -o ./bin/build -ldflags '-extldflags -static' ./pkg/build/cmd @@ -1737,7 +1739,7 @@ services: [] steps: - commands: - echo $DRONE_RUNNER_NAME - image: alpine:3.18.3 + image: alpine:3.18.4 name: identify-runner - commands: - mkdir -p bin @@ -1807,9 +1809,9 @@ steps: image: node:20.9.0-alpine name: build-frontend-packages - commands: - - /src/grafana-build package --distro=linux/amd64,linux/arm64 --go-version=1.20.10 - --yarn-cache=$$YARN_CACHE_FOLDER --build-id=$$DRONE_BUILD_NUMBER --grafana-dir=$$PWD - > packages.txt + - /src/grafana-build artifacts -a targz:grafana:linux/amd64 -a targz:grafana:linux/arm64 + --go-version=1.20.10 --yarn-cache=$$YARN_CACHE_FOLDER --build-id=$$DRONE_BUILD_NUMBER + --grafana-dir=$$PWD > packages.txt depends_on: - update-package-json-version image: grafana/grafana-build:main @@ -1830,7 +1832,7 @@ steps: GF_APP_MODE: development GF_SERVER_HTTP_PORT: "3001" GF_SERVER_ROUTER_LOGGING: "1" - image: alpine:3.18.3 + image: alpine:3.18.4 name: grafana-server - commands: - ./bin/build e2e-tests --port 3001 --suite dashboards-suite @@ -1986,13 +1988,15 @@ steps: - grafana/grafana - commands: - docker run --privileged --rm tonistiigi/binfmt --install all - - /src/grafana-build docker $(cat packages.txt | grep tar.gz | grep -v docker | - grep -v sha256 | awk '{print "--package=" $0}') --ubuntu-base=ubuntu:22.04 --alpine-base=alpine:3.18.3 - --tag-format='{{ .version_base }}-{{ .buildID }}-{{ .arch }}' --ubuntu-tag-format='{{ - .version_base }}-{{ .buildID }}-ubuntu-{{ .arch }}' > docker.txt + - /src/grafana-build artifacts -a docker:grafana:linux/amd64 -a docker:grafana:linux/amd64:ubuntu + -a docker:grafana:linux/arm64 -a docker:grafana:linux/arm64:ubuntu --yarn-cache=$$YARN_CACHE_FOLDER + --build-id=$$DRONE_BUILD_NUMBER --ubuntu-base=ubuntu:22.04 --alpine-base=alpine:3.18.4 + --tag-format='{{ .version_base }}-{{ .buildID }}-{{ .arch }}' --grafana-dir=$$PWD + --ubuntu-tag-format='{{ .version_base }}-{{ .buildID }}-ubuntu-{{ .arch }}' > + docker.txt - find ./dist -name '*docker*.tar.gz' -type f | xargs -n1 docker load -i depends_on: - - rgm-package + - yarn-install image: grafana/grafana-build:main name: rgm-build-docker pull: always @@ -2193,7 +2197,7 @@ steps: name: compile-build-cmd - commands: - echo $DRONE_RUNNER_NAME - image: alpine:3.18.3 + image: alpine:3.18.4 name: identify-runner - commands: - '# It is required that code generated from Thema/CUE be committed and in sync @@ -2506,7 +2510,7 @@ services: [] steps: - commands: - echo $DRONE_RUNNER_NAME - image: alpine:3.18.3 + image: alpine:3.18.4 name: identify-runner - commands: - mkdir -p bin @@ -2838,6 +2842,7 @@ steps: environment: _EXPERIMENTAL_DAGGER_CLOUD_TOKEN: from_secret: dagger_token + ALPINE_BASE: alpine:3.18.4 CDN_DESTINATION: from_secret: rgm_cdn_destination DESTINATION: @@ -2865,6 +2870,7 @@ steps: from_secret: npm_token STORYBOOK_DESTINATION: from_secret: rgm_storybook_destination + UBUNTU_BASE: ubuntu:22.04 image: grafana/grafana-build:main name: rgm-build pull: always @@ -2951,7 +2957,7 @@ services: [] steps: - commands: - echo $DRONE_RUNNER_NAME - image: alpine:3.18.3 + image: alpine:3.18.4 name: identify-runner - commands: - yarn install --immutable @@ -3006,7 +3012,7 @@ services: [] steps: - commands: - echo $DRONE_RUNNER_NAME - image: alpine:3.18.3 + image: alpine:3.18.4 name: identify-runner - commands: - '# It is required that code generated from Thema/CUE be committed and in sync @@ -3087,6 +3093,7 @@ steps: environment: _EXPERIMENTAL_DAGGER_CLOUD_TOKEN: from_secret: dagger_token + ALPINE_BASE: alpine:3.18.4 CDN_DESTINATION: from_secret: rgm_cdn_destination DESTINATION: @@ -3114,6 +3121,7 @@ steps: from_secret: npm_token STORYBOOK_DESTINATION: from_secret: rgm_storybook_destination + UBUNTU_BASE: ubuntu:22.04 image: grafana/grafana-build:main name: rgm-build pull: always @@ -3265,6 +3273,7 @@ steps: environment: _EXPERIMENTAL_DAGGER_CLOUD_TOKEN: from_secret: dagger_token + ALPINE_BASE: alpine:3.18.4 CDN_DESTINATION: from_secret: rgm_cdn_destination DESTINATION: @@ -3292,6 +3301,7 @@ steps: from_secret: npm_token STORYBOOK_DESTINATION: from_secret: rgm_storybook_destination + UBUNTU_BASE: ubuntu:22.04 image: grafana/grafana-build:main name: rgm-build pull: always @@ -3363,7 +3373,7 @@ services: [] steps: - commands: - echo $DRONE_RUNNER_NAME - image: alpine:3.18.3 + image: alpine:3.18.4 name: identify-runner - commands: - yarn install --immutable @@ -3416,7 +3426,7 @@ services: [] steps: - commands: - echo $DRONE_RUNNER_NAME - image: alpine:3.18.3 + image: alpine:3.18.4 name: identify-runner - commands: - '# It is required that code generated from Thema/CUE be committed and in sync @@ -3495,6 +3505,7 @@ steps: environment: _EXPERIMENTAL_DAGGER_CLOUD_TOKEN: from_secret: dagger_token + ALPINE_BASE: alpine:3.18.4 CDN_DESTINATION: from_secret: rgm_cdn_destination DESTINATION: @@ -3522,6 +3533,7 @@ steps: from_secret: npm_token STORYBOOK_DESTINATION: from_secret: rgm_storybook_destination + UBUNTU_BASE: ubuntu:22.04 image: grafana/grafana-build:main name: rgm-build pull: always @@ -3639,6 +3651,7 @@ steps: environment: _EXPERIMENTAL_DAGGER_CLOUD_TOKEN: from_secret: dagger_token + ALPINE_BASE: alpine:3.18.4 CDN_DESTINATION: from_secret: rgm_cdn_destination DESTINATION: @@ -3666,6 +3679,7 @@ steps: from_secret: npm_token STORYBOOK_DESTINATION: from_secret: rgm_storybook_destination + UBUNTU_BASE: ubuntu:22.04 image: grafana/grafana-build:main name: rgm-publish pull: always @@ -3846,7 +3860,7 @@ steps: name: grabpl - commands: - echo $DRONE_RUNNER_NAME - image: alpine:3.18.3 + image: alpine:3.18.4 name: identify-runner - commands: - '# It is required that code generated from Thema/CUE be committed and in sync @@ -4068,7 +4082,7 @@ steps: - commands: - if [ -z "${BUILD_CONTAINER_VERSION}" ]; then echo Missing BUILD_CONTAINER_VERSION; false; fi - image: alpine:3.18.3 + image: alpine:3.18.4 name: validate-version - commands: - printenv GCP_KEY > /tmp/key.json @@ -4392,7 +4406,7 @@ steps: - trivy --exit-code 0 --severity UNKNOWN,LOW,MEDIUM node:20.9.0-alpine - trivy --exit-code 0 --severity UNKNOWN,LOW,MEDIUM google/cloud-sdk:431.0.0 - trivy --exit-code 0 --severity UNKNOWN,LOW,MEDIUM grafana/grafana-ci-deploy:1.3.3 - - trivy --exit-code 0 --severity UNKNOWN,LOW,MEDIUM alpine:3.18.3 + - trivy --exit-code 0 --severity UNKNOWN,LOW,MEDIUM alpine:3.18.4 - trivy --exit-code 0 --severity UNKNOWN,LOW,MEDIUM ubuntu:22.04 - trivy --exit-code 0 --severity UNKNOWN,LOW,MEDIUM byrnedo/alpine-curl:0.1.8 - trivy --exit-code 0 --severity UNKNOWN,LOW,MEDIUM plugins/slack @@ -4426,7 +4440,7 @@ steps: - trivy --exit-code 1 --severity HIGH,CRITICAL node:20.9.0-alpine - trivy --exit-code 1 --severity HIGH,CRITICAL google/cloud-sdk:431.0.0 - trivy --exit-code 1 --severity HIGH,CRITICAL grafana/grafana-ci-deploy:1.3.3 - - trivy --exit-code 1 --severity HIGH,CRITICAL alpine:3.18.3 + - trivy --exit-code 1 --severity HIGH,CRITICAL alpine:3.18.4 - trivy --exit-code 1 --severity HIGH,CRITICAL ubuntu:22.04 - trivy --exit-code 1 --severity HIGH,CRITICAL byrnedo/alpine-curl:0.1.8 - trivy --exit-code 1 --severity HIGH,CRITICAL plugins/slack @@ -4670,6 +4684,6 @@ kind: secret name: gcr_credentials --- kind: signature -hmac: 920b57d70a96a29872ce77dc83ca26e2d141ba37627eee58c9d3beebf9ec4055 +hmac: 88aa054b297d39ae9757ee3a13d16b51491931fabf7c78ba0d0c125a4088da33 ... diff --git a/scripts/drone/pipelines/build.star b/scripts/drone/pipelines/build.star index ee60b7b51030e..f4d5800242c39 100644 --- a/scripts/drone/pipelines/build.star +++ b/scripts/drone/pipelines/build.star @@ -28,8 +28,8 @@ load( ) load( "scripts/drone/steps/rgm.star", + "rgm_artifacts_step", "rgm_build_docker_step", - "rgm_package_step", ) load( "scripts/drone/utils/images.star", @@ -70,14 +70,21 @@ def build_e2e(trigger, ver_mode): [ build_frontend_package_step(), enterprise_downstream_step(ver_mode = ver_mode), - rgm_package_step(distros = "linux/amd64,linux/arm64", file = "packages.txt"), + rgm_artifacts_step(artifacts = ["targz:grafana:linux/amd64", "targz:grafana:linux/arm64"], file = "packages.txt"), ], ) else: build_steps.extend([ update_package_json_version(), build_frontend_package_step(depends_on = ["update-package-json-version"]), - rgm_package_step(depends_on = ["update-package-json-version"], distros = "linux/amd64,linux/arm64", file = "packages.txt"), + rgm_artifacts_step( + artifacts = [ + "targz:grafana:linux/amd64", + "targz:grafana:linux/arm64", + ], + depends_on = ["update-package-json-version"], + file = "packages.txt", + ), ]) build_steps.extend( @@ -104,7 +111,6 @@ def build_e2e(trigger, ver_mode): store_storybook_step(trigger = trigger_oss, ver_mode = ver_mode), frontend_metrics_step(trigger = trigger_oss), rgm_build_docker_step( - "packages.txt", images["ubuntu"], images["alpine"], tag_format = "{{ .version_base }}-{{ .buildID }}-{{ .arch }}", @@ -135,7 +141,6 @@ def build_e2e(trigger, ver_mode): build_steps.extend( [ rgm_build_docker_step( - "packages.txt", images["ubuntu"], images["alpine"], tag_format = "{{ .version_base }}-{{ .buildID }}-{{ .arch }}", diff --git a/scripts/drone/rgm.star b/scripts/drone/rgm.star index 411d4a4c969c6..b411178cee8dd 100644 --- a/scripts/drone/rgm.star +++ b/scripts/drone/rgm.star @@ -137,6 +137,8 @@ def rgm_run(name, script): """ env = { "GO_VERSION": golang_version, + "ALPINE_BASE": images["alpine"], + "UBUNTU_BASE": images["ubuntu"], } rgm_run_step = { "name": name, diff --git a/scripts/drone/steps/rgm.star b/scripts/drone/steps/rgm.star index 6d86ac48b3440..ba12e69a6c826 100644 --- a/scripts/drone/steps/rgm.star +++ b/scripts/drone/steps/rgm.star @@ -8,15 +8,25 @@ load( "golang_version", ) -# rgm_package_step will create a tar.gz for use in e2e tests or other PR testing related activities.. -def rgm_package_step(distros = "linux/amd64,linux/arm64", file = "packages.txt", depends_on = ["yarn-install"]): +def artifacts_cmd(artifacts = []): + cmd = "/src/grafana-build artifacts " + + for artifact in artifacts: + cmd += "-a {} ".format(artifact) + + return cmd + +# rgm_artifacts_step will create artifacts using the '/src/build artifacts' command. +def rgm_artifacts_step(name = "rgm-package", artifacts = ["targz:grafana:linux/amd64", "targz:grafana:linux/arm64"], file = "packages.txt", depends_on = ["yarn-install"]): + cmd = artifacts_cmd(artifacts = artifacts) + return { - "name": "rgm-package", + "name": name, "image": "grafana/grafana-build:main", "pull": "always", "depends_on": depends_on, "commands": [ - "/src/grafana-build package --distro={} ".format(distros) + + cmd + "--go-version={} ".format(golang_version) + "--yarn-cache=$$YARN_CACHE_FOLDER " + "--build-id=$$DRONE_BUILD_NUMBER " + @@ -28,31 +38,27 @@ def rgm_package_step(distros = "linux/amd64,linux/arm64", file = "packages.txt", # rgm_build_backend will create compile the grafana backend for various platforms. It's preferred to use # 'rgm_package_step' if you creating a "usable" artifact. This should really only be used to verify that the code is # compilable. -def rgm_build_backend_step(distros = "linux/amd64,linux/arm64"): - return { - "name": "rgm-package", - "image": "grafana/grafana-build:main", - "pull": "always", - "commands": [ - "/src/grafana-build build " + - "--go-version={} ".format(golang_version) + - "--distro={} --grafana-dir=$$PWD".format(distros), - ], - "volumes": [{"name": "docker", "path": "/var/run/docker.sock"}], - } +def rgm_build_backend_step(artifacts = ["backend:grafana:linux/amd64", "backend:grafana:linux/arm64"]): + return rgm_artifacts_step(name = "rgm-build-backend", artifacts = artifacts, depends_on = []) -def rgm_build_docker_step(packages, ubuntu, alpine, depends_on = ["rgm-package"], file = "docker.txt", tag_format = "{{ .version }}-{{ .arch }}", ubuntu_tag_format = "{{ .version }}-ubuntu-{{ .arch }}"): +def rgm_build_docker_step(ubuntu, alpine, depends_on = ["yarn-install"], file = "docker.txt", tag_format = "{{ .version }}-{{ .arch }}", ubuntu_tag_format = "{{ .version }}-ubuntu-{{ .arch }}"): return { "name": "rgm-build-docker", "image": "grafana/grafana-build:main", "pull": "always", "commands": [ "docker run --privileged --rm tonistiigi/binfmt --install all", - "/src/grafana-build docker " + - "$(cat {} | grep tar.gz | grep -v docker | grep -v sha256 | awk '{{print \"--package=\" $0}}') ".format(packages) + + "/src/grafana-build artifacts " + + "-a docker:grafana:linux/amd64 " + + "-a docker:grafana:linux/amd64:ubuntu " + + "-a docker:grafana:linux/arm64 " + + "-a docker:grafana:linux/arm64:ubuntu " + + "--yarn-cache=$$YARN_CACHE_FOLDER " + + "--build-id=$$DRONE_BUILD_NUMBER " + "--ubuntu-base={} ".format(ubuntu) + "--alpine-base={} ".format(alpine) + "--tag-format='{}' ".format(tag_format) + + "--grafana-dir=$$PWD " + "--ubuntu-tag-format='{}' > {}".format(ubuntu_tag_format, file), "find ./dist -name '*docker*.tar.gz' -type f | xargs -n1 docker load -i", ], diff --git a/scripts/drone/utils/images.star b/scripts/drone/utils/images.star index 454130fbc4a38..c16df446e1703 100644 --- a/scripts/drone/utils/images.star +++ b/scripts/drone/utils/images.star @@ -14,7 +14,7 @@ images = { "node": "node:{}-alpine".format(nodejs_version), "cloudsdk": "google/cloud-sdk:431.0.0", "publish": "grafana/grafana-ci-deploy:1.3.3", - "alpine": "alpine:3.18.3", + "alpine": "alpine:3.18.4", "ubuntu": "ubuntu:22.04", "curl": "byrnedo/alpine-curl:0.1.8", "plugins_slack": "plugins/slack", From d511925fc947a143cab141ee8cbf448ba3dac78b Mon Sep 17 00:00:00 2001 From: Ryan McKinley Date: Tue, 31 Oct 2023 16:17:25 -0700 Subject: [PATCH 009/869] Chore: Add more deprecation notices to packages/grafana-ui/src/components/Graph/ (#77480) more deprecations --- packages/grafana-ui/src/components/Graph/Graph.tsx | 2 ++ packages/grafana-ui/src/components/Graph/GraphContextMenu.tsx | 2 ++ .../grafana-ui/src/components/Graph/GraphSeriesToggler.tsx | 4 ++++ .../src/components/Graph/GraphTooltip/GraphTooltip.tsx | 1 + .../components/Graph/GraphTooltip/MultiModeGraphTooltip.tsx | 2 ++ .../components/Graph/GraphTooltip/SingleModeGraphTooltip.tsx | 1 + .../grafana-ui/src/components/Graph/GraphTooltip/types.ts | 2 ++ packages/grafana-ui/src/components/Graph/types.ts | 2 ++ packages/grafana-ui/src/components/Graph/utils.ts | 4 ++++ 9 files changed, 20 insertions(+) diff --git a/packages/grafana-ui/src/components/Graph/Graph.tsx b/packages/grafana-ui/src/components/Graph/Graph.tsx index b9c48023de721..4b1d4c8e5425c 100644 --- a/packages/grafana-ui/src/components/Graph/Graph.tsx +++ b/packages/grafana-ui/src/components/Graph/Graph.tsx @@ -15,6 +15,7 @@ import { GraphDimensions } from './GraphTooltip/types'; import { FlotPosition, FlotItem } from './types'; import { graphTimeFormat, graphTickFormatter } from './utils'; +/** @deprecated */ export interface GraphProps { ariaLabel?: string; children?: JSX.Element | JSX.Element[]; @@ -31,6 +32,7 @@ export interface GraphProps { onHorizontalRegionSelected?: (from: number, to: number) => void; } +/** @deprecated */ interface GraphState { pos?: FlotPosition; contextPos?: FlotPosition; diff --git a/packages/grafana-ui/src/components/Graph/GraphContextMenu.tsx b/packages/grafana-ui/src/components/Graph/GraphContextMenu.tsx index 963fb8624598b..c58744435db13 100644 --- a/packages/grafana-ui/src/components/Graph/GraphContextMenu.tsx +++ b/packages/grafana-ui/src/components/Graph/GraphContextMenu.tsx @@ -21,8 +21,10 @@ import { SeriesIcon } from '../VizLegend/SeriesIcon'; import { GraphDimensions } from './GraphTooltip/types'; +/** @deprecated */ export type ContextDimensions = { [key in keyof T]: [number, number | undefined] | null }; +/** @deprecated */ export type GraphContextMenuProps = ContextMenuProps & { getContextMenuSource: () => FlotDataPoint | null; timeZone?: TimeZone; diff --git a/packages/grafana-ui/src/components/Graph/GraphSeriesToggler.tsx b/packages/grafana-ui/src/components/Graph/GraphSeriesToggler.tsx index 693cf9e9758f3..fc960695a6817 100644 --- a/packages/grafana-ui/src/components/Graph/GraphSeriesToggler.tsx +++ b/packages/grafana-ui/src/components/Graph/GraphSeriesToggler.tsx @@ -3,22 +3,26 @@ import React, { Component } from 'react'; import { GraphSeriesXY } from '@grafana/data'; +/** @deprecated */ export interface GraphSeriesTogglerAPI { onSeriesToggle: (label: string, event: React.MouseEvent) => void; toggledSeries: GraphSeriesXY[]; } +/** @deprecated */ export interface GraphSeriesTogglerProps { children: (api: GraphSeriesTogglerAPI) => JSX.Element; series: GraphSeriesXY[]; onHiddenSeriesChanged?: (hiddenSeries: string[]) => void; } +/** @deprecated */ export interface GraphSeriesTogglerState { hiddenSeries: string[]; toggledSeries: GraphSeriesXY[]; } +/** @deprecated */ export class GraphSeriesToggler extends Component { constructor(props: GraphSeriesTogglerProps) { super(props); diff --git a/packages/grafana-ui/src/components/Graph/GraphTooltip/GraphTooltip.tsx b/packages/grafana-ui/src/components/Graph/GraphTooltip/GraphTooltip.tsx index 53008e1dd6886..d52b37c01442e 100644 --- a/packages/grafana-ui/src/components/Graph/GraphTooltip/GraphTooltip.tsx +++ b/packages/grafana-ui/src/components/Graph/GraphTooltip/GraphTooltip.tsx @@ -8,6 +8,7 @@ import { MultiModeGraphTooltip } from './MultiModeGraphTooltip'; import { SingleModeGraphTooltip } from './SingleModeGraphTooltip'; import { GraphDimensions } from './types'; +/** @deprecated */ export const GraphTooltip = ({ mode = TooltipDisplayMode.Single, dimensions, diff --git a/packages/grafana-ui/src/components/Graph/GraphTooltip/MultiModeGraphTooltip.tsx b/packages/grafana-ui/src/components/Graph/GraphTooltip/MultiModeGraphTooltip.tsx index f071abdcea252..b41351e60b156 100644 --- a/packages/grafana-ui/src/components/Graph/GraphTooltip/MultiModeGraphTooltip.tsx +++ b/packages/grafana-ui/src/components/Graph/GraphTooltip/MultiModeGraphTooltip.tsx @@ -8,11 +8,13 @@ import { getMultiSeriesGraphHoverInfo } from '../utils'; import { GraphTooltipContentProps } from './types'; +/** @deprecated */ type Props = GraphTooltipContentProps & { // We expect position to figure out correct values when not hovering over a datapoint pos: FlotPosition; }; +/** @deprecated */ export const MultiModeGraphTooltip = ({ dimensions, activeDimensions, pos, timeZone }: Props) => { let activeSeriesIndex: number | null = null; // when no x-axis provided, skip rendering diff --git a/packages/grafana-ui/src/components/Graph/GraphTooltip/SingleModeGraphTooltip.tsx b/packages/grafana-ui/src/components/Graph/GraphTooltip/SingleModeGraphTooltip.tsx index 3b5ad3b2adea6..1f9c5d4699fb0 100644 --- a/packages/grafana-ui/src/components/Graph/GraphTooltip/SingleModeGraphTooltip.tsx +++ b/packages/grafana-ui/src/components/Graph/GraphTooltip/SingleModeGraphTooltip.tsx @@ -11,6 +11,7 @@ import { SeriesTable } from '../../VizTooltip'; import { GraphTooltipContentProps } from './types'; +/** @deprecated */ export const SingleModeGraphTooltip = ({ dimensions, activeDimensions, timeZone }: GraphTooltipContentProps) => { // not hovering over a point, skip rendering if ( diff --git a/packages/grafana-ui/src/components/Graph/GraphTooltip/types.ts b/packages/grafana-ui/src/components/Graph/GraphTooltip/types.ts index ccf04ef11751e..049ca540b1a2a 100644 --- a/packages/grafana-ui/src/components/Graph/GraphTooltip/types.ts +++ b/packages/grafana-ui/src/components/Graph/GraphTooltip/types.ts @@ -2,11 +2,13 @@ import { Dimension, Dimensions, TimeZone } from '@grafana/data'; import { ActiveDimensions } from '../../VizTooltip'; +/** @deprecated */ export interface GraphDimensions extends Dimensions { xAxis: Dimension; yAxis: Dimension; } +/** @deprecated */ export interface GraphTooltipContentProps { dimensions: GraphDimensions; // Dimension[] activeDimensions: ActiveDimensions; diff --git a/packages/grafana-ui/src/components/Graph/types.ts b/packages/grafana-ui/src/components/Graph/types.ts index be3c00dd59cf6..24d5f59588d08 100644 --- a/packages/grafana-ui/src/components/Graph/types.ts +++ b/packages/grafana-ui/src/components/Graph/types.ts @@ -1,3 +1,4 @@ +/** @deprecated */ export interface FlotPosition { pageX: number; pageY: number; @@ -7,6 +8,7 @@ export interface FlotPosition { y1: number; } +/** @deprecated */ export interface FlotItem { datapoint: [number, number]; dataIndex: number; diff --git a/packages/grafana-ui/src/components/Graph/utils.ts b/packages/grafana-ui/src/components/Graph/utils.ts index 9ac252c82fc01..ebc34ad470ea7 100644 --- a/packages/grafana-ui/src/components/Graph/utils.ts +++ b/packages/grafana-ui/src/components/Graph/utils.ts @@ -13,6 +13,7 @@ import { * * @param posX * @param series + * @deprecated */ export const findHoverIndexFromData = (xAxisDimension: Field, xPos: number) => { let lower = 0; @@ -50,6 +51,7 @@ interface MultiSeriesHoverInfo { * * @param seriesList list of series visible on the Graph * @param pos mouse cursor position, based on jQuery.flot position + * @deprecated */ export const getMultiSeriesGraphHoverInfo = ( // x and y axis dimensions order is aligned @@ -102,6 +104,7 @@ export const getMultiSeriesGraphHoverInfo = ( }; }; +/** @deprecated */ export const graphTickFormatter = (epoch: number, axis: any) => { return dateTimeFormat(epoch, { format: axis?.options?.timeformat, @@ -109,6 +112,7 @@ export const graphTickFormatter = (epoch: number, axis: any) => { }); }; +/** @deprecated */ export const graphTimeFormat = (ticks: number | null, min: number | null, max: number | null): string => { if (min && max && ticks) { const range = max - min; From f5cbd4f9d0638a5d25d61adcabe15c23333cf508 Mon Sep 17 00:00:00 2001 From: Alex Khomenko Date: Wed, 1 Nov 2023 08:48:02 +0100 Subject: [PATCH 010/869] grafana/ui: Rename Flex component to Stack (#77453) * grafana/ui: Remove Stack and rename FLex to Stack * Update types * Update grafana/ui imports * Update Grafana imports * Update docs --- .../src/components/Layout/Box/Box.mdx | 2 +- .../src/components/Layout/Box/Box.story.tsx | 42 +-- .../src/components/Layout/Box/Box.tsx | 2 +- .../Layout/Flex/Flex.internal.story.tsx | 202 ---------- .../src/components/Layout/Flex/Flex.mdx | 138 ------- .../src/components/Layout/Flex/Flex.tsx | 89 ----- .../src/components/Layout/Grid/Grid.mdx | 5 - .../Layout/Stack/HorizontalStack.tsx | 20 - .../Layout/Stack/Stack.internal.story.tsx | 354 +++++++++--------- .../src/components/Layout/Stack/Stack.mdx | 39 +- .../src/components/Layout/Stack/Stack.tsx | 89 ++++- .../src/components/Layout/Stack/index.ts | 2 - packages/grafana-ui/src/unstable.ts | 4 +- .../AppChrome/DockedMegaMenu/MegaMenu.tsx | 6 +- public/app/features/admin/Users/OrgUnits.tsx | 4 +- .../features/admin/Users/OrgUsersTable.tsx | 6 +- .../app/features/admin/Users/UsersTable.tsx | 10 +- .../dashboard/dashgrid/DashboardEmpty.tsx | 30 +- .../ServiceAccountsListPage.tsx | 6 +- public/app/features/teams/TeamList.tsx | 6 +- 20 files changed, 312 insertions(+), 744 deletions(-) delete mode 100644 packages/grafana-ui/src/components/Layout/Flex/Flex.internal.story.tsx delete mode 100644 packages/grafana-ui/src/components/Layout/Flex/Flex.mdx delete mode 100644 packages/grafana-ui/src/components/Layout/Flex/Flex.tsx delete mode 100644 packages/grafana-ui/src/components/Layout/Stack/HorizontalStack.tsx delete mode 100644 packages/grafana-ui/src/components/Layout/Stack/index.ts diff --git a/packages/grafana-ui/src/components/Layout/Box/Box.mdx b/packages/grafana-ui/src/components/Layout/Box/Box.mdx index b789bead6d14a..e9ec57ed8127c 100644 --- a/packages/grafana-ui/src/components/Layout/Box/Box.mdx +++ b/packages/grafana-ui/src/components/Layout/Box/Box.mdx @@ -16,7 +16,7 @@ Use it whenever you would use custom CSS. #### When not to use -If you need layout styles, use the Stack, Flex or Grid components instead. +If you need layout styles, use the Stack or Grid components instead. ### How to add a prop to Box diff --git a/packages/grafana-ui/src/components/Layout/Box/Box.story.tsx b/packages/grafana-ui/src/components/Layout/Box/Box.story.tsx index 42d0b7f759e7f..203f9f1310838 100644 --- a/packages/grafana-ui/src/components/Layout/Box/Box.story.tsx +++ b/packages/grafana-ui/src/components/Layout/Box/Box.story.tsx @@ -3,7 +3,7 @@ import React from 'react'; import { SpacingTokenControl } from '../../../utils/storybook/themeStorybookControls'; import { Text } from '../../Text/Text'; -import { Flex } from '../Flex/Flex'; +import { Stack } from '../Stack/Stack'; import { Box, BackgroundColor, BorderColor, BorderStyle, BorderRadius, BoxShadow } from './Box'; import mdx from './Box.mdx'; @@ -48,11 +48,11 @@ const Item = ({ background }: { background?: string }) => { export const Basic: StoryFn = (args) => { return ( - + Box - + ); }; @@ -88,64 +88,64 @@ Basic.args = { export const Background: StoryFn = () => { return ( - + {backgroundOptions.map((background) => ( - + {background} - + ))} - + ); }; export const Border: StoryFn = () => { return ( - +
Border Color - + {borderColorOptions.map((border) => ( - + {border} - + ))} - +
Border Style - + {borderStyleOptions.map((border) => ( - + {border} - + ))} - +
-
+ ); }; export const Shadow: StoryFn = () => { return ( - + {boxShadowOptions.map((shadow) => ( - + {shadow} - + ))} - + ); }; diff --git a/packages/grafana-ui/src/components/Layout/Box/Box.tsx b/packages/grafana-ui/src/components/Layout/Box/Box.tsx index 4a5a7e52e2f62..d6942711747ae 100644 --- a/packages/grafana-ui/src/components/Layout/Box/Box.tsx +++ b/packages/grafana-ui/src/components/Layout/Box/Box.tsx @@ -4,7 +4,7 @@ import React, { ElementType, forwardRef, PropsWithChildren } from 'react'; import { GrafanaTheme2, ThemeSpacingTokens, ThemeShape, ThemeShadows } from '@grafana/data'; import { useStyles2 } from '../../../themes'; -import { AlignItems, JustifyContent } from '../Flex/Flex'; +import { AlignItems, JustifyContent } from '../Stack/Stack'; import { ResponsiveProp, getResponsiveStyle } from '../utils/responsiveness'; type Display = 'flex' | 'block' | 'inline' | 'none'; diff --git a/packages/grafana-ui/src/components/Layout/Flex/Flex.internal.story.tsx b/packages/grafana-ui/src/components/Layout/Flex/Flex.internal.story.tsx deleted file mode 100644 index 4bada87b32b47..0000000000000 --- a/packages/grafana-ui/src/components/Layout/Flex/Flex.internal.story.tsx +++ /dev/null @@ -1,202 +0,0 @@ -import { Meta, StoryFn } from '@storybook/react'; -import React from 'react'; - -import { ThemeSpacingTokens } from '@grafana/data'; - -import { useTheme2 } from '../../../themes'; -import { SpacingTokenControl } from '../../../utils/storybook/themeStorybookControls'; - -import { Flex, JustifyContent, Wrap, Direction } from './Flex'; -import mdx from './Flex.mdx'; - -const Item = ({ color, text, height }: { color: string; text?: string | number; height?: string }) => { - return ( -
- {text &&

{text}

} -
- ); -}; - -const meta: Meta = { - title: 'General/Layout/Flex', - component: Flex, - parameters: { - docs: { - page: mdx, - }, - }, -}; - -export const Basic: StoryFn = ({ direction, wrap, alignItems, justifyContent, gap }) => { - const theme = useTheme2(); - return ( -
- - {Array.from({ length: 5 }).map((_, i) => ( - - ))} - -
- ); -}; - -Basic.argTypes = { - gap: SpacingTokenControl, - direction: { control: 'select', options: ['row', 'row-reverse', 'column', 'column-reverse'] }, - wrap: { control: 'select', options: ['nowrap', 'wrap', 'wrap-reverse'] }, - alignItems: { - control: 'select', - options: ['stretch', 'flex-start', 'flex-end', 'center', 'baseline', 'start', 'end', 'self-start', 'self-end'], - }, - justifyContent: { - control: 'select', - options: [ - 'flex-start', - 'flex-end', - 'center', - 'space-between', - 'space-around', - 'space-evenly', - 'start', - 'end', - 'left', - 'right', - ], - }, -}; - -export const AlignItemsExamples: StoryFn = () => { - const theme = useTheme2(); - - return ( -
-

Align items flex-start

- - {Array.from({ length: 5 }).map((_, i) => ( - - ))} - -

Align items flex-end

- - {Array.from({ length: 5 }).map((_, i) => ( - - ))} - -

Align items baseline

- - {Array.from({ length: 5 }).map((_, i) => ( - - ))} - -

Align items center

- - {Array.from({ length: 5 }).map((_, i) => ( - - ))} - -

Align items stretch

- - - - - - - -
- ); -}; - -export const JustifyContentExamples: StoryFn = () => { - const theme = useTheme2(); - const justifyContentOptions: JustifyContent[] = [ - 'space-between', - 'space-around', - 'space-evenly', - 'flex-start', - 'flex-end', - 'center', - ]; - - return ( -
- {justifyContentOptions.map((justifyContent) => ( - <> -

Justify Content {justifyContent}

- - {Array.from({ length: 5 }).map((_, i) => ( - - ))} - - - ))} -
- ); -}; - -export const GapExamples: StoryFn = () => { - const theme = useTheme2(); - const gapOptions: ThemeSpacingTokens[] = [2, 8, 10]; - return ( -
- {gapOptions.map((gap) => ( - <> -

Gap with spacingToken set to {gap}

- - {Array.from({ length: 5 }).map((_, i) => ( - - ))} - - - ))} -
- ); -}; - -export const WrapExamples: StoryFn = () => { - const theme = useTheme2(); - const wrapOptions: Wrap[] = ['nowrap', 'wrap', 'wrap-reverse']; - return ( -
- {wrapOptions.map((wrap) => ( - <> -

Wrap examples with {wrap} and gap set to spacingToken 2 (16px)

- - {Array.from({ length: 10 }).map((_, i) => ( - - ))} - - - ))} -
- ); -}; - -export const DirectionExamples: StoryFn = () => { - const theme = useTheme2(); - const directionOptions: Direction[] = ['row', 'row-reverse', 'column', 'column-reverse']; - return ( -
- {directionOptions.map((direction) => ( - <> -

Direction {direction}

- - {Array.from({ length: 5 }).map((_, i) => ( - - ))} - - - ))} -
- ); -}; - -export default meta; diff --git a/packages/grafana-ui/src/components/Layout/Flex/Flex.mdx b/packages/grafana-ui/src/components/Layout/Flex/Flex.mdx deleted file mode 100644 index a8dd4a58777b9..0000000000000 --- a/packages/grafana-ui/src/components/Layout/Flex/Flex.mdx +++ /dev/null @@ -1,138 +0,0 @@ -import { Meta, ArgTypes } from '@storybook/blocks'; -import { Flex } from './Flex'; - - - -# Flex - -The Flex Component aims at providing a more efficient way to lay out, align and distribute space among items in a container and -the decision to create it is to ensure consistency in design across Grafana. - -### Usage - -#### When to use - -Use when in need to align components and small parts of the application. Use as parent container to wrap elements that you wish to align in a certain way. - -Also: - - * when working with one dimension layout - * to display the direction of the elements - * to set the elements to wrap - * to align items (vertically or horizontally) - -#### When not to use - -When you need to lay out bigger parts of the application or when you want to create page lay out. - -Also: - - * for complex grid layouts with various rows and columns - * bidirectional layouts - * complex nesting - * equal height columns - -### Variants - -Flex component has few variants that can be used based on the desired alignment you need for your case. - -Some examples of how to use the Flex component can be seen below: - -- AlignItems stretch - -```ts -import { Flex } from '@grafana/ui' -import { useTheme2 } from '../../themes'; - -const theme = useTheme2(); - -
-

Using Flex component to align-items stretch and justify-content to be center

-
- - - - - - -``` - -- Wrap items wrap-reverse - -```ts -import { Flex } from '@grafana/ui' -import { useTheme2 } from '../../themes'; - -const theme = useTheme2(); - -
-

Using Flex component to align-items with wrap-reverse property

-
- - - - - - -``` - -- JustifyContent flex-start - -```ts -import { Flex } from '@grafana/ui' -import { useTheme2 } from '../../themes'; - -const theme = useTheme2(); - -
-

Using Flex component to align-items with justify-content property

-
- - - - - - -``` - -- Gap of 16px using the ThemeSpacingTokens - -```ts -import { Flex } from '@grafana/ui' -import { useTheme2 } from '../../themes'; - -const theme = useTheme2(); - -
-

Using Flex component to align-items with gap of 16px

-
- - - - - - -``` - -- Direction column - -```ts -import { Flex } from '@grafana/ui' -import { useTheme2 } from '../../themes'; - -const theme = useTheme2(); - -
-

Using Flex component to align-items with direction column

-
- - - - - - -``` - -### Props - - diff --git a/packages/grafana-ui/src/components/Layout/Flex/Flex.tsx b/packages/grafana-ui/src/components/Layout/Flex/Flex.tsx deleted file mode 100644 index b54bce575b48a..0000000000000 --- a/packages/grafana-ui/src/components/Layout/Flex/Flex.tsx +++ /dev/null @@ -1,89 +0,0 @@ -import { css } from '@emotion/css'; -import React from 'react'; - -import { GrafanaTheme2, ThemeSpacingTokens } from '@grafana/data'; - -import { useStyles2 } from '../../../themes'; -import { ResponsiveProp, getResponsiveStyle } from '../utils/responsiveness'; - -export type AlignItems = - | 'stretch' - | 'flex-start' - | 'flex-end' - | 'center' - | 'baseline' - | 'start' - | 'end' - | 'self-start' - | 'self-end'; - -export type JustifyContent = - | 'flex-start' - | 'flex-end' - | 'center' - | 'space-between' - | 'space-around' - | 'space-evenly' - | 'start' - | 'end' - | 'left' - | 'right'; - -export type Direction = 'row' | 'row-reverse' | 'column' | 'column-reverse'; - -export type Wrap = 'nowrap' | 'wrap' | 'wrap-reverse'; - -interface FlexProps extends Omit, 'className' | 'style'> { - gap?: ResponsiveProp; - alignItems?: ResponsiveProp; - justifyContent?: ResponsiveProp; - direction?: ResponsiveProp; - wrap?: ResponsiveProp; - children?: React.ReactNode; -} - -export const Flex = React.forwardRef( - ({ gap = 1, alignItems, justifyContent, direction, wrap, children, ...rest }, ref) => { - const styles = useStyles2(getStyles, gap, alignItems, justifyContent, direction, wrap); - - return ( -
- {children} -
- ); - } -); - -Flex.displayName = 'Flex'; - -const getStyles = ( - theme: GrafanaTheme2, - gap: FlexProps['gap'], - alignItems: FlexProps['alignItems'], - justifyContent: FlexProps['justifyContent'], - direction: FlexProps['direction'], - wrap: FlexProps['wrap'] -) => { - return { - flex: css([ - { - display: 'flex', - }, - getResponsiveStyle(theme, direction, (val) => ({ - flexDirection: val, - })), - getResponsiveStyle(theme, wrap, (val) => ({ - flexWrap: val, - })), - getResponsiveStyle(theme, alignItems, (val) => ({ - alignItems: val, - })), - getResponsiveStyle(theme, justifyContent, (val) => ({ - justifyContent: val, - })), - getResponsiveStyle(theme, gap, (val) => ({ - gap: theme.spacing(val), - })), - ]), - }; -}; diff --git a/packages/grafana-ui/src/components/Layout/Grid/Grid.mdx b/packages/grafana-ui/src/components/Layout/Grid/Grid.mdx index 34acd4b15f1b0..7dd7f95f6302b 100644 --- a/packages/grafana-ui/src/components/Layout/Grid/Grid.mdx +++ b/packages/grafana-ui/src/components/Layout/Grid/Grid.mdx @@ -17,11 +17,6 @@ Use the Grid component when you want to create structured and organized layouts Use the `Stack` component instead for these use cases: -- **Simple layouts:** When you need to arrange elements in a linear format, either vertically or horizontally. -- **Regular flow:** When you want a "regular" site flow but with standardized spacing between the elements. - -Use the `Flex` component instead for these use cases: - - **Alignment:** More options for item alignment. - **Flex items:** Custom flex basis or configure how items stretch and wrap. diff --git a/packages/grafana-ui/src/components/Layout/Stack/HorizontalStack.tsx b/packages/grafana-ui/src/components/Layout/Stack/HorizontalStack.tsx deleted file mode 100644 index 57944ad2946c0..0000000000000 --- a/packages/grafana-ui/src/components/Layout/Stack/HorizontalStack.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import React from 'react'; - -import { ThemeSpacingTokens } from '@grafana/data'; - -import { ResponsiveProp } from '../utils/responsiveness'; - -import { Stack } from './Stack'; - -interface HorizontalStackProps extends Omit, 'className' | 'style'> { - gap?: ResponsiveProp; -} - -export const HorizontalStack = React.forwardRef>( - ({ children, gap = 1, ...rest }, ref) => ( - - {children} - - ) -); -HorizontalStack.displayName = 'HorizontalStack'; diff --git a/packages/grafana-ui/src/components/Layout/Stack/Stack.internal.story.tsx b/packages/grafana-ui/src/components/Layout/Stack/Stack.internal.story.tsx index 53ab64778fdcc..eff686a484724 100644 --- a/packages/grafana-ui/src/components/Layout/Stack/Stack.internal.story.tsx +++ b/packages/grafana-ui/src/components/Layout/Stack/Stack.internal.story.tsx @@ -1,16 +1,31 @@ import { Meta, StoryFn } from '@storybook/react'; -import React, { ReactNode } from 'react'; +import React from 'react'; +import { ThemeSpacingTokens } from '@grafana/data'; + +import { useTheme2 } from '../../../themes'; import { SpacingTokenControl } from '../../../utils/storybook/themeStorybookControls'; -import { Alert } from '../../Alert/Alert'; -import { Button } from '../../Button'; -import { Card } from '../../Card/Card'; -import { Text } from '../../Text/Text'; -import { HorizontalStack } from './HorizontalStack'; -import { Stack } from './Stack'; +import { Stack, JustifyContent, Wrap, Direction } from './Stack'; import mdx from './Stack.mdx'; +const Item = ({ color, text, height }: { color: string; text?: string | number; height?: string }) => { + return ( +
+ {text &&

{text}

} +
+ ); +}; + const meta: Meta = { title: 'General/Layout/Stack', component: Stack, @@ -19,204 +34,169 @@ const meta: Meta = { page: mdx, }, }, - argTypes: { - gap: SpacingTokenControl, - direction: { control: 'select', options: ['row', 'column'] }, - }, }; -const Item = ({ children }: { children: ReactNode }) => ( -
{children}
-); - -export const Basic: StoryFn = ({ direction = 'column', gap = 2 }) => { +export const Basic: StoryFn = ({ direction, wrap, alignItems, justifyContent, gap }) => { + const theme = useTheme2(); return ( - - Item 1 - Item 2 - Item 3 - +
+ + {Array.from({ length: 5 }).map((_, i) => ( + + ))} + +
); }; -export const TestCases: StoryFn = () => { - return ( -
- -

Comparisons Stack vs No stack

- - - - - - - - - - - - - - - - - - - - - - - - - I am a card heading - - - - I am a card heading - Ohhhhh - and now a description and some actions - - - - - - - - I am a card heading - Ohhhhh - and now a description! - - - - - - - I am a card heading - - - - I am a card heading - Ohhhhh - and now a description and some actions - - - - - - - - I am a card heading - Ohhhhh - and now a description! - - - - - - -
- - - - - - - - - - - - - - - - - -
- -

Child alignment

- -
- - - -
- - -
- - - -
- - - - - I am a card heading - - - - I am a card heading - Ohhhhh - and now a description and some actions - - - - - - - - I am a card heading - Ohhhhh - and now a description! - - - +Basic.argTypes = { + gap: SpacingTokenControl, + direction: { control: 'select', options: ['row', 'row-reverse', 'column', 'column-reverse'] }, + wrap: { control: 'select', options: ['nowrap', 'wrap', 'wrap-reverse'] }, + alignItems: { + control: 'select', + options: ['stretch', 'flex-start', 'flex-end', 'center', 'baseline', 'start', 'end', 'self-start', 'self-end'], + }, + justifyContent: { + control: 'select', + options: [ + 'flex-start', + 'flex-end', + 'center', + 'space-between', + 'space-around', + 'space-evenly', + 'start', + 'end', + 'left', + 'right', + ], + }, +}; - - - - I am a card heading - +export const AlignItemsExamples: StoryFn = () => { + const theme = useTheme2(); - - I am a card heading - Ohhhhh - and now a description! - + return ( +
+

Align items flex-start

+ + {Array.from({ length: 5 }).map((_, i) => ( + + ))} + +

Align items flex-end

+ + {Array.from({ length: 5 }).map((_, i) => ( + + ))} + +

Align items baseline

+ + {Array.from({ length: 5 }).map((_, i) => ( + + ))} + +

Align items center

+ + {Array.from({ length: 5 }).map((_, i) => ( + + ))} + +

Align items stretch

+ + + + + + + +
+ ); +}; - -
-
+export const JustifyContentExamples: StoryFn = () => { + const theme = useTheme2(); + const justifyContentOptions: JustifyContent[] = [ + 'space-between', + 'space-around', + 'space-evenly', + 'flex-start', + 'flex-end', + 'center', + ]; - - - - - - - - + return ( +
+ {justifyContentOptions.map((justifyContent) => ( + <> +

Justify Content {justifyContent}

+ + {Array.from({ length: 5 }).map((_, i) => ( + + ))} + + + ))} +
+ ); +}; - - - - - - Surprise - a description! What will happen to the height of all the other alerts? - - - - - +export const GapExamples: StoryFn = () => { + const theme = useTheme2(); + const gapOptions: ThemeSpacingTokens[] = [2, 8, 10]; + return ( +
+ {gapOptions.map((gap) => ( + <> +

Gap with spacingToken set to {gap}

+ + {Array.from({ length: 5 }).map((_, i) => ( + + ))} + + + ))}
); }; -function Example({ title, children }: { title: string; children: React.ReactNode }) { +export const WrapExamples: StoryFn = () => { + const theme = useTheme2(); + const wrapOptions: Wrap[] = ['nowrap', 'wrap', 'wrap-reverse']; return ( -
- {title} -
{children}
+
+ {wrapOptions.map((wrap) => ( + <> +

Wrap examples with {wrap} and gap set to spacingToken 2 (16px)

+ + {Array.from({ length: 10 }).map((_, i) => ( + + ))} + + + ))}
); -} +}; -function MyComponent({ children }: { children: React.ReactNode }) { - return
{children}
; -} +export const DirectionExamples: StoryFn = () => { + const theme = useTheme2(); + const directionOptions: Direction[] = ['row', 'row-reverse', 'column', 'column-reverse']; + return ( +
+ {directionOptions.map((direction) => ( + <> +

Direction {direction}

+ + {Array.from({ length: 5 }).map((_, i) => ( + + ))} + + + ))} +
+ ); +}; export default meta; diff --git a/packages/grafana-ui/src/components/Layout/Stack/Stack.mdx b/packages/grafana-ui/src/components/Layout/Stack/Stack.mdx index b21e7652f8ff3..046f414b697ca 100644 --- a/packages/grafana-ui/src/components/Layout/Stack/Stack.mdx +++ b/packages/grafana-ui/src/components/Layout/Stack/Stack.mdx @@ -1,44 +1,27 @@ -import { Meta, ArgTypes, Canvas } from '@storybook/blocks'; -import { Stack, HorizontalStack } from './index'; -import * as Stories from './Stack.internal.story'; +import { Meta, ArgTypes } from '@storybook/blocks'; +import { Stack } from './Stack'; # Stack -The `Stack` component is designed to assist with layout and positioning of elements within a container, offering a simple and flexible way to stack elements vertically or horizontally. This documentation outlines the proper usage of the Stack component and provides guidance on when to use it over the Grid or Flex components. - -There is also a `HorizontalStack` component, which is a thin wrapper around Stack, equivalent to ``. +The Stack component is a simple wrapper around the flexbox layout model that allows to easily create responsive and flexible layouts. It provides a simple and intuitive way to align and distribute items within a container either horizontally or vertically. ### Usage #### When to use -Use the Stack component when you need to arrange elements in a linear format, either vertically or horizontally. It's particularly useful when you want to maintain consistent spacing between items. - -Use the Stack component when: - -- You need a simple way to stack elements with predictable spacing. -- You want to ensure consistent alignment. +- For creating responsive and flexible layouts that can adapt to different screen sizes and orientations. +- When needing a simple and intuitive way to align and distribute items within a container either horizontally or vertically. +- To easily reorder and rearrange elements without changing the HTML structure. +- When aiming to create equal height columns. +- To create a grid-like structure with automatic wrapping and sizing of items based on the available space. #### When not to use -Use the `Grid` component instead for these use cases: - -- **Intricate Layouts:** Grids are ideal for complex dashboard and magazine-style designs with rows and columns. -- **Structured Grid:** Use Grids when items need to span multiple rows/columns, creating structured layouts. - -Use the `Flex` component instead for these use cases: +- For complex multi-dimensional layouts with intricate requirements that are better suited for CSS frameworks or grid systems. +- When precise control over spacing and positioning of elements is necessary. -- **Alignment:** More options for item alignment. -- **Flex items:** Custom flex basis or configure how items stretch and wrap. - -## Props - -### Stack +### Props - -### HorizontalStack - - diff --git a/packages/grafana-ui/src/components/Layout/Stack/Stack.tsx b/packages/grafana-ui/src/components/Layout/Stack/Stack.tsx index 2e24f2332f9d3..27baa49dfc5b7 100644 --- a/packages/grafana-ui/src/components/Layout/Stack/Stack.tsx +++ b/packages/grafana-ui/src/components/Layout/Stack/Stack.tsx @@ -1,26 +1,89 @@ +import { css } from '@emotion/css'; import React from 'react'; -import { ThemeSpacingTokens } from '@grafana/data'; +import { GrafanaTheme2, ThemeSpacingTokens } from '@grafana/data'; + +import { useStyles2 } from '../../../themes'; +import { ResponsiveProp, getResponsiveStyle } from '../utils/responsiveness'; + +export type AlignItems = + | 'stretch' + | 'flex-start' + | 'flex-end' + | 'center' + | 'baseline' + | 'start' + | 'end' + | 'self-start' + | 'self-end'; + +export type JustifyContent = + | 'flex-start' + | 'flex-end' + | 'center' + | 'space-between' + | 'space-around' + | 'space-evenly' + | 'start' + | 'end' + | 'left' + | 'right'; + +export type Direction = 'row' | 'row-reverse' | 'column' | 'column-reverse'; + +export type Wrap = 'nowrap' | 'wrap' | 'wrap-reverse'; -import { Flex } from '../Flex/Flex'; -import { ResponsiveProp } from '../utils/responsiveness'; interface StackProps extends Omit, 'className' | 'style'> { - direction?: ResponsiveProp<'column' | 'row'>; gap?: ResponsiveProp; + alignItems?: ResponsiveProp; + justifyContent?: ResponsiveProp; + direction?: ResponsiveProp; + wrap?: ResponsiveProp; + children?: React.ReactNode; } -export const Stack = React.forwardRef>( - ({ gap = 1, direction = 'column', children, ...rest }, ref) => { +export const Stack = React.forwardRef( + ({ gap = 1, alignItems, justifyContent, direction, wrap, children, ...rest }, ref) => { + const styles = useStyles2(getStyles, gap, alignItems, justifyContent, direction, wrap); + return ( - - {React.Children.toArray(children) - .filter(Boolean) - .map((child, index) => ( -
{child}
- ))} -
+
+ {children} +
); } ); Stack.displayName = 'Stack'; + +const getStyles = ( + theme: GrafanaTheme2, + gap: StackProps['gap'], + alignItems: StackProps['alignItems'], + justifyContent: StackProps['justifyContent'], + direction: StackProps['direction'], + wrap: StackProps['wrap'] +) => { + return { + flex: css([ + { + display: 'flex', + }, + getResponsiveStyle(theme, direction, (val) => ({ + flexDirection: val, + })), + getResponsiveStyle(theme, wrap, (val) => ({ + flexWrap: val, + })), + getResponsiveStyle(theme, alignItems, (val) => ({ + alignItems: val, + })), + getResponsiveStyle(theme, justifyContent, (val) => ({ + justifyContent: val, + })), + getResponsiveStyle(theme, gap, (val) => ({ + gap: theme.spacing(val), + })), + ]), + }; +}; diff --git a/packages/grafana-ui/src/components/Layout/Stack/index.ts b/packages/grafana-ui/src/components/Layout/Stack/index.ts deleted file mode 100644 index 89768e71dc004..0000000000000 --- a/packages/grafana-ui/src/components/Layout/Stack/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { Stack } from './Stack'; -export { HorizontalStack } from './HorizontalStack'; diff --git a/packages/grafana-ui/src/unstable.ts b/packages/grafana-ui/src/unstable.ts index dedc5841a5b60..f90909a1517b4 100644 --- a/packages/grafana-ui/src/unstable.ts +++ b/packages/grafana-ui/src/unstable.ts @@ -9,7 +9,5 @@ * be subject to the standard policies */ -export * from './components/Layout/Flex/Flex'; - export { Grid } from './components/Layout/Grid/Grid'; -export { Stack, HorizontalStack } from './components/Layout/Stack'; +export { Stack } from './components/Layout/Stack/Stack'; diff --git a/public/app/core/components/AppChrome/DockedMegaMenu/MegaMenu.tsx b/public/app/core/components/AppChrome/DockedMegaMenu/MegaMenu.tsx index 3f615d45a8bfd..dff065c4242f3 100644 --- a/public/app/core/components/AppChrome/DockedMegaMenu/MegaMenu.tsx +++ b/public/app/core/components/AppChrome/DockedMegaMenu/MegaMenu.tsx @@ -6,7 +6,7 @@ import { useLocation } from 'react-router-dom'; import { GrafanaTheme2 } from '@grafana/data'; import { selectors } from '@grafana/e2e-selectors'; import { CustomScrollbar, Icon, IconButton, useStyles2 } from '@grafana/ui'; -import { Flex } from '@grafana/ui/src/unstable'; +import { Stack } from '@grafana/ui/src/unstable'; import { useGrafana } from 'app/core/context/GrafanaContext'; import { t } from 'app/core/internationalization'; import { useSelector } from 'app/types'; @@ -55,7 +55,7 @@ export const MegaMenu = React.memo(
    {navItems.map((link, index) => ( - + )} - + ))}
diff --git a/public/app/features/admin/Users/OrgUnits.tsx b/public/app/features/admin/Users/OrgUnits.tsx index 846abb95f704a..08a58be885454 100644 --- a/public/app/features/admin/Users/OrgUnits.tsx +++ b/public/app/features/admin/Users/OrgUnits.tsx @@ -2,7 +2,7 @@ import React, { forwardRef, PropsWithChildren } from 'react'; import { IconName } from '@grafana/data'; import { Icon, Tooltip, Box } from '@grafana/ui'; -import { Flex } from '@grafana/ui/src/unstable'; +import { Stack } from '@grafana/ui/src/unstable'; import { Unit } from 'app/types'; type OrgUnitProps = { units?: Unit[]; icon: IconName }; @@ -15,7 +15,7 @@ export const OrgUnits = ({ units, icon }: OrgUnitProps) => { return units.length > 1 ? ( {units?.map((unit) => {unit.name})}} + content={{units?.map((unit) => {unit.name})}} > {units.length} diff --git a/public/app/features/admin/Users/OrgUsersTable.tsx b/public/app/features/admin/Users/OrgUsersTable.tsx index be169eab5f2dc..7fec328d12716 100644 --- a/public/app/features/admin/Users/OrgUsersTable.tsx +++ b/public/app/features/admin/Users/OrgUsersTable.tsx @@ -16,7 +16,7 @@ import { Avatar, Box, } from '@grafana/ui'; -import { Flex, Stack } from '@grafana/ui/src/unstable'; +import { Stack } from '@grafana/ui/src/unstable'; import { UserRolePicker } from 'app/core/components/RolePicker/UserRolePicker'; import { fetchRoleOptions } from 'app/core/components/RolePicker/api'; import { TagBadge } from 'app/core/components/TagFilter/TagBadge'; @@ -223,9 +223,9 @@ export const OrgUsersTable = ({ getRowId={(user) => String(user.userId)} fetchData={fetchData} /> - + - +
{Boolean(userToRemove) && ( ) => { return ( - + {row.original.isAdmin && ( )} - + ); }, sortType: (a, b) => (a.original.orgs?.length || 0) - (b.original.orgs?.length || 0), @@ -146,9 +146,9 @@ export const UsersTable = ({ String(user.id)} fetchData={fetchData} /> {showPaging && ( - + - + )} ); diff --git a/public/app/features/dashboard/dashgrid/DashboardEmpty.tsx b/public/app/features/dashboard/dashgrid/DashboardEmpty.tsx index 2e9e7c0eef6b3..0a521767cc667 100644 --- a/public/app/features/dashboard/dashgrid/DashboardEmpty.tsx +++ b/public/app/features/dashboard/dashgrid/DashboardEmpty.tsx @@ -5,7 +5,7 @@ import { GrafanaTheme2 } from '@grafana/data'; import { selectors } from '@grafana/e2e-selectors'; import { config, locationService, reportInteraction } from '@grafana/runtime'; import { Button, useStyles2, Text, Box } from '@grafana/ui'; -import { Flex } from '@grafana/ui/src/unstable'; +import { Stack } from '@grafana/ui/src/unstable'; import { Trans } from 'app/core/internationalization'; import { DashboardModel } from 'app/features/dashboard/state'; import { onAddLibraryPanel, onCreateNewPanel, onImportDashboard } from 'app/features/dashboard/utils/dashboard'; @@ -24,11 +24,11 @@ const DashboardEmpty = ({ dashboard, canCreate }: Props) => { const initialDatasource = useSelector((state) => state.dashboard.initialDatasource); return ( - +
- + - + Start your new dashboard by adding a visualization @@ -56,12 +56,12 @@ const DashboardEmpty = ({ dashboard, canCreate }: Props) => { > Add visualization - + - + {config.featureToggles.vizAndWidgetSplit && ( - + Add a widget @@ -82,11 +82,11 @@ const DashboardEmpty = ({ dashboard, canCreate }: Props) => { > Add widget - + )} - + Import panel @@ -109,10 +109,10 @@ const DashboardEmpty = ({ dashboard, canCreate }: Props) => { > Add library panel - + - + Import a dashboard @@ -136,12 +136,12 @@ const DashboardEmpty = ({ dashboard, canCreate }: Props) => { > Import dashboard - + - - + +
-
+ ); }; diff --git a/public/app/features/serviceaccounts/ServiceAccountsListPage.tsx b/public/app/features/serviceaccounts/ServiceAccountsListPage.tsx index 39241d9fb3091..b943e841cb1b5 100644 --- a/public/app/features/serviceaccounts/ServiceAccountsListPage.tsx +++ b/public/app/features/serviceaccounts/ServiceAccountsListPage.tsx @@ -13,7 +13,7 @@ import { InlineField, Pagination, } from '@grafana/ui'; -import { Flex } from '@grafana/ui/src/unstable'; +import { Stack } from '@grafana/ui/src/unstable'; import EmptyListCTA from 'app/core/components/EmptyListCTA/EmptyListCTA'; import { Page } from 'app/core/components/Page/Page'; import PageLoader from 'app/core/components/PageLoader/PageLoader'; @@ -253,9 +253,9 @@ export const ServiceAccountsListPageUnconnected = ({ - + - +
)} diff --git a/public/app/features/teams/TeamList.tsx b/public/app/features/teams/TeamList.tsx index fca388998f8fa..10be5c446374e 100644 --- a/public/app/features/teams/TeamList.tsx +++ b/public/app/features/teams/TeamList.tsx @@ -14,7 +14,7 @@ import { Pagination, Avatar, } from '@grafana/ui'; -import { Stack, Flex } from '@grafana/ui/src/unstable'; +import { Stack } from '@grafana/ui/src/unstable'; import EmptyListCTA from 'app/core/components/EmptyListCTA/EmptyListCTA'; import { Page } from 'app/core/components/Page/Page'; import { fetchRoleOptions } from 'app/core/components/RolePicker/api'; @@ -169,14 +169,14 @@ export const TeamList = ({ getRowId={(team) => String(team.id)} fetchData={changeSort} /> - + - + From 266b3fd41bd5c359aea1bdce6c449c4794b5874a Mon Sep 17 00:00:00 2001 From: Joey <90795735+joey-grafana@users.noreply.github.com> Date: Wed, 1 Nov 2023 08:47:01 +0000 Subject: [PATCH 011/869] Pyroscope: Simplify and update query options to include max nodes in summary (#76942) Simplify and update query options to include max nodes --- .../QueryEditor/QueryOptions.tsx | 62 ++++--------------- 1 file changed, 13 insertions(+), 49 deletions(-) diff --git a/public/app/plugins/datasource/grafana-pyroscope-datasource/QueryEditor/QueryOptions.tsx b/public/app/plugins/datasource/grafana-pyroscope-datasource/QueryEditor/QueryOptions.tsx index 02bd73c38cfb6..50c432d23a345 100644 --- a/public/app/plugins/datasource/grafana-pyroscope-datasource/QueryEditor/QueryOptions.tsx +++ b/public/app/plugins/datasource/grafana-pyroscope-datasource/QueryEditor/QueryOptions.tsx @@ -1,10 +1,10 @@ -import { css, cx } from '@emotion/css'; +import { css } from '@emotion/css'; import React from 'react'; -import { useToggle } from 'react-use'; import { CoreApp, GrafanaTheme2, SelectableValue } from '@grafana/data'; -import { Icon, useStyles2, RadioButtonGroup, MultiSelect, Input, clearButtonStyles, Button } from '@grafana/ui'; +import { useStyles2, RadioButtonGroup, MultiSelect, Input } from '@grafana/ui'; +import { QueryOptionGroup } from '../../prometheus/querybuilder/shared/QueryOptionGroup'; import { Query } from '../types'; import { EditorField } from './EditorField'; @@ -34,7 +34,6 @@ function getTypeOptions(app?: CoreApp) { * Base on QueryOptionGroup component from grafana/ui but that is not available yet. */ export function QueryOptions({ query, onQueryChange, app, labels }: Props) { - const [isOpen, toggleOpen] = useToggle(false); const styles = useStyles2(getStyles); const typeOptions = getTypeOptions(app); const groupByOptions = labels @@ -43,26 +42,18 @@ export function QueryOptions({ query, onQueryChange, app, labels }: Props) { value: l, })) : []; - const buttonStyles = useStyles2(clearButtonStyles); + + let collapsedInfo = [`Type: ${query.queryType}`]; + if (query.groupBy?.length) { + collapsedInfo.push(`Group by: ${query.groupBy.join(', ')}`); + } + if (query.maxNodes) { + collapsedInfo.push(`Max nodes: ${query.maxNodes}`); + } return ( - - {isOpen && ( +
- )} +
); } @@ -120,38 +111,11 @@ const getStyles = (theme: GrafanaTheme2) => { color: theme.colors.text.primary, }, }), - header: css({ - display: 'flex', - cursor: 'pointer', - alignItems: 'baseline', - color: theme.colors.text.primary, - '&:hover': { - background: theme.colors.emphasize(theme.colors.background.primary, 0.03), - }, - }), - title: css({ - flexGrow: 1, - overflow: 'hidden', - fontSize: theme.typography.bodySmall.fontSize, - fontWeight: theme.typography.fontWeightMedium, - margin: 0, - }), - description: css({ - color: theme.colors.text.secondary, - fontSize: theme.typography.bodySmall.fontSize, - paddingLeft: theme.spacing(2), - gap: theme.spacing(2), - display: 'flex', - }), body: css({ display: 'flex', paddingTop: theme.spacing(2), gap: theme.spacing(2), flexWrap: 'wrap', }), - toggle: css({ - color: theme.colors.text.secondary, - marginRight: `${theme.spacing(1)}`, - }), }; }; From 9dcfc51b4fca11d3ec23308c4b68f55cba6a1265 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 1 Nov 2023 09:42:26 +0000 Subject: [PATCH 012/869] Update dependency @types/node to v20 (#77446) 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-ui/package.json | 2 +- .../grafana-testdata-datasource/package.json | 2 +- yarn.lock | 27 ++++++++++++------- 6 files changed, 23 insertions(+), 14 deletions(-) diff --git a/package.json b/package.json index 14ed107f5e2f9..0251a9f97a6f5 100644 --- a/package.json +++ b/package.json @@ -124,7 +124,7 @@ "@types/lucene": "^2", "@types/marked": "5.0.1", "@types/mousetrap": "1.6.11", - "@types/node": "18.18.4", + "@types/node": "20.8.10", "@types/node-forge": "^1", "@types/ol-ext": "npm:@siedlerchr/types-ol-ext@3.2.0", "@types/papaparse": "5.3.7", diff --git a/packages/grafana-data/package.json b/packages/grafana-data/package.json index baaa84f7208b5..7190429628dcc 100644 --- a/packages/grafana-data/package.json +++ b/packages/grafana-data/package.json @@ -76,7 +76,7 @@ "@types/jquery": "3.5.16", "@types/lodash": "4.14.195", "@types/marked": "5.0.1", - "@types/node": "18.18.4", + "@types/node": "20.8.10", "@types/papaparse": "5.3.7", "@types/react": "18.2.15", "@types/react-dom": "18.2.7", diff --git a/packages/grafana-e2e-selectors/package.json b/packages/grafana-e2e-selectors/package.json index a14c3e70282ed..b7d8ed93e7ff1 100644 --- a/packages/grafana-e2e-selectors/package.json +++ b/packages/grafana-e2e-selectors/package.json @@ -41,7 +41,7 @@ "devDependencies": { "@rollup/plugin-commonjs": "25.0.2", "@rollup/plugin-node-resolve": "15.2.3", - "@types/node": "18.18.4", + "@types/node": "20.8.10", "esbuild": "0.18.12", "rimraf": "5.0.1", "rollup": "2.79.1", diff --git a/packages/grafana-ui/package.json b/packages/grafana-ui/package.json index e98c75f3984eb..c3a64c568ba23 100644 --- a/packages/grafana-ui/package.json +++ b/packages/grafana-ui/package.json @@ -145,7 +145,7 @@ "@types/jquery": "3.5.16", "@types/lodash": "4.14.195", "@types/mock-raf": "1.0.3", - "@types/node": "18.18.4", + "@types/node": "20.8.10", "@types/prismjs": "1.26.0", "@types/react": "18.2.15", "@types/react-beautiful-dnd": "13.1.4", diff --git a/public/app/plugins/datasource/grafana-testdata-datasource/package.json b/public/app/plugins/datasource/grafana-testdata-datasource/package.json index 3f3e7d214588b..4b4fa493d5714 100644 --- a/public/app/plugins/datasource/grafana-testdata-datasource/package.json +++ b/public/app/plugins/datasource/grafana-testdata-datasource/package.json @@ -23,7 +23,7 @@ "@testing-library/user-event": "14.5.1", "@types/jest": "29.5.4", "@types/lodash": "4.14.195", - "@types/node": "18.18.5", + "@types/node": "20.8.10", "@types/react": "18.2.15", "@types/testing-library__jest-dom": "5.14.8", "ts-node": "10.9.1", diff --git a/yarn.lock b/yarn.lock index 7551e3fc6546e..b18511fbfce88 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2866,7 +2866,7 @@ __metadata: "@testing-library/user-event": "npm:14.5.1" "@types/jest": "npm:29.5.4" "@types/lodash": "npm:4.14.195" - "@types/node": "npm:18.18.5" + "@types/node": "npm:20.8.10" "@types/react": "npm:18.2.15" "@types/testing-library__jest-dom": "npm:5.14.8" lodash: "npm:4.17.21" @@ -2944,7 +2944,7 @@ __metadata: "@types/jquery": "npm:3.5.16" "@types/lodash": "npm:4.14.195" "@types/marked": "npm:5.0.1" - "@types/node": "npm:18.18.4" + "@types/node": "npm:20.8.10" "@types/papaparse": "npm:5.3.7" "@types/react": "npm:18.2.15" "@types/react-dom": "npm:18.2.7" @@ -3006,7 +3006,7 @@ __metadata: "@grafana/tsconfig": "npm:^1.2.0-rc1" "@rollup/plugin-commonjs": "npm:25.0.2" "@rollup/plugin-node-resolve": "npm:15.2.3" - "@types/node": "npm:18.18.4" + "@types/node": "npm:20.8.10" esbuild: "npm:0.18.12" rimraf: "npm:5.0.1" rollup: "npm:2.79.1" @@ -3422,7 +3422,7 @@ __metadata: "@types/jquery": "npm:3.5.16" "@types/lodash": "npm:4.14.195" "@types/mock-raf": "npm:1.0.3" - "@types/node": "npm:18.18.4" + "@types/node": "npm:20.8.10" "@types/prismjs": "npm:1.26.0" "@types/react": "npm:18.2.15" "@types/react-beautiful-dnd": "npm:13.1.4" @@ -8762,10 +8762,12 @@ __metadata: languageName: node linkType: hard -"@types/node@npm:*, @types/node@npm:18.18.5, @types/node@npm:>=13.7.0": - version: 18.18.5 - resolution: "@types/node@npm:18.18.5" - checksum: a7363aab9f402290799d3e2696fbc70c76a8a65e2354f72b8f399c38edc346f600066f8ac59dde985cfc64160cfeb63ed7fc917aecdfe7ec469345d3ce029bda +"@types/node@npm:*, @types/node@npm:20.8.10, @types/node@npm:>=13.7.0": + version: 20.8.10 + resolution: "@types/node@npm:20.8.10" + dependencies: + undici-types: "npm:~5.26.4" + checksum: 8930039077c8ad74de74c724909412bea8110c3f8892bcef8dda3e9629073bed65632ee755f94b252bcdae8ca71cf83e89a4a440a105e2b1b7c9797b43483049 languageName: node linkType: hard @@ -17195,7 +17197,7 @@ __metadata: "@types/lucene": "npm:^2" "@types/marked": "npm:5.0.1" "@types/mousetrap": "npm:1.6.11" - "@types/node": "npm:18.18.4" + "@types/node": "npm:20.8.10" "@types/node-forge": "npm:^1" "@types/ol-ext": "npm:@siedlerchr/types-ol-ext@3.2.0" "@types/papaparse": "npm:5.3.7" @@ -28979,6 +28981,13 @@ __metadata: languageName: node linkType: hard +"undici-types@npm:~5.26.4": + version: 5.26.5 + resolution: "undici-types@npm:5.26.5" + checksum: 0097779d94bc0fd26f0418b3a05472410408877279141ded2bd449167be1aed7ea5b76f756562cb3586a07f251b90799bab22d9019ceba49c037c76445f7cddd + languageName: node + linkType: hard + "unicode-canonical-property-names-ecmascript@npm:^2.0.0": version: 2.0.0 resolution: "unicode-canonical-property-names-ecmascript@npm:2.0.0" From 375f0c98b2f2574fd316fc5c33650aed9a0beff4 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 1 Nov 2023 09:43:32 +0000 Subject: [PATCH 013/869] Update dependency @testing-library/jest-dom to v6 (#77445) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package.json | 2 +- packages/grafana-data/package.json | 2 +- packages/grafana-ui/package.json | 2 +- yarn.lock | 44 ++++++------------------------ 4 files changed, 12 insertions(+), 38 deletions(-) diff --git a/package.json b/package.json index 0251a9f97a6f5..1745d51f78c41 100644 --- a/package.json +++ b/package.json @@ -96,7 +96,7 @@ "@swc/core": "1.3.38", "@swc/helpers": "0.4.14", "@testing-library/dom": "9.3.3", - "@testing-library/jest-dom": "5.16.5", + "@testing-library/jest-dom": "6.1.4", "@testing-library/react": "14.0.0", "@testing-library/user-event": "14.5.1", "@types/angular": "1.8.5", diff --git a/packages/grafana-data/package.json b/packages/grafana-data/package.json index 7190429628dcc..10820169eb1c7 100644 --- a/packages/grafana-data/package.json +++ b/packages/grafana-data/package.json @@ -67,7 +67,7 @@ "@rollup/plugin-json": "6.0.0", "@rollup/plugin-node-resolve": "15.2.3", "@testing-library/dom": "9.3.3", - "@testing-library/jest-dom": "5.16.5", + "@testing-library/jest-dom": "6.1.4", "@testing-library/react": "14.0.0", "@testing-library/user-event": "14.5.1", "@types/dompurify": "^2", diff --git a/packages/grafana-ui/package.json b/packages/grafana-ui/package.json index c3a64c568ba23..3803d3c633028 100644 --- a/packages/grafana-ui/package.json +++ b/packages/grafana-ui/package.json @@ -134,7 +134,7 @@ "@storybook/react-webpack5": "7.4.5", "@storybook/theming": "7.4.5", "@testing-library/dom": "9.3.3", - "@testing-library/jest-dom": "5.16.5", + "@testing-library/jest-dom": "6.1.4", "@testing-library/react": "14.0.0", "@testing-library/user-event": "14.5.1", "@types/common-tags": "^1.8.0", diff --git a/yarn.lock b/yarn.lock index b18511fbfce88..3adbcf5516636 100644 --- a/yarn.lock +++ b/yarn.lock @@ -12,7 +12,7 @@ __metadata: languageName: node linkType: hard -"@adobe/css-tools@npm:^4.0.1, @adobe/css-tools@npm:^4.3.0": +"@adobe/css-tools@npm:^4.3.1": version: 4.3.1 resolution: "@adobe/css-tools@npm:4.3.1" checksum: 039a42ffdd41ecf3abcaf09c9fef0ffd634ccbe81c04002fc989e74564eba99bb19169a8f48dadf6442aa2c5c9f0925a7b27ec5c36a1ed1a3515fe77d6930996 @@ -2934,7 +2934,7 @@ __metadata: "@rollup/plugin-json": "npm:6.0.0" "@rollup/plugin-node-resolve": "npm:15.2.3" "@testing-library/dom": "npm:9.3.3" - "@testing-library/jest-dom": "npm:5.16.5" + "@testing-library/jest-dom": "npm:6.1.4" "@testing-library/react": "npm:14.0.0" "@testing-library/user-event": "npm:14.5.1" "@types/d3-interpolate": "npm:^3.0.0" @@ -3411,7 +3411,7 @@ __metadata: "@storybook/react-webpack5": "npm:7.4.5" "@storybook/theming": "npm:7.4.5" "@testing-library/dom": "npm:9.3.3" - "@testing-library/jest-dom": "npm:5.16.5" + "@testing-library/jest-dom": "npm:6.1.4" "@testing-library/react": "npm:14.0.0" "@testing-library/user-event": "npm:14.5.1" "@types/common-tags": "npm:^1.8.0" @@ -7677,28 +7677,11 @@ __metadata: languageName: node linkType: hard -"@testing-library/jest-dom@npm:5.16.5": - version: 5.16.5 - resolution: "@testing-library/jest-dom@npm:5.16.5" +"@testing-library/jest-dom@npm:6.1.4, @testing-library/jest-dom@npm:^6.1.2": + version: 6.1.4 + resolution: "@testing-library/jest-dom@npm:6.1.4" dependencies: - "@adobe/css-tools": "npm:^4.0.1" - "@babel/runtime": "npm:^7.9.2" - "@types/testing-library__jest-dom": "npm:^5.9.1" - aria-query: "npm:^5.0.0" - chalk: "npm:^3.0.0" - css.escape: "npm:^1.5.1" - dom-accessibility-api: "npm:^0.5.6" - lodash: "npm:^4.17.15" - redent: "npm:^3.0.0" - checksum: 472a14b6295a18af28b5133ecaf4d22a7bb9c50bf05e1f04a076b2e2d7c596e76cdd56a95387ad6d2a4dda0c46bc93d95cbca5b314fabe0fd13362f29118749e - languageName: node - linkType: hard - -"@testing-library/jest-dom@npm:^6.1.2": - version: 6.1.2 - resolution: "@testing-library/jest-dom@npm:6.1.2" - dependencies: - "@adobe/css-tools": "npm:^4.3.0" + "@adobe/css-tools": "npm:^4.3.1" "@babel/runtime": "npm:^7.9.2" aria-query: "npm:^5.0.0" chalk: "npm:^3.0.0" @@ -7720,7 +7703,7 @@ __metadata: optional: true vitest: optional: true - checksum: 36e27f9011dd60de8936d3edb2a81753ef7a370480019e571c6e85b935b5fa2aba47244104badf6d6b636fd170fdee5c0fdd92fb3630957d4a6f11d302b57398 + checksum: e5a0cdb96eec509c0c85f2b7a0d08fc1c9f6c10aa49bba0d738bf4bb114c3472b92ace5067aedfaaf848ae13b38ba9296047c219aa24b66c87aa16de33341fdb languageName: node linkType: hard @@ -9211,15 +9194,6 @@ __metadata: languageName: node linkType: hard -"@types/testing-library__jest-dom@npm:^5.9.1": - version: 5.14.9 - resolution: "@types/testing-library__jest-dom@npm:5.14.9" - dependencies: - "@types/jest": "npm:*" - checksum: e257de95a4a9385cc09ae4ca3396d23ad4b5cfb8e021a1ca3454c424c34636075f6fe151b2f881f79bf9d497aa04fbfae62449b135f293e8d2d614fa899898a8 - languageName: node - linkType: hard - "@types/tinycolor2@npm:1.4.3": version: 1.4.3 resolution: "@types/tinycolor2@npm:1.4.3" @@ -17168,7 +17142,7 @@ __metadata: "@swc/core": "npm:1.3.38" "@swc/helpers": "npm:0.4.14" "@testing-library/dom": "npm:9.3.3" - "@testing-library/jest-dom": "npm:5.16.5" + "@testing-library/jest-dom": "npm:6.1.4" "@testing-library/react": "npm:14.0.0" "@testing-library/react-hooks": "npm:^8.0.1" "@testing-library/user-event": "npm:14.5.1" From 65398dabf4ccdef5eba0a409d7f5105a4a30aa55 Mon Sep 17 00:00:00 2001 From: George Robinson Date: Wed, 1 Nov 2023 09:59:15 +0000 Subject: [PATCH 014/869] Alerting: Update Alertmanager to latest main (70c52bf) (#77485) --- go.mod | 16 ++++++++-------- go.sum | 38 +++++++++++++++++++------------------- 2 files changed, 27 insertions(+), 27 deletions(-) diff --git a/go.mod b/go.mod index c2fb95f1d942f..0131234142867 100644 --- a/go.mod +++ b/go.mod @@ -61,7 +61,7 @@ require ( github.com/google/uuid v1.3.1 // @grafana/backend-platform github.com/google/wire v0.5.0 // @grafana/backend-platform github.com/gorilla/websocket v1.5.0 // @grafana/grafana-app-platform-squad - github.com/grafana/alerting v0.0.0-20231026192550-079966731bbe // @grafana/alerting-squad-backend + github.com/grafana/alerting v0.0.0-20231101090315-bf12694896a8 // @grafana/alerting-squad-backend github.com/grafana/cuetsy v0.1.10 // @grafana/grafana-as-code github.com/grafana/grafana-aws-sdk v0.19.1 // @grafana/aws-datasources github.com/grafana/grafana-azure-sdk-go v1.9.0 // @grafana/backend-platform @@ -88,8 +88,8 @@ require ( github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/prometheus/alertmanager v0.25.0 // @grafana/alerting-squad-backend - github.com/prometheus/client_golang v1.16.0 // @grafana/alerting-squad-backend - github.com/prometheus/client_model v0.4.0 // @grafana/backend-platform + github.com/prometheus/client_golang v1.17.0 // @grafana/alerting-squad-backend + github.com/prometheus/client_model v0.4.1-0.20230718164431-9a2bf3000d16 // @grafana/backend-platform github.com/prometheus/common v0.44.0 // @grafana/alerting-squad-backend github.com/prometheus/prometheus v1.8.2-0.20221021121301-51a44e6657c3 // @grafana/alerting-squad-backend github.com/robfig/cron/v3 v3.0.1 // @grafana/backend-platform @@ -113,7 +113,7 @@ require ( golang.org/x/oauth2 v0.13.0 // @grafana/grafana-authnz-team golang.org/x/sync v0.4.0 // @grafana/alerting-squad-backend golang.org/x/time v0.3.0 // @grafana/backend-platform - golang.org/x/tools v0.12.0 // @grafana/grafana-as-code + golang.org/x/tools v0.13.0 // @grafana/grafana-as-code gonum.org/v1/gonum v0.12.0 // @grafana/observability-metrics google.golang.org/api v0.148.0 // @grafana/backend-platform google.golang.org/grpc v1.58.3 // @grafana/plugins-platform-backend @@ -199,9 +199,9 @@ require ( github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/prometheus/common/sigv4 v0.1.0 // indirect github.com/prometheus/exporter-toolkit v0.10.0 // indirect - github.com/prometheus/procfs v0.11.0 // indirect + github.com/prometheus/procfs v0.11.1 // indirect github.com/protocolbuffers/txtpbfmt v0.0.0-20220428173112-74888fd59c2b // indirect - github.com/rs/cors v1.9.0 // indirect + github.com/rs/cors v1.10.1 // indirect github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 // indirect github.com/segmentio/encoding v0.3.6 // indirect github.com/sergi/go-diff v1.3.1 // indirect @@ -425,7 +425,7 @@ require ( sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.1.2 // indirect sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect - sigs.k8s.io/yaml v1.3.0 // @grafana-app-platform-squad + sigs.k8s.io/yaml v1.3.0 // indirect; @grafana-app-platform-squad ) require ( @@ -500,6 +500,6 @@ replace xorm.io/xorm => github.com/grafana/xorm v0.8.3-0.20230627081928-d04aa38a // Use our fork of the upstream alertmanagers. // This is required in order to get notification delivery errors from the receivers API. -replace github.com/prometheus/alertmanager => github.com/grafana/prometheus-alertmanager v0.25.1-0.20230918083811-3513be6760b7 +replace github.com/prometheus/alertmanager => github.com/grafana/prometheus-alertmanager v0.25.1-0.20231027171310-70c52bf65758 exclude github.com/mattn/go-sqlite3 v2.0.3+incompatible diff --git a/go.sum b/go.sum index 37493c11f44a9..66cb96043d6d5 100644 --- a/go.sum +++ b/go.sum @@ -1799,10 +1799,10 @@ github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWm github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gotestyourself/gotestyourself v1.3.0/go.mod h1:zZKM6oeNM8k+FRljX1mnzVYeS8wiGgQyvST1/GafPbY= github.com/gotestyourself/gotestyourself v2.2.0+incompatible/go.mod h1:zZKM6oeNM8k+FRljX1mnzVYeS8wiGgQyvST1/GafPbY= -github.com/grafana/alerting v0.0.0-20231017091417-a53b5db2235d h1:fxHDUyKFc1mfyJAtW+Qxi66dMahYu+o5/lH+f8SoMHg= -github.com/grafana/alerting v0.0.0-20231017091417-a53b5db2235d/go.mod h1:6BES5CyEqz7fDAG3MYvJLe0hqGwvIoGDN8A1aNrLGus= github.com/grafana/alerting v0.0.0-20231026192550-079966731bbe h1:6jY5mWR//GbYOjvqpnoncgkdxbeYImkTsy9rPvVFOlk= github.com/grafana/alerting v0.0.0-20231026192550-079966731bbe/go.mod h1:6BES5CyEqz7fDAG3MYvJLe0hqGwvIoGDN8A1aNrLGus= +github.com/grafana/alerting v0.0.0-20231101090315-bf12694896a8 h1:uoaAgS743libLwhuQUQEDO2YpFaQI5viyY4NrcyibUg= +github.com/grafana/alerting v0.0.0-20231101090315-bf12694896a8/go.mod h1:lR9bhQrESIeOqKtC4Y+fK4mqtLmJFDffFt9q4cWRa8k= github.com/grafana/codejen v0.0.3 h1:tAWxoTUuhgmEqxJPOLtJoxlPBbMULFwKFOcRsPRPXDw= github.com/grafana/codejen v0.0.3/go.mod h1:zmwwM/DRyQB7pfuBjTWII3CWtxcXh8LTwAYGfDfpR6s= github.com/grafana/cue v0.0.0-20230926092038-971951014e3f h1:TmYAMnqg3d5KYEAaT6PtTguL2GjLfvr6wnAX8Azw6tQ= @@ -1829,8 +1829,8 @@ github.com/grafana/grafana-plugin-sdk-go v0.187.0 h1:lOwoFbbTs27KqR3F32GvOX9Et3E github.com/grafana/grafana-plugin-sdk-go v0.187.0/go.mod h1:PHK8eQOz3ES28RmImdTHNOTxBZaH6mb/ytJGxk7VVJc= github.com/grafana/kindsys v0.0.0-20230508162304-452481b63482 h1:1YNoeIhii4UIIQpCPU+EXidnqf449d0C3ZntAEt4KSo= github.com/grafana/kindsys v0.0.0-20230508162304-452481b63482/go.mod h1:GNcfpy5+SY6RVbNGQW264gC0r336Dm+0zgQ5vt6+M8Y= -github.com/grafana/prometheus-alertmanager v0.25.1-0.20230918083811-3513be6760b7 h1:7gsywzIb39SYZEp9qOnNaxD4d9OOkAfJGvnRUQUtlTM= -github.com/grafana/prometheus-alertmanager v0.25.1-0.20230918083811-3513be6760b7/go.mod h1:0SOsbwZb277OdNdUR+376IxcAYV3Pp+igiAOJdr/98M= +github.com/grafana/prometheus-alertmanager v0.25.1-0.20231027171310-70c52bf65758 h1:ATUhvJSJwzdzhnmzUI92fxVFqyqmcnzJ47wtHTK3LW4= +github.com/grafana/prometheus-alertmanager v0.25.1-0.20231027171310-70c52bf65758/go.mod h1:MmLemcsGjpbOwEeT3k7K+gnvIImXgkatCfVX6sOtx80= github.com/grafana/pyroscope/api v0.2.0 h1:TzOxL0s6SiaLEy944ZAKgHcx/JDRJXu4O8ObwkqR6p4= github.com/grafana/pyroscope/api v0.2.0/go.mod h1:nhH+xai9cYFgs6lMy/+L0pKj0d5yCMwji/QAiQFCP+U= github.com/grafana/regexp v0.0.0-20221122212121-6b5c0a4cb7fd/go.mod h1:M5qHK+eWfAv8VR/265dIuEpL3fNfeC21tXXp9itM24A= @@ -2558,8 +2558,8 @@ github.com/prometheus/client_golang v1.12.1/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrb github.com/prometheus/client_golang v1.13.0/go.mod h1:vTeo+zgvILHsnnj/39Ou/1fPN5nJFOEMgftOUOmlvYQ= github.com/prometheus/client_golang v1.14.0/go.mod h1:8vpkKitgIVNcqrRBWh1C4TIUQgYNtG/XQE4E/Zae36Y= github.com/prometheus/client_golang v1.15.1/go.mod h1:e9yaBhRPU2pPNsZwE+JdQl0KEt1N9XgF6zxWmaC0xOk= -github.com/prometheus/client_golang v1.16.0 h1:yk/hx9hDbrGHovbci4BY+pRMfSuuat626eFsHb7tmT8= -github.com/prometheus/client_golang v1.16.0/go.mod h1:Zsulrv/L9oM40tJ7T815tM89lFEugiJ9HzIqaAx4LKc= +github.com/prometheus/client_golang v1.17.0 h1:rl2sfwZMtSthVU752MqfjQozy7blglC+1SOtjMAMh+Q= +github.com/prometheus/client_golang v1.17.0/go.mod h1:VeL+gMmOAxkS2IqfCq0ZmHSL+LjWfWDUmp1mBz9JgUY= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= @@ -2567,8 +2567,9 @@ github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1: github.com/prometheus/client_model v0.1.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w= -github.com/prometheus/client_model v0.4.0 h1:5lQXD3cAg1OXBf4Wq03gTrXHeaV0TQvGfUooCfx1yqY= github.com/prometheus/client_model v0.4.0/go.mod h1:oMQmHW1/JoDwqLtg57MGgP/Fb1CJEYF2imWWhWtMkYU= +github.com/prometheus/client_model v0.4.1-0.20230718164431-9a2bf3000d16 h1:v7DLqVdK4VrYkVD5diGdl4sxJurKJEMnODWRJlxV9oM= +github.com/prometheus/client_model v0.4.1-0.20230718164431-9a2bf3000d16/go.mod h1:oMQmHW1/JoDwqLtg57MGgP/Fb1CJEYF2imWWhWtMkYU= github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= github.com/prometheus/common v0.0.0-20181126121408-4724e9255275/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= github.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= @@ -2608,9 +2609,8 @@ github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1 github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= github.com/prometheus/procfs v0.8.0/go.mod h1:z7EfXMXOkbkqb9IINtpCn86r/to3BnA0uaxHdg830/4= github.com/prometheus/procfs v0.9.0/go.mod h1:+pB4zwohETzFnmlpe6yd2lSc+0/46IYZRB/chUwxUZY= -github.com/prometheus/procfs v0.10.1/go.mod h1:nwNm2aOCAYw8uTR/9bWRREkZFxAUcWzPHWJq+XBB/FM= -github.com/prometheus/procfs v0.11.0 h1:5EAgkfkMl659uZPbe9AS2N68a7Cc1TJbPEuGzFuRbyk= -github.com/prometheus/procfs v0.11.0/go.mod h1:nwNm2aOCAYw8uTR/9bWRREkZFxAUcWzPHWJq+XBB/FM= +github.com/prometheus/procfs v0.11.1 h1:xRC8Iq1yyca5ypa9n1EZnWZkt7dwcoRPQwX/5gwaUuI= +github.com/prometheus/procfs v0.11.1/go.mod h1:eesXgaPo1q7lBpVMoMy0ZOFTth9hBn4W/y0/p/ScXhY= github.com/prometheus/prometheus v0.43.0 h1:18iCSfrbAHbXvYFvR38U1Pt4uZmU9SmDcCpCrBKUiGg= github.com/prometheus/prometheus v0.43.0/go.mod h1:2BA14LgBeqlPuzObSEbh+Y+JwLH2GcqDlJKbF2sA6FM= github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= @@ -2649,8 +2649,8 @@ github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncj github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= github.com/rs/cors v1.6.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU= -github.com/rs/cors v1.9.0 h1:l9HGsTsHJcvW14Nk7J9KFz8bzeAWXn3CG6bgt7LsrAE= -github.com/rs/cors v1.9.0/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= +github.com/rs/cors v1.10.1 h1:L0uuZVXIKlI1SShY2nhFfo44TYvDPQ1w4oFkUJNfhyo= +github.com/rs/cors v1.10.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU= github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc= @@ -3134,7 +3134,7 @@ golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58 golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE= golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0= -golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw= +golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc= golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -3313,7 +3313,7 @@ golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= -golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI= +golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= @@ -3370,7 +3370,6 @@ golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20220819030929-7fc1605a5dde/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220929204114-8fcdb60fdcc0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.2.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sync v0.4.0 h1:zxkM55ReGkDlKSM+Fu41A+zmbZuaPVbGMzvvdUPznYQ= golang.org/x/sync v0.4.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= @@ -3540,7 +3539,9 @@ golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= @@ -3555,7 +3556,7 @@ golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= -golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU= +golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= golang.org/x/term v0.13.0 h1:bb+I9cTfFazGW51MZqBVmZy7+JEJMouUHTUSKVQLBek= golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -3574,7 +3575,6 @@ golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -3714,8 +3714,8 @@ golang.org/x/tools v0.3.0/go.mod h1:/rWhSS2+zyEVwoJf8YAX6L2f0ntZ7Kn/mGgAWcipA5k= golang.org/x/tools v0.4.0/go.mod h1:UE5sM2OK9E/d67R0ANs2xJizIymRP5gJU295PvKXxjQ= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s= -golang.org/x/tools v0.12.0 h1:YW6HUoUmYBpwSgyaGaZq1fHjrBjX1rlpZ54T6mu2kss= -golang.org/x/tools v0.12.0/go.mod h1:Sc0INKfu04TlqNoRA1hgpFZbhYXHPr4V5DzpSBTPqQM= +golang.org/x/tools v0.13.0 h1:Iey4qkscZuv0VvIt8E0neZjtPVQFSc870HQ448QgEmQ= +golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= From 6c6c54637f00f2c5c642bc14d28050b0de593bf9 Mon Sep 17 00:00:00 2001 From: Ivana Huckova <30407135+ivanahuckova@users.noreply.github.com> Date: Wed, 1 Nov 2023 11:05:28 +0100 Subject: [PATCH 015/869] Loki: Update developers docs with QueryEditor component (#77463) --- .../loki/docs/app_plugin_developer_documentation.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/public/app/plugins/datasource/loki/docs/app_plugin_developer_documentation.md b/public/app/plugins/datasource/loki/docs/app_plugin_developer_documentation.md index b21e6e792bba1..04e8f9962921d 100644 --- a/public/app/plugins/datasource/loki/docs/app_plugin_developer_documentation.md +++ b/public/app/plugins/datasource/loki/docs/app_plugin_developer_documentation.md @@ -4,7 +4,7 @@ Welcome to the developer documentation for the Loki data source! The purpose of ## Introduction -The Loki data source provides a variety of methods, but not all of them are suitable for external use. In this documentation, we will focus on the key methods that are highly recommended for app plugin development. +The Loki data source provides a variety of methods and components, but not all of them are suitable for external use. In this documentation, we will focus on the key methods that are highly recommended for app plugin development. It's important to note some methods and APIs were deliberately omitted, as those may undergo changes or are not suitable for external integration. Therefore, we do not recommend relying on them for your development needs. @@ -162,3 +162,9 @@ try { ``` If you find that there are methods missing or have ideas for new features, please don't hesitate to inform us. You can submit your suggestions and feature requests through the [Grafana repository](https://github.com/grafana/grafana/issues/new?assignees=&labels=type%2Ffeature-request&projects=&template=1-feature_requests.md). Your feedback is essential to help us improve and enhance the Loki data source and Grafana as a whole. We appreciate your contributions and look forward to hearing your ideas! + +## Recommended components + +### QueryEditor + +The Loki data source provides an export of the `QueryEditor` component, which can be accessed through `components?.QueryEditor`. This component is designed to enable users to create and customize Loki queries to suit their specific requirements. From f42bb8666765fb00ffda4cde348abfa9e8541646 Mon Sep 17 00:00:00 2001 From: Joao Silva <100691367+JoaoSilvaGrafana@users.noreply.github.com> Date: Wed, 1 Nov 2023 10:07:08 +0000 Subject: [PATCH 016/869] GrafanaUI: Make sure ContextMenu does not get cut off at the top (#77435) --- packages/grafana-ui/src/components/ContextMenu/ContextMenu.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/grafana-ui/src/components/ContextMenu/ContextMenu.tsx b/packages/grafana-ui/src/components/ContextMenu/ContextMenu.tsx index 9a9fe8ff3aba3..a6a90a6ed35ac 100644 --- a/packages/grafana-ui/src/components/ContextMenu/ContextMenu.tsx +++ b/packages/grafana-ui/src/components/ContextMenu/ContextMenu.tsx @@ -39,7 +39,7 @@ export const ContextMenu = React.memo( setPositionStyles({ position: 'fixed', left: collisions.right ? x - rect.width - OFFSET : x - OFFSET, - top: collisions.bottom ? y - rect.height - OFFSET : y + OFFSET, + top: Math.max(0, collisions.bottom ? y - rect.height - OFFSET : y + OFFSET), }); } }, [x, y]); From c39e9a8f527b79881b95f64fd1c413b4bff42983 Mon Sep 17 00:00:00 2001 From: Joey <90795735+joey-grafana@users.noreply.github.com> Date: Wed, 1 Nov 2023 10:14:24 +0000 Subject: [PATCH 017/869] Tracing: Trace to profiles (#76670) * Update Tempo devenv to include profiles * Update devenv to scrape profiles from local services * Cleanup devenv * Fix issue with flame graph * Add width prop to ProfileTypeCascader * Add trace to profiles settings * Add new spanSelector API * Add spanSelector to query editor * Update span link query * Conditionally show span link * Combine profile and spanProfile query types and run specific query type in backend based on spanSelector presence * Update placeholder * Create feature toggle * Remove spanProfile query type * Cleanup * Use feeature toggle * Update feature toggle * Update devenv * Update devenv * Tests * Tests * Profiles for this span * Styling * Types * Update type check * Tidier funcs * Add config links from dataframe * Remove time shift * Update tests * Update range in test * Simplify span link logic * Update default keys * Update pyro link * Use const --- .../dataquery/schema-reference.md | 1 + .../feature-toggles/index.md | 1 + go.mod | 6 +- go.sum | 7 + .../src/types/featureToggles.gen.ts | 1 + .../x/GrafanaPyroscopeDataQuery_types.gen.ts | 5 + pkg/services/featuremgmt/registry.go | 7 + pkg/services/featuremgmt/toggles_gen.csv | 1 + pkg/services/featuremgmt/toggles_gen.go | 4 + .../grafana-pyroscope-datasource/instance.go | 1 + .../kinds/dataquery/types_dataquery_gen.go | 3 + .../pyroscopeClient.go | 43 ++- .../pyroscopeClient_test.go | 4 + .../grafana-pyroscope-datasource/query.go | 32 ++- .../query_test.go | 16 ++ .../TraceToProfilesSettings.test.tsx | 56 ++++ .../TraceToProfilesSettings.tsx | 187 +++++++++++++ .../features/explore/TraceView/TraceView.tsx | 5 + .../TraceTimelineViewer/SpanDetail/index.tsx | 81 +++--- .../TraceView/components/types/links.ts | 1 + .../explore/TraceView/createSpanLink.test.ts | 258 +++++++++++++++++- .../explore/TraceView/createSpanLink.tsx | 62 ++++- .../QueryEditor/ProfileTypesCascader.tsx | 2 + .../QueryEditor/QueryOptions.tsx | 16 ++ .../dataquery.cue | 2 + .../dataquery.gen.ts | 5 + .../tempo/configuration/ConfigEditor.tsx | 8 + .../plugins/datasource/tempo/datasource.ts | 2 +- .../datasource/tempo/resultTransformer.ts | 54 +++- 29 files changed, 804 insertions(+), 67 deletions(-) create mode 100644 public/app/core/components/TraceToProfiles/TraceToProfilesSettings.test.tsx create mode 100644 public/app/core/components/TraceToProfiles/TraceToProfilesSettings.tsx diff --git a/docs/sources/developers/kinds/composable/grafanapyroscope/dataquery/schema-reference.md b/docs/sources/developers/kinds/composable/grafanapyroscope/dataquery/schema-reference.md index f65e65989ffcd..3b7a95d56c6c4 100644 --- a/docs/sources/developers/kinds/composable/grafanapyroscope/dataquery/schema-reference.md +++ b/docs/sources/developers/kinds/composable/grafanapyroscope/dataquery/schema-reference.md @@ -28,5 +28,6 @@ title: GrafanaPyroscopeDataQuery kind | `hide` | boolean | No | | true if query is disabled (ie should not be returned to the dashboard)
Note this does not always imply that the query should not be executed since
the results from a hidden query may be used as the input to other queries (SSE etc) | | `maxNodes` | integer | No | | Sets the maximum number of nodes in the flamegraph. | | `queryType` | string | No | | Specify the query flavor
TODO make this required and give it a default | +| `spanSelector` | string[] | No | | Specifies the query span selectors. | 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 36cec2c1fe74e..705b76079a00c 100644 --- a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md +++ b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md @@ -132,6 +132,7 @@ Experimental features might be changed or removed without prior notice. | `grafanaAPIServer` | Enable Kubernetes API Server for Grafana resources | | `grafanaAPIServerWithExperimentalAPIs` | Register experimental APIs with the k8s API server | | `featureToggleAdminPage` | Enable admin page for managing feature toggles from the Grafana front-end | +| `traceToProfiles` | Enables linking between traces and profiles | | `permissionsFilterRemoveSubquery` | Alternative permission filter implementation that does not use subqueries for fetching the dashboard folder | | `influxdbSqlSupport` | Enable InfluxDB SQL query language support with new querying UI | | `angularDeprecationUI` | Display new Angular deprecation-related UI features | diff --git a/go.mod b/go.mod index 0131234142867..752461e37df41 100644 --- a/go.mod +++ b/go.mod @@ -164,7 +164,7 @@ require ( github.com/go-openapi/validate v0.22.1 // indirect github.com/golang-jwt/jwt/v4 v4.5.0 // @grafana/backend-platform github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect - github.com/golang/glog v1.1.0 // indirect + github.com/golang/glog v1.1.1 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/protobuf v1.5.3 // @grafana/backend-platform github.com/google/btree v1.1.2 // indirect @@ -286,7 +286,7 @@ require ( require github.com/grafana/gofpdf v0.0.0-20231002120153-857cc45be447 // @grafana/sharing-squad -require github.com/grafana/pyroscope/api v0.2.0 // @grafana/observability-traces-and-profiling +require github.com/grafana/pyroscope/api v0.2.1 // @grafana/observability-traces-and-profiling require github.com/apache/arrow/go/v13 v13.0.0 // @grafana/observability-metrics @@ -316,7 +316,7 @@ require ( github.com/cristalhq/jwt/v4 v4.0.2 // indirect github.com/dave/jennifer v1.5.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect - github.com/dgraph-io/ristretto v0.1.0 // indirect + github.com/dgraph-io/ristretto v0.1.1 // indirect github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 // indirect github.com/docker/distribution v2.8.1+incompatible // indirect github.com/docker/go-connections v0.4.0 // indirect diff --git a/go.sum b/go.sum index 66cb96043d6d5..448cfe5e7aef6 100644 --- a/go.sum +++ b/go.sum @@ -992,6 +992,8 @@ github.com/dgraph-io/ristretto v0.0.1/go.mod h1:T40EBc7CJke8TkpiYfGGKAeFjSaxuFXh github.com/dgraph-io/ristretto v0.0.2/go.mod h1:KPxhHT9ZxKefz+PCeOGsrHpl1qZ7i70dGTu2u+Ahh6E= github.com/dgraph-io/ristretto v0.1.0 h1:Jv3CGQHp9OjuMBSne1485aDpUkTKEcUqF+jm/LuerPI= github.com/dgraph-io/ristretto v0.1.0/go.mod h1:fux0lOrBhrVCJd3lcTHsIJhq1T2rokOu6v9Vcb3Q9ug= +github.com/dgraph-io/ristretto v0.1.1 h1:6CWw5tJNgpegArSHpNHJKldNeq03FQCwYvfMVWajOK8= +github.com/dgraph-io/ristretto v0.1.1/go.mod h1:S1GPSBCYCIhmVNfcth17y2zZtQT6wzkzgwUve0VDWWA= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 h1:fAjc9m62+UWV/WAFKLNi6ZS0675eEUC9y3AlwSbQu1Y= @@ -1620,6 +1622,8 @@ github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfU github.com/golang/glog v1.0.0/go.mod h1:EWib/APOK0SL3dFbYqvxE3UYd8E6s1ouQ7iEp/0LWV4= github.com/golang/glog v1.1.0 h1:/d3pCKDPWNnvIWe0vVUpNP32qc8U3PDVxySP/y360qE= github.com/golang/glog v1.1.0/go.mod h1:pfYeQZ3JWZoXTV5sFc986z3HTpwQs9At6P4ImfuP3NQ= +github.com/golang/glog v1.1.1 h1:jxpi2eWoU84wbX9iIEyAeeoac3FLuifZpY9tcNUD9kw= +github.com/golang/glog v1.1.1/go.mod h1:zR+okUeTbrL6EL3xHUDxZuEtGv04p5shwip1+mL/rLQ= github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= @@ -1833,6 +1837,8 @@ github.com/grafana/prometheus-alertmanager v0.25.1-0.20231027171310-70c52bf65758 github.com/grafana/prometheus-alertmanager v0.25.1-0.20231027171310-70c52bf65758/go.mod h1:MmLemcsGjpbOwEeT3k7K+gnvIImXgkatCfVX6sOtx80= github.com/grafana/pyroscope/api v0.2.0 h1:TzOxL0s6SiaLEy944ZAKgHcx/JDRJXu4O8ObwkqR6p4= github.com/grafana/pyroscope/api v0.2.0/go.mod h1:nhH+xai9cYFgs6lMy/+L0pKj0d5yCMwji/QAiQFCP+U= +github.com/grafana/pyroscope/api v0.2.1 h1:V/GSrwSN5HgA4Ijf/2SN9Sib55E/xObswaCMkdOOsxs= +github.com/grafana/pyroscope/api v0.2.1/go.mod h1:vNO/Rym3pwNIN4y/f0ACrk5iR7DdWlsdfZGSZE+XChU= github.com/grafana/regexp v0.0.0-20221122212121-6b5c0a4cb7fd/go.mod h1:M5qHK+eWfAv8VR/265dIuEpL3fNfeC21tXXp9itM24A= github.com/grafana/regexp v0.0.0-20221123153739-15dc172cd2db h1:7aN5cccjIqCLTzedH7MZzRZt5/lsAHch6Z3L2ZGn5FA= github.com/grafana/regexp v0.0.0-20221123153739-15dc172cd2db/go.mod h1:M5qHK+eWfAv8VR/265dIuEpL3fNfeC21tXXp9itM24A= @@ -3531,6 +3537,7 @@ golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220829200755-d48e67d00261/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20221010170243-090e33056c14/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index ef42c20708622..b99a9573f1147 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -112,6 +112,7 @@ export interface FeatureToggles { awsAsyncQueryCaching?: boolean; splitScopes?: boolean; azureMonitorDataplane?: boolean; + traceToProfiles?: boolean; permissionsFilterRemoveSubquery?: boolean; prometheusConfigOverhaulAuth?: boolean; configurableSchedulerTick?: boolean; diff --git a/packages/grafana-schema/src/raw/composable/grafanapyroscope/dataquery/x/GrafanaPyroscopeDataQuery_types.gen.ts b/packages/grafana-schema/src/raw/composable/grafanapyroscope/dataquery/x/GrafanaPyroscopeDataQuery_types.gen.ts index 08d7d682b9398..30e597171618c 100644 --- a/packages/grafana-schema/src/raw/composable/grafanapyroscope/dataquery/x/GrafanaPyroscopeDataQuery_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/grafanapyroscope/dataquery/x/GrafanaPyroscopeDataQuery_types.gen.ts @@ -34,9 +34,14 @@ export interface GrafanaPyroscopeDataQuery extends common.DataQuery { * Specifies the type of profile to query. */ profileTypeId: string; + /** + * Specifies the query span selectors. + */ + spanSelector?: Array; } export const defaultGrafanaPyroscopeDataQuery: Partial = { groupBy: [], labelSelector: '{}', + spanSelector: [], }; diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index 62d3c7f948a67..3db02aff0bf6a 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -653,6 +653,13 @@ var ( Owner: grafanaPartnerPluginsSquad, Expression: "true", // on by default }, + { + Name: "traceToProfiles", + Description: "Enables linking between traces and profiles", + Stage: FeatureStageExperimental, + FrontendOnly: true, + Owner: grafanaObservabilityTracesAndProfilingSquad, + }, { Name: "permissionsFilterRemoveSubquery", Description: "Alternative permission filter implementation that does not use subqueries for fetching the dashboard folder", diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv index 9deb2bc15f986..1dfdae95a8dd2 100644 --- a/pkg/services/featuremgmt/toggles_gen.csv +++ b/pkg/services/featuremgmt/toggles_gen.csv @@ -93,6 +93,7 @@ featureToggleAdminPage,experimental,@grafana/grafana-operator-experience-squad,f awsAsyncQueryCaching,preview,@grafana/aws-datasources,false,false,false,false splitScopes,preview,@grafana/grafana-authnz-team,false,false,true,false azureMonitorDataplane,GA,@grafana/partner-datasources,false,false,false,false +traceToProfiles,experimental,@grafana/observability-traces-and-profiling,false,false,false,true permissionsFilterRemoveSubquery,experimental,@grafana/backend-platform,false,false,false,false prometheusConfigOverhaulAuth,GA,@grafana/observability-metrics,false,false,false,false configurableSchedulerTick,experimental,@grafana/alerting-squad,false,false,true,false diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go index 7ca935ff87465..24e183d6ac190 100644 --- a/pkg/services/featuremgmt/toggles_gen.go +++ b/pkg/services/featuremgmt/toggles_gen.go @@ -383,6 +383,10 @@ const ( // Adds dataplane compliant frame metadata in the Azure Monitor datasource FlagAzureMonitorDataplane = "azureMonitorDataplane" + // FlagTraceToProfiles + // Enables linking between traces and profiles + FlagTraceToProfiles = "traceToProfiles" + // FlagPermissionsFilterRemoveSubquery // Alternative permission filter implementation that does not use subqueries for fetching the dashboard folder FlagPermissionsFilterRemoveSubquery = "permissionsFilterRemoveSubquery" diff --git a/pkg/tsdb/grafana-pyroscope-datasource/instance.go b/pkg/tsdb/grafana-pyroscope-datasource/instance.go index bf1dd60558ad5..02bec86eadb23 100644 --- a/pkg/tsdb/grafana-pyroscope-datasource/instance.go +++ b/pkg/tsdb/grafana-pyroscope-datasource/instance.go @@ -31,6 +31,7 @@ type ProfilingClient interface { LabelValues(ctx context.Context, label string) ([]string, error) GetSeries(ctx context.Context, profileTypeID string, labelSelector string, start int64, end int64, groupBy []string, step float64) (*SeriesResponse, error) GetProfile(ctx context.Context, profileTypeID string, labelSelector string, start int64, end int64, maxNodes *int64) (*ProfileResponse, error) + GetSpanProfile(ctx context.Context, profileTypeID string, labelSelector string, spanSelector []string, start int64, end int64, maxNodes *int64) (*ProfileResponse, error) } // PyroscopeDatasource is a datasource for querying application performance profiles. diff --git a/pkg/tsdb/grafana-pyroscope-datasource/kinds/dataquery/types_dataquery_gen.go b/pkg/tsdb/grafana-pyroscope-datasource/kinds/dataquery/types_dataquery_gen.go index 0f2bf677b77bd..44f2fb912bd2c 100644 --- a/pkg/tsdb/grafana-pyroscope-datasource/kinds/dataquery/types_dataquery_gen.go +++ b/pkg/tsdb/grafana-pyroscope-datasource/kinds/dataquery/types_dataquery_gen.go @@ -79,6 +79,9 @@ type GrafanaPyroscopeDataQuery struct { // In server side expressions, the refId is used as a variable name to identify results. // By default, the UI will assign A->Z; however setting meaningful names may be useful. RefId string `json:"refId"` + + // Specifies the query span selectors. + SpanSelector []string `json:"spanSelector,omitempty"` } // PyroscopeQueryType defines model for PyroscopeQueryType. diff --git a/pkg/tsdb/grafana-pyroscope-datasource/pyroscopeClient.go b/pkg/tsdb/grafana-pyroscope-datasource/pyroscopeClient.go index 32e26af37a630..b7d0bc24bf255 100644 --- a/pkg/tsdb/grafana-pyroscope-datasource/pyroscopeClient.go +++ b/pkg/tsdb/grafana-pyroscope-datasource/pyroscopeClient.go @@ -175,8 +175,41 @@ func (c *PyroscopeClient) GetProfile(ctx context.Context, profileTypeID, labelSe return nil, nil } - levels := make([]*Level, len(resp.Msg.Flamegraph.Levels)) - for i, level := range resp.Msg.Flamegraph.Levels { + return profileQuery(ctx, err, span, resp.Msg.Flamegraph, profileTypeID) +} + +func (c *PyroscopeClient) GetSpanProfile(ctx context.Context, profileTypeID, labelSelector string, spanSelector []string, start, end int64, maxNodes *int64) (*ProfileResponse, error) { + ctx, span := tracing.DefaultTracer().Start(ctx, "datasource.pyroscope.GetSpanProfile", trace.WithAttributes(attribute.String("profileTypeID", profileTypeID), attribute.String("labelSelector", labelSelector), attribute.String("spanSelector", strings.Join(spanSelector, ",")))) + defer span.End() + req := &connect.Request[querierv1.SelectMergeSpanProfileRequest]{ + Msg: &querierv1.SelectMergeSpanProfileRequest{ + ProfileTypeID: profileTypeID, + LabelSelector: labelSelector, + SpanSelector: spanSelector, + Start: start, + End: end, + MaxNodes: maxNodes, + }, + } + + resp, err := c.connectClient.SelectMergeSpanProfile(ctx, req) + if err != nil { + span.RecordError(err) + span.SetStatus(codes.Error, err.Error()) + return nil, err + } + + if resp.Msg.Flamegraph == nil { + // Not an error, can happen when querying data oout of range. + return nil, nil + } + + return profileQuery(ctx, err, span, resp.Msg.Flamegraph, profileTypeID) +} + +func profileQuery(ctx context.Context, err error, span trace.Span, flamegraph *querierv1.FlameGraph, profileTypeID string) (*ProfileResponse, error) { + levels := make([]*Level, len(flamegraph.Levels)) + for i, level := range flamegraph.Levels { levels[i] = &Level{ Values: level.Values, } @@ -184,10 +217,10 @@ func (c *PyroscopeClient) GetProfile(ctx context.Context, profileTypeID, labelSe return &ProfileResponse{ Flamebearer: &Flamebearer{ - Names: resp.Msg.Flamegraph.Names, + Names: flamegraph.Names, Levels: levels, - Total: resp.Msg.Flamegraph.Total, - MaxSelf: resp.Msg.Flamegraph.MaxSelf, + Total: flamegraph.Total, + MaxSelf: flamegraph.MaxSelf, }, Units: getUnits(profileTypeID), }, nil diff --git a/pkg/tsdb/grafana-pyroscope-datasource/pyroscopeClient_test.go b/pkg/tsdb/grafana-pyroscope-datasource/pyroscopeClient_test.go index 28c39e2108c89..cc93e7b304954 100644 --- a/pkg/tsdb/grafana-pyroscope-datasource/pyroscopeClient_test.go +++ b/pkg/tsdb/grafana-pyroscope-datasource/pyroscopeClient_test.go @@ -129,3 +129,7 @@ func (f *FakePyroscopeConnectClient) SelectSeries(ctx context.Context, req *conn func (f *FakePyroscopeConnectClient) SelectMergeProfile(ctx context.Context, c *connect.Request[querierv1.SelectMergeProfileRequest]) (*connect.Response[googlev1.Profile], error) { panic("implement me") } + +func (f *FakePyroscopeConnectClient) SelectMergeSpanProfile(ctx context.Context, c *connect.Request[querierv1.SelectMergeSpanProfileRequest]) (*connect.Response[querierv1.SelectMergeSpanProfileResponse], error) { + panic("implement me") +} diff --git a/pkg/tsdb/grafana-pyroscope-datasource/query.go b/pkg/tsdb/grafana-pyroscope-datasource/query.go index 0501ef199857a..9bbb385698006 100644 --- a/pkg/tsdb/grafana-pyroscope-datasource/query.go +++ b/pkg/tsdb/grafana-pyroscope-datasource/query.go @@ -98,18 +98,32 @@ func (d *PyroscopeDatasource) query(ctx context.Context, pCtx backend.PluginCont if query.QueryType == queryTypeProfile || query.QueryType == queryTypeBoth { g.Go(func() error { - logger.Debug("Calling GetProfile", "queryModel", qm, "function", logEntrypoint()) - prof, err := d.client.GetProfile(gCtx, qm.ProfileTypeId, qm.LabelSelector, query.TimeRange.From.UnixMilli(), query.TimeRange.To.UnixMilli(), qm.MaxNodes) - if err != nil { - span.RecordError(err) - span.SetStatus(codes.Error, err.Error()) - logger.Error("Error GetProfile()", "err", err, "function", logEntrypoint()) - return err + var profileResp *ProfileResponse + if len(qm.SpanSelector) > 0 { + logger.Debug("Calling GetSpanProfile", "queryModel", qm, "function", logEntrypoint()) + prof, err := d.client.GetSpanProfile(gCtx, qm.ProfileTypeId, qm.LabelSelector, qm.SpanSelector, query.TimeRange.From.UnixMilli(), query.TimeRange.To.UnixMilli(), qm.MaxNodes) + if err != nil { + span.RecordError(err) + span.SetStatus(codes.Error, err.Error()) + logger.Error("Error GetSpanProfile()", "err", err, "function", logEntrypoint()) + return err + } + profileResp = prof + } else { + logger.Debug("Calling GetProfile", "queryModel", qm, "function", logEntrypoint()) + prof, err := d.client.GetProfile(gCtx, qm.ProfileTypeId, qm.LabelSelector, query.TimeRange.From.UnixMilli(), query.TimeRange.To.UnixMilli(), qm.MaxNodes) + if err != nil { + span.RecordError(err) + span.SetStatus(codes.Error, err.Error()) + logger.Error("Error GetProfile()", "err", err, "function", logEntrypoint()) + return err + } + profileResp = prof } var frame *data.Frame - if prof != nil { - frame = responseToDataFrames(prof) + if profileResp != nil { + frame = responseToDataFrames(profileResp) // If query called with streaming on then return a channel // to subscribe on a client-side and consume updates from a plugin. diff --git a/pkg/tsdb/grafana-pyroscope-datasource/query_test.go b/pkg/tsdb/grafana-pyroscope-datasource/query_test.go index 1ed8aa723a2c7..e66db78fc67de 100644 --- a/pkg/tsdb/grafana-pyroscope-datasource/query_test.go +++ b/pkg/tsdb/grafana-pyroscope-datasource/query_test.go @@ -312,6 +312,22 @@ func (f *FakeClient) GetProfile(ctx context.Context, profileTypeID, labelSelecto }, nil } +func (f *FakeClient) GetSpanProfile(ctx context.Context, profileTypeID, labelSelector string, spanSelector []string, start, end int64, maxNodes *int64) (*ProfileResponse, error) { + return &ProfileResponse{ + Flamebearer: &Flamebearer{ + Names: []string{"foo", "bar", "baz"}, + Levels: []*Level{ + {Values: []int64{0, 10, 0, 0}}, + {Values: []int64{0, 9, 0, 1}}, + {Values: []int64{0, 8, 8, 2}}, + }, + Total: 100, + MaxSelf: 56, + }, + Units: "count", + }, nil +} + func (f *FakeClient) GetSeries(ctx context.Context, profileTypeID, labelSelector string, start, end int64, groupBy []string, step float64) (*SeriesResponse, error) { f.Args = []any{profileTypeID, labelSelector, start, end, groupBy, step} return &SeriesResponse{ diff --git a/public/app/core/components/TraceToProfiles/TraceToProfilesSettings.test.tsx b/public/app/core/components/TraceToProfiles/TraceToProfilesSettings.test.tsx new file mode 100644 index 0000000000000..121c0c9159057 --- /dev/null +++ b/public/app/core/components/TraceToProfiles/TraceToProfilesSettings.test.tsx @@ -0,0 +1,56 @@ +import { render, screen, waitFor } from '@testing-library/react'; +import React from 'react'; + +import { DataSourceInstanceSettings, DataSourceSettings } from '@grafana/data'; +import { DataSourceSrv, setDataSourceSrv } from '@grafana/runtime'; + +import { TraceToProfilesData, TraceToProfilesSettings } from './TraceToProfilesSettings'; + +const defaultOption: DataSourceSettings = { + jsonData: { + tracesToProfilesV2: { + datasourceUid: 'profiling1_uid', + tags: [{ key: 'someTag', value: 'newName' }], + spanStartTimeShift: '1m', + spanEndTimeShift: '1m', + customQuery: true, + query: '{${__tags}}', + }, + }, +} as unknown as DataSourceSettings; + +const pyroSettings = { + uid: 'profiling1_uid', + name: 'profiling1', + type: 'grafana-pyroscope-datasource', + meta: { info: { logos: { small: '' } } }, +} as unknown as DataSourceInstanceSettings; + +describe('TraceToProfilesSettings', () => { + beforeAll(() => { + setDataSourceSrv({ + getList() { + return [pyroSettings]; + }, + getInstanceSettings() { + return pyroSettings; + }, + } as unknown as DataSourceSrv); + }); + + it('should render without error', () => { + waitFor(() => { + expect(() => + render( {}} />) + ).not.toThrow(); + }); + }); + + it('should render all options', () => { + render( {}} />); + expect(screen.getByText('Select data source')).toBeInTheDocument(); + expect(screen.getByText('Tags')).toBeInTheDocument(); + expect(screen.getByText('Profile type')).toBeInTheDocument(); + expect(screen.getByText('Use custom query')).toBeInTheDocument(); + }); +}); diff --git a/public/app/core/components/TraceToProfiles/TraceToProfilesSettings.tsx b/public/app/core/components/TraceToProfiles/TraceToProfilesSettings.tsx new file mode 100644 index 0000000000000..e64bc152fc4fd --- /dev/null +++ b/public/app/core/components/TraceToProfiles/TraceToProfilesSettings.tsx @@ -0,0 +1,187 @@ +import { css } from '@emotion/css'; +import React, { useEffect, useMemo, useState } from 'react'; +import { useAsync } from 'react-use'; + +import { + DataSourceJsonData, + DataSourceInstanceSettings, + DataSourcePluginOptionsEditorProps, + updateDatasourcePluginJsonDataOption, +} from '@grafana/data'; +import { ConfigSection } from '@grafana/experimental'; +import { getDataSourceSrv } from '@grafana/runtime'; +import { InlineField, InlineFieldRow, Input, InlineSwitch } from '@grafana/ui'; +import { ConfigDescriptionLink } from 'app/core/components/ConfigDescriptionLink'; +import { DataSourcePicker } from 'app/features/datasources/components/picker/DataSourcePicker'; +import { ProfileTypesCascader } from 'app/plugins/datasource/grafana-pyroscope-datasource/QueryEditor/ProfileTypesCascader'; +import { PyroscopeDataSource } from 'app/plugins/datasource/grafana-pyroscope-datasource/datasource'; +import { ProfileTypeMessage } from 'app/plugins/datasource/grafana-pyroscope-datasource/types'; + +import { TagMappingInput } from '../TraceToLogs/TagMappingInput'; +export interface TraceToProfilesOptions { + datasourceUid?: string; + tags?: Array<{ key: string; value?: string }>; + query?: string; + profileTypeId?: string; + customQuery: boolean; +} + +export interface TraceToProfilesData extends DataSourceJsonData { + tracesToProfiles?: TraceToProfilesOptions; +} + +interface Props extends DataSourcePluginOptionsEditorProps {} + +export function TraceToProfilesSettings({ options, onOptionsChange }: Props) { + const supportedDataSourceTypes = useMemo(() => ['grafana-pyroscope-datasource'], []); + + const [profileTypes, setProfileTypes] = useState([]); + const profileTypesPlaceholder = useMemo(() => { + let placeholder = profileTypes.length === 0 ? 'No profile types found' : 'Select profile type'; + if (!options.jsonData.tracesToProfiles?.datasourceUid) { + placeholder = 'Please select profiling data source'; + } + return placeholder; + }, [options.jsonData.tracesToProfiles?.datasourceUid, profileTypes]); + + const { value: dataSource } = useAsync(async () => { + return await getDataSourceSrv().get(options.jsonData.tracesToProfiles?.datasourceUid); + }, [options.jsonData.tracesToProfiles?.datasourceUid]); + + useEffect(() => { + if ( + dataSource && + dataSource instanceof PyroscopeDataSource && + supportedDataSourceTypes.includes(dataSource.type) && + dataSource.uid === options.jsonData.tracesToProfiles?.datasourceUid + ) { + dataSource.getProfileTypes().then((profileTypes) => { + setProfileTypes(profileTypes); + }); + } else { + setProfileTypes([]); + } + }, [dataSource, onOptionsChange, options, supportedDataSourceTypes]); + + return ( +
+ + + supportedDataSourceTypes.includes(ds.type)} + current={options.jsonData.tracesToProfiles?.datasourceUid} + noDefault={true} + width={40} + onChange={(ds: DataSourceInstanceSettings) => { + console.log(options.jsonData.tracesToProfiles, ds); + updateDatasourcePluginJsonDataOption({ onOptionsChange, options }, 'tracesToProfiles', { + ...options.jsonData.tracesToProfiles, + datasourceUid: ds.uid, + }); + }} + /> + + + + + + { + updateDatasourcePluginJsonDataOption({ onOptionsChange, options }, 'tracesToProfiles', { + ...options.jsonData.tracesToProfiles, + tags: v, + }); + }} + /> + + + + + + { + updateDatasourcePluginJsonDataOption({ onOptionsChange, options }, 'tracesToProfiles', { + ...options.jsonData.tracesToProfiles, + profileTypeId: val, + }); + }} + width={40} + /> + + + + + + ) => + updateDatasourcePluginJsonDataOption({ onOptionsChange, options }, 'tracesToProfiles', { + ...options.jsonData.tracesToProfiles, + customQuery: event.currentTarget.checked, + }) + } + /> + + + + {options.jsonData.tracesToProfiles?.customQuery && ( + + + updateDatasourcePluginJsonDataOption({ onOptionsChange, options }, 'tracesToProfiles', { + ...options.jsonData.tracesToProfiles, + query: e.currentTarget.value, + }) + } + /> + + )} +
+ ); +} + +export const TraceToProfilesSection = ({ options, onOptionsChange }: DataSourcePluginOptionsEditorProps) => { + return ( + + } + isCollapsible={true} + isInitiallyOpen={true} + > + + + ); +}; diff --git a/public/app/features/explore/TraceView/TraceView.tsx b/public/app/features/explore/TraceView/TraceView.tsx index b759fa36df1ac..0bac749dd80e4 100644 --- a/public/app/features/explore/TraceView/TraceView.tsx +++ b/public/app/features/explore/TraceView/TraceView.tsx @@ -20,6 +20,7 @@ import { DataQuery } from '@grafana/schema'; import { useStyles2 } from '@grafana/ui'; import { getTraceToLogsOptions } from 'app/core/components/TraceToLogs/TraceToLogsSettings'; import { TraceToMetricsData } from 'app/core/components/TraceToMetrics/TraceToMetricsSettings'; +import { TraceToProfilesData } from 'app/core/components/TraceToProfiles/TraceToProfilesSettings'; import { getDatasourceSrv } from 'app/features/plugins/datasource_srv'; import { getTimeZone } from 'app/features/profile/state/selectors'; import { TempoQuery } from 'app/plugins/datasource/tempo/types'; @@ -132,6 +133,8 @@ export function TraceView(props: Props) { const traceToLogsOptions = getTraceToLogsOptions(instanceSettings?.jsonData); const traceToMetrics: TraceToMetricsData | undefined = instanceSettings?.jsonData; const traceToMetricsOptions = traceToMetrics?.tracesToMetrics; + const traceToProfilesData: TraceToProfilesData | undefined = instanceSettings?.jsonData; + const traceToProfilesOptions = traceToProfilesData?.tracesToProfiles; const spanBarOptions: SpanBarOptionsData | undefined = instanceSettings?.jsonData; const createSpanLink = useMemo( @@ -141,6 +144,7 @@ export function TraceView(props: Props) { splitOpenFn: props.splitOpenFn!, traceToLogsOptions, traceToMetricsOptions, + traceToProfilesOptions, dataFrame: props.dataFrames[0], createFocusSpanLink, trace: traceProp, @@ -149,6 +153,7 @@ export function TraceView(props: Props) { props.splitOpenFn, traceToLogsOptions, traceToMetricsOptions, + traceToProfilesOptions, props.dataFrames, createFocusSpanLink, traceProp, diff --git a/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/index.tsx b/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/index.tsx index caea847d349a4..c54b1735d145e 100644 --- a/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/index.tsx +++ b/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/index.tsx @@ -17,16 +17,17 @@ import { SpanStatusCode } from '@opentelemetry/api'; import cx from 'classnames'; import React from 'react'; -import { dateTimeFormat, GrafanaTheme2, LinkModel, TimeZone } from '@grafana/data'; +import { dateTimeFormat, GrafanaTheme2, IconName, LinkModel, TimeZone } from '@grafana/data'; import { config, locationService, reportInteraction } from '@grafana/runtime'; import { DataLinkButton, Icon, TextArea, useStyles2 } from '@grafana/ui'; +import { RelatedProfilesTitle } from 'app/plugins/datasource/tempo/resultTransformer'; import { autoColor } from '../../Theme'; import { Divider } from '../../common/Divider'; import LabeledList from '../../common/LabeledList'; import { KIND, LIBRARY_NAME, LIBRARY_VERSION, STATUS, STATUS_MESSAGE, TRACE_STATE } from '../../constants/span'; import { SpanLinkFunc, TNil } from '../../types'; -import { SpanLinkType } from '../../types/links'; +import { SpanLinkDef, SpanLinkType } from '../../types/links'; import { TraceKeyValuePair, TraceLink, TraceLog, TraceSpan, TraceSpanReference } from '../../types/trace'; import { uAlignIcon, ubM0, ubMb1, ubMy1, ubTxRightAlign } from '../../uberUtilityStyles'; import { TopOfViewRefType } from '../VirtualizedTraceView'; @@ -241,39 +242,50 @@ export default function SpanDetail(props: SpanDetailProps) { const styles = useStyles2(getStyles); - let logLinkButton: JSX.Element | undefined = undefined; + const createLinkButton = (link: SpanLinkDef, type: SpanLinkType, title: string, icon: IconName) => { + return ( + { + // DataLinkButton assumes if you provide an onClick event you would want to prevent default behavior like navigation + // In this case, if an onClick is not defined, restore navigation to the provided href while keeping the tracking + // this interaction will not be tracked with link right clicks + reportInteraction('grafana_traces_trace_view_span_link_clicked', { + datasourceType: datasourceType, + grafana_version: config.buildInfo.version, + type, + location: 'spanDetails', + }); + + if (link.onClick) { + link.onClick?.(event); + } else { + locationService.push(link.href); + } + }, + }} + buttonProps={{ icon }} + /> + ); + }; + + let logLinkButton: JSX.Element | null = null; + let profileLinkButton: JSX.Element | null = null; if (createSpanLink) { const links = createSpanLink(span); - const logLinks = links?.filter((link) => link.type === SpanLinkType.Logs); - if (links && logLinks && logLinks.length > 0) { - logLinkButton = ( - { - // DataLinkButton assumes if you provide an onClick event you would want to prevent default behavior like navigation - // In this case, if an onClick is not defined, restore navigation to the provided href while keeping the tracking - // this interaction will not be tracked with link right clicks - reportInteraction('grafana_traces_trace_view_span_link_clicked', { - datasourceType: datasourceType, - grafana_version: config.buildInfo.version, - type: 'log', - location: 'spanDetails', - }); - - if (logLinks?.[0].onClick) { - logLinks?.[0].onClick?.(event); - } else { - locationService.push(logLinks?.[0].href); - } - }, - }} - buttonProps={{ icon: 'gf-logs' }} - /> - ); + const logsLink = links?.filter((link) => link.type === SpanLinkType.Logs); + if (links && logsLink && logsLink.length > 0) { + logLinkButton = createLinkButton(logsLink[0], SpanLinkType.Logs, 'Logs for this span', 'gf-logs'); + } + const profilesLink = links?.filter( + (link) => link.type === SpanLinkType.Profiles && link.title === RelatedProfilesTitle + ); + if (links && profilesLink && profilesLink.length > 0) { + profileLinkButton = createLinkButton(profilesLink[0], SpanLinkType.Profiles, 'Profiles for this span', 'link'); } } @@ -286,7 +298,8 @@ export default function SpanDetail(props: SpanDetailProps) {
- {logLinkButton} + {logLinkButton} + {profileLinkButton}
diff --git a/public/app/features/explore/TraceView/components/types/links.ts b/public/app/features/explore/TraceView/components/types/links.ts index 3a2e5f05e0e4a..0ffe2ec5ec130 100644 --- a/public/app/features/explore/TraceView/components/types/links.ts +++ b/public/app/features/explore/TraceView/components/types/links.ts @@ -8,6 +8,7 @@ export enum SpanLinkType { Logs = 'log', Traces = 'trace', Metrics = 'metric', + Profiles = 'profile', Unknown = 'unknown', } diff --git a/public/app/features/explore/TraceView/createSpanLink.test.ts b/public/app/features/explore/TraceView/createSpanLink.test.ts index e4517360b16ff..743e3290c1fea 100644 --- a/public/app/features/explore/TraceView/createSpanLink.test.ts +++ b/public/app/features/explore/TraceView/createSpanLink.test.ts @@ -5,8 +5,9 @@ import { SupportedTransformationType, DataLinkConfigOrigin, FieldType, + DataFrame, } from '@grafana/data'; -import { DataSourceSrv, setDataSourceSrv, setTemplateSrv } from '@grafana/runtime'; +import { config, DataSourceSrv, setDataSourceSrv, setTemplateSrv } from '@grafana/runtime'; import { TraceToMetricsOptions } from 'app/core/components/TraceToMetrics/TraceToMetricsSettings'; import { DatasourceSrv } from 'app/features/plugins/datasource_srv'; @@ -19,7 +20,43 @@ import { SpanLinkType } from './components/types/links'; import { createSpanLinkFactory } from './createSpanLink'; const dummyTraceData = { duration: 10, traceID: 'trace1', traceName: 'test trace' } as unknown as Trace; -const dummyDataFrame = createDataFrame({ fields: [{ name: 'traceId', values: ['trace1'] }] }); +const dummyDataFrame = createDataFrame({ + fields: [ + { name: 'traceId', values: ['trace1'] }, + { name: 'spanID', values: ['testSpanId'] }, + ], +}); +const dummyDataFrameForProfiles = createDataFrame({ + fields: [ + { name: 'traceId', values: ['trace1'] }, + { name: 'spanID', values: ['testSpanId'] }, + { + name: 'tags', + config: { + links: [ + { + internal: { + query: { + labelSelector: '{${__tags}}', + groupBy: [], + profileTypeId: '', + queryType: 'profile', + spanSelector: ['${__span.spanId}'], + refId: '', + }, + datasourceUid: 'pyroscopeUid', + datasourceName: 'pyroscope', + }, + url: '', + title: 'Test', + origin: DataLinkConfigOrigin.Datasource, + }, + ], + }, + values: [{ key: 'test', value: 'test' }], + }, + ], +}); jest.mock('app/core/services/context_srv', () => ({ contextSrv: { @@ -1229,6 +1266,206 @@ describe('createSpanLinkFactory', () => { ); }); }); + + describe('should return pyroscope link', () => { + beforeAll(() => { + setDataSourceSrv({ + getInstanceSettings() { + return { + uid: 'pyroscopeUid', + name: 'pyroscope', + type: 'grafana-pyroscope-datasource', + } as unknown as DataSourceInstanceSettings; + }, + } as unknown as DataSourceSrv); + + setLinkSrv(new LinkSrv()); + setTemplateSrv(new TemplateSrv()); + config.featureToggles.traceToProfiles = true; + }); + + it('with default keys when tags not configured', () => { + const createLink = setupSpanLinkFactory({}, '', dummyDataFrameForProfiles); + expect(createLink).toBeDefined(); + const links = createLink!(createTraceSpan()); + const linkDef = links?.[0]; + expect(linkDef).toBeDefined(); + expect(linkDef?.type).toBe(SpanLinkType.Profiles); + expect(linkDef!.href).toBe( + `/explore?left=${encodeURIComponent( + '{"range":{"from":"1602637140000","to":"1602637261000"},"datasource":"pyroscopeUid","queries":[{"labelSelector":"{service_namespace=\\"namespace1\\"}","groupBy":[],"profileTypeId":"","queryType":"profile","spanSelector":["6605c7b08e715d6c"],"refId":""}]}' + )}` + ); + }); + + it('with tags that passed in and without tags that are not in the span', () => { + const createLink = setupSpanLinkFactory( + { + tags: [{ key: 'ip' }, { key: 'newTag' }], + }, + '', + dummyDataFrameForProfiles + ); + expect(createLink).toBeDefined(); + const links = createLink!( + createTraceSpan({ + process: { + serviceName: 'service', + tags: [ + { key: 'hostname', value: 'hostname1' }, + { key: 'ip', value: '192.168.0.1' }, + ], + }, + }) + ); + const linkDef = links?.[0]; + expect(linkDef).toBeDefined(); + expect(linkDef?.type).toBe(SpanLinkType.Profiles); + expect(linkDef!.href).toBe( + `/explore?left=${encodeURIComponent( + '{"range":{"from":"1602637140000","to":"1602637261000"},"datasource":"pyroscopeUid","queries":[{"labelSelector":"{ip=\\"192.168.0.1\\"}","groupBy":[],"profileTypeId":"","queryType":"profile","spanSelector":["6605c7b08e715d6c"],"refId":""}]}' + )}` + ); + }); + + it('from tags and process tags as well', () => { + const createLink = setupSpanLinkFactory( + { + tags: [{ key: 'ip' }, { key: 'host' }], + }, + '', + dummyDataFrameForProfiles + ); + expect(createLink).toBeDefined(); + const links = createLink!( + createTraceSpan({ + process: { + serviceName: 'service', + tags: [ + { key: 'hostname', value: 'hostname1' }, + { key: 'ip', value: '192.168.0.1' }, + ], + }, + }) + ); + const linkDef = links?.[0]; + expect(linkDef).toBeDefined(); + expect(linkDef?.type).toBe(SpanLinkType.Profiles); + expect(linkDef!.href).toBe( + `/explore?left=${encodeURIComponent( + '{"range":{"from":"1602637140000","to":"1602637261000"},"datasource":"pyroscopeUid","queries":[{"labelSelector":"{ip=\\"192.168.0.1\\", host=\\"host\\"}","groupBy":[],"profileTypeId":"","queryType":"profile","spanSelector":["6605c7b08e715d6c"],"refId":""}]}' + )}` + ); + }); + + it('creates link from dataFrame', () => { + const splitOpenFn = jest.fn(); + const createLink = createSpanLinkFactory({ + splitOpenFn, + dataFrame: createDataFrame({ + fields: [ + { name: 'traceID', values: ['testTraceId'] }, + { + name: 'spanID', + config: { links: [{ title: 'link', url: '${__data.fields.spanID}' }] }, + values: ['testSpanId'], + }, + ], + }), + trace: dummyTraceData, + }); + expect(createLink).toBeDefined(); + const links = createLink!(createTraceSpan()); + + const linkDef = links?.[0]; + expect(linkDef).toBeDefined(); + expect(linkDef?.type).toBe(SpanLinkType.Unknown); + expect(linkDef!.href).toBe('testSpanId'); + }); + + it('handles renamed tags', () => { + const createLink = setupSpanLinkFactory( + { + tags: [ + { key: 'service.name', value: 'service' }, + { key: 'k8s.pod.name', value: 'pod' }, + ], + }, + '', + dummyDataFrameForProfiles + ); + expect(createLink).toBeDefined(); + const links = createLink!( + createTraceSpan({ + process: { + serviceName: 'service', + tags: [ + { key: 'service.name', value: 'serviceName' }, + { key: 'k8s.pod.name', value: 'podName' }, + ], + }, + }) + ); + + const linkDef = links?.[0]; + expect(linkDef).toBeDefined(); + expect(linkDef?.type).toBe(SpanLinkType.Profiles); + expect(linkDef!.href).toBe( + `/explore?left=${encodeURIComponent( + '{"range":{"from":"1602637140000","to":"1602637261000"},"datasource":"pyroscopeUid","queries":[{"labelSelector":"{service=\\"serviceName\\", pod=\\"podName\\"}","groupBy":[],"profileTypeId":"","queryType":"profile","spanSelector":["6605c7b08e715d6c"],"refId":""}]}' + )}` + ); + }); + + it('handles incomplete renamed tags', () => { + const createLink = setupSpanLinkFactory( + { + tags: [ + { key: 'service.name', value: '' }, + { key: 'k8s.pod.name', value: 'pod' }, + ], + }, + '', + dummyDataFrameForProfiles + ); + expect(createLink).toBeDefined(); + const links = createLink!( + createTraceSpan({ + process: { + serviceName: 'service', + tags: [ + { key: 'service.name', value: 'serviceName' }, + { key: 'k8s.pod.name', value: 'podName' }, + ], + }, + }) + ); + + const linkDef = links?.[0]; + expect(linkDef).toBeDefined(); + expect(linkDef?.type).toBe(SpanLinkType.Profiles); + expect(linkDef!.href).toBe( + `/explore?left=${encodeURIComponent( + '{"range":{"from":"1602637140000","to":"1602637261000"},"datasource":"pyroscopeUid","queries":[{"labelSelector":"{service.name=\\"serviceName\\", pod=\\"podName\\"}","groupBy":[],"profileTypeId":"","queryType":"profile","spanSelector":["6605c7b08e715d6c"],"refId":""}]}' + )}` + ); + }); + + it('interpolates span intrinsics', () => { + const createLink = setupSpanLinkFactory( + { + tags: [{ key: 'name', value: 'spanName' }], + }, + '', + dummyDataFrameForProfiles + ); + expect(createLink).toBeDefined(); + const links = createLink!(createTraceSpan()); + expect(links).toBeDefined(); + expect(links![0].type).toBe(SpanLinkType.Profiles); + expect(decodeURIComponent(links![0].href)).toContain('spanName=\\"operation\\"'); + }); + }); }); describe('dataFrame links', () => { @@ -1272,7 +1509,11 @@ describe('dataFrame links', () => { }); }); -function setupSpanLinkFactory(options: Partial = {}, datasourceUid = 'lokiUid') { +function setupSpanLinkFactory( + options: Partial = {}, + datasourceUid = 'lokiUid', + dummyDataFrameForProfiles?: DataFrame +) { const splitOpenFn = jest.fn(); return createSpanLinkFactory({ splitOpenFn, @@ -1281,13 +1522,18 @@ function setupSpanLinkFactory(options: Partial = {}, datas datasourceUid, ...options, }, + traceToProfilesOptions: { + customQuery: false, + datasourceUid: 'pyroscopeUid', + ...options, + }, createFocusSpanLink: (traceId, spanId) => { return { href: `${traceId}-${spanId}`, } as unknown as LinkModel; }, trace: dummyTraceData, - dataFrame: dummyDataFrame, + dataFrame: dummyDataFrameForProfiles ? dummyDataFrameForProfiles : dummyDataFrame, }); } @@ -1308,6 +1554,10 @@ function createTraceSpan(overrides: Partial = {}) { key: 'host', value: 'host', }, + { + key: 'pyroscope.profiling.enabled', + value: 'hdgfljn23u982nj', + }, ], process: { serviceName: 'test service', diff --git a/public/app/features/explore/TraceView/createSpanLink.tsx b/public/app/features/explore/TraceView/createSpanLink.tsx index 930edd74c8945..4de87bdd33e4e 100644 --- a/public/app/features/explore/TraceView/createSpanLink.tsx +++ b/public/app/features/explore/TraceView/createSpanLink.tsx @@ -19,6 +19,7 @@ import { DataQuery } from '@grafana/schema'; import { Icon } from '@grafana/ui'; import { TraceToLogsOptionsV2, TraceToLogsTag } from 'app/core/components/TraceToLogs/TraceToLogsSettings'; import { TraceToMetricQuery, TraceToMetricsOptions } from 'app/core/components/TraceToMetrics/TraceToMetricsSettings'; +import { TraceToProfilesOptions } from 'app/core/components/TraceToProfiles/TraceToProfilesSettings'; import { getDatasourceSrv } from 'app/features/plugins/datasource_srv'; import { PromQuery } from 'app/plugins/datasource/prometheus/types'; @@ -37,6 +38,7 @@ export function createSpanLinkFactory({ splitOpenFn, traceToLogsOptions, traceToMetricsOptions, + traceToProfilesOptions, dataFrame, createFocusSpanLink, trace, @@ -44,6 +46,7 @@ export function createSpanLinkFactory({ splitOpenFn: SplitOpen; traceToLogsOptions?: TraceToLogsOptionsV2; traceToMetricsOptions?: TraceToMetricsOptions; + traceToProfilesOptions?: TraceToProfilesOptions; dataFrame?: DataFrame; createFocusSpanLink?: (traceId: string, spanId: string) => LinkModel; trace: Trace; @@ -72,17 +75,26 @@ export function createSpanLinkFactory({ scopedVars = { ...scopedVars, ...scopedVarsFromSpan(span), + ...scopedVarsFromTags(span, traceToProfilesOptions), }; // We should be here only if there are some links in the dataframe const fields = dataFrame.fields.filter((f) => Boolean(f.config.links?.length))!; try { + let profilesDataSourceSettings: DataSourceInstanceSettings | undefined; + if (traceToProfilesOptions?.datasourceUid) { + profilesDataSourceSettings = getDatasourceSrv().getInstanceSettings(traceToProfilesOptions.datasourceUid); + } + const hasConfiguredPyroscopeDS = profilesDataSourceSettings?.type === 'grafana-pyroscope-datasource'; + const hasPyroscopeProfile = span.tags.filter((tag) => tag.key === 'pyroscope.profiling.enabled').length > 0; + const shouldCreatePyroscopeLink = hasConfiguredPyroscopeDS && hasPyroscopeProfile; + let links: ExploreFieldLinkModel[] = []; fields.forEach((field) => { const fieldLinksForExplore = getFieldLinksForExplore({ field, rowIndex: span.dataFrameRowIndex!, splitOpenFn, - range: getTimeRangeFromSpan(span), + range: getTimeRangeFromSpan(span, undefined, undefined, shouldCreatePyroscopeLink), dataFrame, vars: scopedVars, }); @@ -96,7 +108,7 @@ export function createSpanLinkFactory({ onClick: link.onClick, content: , field: link.origin, - type: SpanLinkType.Unknown, + type: shouldCreatePyroscopeLink ? SpanLinkType.Profiles : SpanLinkType.Unknown, }; }); @@ -115,10 +127,14 @@ export function createSpanLinkFactory({ /** * Default keys to use when there are no configured tags. */ -const defaultKeys = ['cluster', 'hostname', 'namespace', 'pod', 'service.name', 'service.namespace'].map((k) => ({ - key: k, - value: k.includes('.') ? k.replace('.', '_') : undefined, -})); +const formatDefaultKeys = (keys: string[]) => { + return keys.map((k) => ({ + key: k, + value: k.includes('.') ? k.replace('.', '_') : undefined, + })); +}; +const defaultKeys = formatDefaultKeys(['cluster', 'hostname', 'namespace', 'pod', 'service.name', 'service.namespace']); +const defaultProfilingKeys = formatDefaultKeys(['service.name', 'service.namespace']); function legacyCreateSpanLinkFactory( splitOpenFn: SplitOpen, @@ -514,16 +530,19 @@ function getFormattedTags( function getTimeRangeFromSpan( span: TraceSpan, timeShift: { startMs: number; endMs: number } = { startMs: 0, endMs: 0 }, - isSplunkDS = false + isSplunkDS = false, + shouldCreatePyroscopeLink = false ): TimeRange { - const adjustedStartTime = Math.floor(span.startTime / 1000 + timeShift.startMs); - const from = dateTime(adjustedStartTime); + let adjustedStartTime = Math.floor(span.startTime / 1000 + timeShift.startMs); const spanEndMs = (span.startTime + span.duration) / 1000; let adjustedEndTime = Math.floor(spanEndMs + timeShift.endMs); // Splunk requires a time interval of >= 1s, rather than >=1ms like Loki timerange in below elseif block if (isSplunkDS && adjustedEndTime - adjustedStartTime < 1000) { adjustedEndTime = adjustedStartTime + 1000; + } else if (shouldCreatePyroscopeLink) { + adjustedStartTime = adjustedStartTime - 60000; + adjustedEndTime = adjustedEndTime + 60000; } else if (adjustedStartTime === adjustedEndTime) { // Because we can only pass milliseconds in the url we need to check if they equal. // We need end time to be later than start time @@ -531,6 +550,7 @@ function getTimeRangeFromSpan( } const to = dateTime(adjustedEndTime); + const from = dateTime(adjustedStartTime); // Beware that public/app/features/explore/state/main.ts SplitOpen fn uses the range from here. No matter what is in the url. return { @@ -617,3 +637,27 @@ function scopedVarsFromSpan(span: TraceSpan): ScopedVars { }, }; } + +/** + * Variables from tags that can be used in the query + * @param span + */ +function scopedVarsFromTags(span: TraceSpan, traceToProfilesOptions: TraceToProfilesOptions | undefined): ScopedVars { + let tags: ScopedVars = {}; + + if (traceToProfilesOptions) { + const profileTags = + traceToProfilesOptions.tags && traceToProfilesOptions.tags.length > 0 + ? traceToProfilesOptions.tags + : defaultProfilingKeys; + + tags = { + __tags: { + text: 'Tags', + value: getFormattedTags(span, profileTags), + }, + }; + } + + return tags; +} diff --git a/public/app/plugins/datasource/grafana-pyroscope-datasource/QueryEditor/ProfileTypesCascader.tsx b/public/app/plugins/datasource/grafana-pyroscope-datasource/QueryEditor/ProfileTypesCascader.tsx index 17b6608fbb6e1..f04bb77b7dec9 100644 --- a/public/app/plugins/datasource/grafana-pyroscope-datasource/QueryEditor/ProfileTypesCascader.tsx +++ b/public/app/plugins/datasource/grafana-pyroscope-datasource/QueryEditor/ProfileTypesCascader.tsx @@ -10,6 +10,7 @@ type Props = { profileTypes?: ProfileTypeMessage[]; onChange: (value: string) => void; placeholder?: string; + width?: number; }; export function ProfileTypesCascader(props: Props) { @@ -25,6 +26,7 @@ export function ProfileTypesCascader(props: Props) { onSelect={props.onChange} options={cascaderOptions} changeOnSelect={false} + width={props.width ?? 26} /> ); } diff --git a/public/app/plugins/datasource/grafana-pyroscope-datasource/QueryEditor/QueryOptions.tsx b/public/app/plugins/datasource/grafana-pyroscope-datasource/QueryEditor/QueryOptions.tsx index 50c432d23a345..94ade2699b3b1 100644 --- a/public/app/plugins/datasource/grafana-pyroscope-datasource/QueryEditor/QueryOptions.tsx +++ b/public/app/plugins/datasource/grafana-pyroscope-datasource/QueryEditor/QueryOptions.tsx @@ -2,6 +2,7 @@ import { css } from '@emotion/css'; import React from 'react'; import { CoreApp, GrafanaTheme2, SelectableValue } from '@grafana/data'; +import { config } from '@grafana/runtime'; import { useStyles2, RadioButtonGroup, MultiSelect, Input } from '@grafana/ui'; import { QueryOptionGroup } from '../../prometheus/querybuilder/shared/QueryOptionGroup'; @@ -83,6 +84,21 @@ export function QueryOptions({ query, onQueryChange, app, labels }: Props) { }} /> + {config.featureToggles.traceToProfiles && ( + Sets the span ID from which to search for profiles.}> + ) => { + onQueryChange({ + ...query, + spanSelector: event.currentTarget.value !== '' ? [event.currentTarget.value] : [], + }); + }} + /> + + )} Sets the maximum number of nodes to return in the flamegraph.}> ; } export const defaultGrafanaPyroscope: Partial = { groupBy: [], labelSelector: '{}', + spanSelector: [], }; diff --git a/public/app/plugins/datasource/tempo/configuration/ConfigEditor.tsx b/public/app/plugins/datasource/tempo/configuration/ConfigEditor.tsx index 43d5cf502cfa2..5b02f520b8713 100644 --- a/public/app/plugins/datasource/tempo/configuration/ConfigEditor.tsx +++ b/public/app/plugins/datasource/tempo/configuration/ConfigEditor.tsx @@ -18,6 +18,7 @@ import { Divider } from 'app/core/components/Divider'; import { NodeGraphSection } from 'app/core/components/NodeGraphSettings'; import { TraceToLogsSection } from 'app/core/components/TraceToLogs/TraceToLogsSettings'; import { TraceToMetricsSection } from 'app/core/components/TraceToMetrics/TraceToMetricsSettings'; +import { TraceToProfilesSection } from 'app/core/components/TraceToProfiles/TraceToProfilesSettings'; import { SpanBarSection } from 'app/features/explore/TraceView/components/settings/SpanBarSettings'; import { LokiSearchSettings } from './LokiSearchSettings'; @@ -60,6 +61,13 @@ export const ConfigEditor = ({ options, onOptionsChange }: Props) => { ) : null} + {config.featureToggles.traceToProfiles && ( + <> + + + + )} + , + nodeGraph = false +): DataQueryResponse { const frame = response.data[0]; if (!frame) { return emptyDataQueryResponse; } + // Get profiles links + if (config.featureToggles.traceToProfiles) { + const traceToProfilesData: TraceToProfilesData | undefined = instanceSettings?.jsonData; + const traceToProfilesOptions = traceToProfilesData?.tracesToProfiles; + let profilesDataSourceSettings: DataSourceInstanceSettings | undefined; + if (traceToProfilesOptions?.datasourceUid) { + profilesDataSourceSettings = getDatasourceSrv().getInstanceSettings(traceToProfilesOptions.datasourceUid); + } + + if (traceToProfilesOptions && profilesDataSourceSettings) { + const customQuery = traceToProfilesOptions.customQuery ? traceToProfilesOptions.query : undefined; + const dataLink: DataLink = { + title: RelatedProfilesTitle, + url: '', + internal: { + datasourceUid: profilesDataSourceSettings.uid, + datasourceName: profilesDataSourceSettings.name, + query: { + labelSelector: customQuery ? customQuery : '{${__tags}}', + groupBy: [], + profileTypeId: traceToProfilesOptions.profileTypeId ?? '', + queryType: 'profile', + spanSelector: ['${__span.spanId}'], + refId: 'profile', + }, + }, + origin: DataLinkConfigOrigin.Datasource, + }; + + frame.fields.forEach((field: Field) => { + if (field.name === 'tags') { + field.config.links = [dataLink]; + } + }); + } + } + let data = [...response.data]; if (nodeGraph) { data.push(...createGraphFrames(toDataFrame(frame))); From 78df641b389386d9d2100a938dd9b41a72fc2306 Mon Sep 17 00:00:00 2001 From: Ashley Harrison Date: Wed, 1 Nov 2023 10:41:30 +0000 Subject: [PATCH 018/869] Navigation: Make page container automatically scroll when overflowing (#77489) add overflow: auto to page container --- public/app/core/components/AppChrome/AppChrome.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/public/app/core/components/AppChrome/AppChrome.tsx b/public/app/core/components/AppChrome/AppChrome.tsx index b73009538a613..dc5d59cd06995 100644 --- a/public/app/core/components/AppChrome/AppChrome.tsx +++ b/public/app/core/components/AppChrome/AppChrome.tsx @@ -163,6 +163,7 @@ const getStyles = (theme: GrafanaTheme2) => { flexGrow: 1, minHeight: 0, minWidth: 0, + overflow: 'auto', }), skipLink: css({ position: 'absolute', From d1798819c054e4bfe7aca26c59241bc6e4f26679 Mon Sep 17 00:00:00 2001 From: ssama88 <148904725+ssama88@users.noreply.github.com> Date: Wed, 1 Nov 2023 03:47:56 -0700 Subject: [PATCH 019/869] Storybook: Formatted SegmentAsync story (#77307) --- packages/grafana-ui/src/components/Segment/Segment.story.tsx | 2 +- .../grafana-ui/src/components/Segment/SegmentAsync.story.tsx | 3 ++- .../grafana-ui/src/components/Segment/SegmentInput.story.tsx | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/grafana-ui/src/components/Segment/Segment.story.tsx b/packages/grafana-ui/src/components/Segment/Segment.story.tsx index c17f6729d3d57..771fc011ba7af 100644 --- a/packages/grafana-ui/src/components/Segment/Segment.story.tsx +++ b/packages/grafana-ui/src/components/Segment/Segment.story.tsx @@ -33,7 +33,7 @@ const SegmentFrame = ({ children: React.ReactNode; }) => ( <> - + {children} action('New value added')(value)} options={options} /> diff --git a/packages/grafana-ui/src/components/Segment/SegmentAsync.story.tsx b/packages/grafana-ui/src/components/Segment/SegmentAsync.story.tsx index 918a5af232f79..edf49d2564729 100644 --- a/packages/grafana-ui/src/components/Segment/SegmentAsync.story.tsx +++ b/packages/grafana-ui/src/components/Segment/SegmentAsync.story.tsx @@ -29,12 +29,13 @@ const SegmentFrame = ({ loadOptions: (options: Array>) => Promise>>; }>) => ( <> - + {children} action('New value added')(value)} loadOptions={() => loadOptions(options)} + inputMinWidth={100} /> diff --git a/packages/grafana-ui/src/components/Segment/SegmentInput.story.tsx b/packages/grafana-ui/src/components/Segment/SegmentInput.story.tsx index 0f596e5f33444..85e9122ef4f94 100644 --- a/packages/grafana-ui/src/components/Segment/SegmentInput.story.tsx +++ b/packages/grafana-ui/src/components/Segment/SegmentInput.story.tsx @@ -8,7 +8,7 @@ import { SegmentInputProps } from './SegmentInput'; const SegmentFrame = ({ children }: React.PropsWithChildren) => ( <> - {children} + {children} ); From cf7a2ea73354603b5b61a051f86ba31f546350d8 Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Wed, 1 Nov 2023 11:57:02 +0100 Subject: [PATCH 020/869] RolePicker: Optimise rendering inside lists of items (#77297) * Role picker: Load users roles in batch * Use orgId in request * Add roles to OrgUser type * Improve loading logic * Improve loading indicator * Fix org page * Update service accounts page * Use bulk roles query for teams * Use POST requests for search * Use post request for teams * Update betterer results * Review suggestions * AdminEditOrgPage: move API calls to separate file --- .betterer.results | 3 +- .../core/components/RolePicker/RolePicker.tsx | 14 +----- .../components/RolePicker/RolePickerInput.tsx | 46 ++++++++++++------- .../components/RolePicker/TeamRolePicker.tsx | 15 ++++-- .../components/RolePicker/UserRolePicker.tsx | 15 ++++-- .../app/features/admin/AdminEditOrgPage.tsx | 37 ++++----------- .../features/admin/Users/OrgUsersTable.tsx | 6 ++- public/app/features/admin/api.ts | 38 +++++++++++++++ .../components/ServiceAccountsListItem.tsx | 1 + .../features/serviceaccounts/state/actions.ts | 14 ++++++ .../serviceaccounts/state/reducers.ts | 16 ++++++- public/app/features/teams/TeamList.test.tsx | 1 + public/app/features/teams/TeamList.tsx | 16 ++++++- public/app/features/teams/state/actions.ts | 12 +++++ public/app/features/teams/state/reducers.ts | 9 +++- .../app/features/users/UsersListPage.test.tsx | 1 + public/app/features/users/UsersListPage.tsx | 3 ++ public/app/features/users/state/actions.ts | 27 +++++++++-- public/app/features/users/state/reducers.ts | 16 +++++++ public/app/types/serviceaccount.ts | 2 + public/app/types/teams.ts | 6 +++ public/app/types/user.ts | 5 ++ 22 files changed, 227 insertions(+), 76 deletions(-) create mode 100644 public/app/features/admin/api.ts diff --git a/.betterer.results b/.betterer.results index 41d23da9f1c11..8d5def88db493 100644 --- a/.betterer.results +++ b/.betterer.results @@ -1377,8 +1377,7 @@ exports[`better eslint`] = { [0, 0, 0, "Styles should be written using objects.", "3"], [0, 0, 0, "Styles should be written using objects.", "4"], [0, 0, 0, "Styles should be written using objects.", "5"], - [0, 0, 0, "Styles should be written using objects.", "6"], - [0, 0, 0, "Styles should be written using objects.", "7"] + [0, 0, 0, "Styles should be written using objects.", "6"] ], "public/app/core/components/RolePicker/RolePickerMenu.tsx:5381": [ [0, 0, 0, "Styles should be written using objects.", "0"] diff --git a/public/app/core/components/RolePicker/RolePicker.tsx b/public/app/core/components/RolePicker/RolePicker.tsx index 41592ae22a0dc..2cd858380b509 100644 --- a/public/app/core/components/RolePicker/RolePicker.tsx +++ b/public/app/core/components/RolePicker/RolePicker.tsx @@ -1,12 +1,11 @@ import React, { FormEvent, useCallback, useEffect, useState, useRef } from 'react'; -import { ClickOutsideWrapper, Spinner, useStyles2, useTheme2 } from '@grafana/ui'; +import { ClickOutsideWrapper, useTheme2 } from '@grafana/ui'; import { Role, OrgRole } from 'app/types'; import { RolePickerInput } from './RolePickerInput'; import { RolePickerMenu } from './RolePickerMenu'; import { MENU_MAX_HEIGHT, ROLE_PICKER_SUBMENU_MIN_WIDTH, ROLE_PICKER_WIDTH } from './constants'; -import { getStyles } from './styles'; export interface Props { basicRole?: OrgRole; @@ -50,7 +49,6 @@ export const RolePicker = ({ const [query, setQuery] = useState(''); const [offset, setOffset] = useState({ vertical: 0, horizontal: 0 }); const ref = useRef(null); - const styles = useStyles2(getStyles); const theme = useTheme2(); const widthPx = typeof width === 'number' ? theme.spacing(width) : width; @@ -152,15 +150,6 @@ export const RolePicker = ({ return options; }; - if (isLoading) { - return ( -
- Loading... - -
- ); - } - return (
{isOpen && ( { isFocused?: boolean; disabled?: boolean; width?: string; + isLoading?: boolean; onQueryChange: (query?: string) => void; onOpen: (event: FormEvent) => void; onClose: () => void; @@ -32,6 +33,7 @@ export const RolePickerInput = ({ query, showBasicRole, width, + isLoading, onOpen, onClose, onQueryChange, @@ -63,6 +65,11 @@ export const RolePickerInput = ({ numberOfRoles={appliedRoles.length} showBuiltInRole={showBasicRoleOnLabel} /> + {isLoading && ( +
+ +
+ )}
) : (
@@ -141,22 +148,22 @@ const getRolePickerInputStyles = ( ${styleMixins.focusCss(theme.v1)} `, disabled && styles.inputDisabled, - css` - min-width: ${width || ROLE_PICKER_WIDTH + 'px'}; - width: ${width}; - min-height: 32px; - height: auto; - flex-direction: row; - padding-right: 24px; - max-width: 100%; - align-items: center; - display: flex; - flex-wrap: wrap; - justify-content: flex-start; - position: relative; - box-sizing: border-box; - cursor: default; - `, + css({ + minWidth: width || ROLE_PICKER_WIDTH + 'px', + width: width, + minHeight: '32px', + height: 'auto', + flexDirection: 'row', + paddingRight: theme.spacing(1), + maxWidth: '100%', + alignItems: 'center', + display: 'flex', + flexWrap: 'wrap', + justifyContent: 'flex-start', + position: 'relative', + boxSizing: 'border-box', + cursor: 'default', + }), withPrefix && css` padding-left: 0; @@ -184,6 +191,11 @@ const getRolePickerInputStyles = ( margin-bottom: ${theme.spacing(0.5)}; } `, + spinner: css({ + display: 'flex', + flexGrow: 1, + justifyContent: 'flex-end', + }), }; }; diff --git a/public/app/core/components/RolePicker/TeamRolePicker.tsx b/public/app/core/components/RolePicker/TeamRolePicker.tsx index 98d292da3cf52..c380a12094498 100644 --- a/public/app/core/components/RolePicker/TeamRolePicker.tsx +++ b/public/app/core/components/RolePicker/TeamRolePicker.tsx @@ -12,6 +12,7 @@ export interface Props { orgId?: number; roleOptions: Role[]; disabled?: boolean; + roles?: Role[]; onApplyRoles?: (newRoles: Role[]) => void; pendingRoles?: Role[]; /** @@ -28,20 +29,26 @@ export interface Props { apply?: boolean; maxWidth?: string | number; width?: string | number; + isLoading?: boolean; } export const TeamRolePicker = ({ teamId, roleOptions, disabled, + roles, onApplyRoles, pendingRoles, apply = false, maxWidth, width, + isLoading, }: Props) => { - const [{ loading, value: appliedRoles = [] }, getTeamRoles] = useAsyncFn(async () => { + const [{ loading, value: appliedRoles = roles || [] }, getTeamRoles] = useAsyncFn(async () => { try { + if (roles) { + return roles; + } if (apply && Boolean(pendingRoles?.length)) { return pendingRoles; } @@ -53,11 +60,11 @@ export const TeamRolePicker = ({ console.error('Error loading options', e); } return []; - }, [teamId, pendingRoles]); + }, [teamId, pendingRoles, roles]); useEffect(() => { getTeamRoles(); - }, [teamId, getTeamRoles, pendingRoles]); + }, [getTeamRoles]); const onRolesChange = async (roles: Role[]) => { if (!apply) { @@ -78,7 +85,7 @@ export const TeamRolePicker = ({ onRolesChange={onRolesChange} roleOptions={roleOptions} appliedRoles={appliedRoles} - isLoading={loading} + isLoading={loading || isLoading} disabled={disabled} basicRoleDisabled={true} canUpdateRoles={canUpdateRoles} diff --git a/public/app/core/components/RolePicker/UserRolePicker.tsx b/public/app/core/components/RolePicker/UserRolePicker.tsx index ca7cecdc07b1d..568344571912b 100644 --- a/public/app/core/components/RolePicker/UserRolePicker.tsx +++ b/public/app/core/components/RolePicker/UserRolePicker.tsx @@ -9,6 +9,7 @@ import { fetchUserRoles, updateUserRoles } from './api'; export interface Props { basicRole: OrgRole; + roles?: Role[]; userId: number; orgId?: number; onBasicRoleChange: (newRole: OrgRole) => void; @@ -32,10 +33,12 @@ export interface Props { pendingRoles?: Role[]; maxWidth?: string | number; width?: string | number; + isLoading?: boolean; } export const UserRolePicker = ({ basicRole, + roles, userId, orgId, onBasicRoleChange, @@ -48,9 +51,13 @@ export const UserRolePicker = ({ pendingRoles, maxWidth, width, + isLoading, }: Props) => { - const [{ loading, value: appliedRoles = [] }, getUserRoles] = useAsyncFn(async () => { + const [{ loading, value: appliedRoles = roles || [] }, getUserRoles] = useAsyncFn(async () => { try { + if (roles) { + return roles; + } if (apply && Boolean(pendingRoles?.length)) { return pendingRoles; } @@ -63,14 +70,14 @@ export const UserRolePicker = ({ console.error('Error loading options'); } return []; - }, [orgId, userId, pendingRoles]); + }, [orgId, userId, pendingRoles, roles]); useEffect(() => { // only load roles when there is an Org selected if (orgId) { getUserRoles(); } - }, [orgId, getUserRoles, pendingRoles]); + }, [getUserRoles, orgId]); const onRolesChange = async (roles: Role[]) => { if (!apply) { @@ -92,7 +99,7 @@ export const UserRolePicker = ({ onRolesChange={onRolesChange} onBasicRoleChange={onBasicRoleChange} roleOptions={roleOptions} - isLoading={loading} + isLoading={loading || isLoading} disabled={disabled} basicRoleDisabled={basicRoleDisabled} basicRoleDisabledMessage={basicRoleDisabledMessage} diff --git a/public/app/features/admin/AdminEditOrgPage.tsx b/public/app/features/admin/AdminEditOrgPage.tsx index 7c4400acc66a6..e1cab49a2945e 100644 --- a/public/app/features/admin/AdminEditOrgPage.tsx +++ b/public/app/features/admin/AdminEditOrgPage.tsx @@ -1,42 +1,20 @@ import React, { useState, useEffect } from 'react'; import { useAsyncFn } from 'react-use'; -import { NavModelItem, UrlQueryValue } from '@grafana/data'; -import { getBackendSrv } from '@grafana/runtime'; +import { NavModelItem } from '@grafana/data'; import { Form, Field, Input, Button, Legend, Alert } from '@grafana/ui'; import { Page } from 'app/core/components/Page/Page'; import { contextSrv } from 'app/core/core'; import { GrafanaRouteComponentProps } from 'app/core/navigation/types'; -import { accessControlQueryParam } from 'app/core/utils/accessControl'; import { OrgUser, AccessControlAction, OrgRole } from 'app/types'; import { OrgUsersTable } from './Users/OrgUsersTable'; - -const perPage = 30; +import { getOrg, getOrgUsers, getUsersRoles, removeOrgUser, updateOrgName, updateOrgUserRole } from './api'; interface OrgNameDTO { orgName: string; } -const getOrg = async (orgId: UrlQueryValue) => { - return await getBackendSrv().get(`/api/orgs/${orgId}`); -}; - -const getOrgUsers = async (orgId: UrlQueryValue, page: number) => { - if (contextSrv.hasPermission(AccessControlAction.OrgUsersRead)) { - return getBackendSrv().get(`/api/orgs/${orgId}/users/search`, accessControlQueryParam({ perpage: perPage, page })); - } - return { orgUsers: [] }; -}; - -const updateOrgUserRole = (orgUser: OrgUser, orgId: UrlQueryValue) => { - return getBackendSrv().patch(`/api/orgs/${orgId}/users/${orgUser.userId}`, orgUser); -}; - -const removeOrgUser = (orgUser: OrgUser, orgId: UrlQueryValue) => { - return getBackendSrv().delete(`/api/orgs/${orgId}/users/${orgUser.userId}`); -}; - interface Props extends GrafanaRouteComponentProps<{ id: string }> {} const AdminEditOrgPage = ({ match }: Props) => { @@ -51,6 +29,11 @@ const AdminEditOrgPage = ({ match }: Props) => { const [orgState, fetchOrg] = useAsyncFn(() => getOrg(orgId), []); const [, fetchOrgUsers] = useAsyncFn(async (page) => { const result = await getOrgUsers(orgId, page); + + if (contextSrv.licensedAccessControlEnabled()) { + await getUsersRoles(orgId, result.orgUsers); + } + const totalPages = result?.perPage !== 0 ? Math.ceil(result.totalCount / result.perPage) : 0; setTotalPages(totalPages); setUsers(result.orgUsers); @@ -62,8 +45,8 @@ const AdminEditOrgPage = ({ match }: Props) => { fetchOrgUsers(page); }, [fetchOrg, fetchOrgUsers, page]); - const updateOrgName = async (name: string) => { - return await getBackendSrv().put(`/api/orgs/${orgId}`, { ...orgState.value, name }); + const onUpdateOrgName = async (name: string) => { + await updateOrgName(name, orgId); }; const renderMissingPermissionMessage = () => ( @@ -101,7 +84,7 @@ const AdminEditOrgPage = ({ match }: Props) => { {orgState.value && (
updateOrgName(values.orgName)} + onSubmit={(values: OrgNameDTO) => onUpdateOrgName(values.orgName)} > {({ register, errors }) => ( <> diff --git a/public/app/features/admin/Users/OrgUsersTable.tsx b/public/app/features/admin/Users/OrgUsersTable.tsx index 7fec328d12716..0e3df7d60a710 100644 --- a/public/app/features/admin/Users/OrgUsersTable.tsx +++ b/public/app/features/admin/Users/OrgUsersTable.tsx @@ -57,6 +57,7 @@ export interface Props { changePage: (page: number) => void; page: number; totalPages: number; + rolesLoading?: boolean; } export const OrgUsersTable = ({ @@ -68,6 +69,7 @@ export const OrgUsersTable = ({ changePage, page, totalPages, + rolesLoading, }: Props) => { const [userToRemove, setUserToRemove] = useState(null); const [roleOptions, setRoleOptions] = useState([]); @@ -127,6 +129,8 @@ export const OrgUsersTable = ({ return contextSrv.licensedAccessControlEnabled() ? ( { + return await getBackendSrv().get(`/api/orgs/${orgId}`); +}; + +export const getOrgUsers = async (orgId: UrlQueryValue, page: number) => { + if (contextSrv.hasPermission(AccessControlAction.OrgUsersRead)) { + return getBackendSrv().get(`/api/orgs/${orgId}/users/search`, accessControlQueryParam({ perpage: perPage, page })); + } + return { orgUsers: [] }; +}; + +export const getUsersRoles = async (orgId: number, users: OrgUser[]) => { + const userIds = users.map((u) => u.userId); + const roles = await getBackendSrv().post(`/api/access-control/users/roles/search`, { userIds, orgId }); + users.forEach((u) => { + u.roles = roles ? roles[u.userId] || [] : []; + }); +}; + +export const updateOrgUserRole = (orgUser: OrgUser, orgId: UrlQueryValue) => { + return getBackendSrv().patch(`/api/orgs/${orgId}/users/${orgUser.userId}`, orgUser); +}; + +export const removeOrgUser = (orgUser: OrgUser, orgId: UrlQueryValue) => { + return getBackendSrv().delete(`/api/orgs/${orgId}/users/${orgUser.userId}`); +}; + +export const updateOrgName = (name: string, orgId: number) => { + return getBackendSrv().put(`/api/orgs/${orgId}`, { name }); +}; diff --git a/public/app/features/serviceaccounts/components/ServiceAccountsListItem.tsx b/public/app/features/serviceaccounts/components/ServiceAccountsListItem.tsx index 2d10943a4d5ec..f8677e2c10062 100644 --- a/public/app/features/serviceaccounts/components/ServiceAccountsListItem.tsx +++ b/public/app/features/serviceaccounts/components/ServiceAccountsListItem.tsx @@ -77,6 +77,7 @@ const ServiceAccountListItem = memo( userId={serviceAccount.id} orgId={serviceAccount.orgId} basicRole={serviceAccount.role} + roles={serviceAccount.roles || []} onBasicRoleChange={(newRole) => onRoleChange(newRole, serviceAccount)} roleOptions={roleOptions} basicRoleDisabled={!canUpdateRole} diff --git a/public/app/features/serviceaccounts/state/actions.ts b/public/app/features/serviceaccounts/state/actions.ts index 7409beccba5f4..9a9433a24f160 100644 --- a/public/app/features/serviceaccounts/state/actions.ts +++ b/public/app/features/serviceaccounts/state/actions.ts @@ -11,6 +11,8 @@ import { acOptionsLoaded, pageChanged, queryChanged, + rolesFetchBegin, + rolesFetchEnd, serviceAccountsFetchBegin, serviceAccountsFetched, serviceAccountsFetchEnd, @@ -51,6 +53,18 @@ export function fetchServiceAccounts( serviceAccountStateFilter )}&accesscontrol=true` ); + + if (contextSrv.licensedAccessControlEnabled()) { + dispatch(rolesFetchBegin()); + const orgId = contextSrv.user.orgId; + const userIds = result?.serviceAccounts.map((u: ServiceAccountDTO) => u.id); + const roles = await getBackendSrv().post(`/api/access-control/users/roles/search`, { userIds, orgId }); + result.serviceAccounts.forEach((u: ServiceAccountDTO) => { + u.roles = roles ? roles[u.id] || [] : []; + }); + dispatch(rolesFetchEnd()); + } + dispatch(serviceAccountsFetched(result)); } } catch (error) { diff --git a/public/app/features/serviceaccounts/state/reducers.ts b/public/app/features/serviceaccounts/state/reducers.ts index 0286d295cc922..5794233d43460 100644 --- a/public/app/features/serviceaccounts/state/reducers.ts +++ b/public/app/features/serviceaccounts/state/reducers.ts @@ -32,12 +32,24 @@ export const serviceAccountProfileSlice = createSlice({ serviceAccountTokensLoaded: (state, action: PayloadAction): ServiceAccountProfileState => { return { ...state, tokens: action.payload, isLoading: false }; }, + rolesFetchBegin: (state) => { + return { ...state, rolesLoading: true }; + }, + rolesFetchEnd: (state) => { + return { ...state, rolesLoading: false }; + }, }, }); export const serviceAccountProfileReducer = serviceAccountProfileSlice.reducer; -export const { serviceAccountLoaded, serviceAccountTokensLoaded, serviceAccountFetchBegin, serviceAccountFetchEnd } = - serviceAccountProfileSlice.actions; +export const { + serviceAccountLoaded, + serviceAccountTokensLoaded, + serviceAccountFetchBegin, + serviceAccountFetchEnd, + rolesFetchBegin, + rolesFetchEnd, +} = serviceAccountProfileSlice.actions; // serviceAccountsListPage export const initialStateList: ServiceAccountsState = { diff --git a/public/app/features/teams/TeamList.test.tsx b/public/app/features/teams/TeamList.test.tsx index 76dd014644ae7..c9f4dc4b686e2 100644 --- a/public/app/features/teams/TeamList.test.tsx +++ b/public/app/features/teams/TeamList.test.tsx @@ -31,6 +31,7 @@ const setup = (propOverrides?: object) => { page: 0, hasFetched: false, perPage: 10, + rolesLoading: false, }; Object.assign(props, propOverrides); diff --git a/public/app/features/teams/TeamList.tsx b/public/app/features/teams/TeamList.tsx index 10be5c446374e..bd762baf1d049 100644 --- a/public/app/features/teams/TeamList.tsx +++ b/public/app/features/teams/TeamList.tsx @@ -43,6 +43,7 @@ export const TeamList = ({ changeQuery, totalPages, page, + rolesLoading, changePage, changeSort, }: Props) => { @@ -96,7 +97,17 @@ export const TeamList = ({ AccessControlAction.ActionTeamsRolesList, original ); - return canSeeTeamRoles && ; + return ( + canSeeTeamRoles && ( + + ) + ); }, }, ] @@ -132,7 +143,7 @@ export const TeamList = ({ }, }, ], - [displayRolePicker, roleOptions, deleteTeam] + [displayRolePicker, rolesLoading, roleOptions, deleteTeam] ); return ( @@ -203,6 +214,7 @@ function mapStateToProps(state: StoreState) { noTeams: state.teams.noTeams, totalPages: state.teams.totalPages, hasFetched: state.teams.hasFetched, + rolesLoading: state.teams.rolesLoading, }; } diff --git a/public/app/features/teams/state/actions.ts b/public/app/features/teams/state/actions.ts index 3d28044b3eb66..d4f5ec470aa04 100644 --- a/public/app/features/teams/state/actions.ts +++ b/public/app/features/teams/state/actions.ts @@ -16,6 +16,8 @@ import { teamMembersLoaded, teamsLoaded, sortChanged, + rolesFetchBegin, + rolesFetchEnd, } from './reducers'; export function loadTeams(initial = false): ThunkResult { @@ -39,6 +41,16 @@ export function loadTeams(initial = false): ThunkResult { noTeams = response.teams.length === 0; } + if (contextSrv.licensedAccessControlEnabled()) { + dispatch(rolesFetchBegin()); + const teamIds = response?.teams.map((t: Team) => t.id); + const roles = await getBackendSrv().post(`/api/access-control/teams/roles/search`, { teamIds }); + response.teams.forEach((t: Team) => { + t.roles = roles ? roles[t.id] || [] : []; + }); + dispatch(rolesFetchEnd()); + } + dispatch(teamsLoaded({ noTeams, ...response })); }; } diff --git a/public/app/features/teams/state/reducers.ts b/public/app/features/teams/state/reducers.ts index a854ddc1bd854..4602f33540c76 100644 --- a/public/app/features/teams/state/reducers.ts +++ b/public/app/features/teams/state/reducers.ts @@ -38,10 +38,17 @@ const teamsSlice = createSlice({ sortChanged: (state, action: PayloadAction): TeamsState => { return { ...state, sort: action.payload, page: 1 }; }, + rolesFetchBegin: (state) => { + return { ...state, rolesLoading: true }; + }, + rolesFetchEnd: (state) => { + return { ...state, rolesLoading: false }; + }, }, }); -export const { teamsLoaded, queryChanged, pageChanged, sortChanged } = teamsSlice.actions; +export const { teamsLoaded, queryChanged, pageChanged, sortChanged, rolesFetchBegin, rolesFetchEnd } = + teamsSlice.actions; export const teamsReducer = teamsSlice.reducer; diff --git a/public/app/features/users/UsersListPage.test.tsx b/public/app/features/users/UsersListPage.test.tsx index 6b1e231166b7c..64b286a417a69 100644 --- a/public/app/features/users/UsersListPage.test.tsx +++ b/public/app/features/users/UsersListPage.test.tsx @@ -38,6 +38,7 @@ const setup = (propOverrides?: object) => { changePage: mockToolkitActionCreator(pageChanged), changeSort: mockToolkitActionCreator(sortChanged), isLoading: false, + rolesLoading: false, }; Object.assign(props, propOverrides); diff --git a/public/app/features/users/UsersListPage.tsx b/public/app/features/users/UsersListPage.tsx index 9fbf57a50b880..2664d689ba1d3 100644 --- a/public/app/features/users/UsersListPage.tsx +++ b/public/app/features/users/UsersListPage.tsx @@ -26,6 +26,7 @@ function mapStateToProps(state: StoreState) { invitees: selectInvitesMatchingQuery(state.invites, searchQuery), externalUserMngInfo: state.users.externalUserMngInfo, isLoading: state.users.isLoading, + rolesLoading: state.users.rolesLoading, }; } @@ -53,6 +54,7 @@ export const UsersListPageUnconnected = ({ invitees, externalUserMngInfo, isLoading, + rolesLoading, loadUsers, fetchInvitees, changePage, @@ -86,6 +88,7 @@ export const UsersListPageUnconnected = ({ { return async (dispatch, getState) => { try { + dispatch(usersFetchBegin()); const { perPage, page, searchQuery, sort } = getState().users; const users = await getBackendSrv().get( `/api/org/users/search`, accessControlQueryParam({ perpage: perPage, page, query: searchQuery, sort }) ); + + if (contextSrv.licensedAccessControlEnabled()) { + dispatch(rolesFetchBegin()); + const orgId = contextSrv.user.orgId; + const userIds = users?.orgUsers.map((u: OrgUser) => u.userId); + const roles = await getBackendSrv().post(`/api/access-control/users/roles/search`, { userIds, orgId }); + users.orgUsers.forEach((u: OrgUser) => { + u.roles = roles ? roles[u.userId] || [] : []; + }); + dispatch(rolesFetchEnd()); + } dispatch(usersLoaded(users)); } catch (error) { usersFetchEnd(); @@ -42,7 +64,6 @@ export function removeUser(userId: number): ThunkResult { export function changePage(page: number): ThunkResult { return async (dispatch) => { - dispatch(usersFetchBegin()); dispatch(pageChanged(page)); dispatch(loadUsers()); }; @@ -51,7 +72,6 @@ export function changePage(page: number): ThunkResult { export function changeSort({ sortBy }: FetchDataArgs): ThunkResult { const sort = sortBy.length ? `${sortBy[0].id}-${sortBy[0].desc ? 'desc' : 'asc'}` : undefined; return async (dispatch) => { - dispatch(usersFetchBegin()); dispatch(sortChanged(sort)); dispatch(loadUsers()); }; @@ -59,7 +79,6 @@ export function changeSort({ sortBy }: FetchDataArgs): ThunkResult { return async (dispatch) => { - dispatch(usersFetchBegin()); dispatch(searchQueryChanged(query)); fetchUsersWithDebounce(dispatch); }; diff --git a/public/app/features/users/state/reducers.ts b/public/app/features/users/state/reducers.ts index 8b1a987a96e13..5d9b576b9a0e1 100644 --- a/public/app/features/users/state/reducers.ts +++ b/public/app/features/users/state/reducers.ts @@ -13,6 +13,7 @@ export const initialState: UsersState = { externalUserMngLinkName: config.externalUserMngLinkName, externalUserMngLinkUrl: config.externalUserMngLinkUrl, isLoading: false, + rolesLoading: false, }; export interface UsersFetchResult { @@ -22,6 +23,13 @@ export interface UsersFetchResult { totalCount: number; } +export interface UsersRolesFetchResult { + orgUsers: OrgUser[]; + perPage: number; + page: number; + totalCount: number; +} + const usersSlice = createSlice({ name: 'users', initialState, @@ -60,6 +68,12 @@ const usersSlice = createSlice({ usersFetchEnd: (state) => { return { ...state, isLoading: false }; }, + rolesFetchBegin: (state) => { + return { ...state, rolesLoading: true }; + }, + rolesFetchEnd: (state) => { + return { ...state, rolesLoading: false }; + }, }, }); @@ -71,6 +85,8 @@ export const { usersFetchEnd, pageChanged, sortChanged, + rolesFetchBegin, + rolesFetchEnd, } = usersSlice.actions; export const usersReducer = usersSlice.reducer; diff --git a/public/app/types/serviceaccount.ts b/public/app/types/serviceaccount.ts index cad4fea10d926..b14751e122f49 100644 --- a/public/app/types/serviceaccount.ts +++ b/public/app/types/serviceaccount.ts @@ -36,6 +36,7 @@ export interface ServiceAccountDTO extends WithAccessControlMetadata { isDisabled: boolean; teams: string[]; role: OrgRole; + roles?: Role[]; } export interface ServiceAccountCreateApiResponse { @@ -52,6 +53,7 @@ export interface ServiceAccountCreateApiResponse { export interface ServiceAccountProfileState { serviceAccount: ServiceAccountDTO; isLoading: boolean; + rolesLoading?: boolean; tokens: ApiKey[]; } diff --git a/public/app/types/teams.ts b/public/app/types/teams.ts index 5e4c7bf16b2a8..38f5abcf01a5a 100644 --- a/public/app/types/teams.ts +++ b/public/app/types/teams.ts @@ -1,5 +1,6 @@ import { Team as TeamDTO } from '@grafana/schema/src/raw/team/x/team_types.gen'; +import { Role } from './accessControl'; import { TeamPermissionLevel } from './acl'; // The team resource @@ -37,6 +38,10 @@ export interface Team { * TODO - it seems it's a team_member.permission, unlikely it should belong to the team kind */ permission: TeamPermissionLevel; + /** + * RBAC roles assigned to the team. + */ + roles?: Role[]; } export interface TeamMember { @@ -64,6 +69,7 @@ export interface TeamsState { totalPages: number; hasFetched: boolean; sort?: string; + rolesLoading?: boolean; } export interface TeamState { diff --git a/public/app/types/user.ts b/public/app/types/user.ts index 66107cd1f271a..fcbf37ec4eeaa 100644 --- a/public/app/types/user.ts +++ b/public/app/types/user.ts @@ -1,6 +1,8 @@ import { SelectableValue, WithAccessControlMetadata } from '@grafana/data'; +import { Role } from 'app/types'; import { OrgRole } from '.'; + export interface OrgUser extends WithAccessControlMetadata { avatarUrl: string; email: string; @@ -10,6 +12,8 @@ export interface OrgUser extends WithAccessControlMetadata { name: string; orgId: number; role: OrgRole; + // RBAC roles + roles?: Role[]; userId: number; isDisabled: boolean; authLabels?: string[]; @@ -76,6 +80,7 @@ export interface UsersState { externalUserMngLinkName: string; externalUserMngInfo: string; isLoading: boolean; + rolesLoading?: boolean; page: number; perPage: number; totalPages: number; From f1e42fefb40b84380e9e151fb87ed9208bcf1686 Mon Sep 17 00:00:00 2001 From: Victor Marin <36818606+mdvictor@users.noreply.github.com> Date: Wed, 1 Nov 2023 13:28:28 +0200 Subject: [PATCH 021/869] Move datagrid e2e tests to panels-suite (#77031) move datagrid e2e tests to panels-suite --- .../datagrid-data-change.spec.ts | 3 ++- .../datagrid-editing-features.spec.ts | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) rename e2e/{datagrid-suite => panels-suite}/datagrid-data-change.spec.ts (94%) rename e2e/{datagrid-suite => panels-suite}/datagrid-editing-features.spec.ts (98%) diff --git a/e2e/datagrid-suite/datagrid-data-change.spec.ts b/e2e/panels-suite/datagrid-data-change.spec.ts similarity index 94% rename from e2e/datagrid-suite/datagrid-data-change.spec.ts rename to e2e/panels-suite/datagrid-data-change.spec.ts index 48c14f26ee39d..583f30e267f91 100644 --- a/e2e/datagrid-suite/datagrid-data-change.spec.ts +++ b/e2e/panels-suite/datagrid-data-change.spec.ts @@ -3,7 +3,8 @@ import { e2e } from '../utils'; const DASHBOARD_ID = 'c01bf42b-b783-4447-a304-8554cee1843b'; const DATAGRID_SELECT_SERIES = 'Datagrid Select series'; -describe('Datagrid data changes', () => { +//TODO enable this test when panel goes live +describe.skip('Datagrid data changes', () => { beforeEach(() => { e2e.flows.login(Cypress.env('USERNAME'), Cypress.env('PASSWORD')); }); diff --git a/e2e/datagrid-suite/datagrid-editing-features.spec.ts b/e2e/panels-suite/datagrid-editing-features.spec.ts similarity index 98% rename from e2e/datagrid-suite/datagrid-editing-features.spec.ts rename to e2e/panels-suite/datagrid-editing-features.spec.ts index 60c5f73459ece..e3592f60388ca 100644 --- a/e2e/datagrid-suite/datagrid-editing-features.spec.ts +++ b/e2e/panels-suite/datagrid-editing-features.spec.ts @@ -3,7 +3,8 @@ import { e2e } from '../utils'; const DASHBOARD_ID = 'c01bf42b-b783-4447-a304-8554cee1843b'; const DATAGRID_CANVAS = 'data-grid-canvas'; -describe('Datagrid data changes', () => { +//TODO enable this test when panel goes live +describe.skip('Datagrid data changes', () => { beforeEach(() => { e2e.flows.login(Cypress.env('USERNAME'), Cypress.env('PASSWORD')); }); From 2d5e602b2d1e21232d09c6ef66cba7fbcecfcb55 Mon Sep 17 00:00:00 2001 From: Ashley Harrison Date: Wed, 1 Nov 2023 11:56:58 +0000 Subject: [PATCH 022/869] Navigation: Minor tweak to `dockedMegaMenu` to make it slightly tighter (#77493) minor tweak to dockedMegaMenu to make it slightly tighter --- .../AppChrome/DockedMegaMenu/MegaMenuItem.tsx | 13 +++++++++---- .../AppChrome/DockedMegaMenu/MegaMenuItemText.tsx | 1 - 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/public/app/core/components/AppChrome/DockedMegaMenu/MegaMenuItem.tsx b/public/app/core/components/AppChrome/DockedMegaMenu/MegaMenuItem.tsx index 4801007f4b9a8..afe44fded86e8 100644 --- a/public/app/core/components/AppChrome/DockedMegaMenu/MegaMenuItem.tsx +++ b/public/app/core/components/AppChrome/DockedMegaMenu/MegaMenuItem.tsx @@ -63,7 +63,7 @@ export function MegaMenuItem({ link, activeItem, level = 0, onClick }: Props) {
  • {level !== 0 && } @@ -93,6 +93,7 @@ export function MegaMenuItem({ link, activeItem, level = 0, onClick }: Props) {
    {level === 0 && link.icon && ( @@ -141,9 +142,12 @@ const getStyles = (theme: GrafanaTheme2) => ({ alignItems: 'center', gap: theme.spacing(1), height: theme.spacing(4), - paddingLeft: theme.spacing(1), + paddingLeft: theme.spacing(0.5), position: 'relative', }), + menuItemWithIcon: css({ + paddingLeft: theme.spacing(0), + }), collapseButtonWrapper: css({ display: 'flex', justifyContent: 'center', @@ -178,9 +182,10 @@ const getStyles = (theme: GrafanaTheme2) => ({ alignItems: 'center', gap: theme.spacing(2), minWidth: 0, + paddingLeft: theme.spacing(1), }), - hasIcon: css({ - paddingLeft: theme.spacing(0), + labelWrapperWithIcon: css({ + paddingLeft: theme.spacing(0.5), }), hasActiveChild: css({ color: theme.colors.text.primary, diff --git a/public/app/core/components/AppChrome/DockedMegaMenu/MegaMenuItemText.tsx b/public/app/core/components/AppChrome/DockedMegaMenu/MegaMenuItemText.tsx index 910a571e2afb8..6510fda9a2799 100644 --- a/public/app/core/components/AppChrome/DockedMegaMenu/MegaMenuItemText.tsx +++ b/public/app/core/components/AppChrome/DockedMegaMenu/MegaMenuItemText.tsx @@ -88,7 +88,6 @@ const getStyles = (theme: GrafanaTheme2, isActive: Props['isActive']) => ({ display: 'flex', gap: '0.5rem', height: '100%', - paddingLeft: theme.spacing(1), width: '100%', }), }); From 57835c71b12a7d74ee3a7941630a8f5dc9eb4b47 Mon Sep 17 00:00:00 2001 From: Sven Grossmann Date: Wed, 1 Nov 2023 13:42:28 +0100 Subject: [PATCH 023/869] Loki: Add `supportingQueryType` to datasample queries (#77482) add `supportingQueryType` to datasamples --- public/app/plugins/datasource/loki/datasource.test.ts | 9 +++++++++ public/app/plugins/datasource/loki/datasource.ts | 1 + 2 files changed, 10 insertions(+) diff --git a/public/app/plugins/datasource/loki/datasource.test.ts b/public/app/plugins/datasource/loki/datasource.test.ts index 6a729aa41a0ed..6afcf983a8f07 100644 --- a/public/app/plugins/datasource/loki/datasource.test.ts +++ b/public/app/plugins/datasource/loki/datasource.test.ts @@ -1254,6 +1254,15 @@ describe('LokiDatasource', () => { }) ); }); + it('sets the supporting query type in the request', () => { + const spy = jest.spyOn(ds, 'query').mockImplementation(() => of({} as DataQueryResponse)); + ds.getDataSamples({ expr: '{job="bar"}', refId: 'A' }); + expect(spy).toHaveBeenCalledWith( + expect.objectContaining({ + targets: [expect.objectContaining({ supportingQueryType: SupportingQueryType.DataSample })], + }) + ); + }); }); describe('Query splitting', () => { diff --git a/public/app/plugins/datasource/loki/datasource.ts b/public/app/plugins/datasource/loki/datasource.ts index 681144242f633..e75462608c215 100644 --- a/public/app/plugins/datasource/loki/datasource.ts +++ b/public/app/plugins/datasource/loki/datasource.ts @@ -763,6 +763,7 @@ export class LokiDatasource refId: REF_ID_DATA_SAMPLES, // For samples we limit the request to 10 lines, so queries are small and fast maxLines: 10, + supportingQueryType: SupportingQueryType.DataSample, }; const timeRange = this.getTimeRange(); From 20c0fbf92d1624d7197b52ec870bd1ef15c424cb Mon Sep 17 00:00:00 2001 From: Utkarsh Deepak Date: Wed, 1 Nov 2023 18:20:33 +0530 Subject: [PATCH 024/869] ValueFormats: Use plural for time units (#77337) * Dashboard: Use plural values for time units * Cleaned up code a bit and added support for negative values. --------- Co-authored-by: Marcus Andersson --- .../valueFormats/dateTimeFormatters.test.ts | 10 ++++++++-- .../src/valueFormats/valueFormats.test.ts | 4 +++- .../src/valueFormats/valueFormats.ts | 19 ++++++++++++++++++- public/app/core/utils/kbn.test.ts | 2 +- 4 files changed, 30 insertions(+), 5 deletions(-) diff --git a/packages/grafana-data/src/valueFormats/dateTimeFormatters.test.ts b/packages/grafana-data/src/valueFormats/dateTimeFormatters.test.ts index d61beabce0d21..8537fed6c6be4 100644 --- a/packages/grafana-data/src/valueFormats/dateTimeFormatters.test.ts +++ b/packages/grafana-data/src/valueFormats/dateTimeFormatters.test.ts @@ -350,19 +350,25 @@ describe('to nanoseconds', () => { it('should correctly display as minutes', () => { const eightMinutes = toNanoSeconds(480000000000); expect(eightMinutes.text).toBe('8'); + expect(eightMinutes.suffix).toBe(' mins'); + }); + + it('should correctly display as minute', () => { + const eightMinutes = toNanoSeconds(60000000000); + expect(eightMinutes.text).toBe('1'); expect(eightMinutes.suffix).toBe(' min'); }); it('should correctly display as hours', () => { const nineHours = toNanoSeconds(32400000000000); expect(nineHours.text).toBe('9'); - expect(nineHours.suffix).toBe(' hour'); + expect(nineHours.suffix).toBe(' hours'); }); it('should correctly display as days', () => { const tenDays = toNanoSeconds(864000000000000); expect(tenDays.text).toBe('10'); - expect(tenDays.suffix).toBe(' day'); + expect(tenDays.suffix).toBe(' days'); }); }); diff --git a/packages/grafana-data/src/valueFormats/valueFormats.test.ts b/packages/grafana-data/src/valueFormats/valueFormats.test.ts index 2b2e8d0ed63ac..5bac61ae896ef 100644 --- a/packages/grafana-data/src/valueFormats/valueFormats.test.ts +++ b/packages/grafana-data/src/valueFormats/valueFormats.test.ts @@ -25,7 +25,8 @@ describe('valueFormats', () => { ${'ms'} | ${4} | ${0.0024} | ${'0.0024 ms'} ${'ms'} | ${0} | ${100} | ${'100 ms'} ${'ms'} | ${2} | ${1250} | ${'1.25 s'} - ${'ms'} | ${1} | ${10000086.123} | ${'2.8 hour'} + ${'ms'} | ${1} | ${10000086.123} | ${'2.8 hours'} + ${'ms'} | ${1} | ${-10000086.123} | ${'-2.8 hours'} ${'ms'} | ${undefined} | ${1000} | ${'1 s'} ${'ms'} | ${0} | ${1200} | ${'1 s'} ${'short'} | ${undefined} | ${1000} | ${'1 K'} @@ -69,6 +70,7 @@ describe('valueFormats', () => { ${'dateTimeAsUS'} | ${0} | ${dateTime(new Date(2010, 6, 2)).valueOf()} | ${'07/02/2010 12:00:00 am'} ${'dateTimeAsSystem'} | ${0} | ${dateTime(new Date(2010, 6, 2)).valueOf()} | ${'2010-07-02 00:00:00'} ${'dtdurationms'} | ${undefined} | ${100000} | ${'1 minute'} + ${'dtdurationms'} | ${undefined} | ${150000} | ${'2 minutes'} `( 'With format=$format decimals=$decimals and value=$value then result shoudl be = $expected', async ({ format, value, decimals, expected }) => { diff --git a/packages/grafana-data/src/valueFormats/valueFormats.ts b/packages/grafana-data/src/valueFormats/valueFormats.ts index f8753bad0e20f..e4d99adcc91b0 100644 --- a/packages/grafana-data/src/valueFormats/valueFormats.ts +++ b/packages/grafana-data/src/valueFormats/valueFormats.ts @@ -102,10 +102,27 @@ function getDecimalsForValue(value: number): number { export function toFixedScaled(value: number, decimals: DecimalCount, ext?: string): FormattedValue { return { text: toFixed(value, decimals), - suffix: ext, + suffix: appendPluralIf(ext, Math.abs(value) > 1), }; } +function appendPluralIf(ext: string | undefined, condition: boolean): string | undefined { + if (!condition) { + return ext; + } + + switch (ext) { + case ' min': + case ' hour': + case ' day': + case ' week': + case ' year': + return `${ext}s`; + default: + return ext; + } +} + export function toFixedUnit(unit: string, asPrefix?: boolean): ValueFormatter { return (size: number, decimals?: DecimalCount) => { if (size === null) { diff --git a/public/app/core/utils/kbn.test.ts b/public/app/core/utils/kbn.test.ts index 6e291f3d1b275..bb0130fe5f9c8 100644 --- a/public/app/core/utils/kbn.test.ts +++ b/public/app/core/utils/kbn.test.ts @@ -27,7 +27,7 @@ const formatTests: ValueFormatTest[] = [ { id: 'ms', decimals: 4, value: 0.0024, result: '0.0024 ms' }, { id: 'ms', decimals: 0, value: 100, result: '100 ms' }, { id: 'ms', decimals: 2, value: 1250, result: '1.25 s' }, - { id: 'ms', decimals: 1, value: 10000086.123, result: '2.8 hour' }, + { id: 'ms', decimals: 1, value: 10000086.123, result: '2.8 hours' }, { id: 'ms', decimals: 0, value: 1200, result: '1 s' }, { id: 'short', decimals: 0, value: 98765, result: '99 K' }, { id: 'short', decimals: 0, value: 9876543, result: '10 Mil' }, From e8f0f6b7c81644ce14c173f2e32bad9e37fe2602 Mon Sep 17 00:00:00 2001 From: Josh Hunt Date: Wed, 1 Nov 2023 12:58:18 +0000 Subject: [PATCH 025/869] Page: Remove Canvas background from primary background pages (#77008) * Remove canvas background for primary background pages * refactor styles --- .../core/components/AppChrome/AppChrome.tsx | 2 +- public/app/core/components/Page/Page.tsx | 43 ++++++++++++------- 2 files changed, 28 insertions(+), 17 deletions(-) diff --git a/public/app/core/components/AppChrome/AppChrome.tsx b/public/app/core/components/AppChrome/AppChrome.tsx index dc5d59cd06995..a23b75fa50ebe 100644 --- a/public/app/core/components/AppChrome/AppChrome.tsx +++ b/public/app/core/components/AppChrome/AppChrome.tsx @@ -141,7 +141,7 @@ const getStyles = (theme: GrafanaTheme2) => { zIndex: theme.zIndex.navbarFixed, left: 0, right: 0, - boxShadow: shadow, + boxShadow: config.featureToggles.dockedMegaMenu ? undefined : shadow, background: theme.colors.background.primary, flexDirection: 'column', borderBottom: `1px solid ${theme.colors.border.weak}`, diff --git a/public/app/core/components/Page/Page.tsx b/public/app/core/components/Page/Page.tsx index f2490a2d91f97..3af15a6629c1e 100644 --- a/public/app/core/components/Page/Page.tsx +++ b/public/app/core/components/Page/Page.tsx @@ -96,23 +96,34 @@ const getStyles = (theme: GrafanaTheme2) => { label: 'page-content', flexGrow: 1, }), - pageInner: css({ - label: 'page-inner', - padding: theme.spacing(2), - borderRadius: theme.shape.radius.default, - border: `1px solid ${theme.colors.border.weak}`, - borderBottom: 'none', - background: theme.colors.background.primary, - display: 'flex', - flexDirection: 'column', - flexGrow: 1, - margin: theme.spacing(0, 0, 0, 0), - - [theme.breakpoints.up('md')]: { - margin: theme.spacing(2, 2, 0, config.featureToggles.dockedMegaMenu ? 2 : 1), - padding: theme.spacing(3), + pageInner: css( + { + label: 'page-inner', + padding: theme.spacing(2), + borderBottom: 'none', + background: theme.colors.background.primary, + display: 'flex', + flexDirection: 'column', + flexGrow: 1, + margin: theme.spacing(0, 0, 0, 0), }, - }), + config.featureToggles.dockedMegaMenu + ? { + [theme.breakpoints.up('md')]: { + padding: theme.spacing(4), + }, + } + : { + borderRadius: theme.shape.radius.default, + border: `1px solid ${theme.colors.border.weak}`, + + [theme.breakpoints.up('md')]: { + margin: theme.spacing(2, 2, 0, 1), + padding: theme.spacing(3), + }, + } + ), + canvasContent: css({ label: 'canvas-content', display: 'flex', From 384f5ccdc6be681cb8298d2a1690f052031db64b Mon Sep 17 00:00:00 2001 From: Todd Treece <360020+toddtreece@users.noreply.github.com> Date: Wed, 1 Nov 2023 09:44:04 -0400 Subject: [PATCH 026/869] Playlist: Add internal API version (#77318) --- pkg/api/playlist.go | 12 +- .../playlist/{v0alpha1 => }/conversions.go | 2 +- .../{v0alpha1 => }/conversions_test.go | 2 +- pkg/apis/playlist/doc.go | 4 + .../playlist/{v0alpha1 => }/legacy_storage.go | 2 +- pkg/apis/playlist/register.go | 118 ++++++++++++ pkg/apis/playlist/{v0alpha1 => }/storage.go | 2 +- pkg/apis/playlist/types.go | 65 +++++++ pkg/apis/playlist/v0alpha1/register.go | 53 +----- .../v0alpha1/zz_generated.conversion.go | 170 ++++++++++++++++++ .../v0alpha1/zz_generated.deepcopy.go | 16 +- .../v0alpha1/zz_generated.defaults.go | 33 ++++ pkg/apis/playlist/zz_generated.deepcopy.go | 123 +++++++++++++ pkg/apis/wireset.go | 2 + pkg/registry/apis/apis.go | 2 + pkg/services/grafana-apiserver/service.go | 3 + .../grafana-apiserver/storage/file/file.go | 12 +- 17 files changed, 553 insertions(+), 68 deletions(-) rename pkg/apis/playlist/{v0alpha1 => }/conversions.go (99%) rename pkg/apis/playlist/{v0alpha1 => }/conversions_test.go (98%) create mode 100644 pkg/apis/playlist/doc.go rename pkg/apis/playlist/{v0alpha1 => }/legacy_storage.go (99%) create mode 100644 pkg/apis/playlist/register.go rename pkg/apis/playlist/{v0alpha1 => }/storage.go (98%) create mode 100644 pkg/apis/playlist/types.go create mode 100644 pkg/apis/playlist/v0alpha1/zz_generated.conversion.go create mode 100644 pkg/apis/playlist/v0alpha1/zz_generated.defaults.go create mode 100644 pkg/apis/playlist/zz_generated.deepcopy.go diff --git a/pkg/api/playlist.go b/pkg/api/playlist.go index 65c1605a13fa2..30be0e3b55007 100644 --- a/pkg/api/playlist.go +++ b/pkg/api/playlist.go @@ -12,7 +12,7 @@ import ( "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/api/routing" - "github.com/grafana/grafana/pkg/apis/playlist/v0alpha1" + internalplaylist "github.com/grafana/grafana/pkg/apis/playlist" "github.com/grafana/grafana/pkg/middleware" contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/featuremgmt" @@ -49,8 +49,8 @@ func (hs *HTTPServer) registerPlaylistAPI(apiRoute routing.RouteRegister) { if hs.Features.IsEnabled(featuremgmt.FlagKubernetesPlaylistsAPI) { namespacer := request.GetNamespaceMapper(hs.Cfg) gvr := schema.GroupVersionResource{ - Group: v0alpha1.GroupName, - Version: v0alpha1.VersionID, + Group: internalplaylist.GroupName, + Version: internalplaylist.VersionID, Resource: "playlists", } @@ -88,7 +88,7 @@ func (hs *HTTPServer) registerPlaylistAPI(apiRoute routing.RouteRegister) { query := strings.ToUpper(c.Query("query")) playlists := []playlist.Playlist{} for _, item := range out.Items { - p := v0alpha1.UnstructuredToLegacyPlaylist(item) + p := internalplaylist.UnstructuredToLegacyPlaylist(item) if p == nil { continue } @@ -111,7 +111,7 @@ func (hs *HTTPServer) registerPlaylistAPI(apiRoute routing.RouteRegister) { errorWriter(c, err) return } - c.JSON(http.StatusOK, v0alpha1.UnstructuredToLegacyPlaylistDTO(*out)) + c.JSON(http.StatusOK, internalplaylist.UnstructuredToLegacyPlaylistDTO(*out)) }} handler.GetPlaylistItems = []web.Handler{func(c *contextmodel.ReqContext) { @@ -125,7 +125,7 @@ func (hs *HTTPServer) registerPlaylistAPI(apiRoute routing.RouteRegister) { errorWriter(c, err) return } - c.JSON(http.StatusOK, v0alpha1.UnstructuredToLegacyPlaylistDTO(*out).Items) + c.JSON(http.StatusOK, internalplaylist.UnstructuredToLegacyPlaylistDTO(*out).Items) }} } diff --git a/pkg/apis/playlist/v0alpha1/conversions.go b/pkg/apis/playlist/conversions.go similarity index 99% rename from pkg/apis/playlist/v0alpha1/conversions.go rename to pkg/apis/playlist/conversions.go index 121eb491458ba..bbe77f689fa26 100644 --- a/pkg/apis/playlist/v0alpha1/conversions.go +++ b/pkg/apis/playlist/conversions.go @@ -1,4 +1,4 @@ -package v0alpha1 +package playlist import ( "encoding/json" diff --git a/pkg/apis/playlist/v0alpha1/conversions_test.go b/pkg/apis/playlist/conversions_test.go similarity index 98% rename from pkg/apis/playlist/v0alpha1/conversions_test.go rename to pkg/apis/playlist/conversions_test.go index 1c64a1ec08256..f2d12e5dace15 100644 --- a/pkg/apis/playlist/v0alpha1/conversions_test.go +++ b/pkg/apis/playlist/conversions_test.go @@ -1,4 +1,4 @@ -package v0alpha1 +package playlist import ( "encoding/json" diff --git a/pkg/apis/playlist/doc.go b/pkg/apis/playlist/doc.go new file mode 100644 index 0000000000000..f4eff4cd63aa2 --- /dev/null +++ b/pkg/apis/playlist/doc.go @@ -0,0 +1,4 @@ +// +k8s:deepcopy-gen=package +// +groupName=playlist.grafana.app + +package playlist // import "github.com/grafana/grafana/pkg/apis/playlist" diff --git a/pkg/apis/playlist/v0alpha1/legacy_storage.go b/pkg/apis/playlist/legacy_storage.go similarity index 99% rename from pkg/apis/playlist/v0alpha1/legacy_storage.go rename to pkg/apis/playlist/legacy_storage.go index e504d37b62e39..dab3ce7f8133e 100644 --- a/pkg/apis/playlist/v0alpha1/legacy_storage.go +++ b/pkg/apis/playlist/legacy_storage.go @@ -1,4 +1,4 @@ -package v0alpha1 +package playlist import ( "context" diff --git a/pkg/apis/playlist/register.go b/pkg/apis/playlist/register.go new file mode 100644 index 0000000000000..1658447dec229 --- /dev/null +++ b/pkg/apis/playlist/register.go @@ -0,0 +1,118 @@ +package playlist + +import ( + "fmt" + "time" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/runtime/serializer" + "k8s.io/apiserver/pkg/registry/generic" + "k8s.io/apiserver/pkg/registry/rest" + genericapiserver "k8s.io/apiserver/pkg/server" + common "k8s.io/kube-openapi/pkg/common" + + grafanaapiserver "github.com/grafana/grafana/pkg/services/grafana-apiserver" + "github.com/grafana/grafana/pkg/services/grafana-apiserver/endpoints/request" + grafanarest "github.com/grafana/grafana/pkg/services/grafana-apiserver/rest" + "github.com/grafana/grafana/pkg/services/grafana-apiserver/utils" + "github.com/grafana/grafana/pkg/services/playlist" + "github.com/grafana/grafana/pkg/setting" +) + +// GroupName is the group name for this API. +const GroupName = "playlist.grafana.app" +const VersionID = runtime.APIVersionInternal + +var _ grafanaapiserver.APIGroupBuilder = (*PlaylistAPIBuilder)(nil) + +// This is used just so wire has something unique to return +type PlaylistAPIBuilder struct { + service playlist.Service + namespacer request.NamespaceMapper + gv schema.GroupVersion +} + +func RegisterAPIService(p playlist.Service, + apiregistration grafanaapiserver.APIRegistrar, + cfg *setting.Cfg, +) *PlaylistAPIBuilder { + builder := &PlaylistAPIBuilder{ + service: p, + namespacer: request.GetNamespaceMapper(cfg), + gv: schema.GroupVersion{Group: GroupName, Version: VersionID}, + } + apiregistration.RegisterAPI(builder) + return builder +} + +func (b *PlaylistAPIBuilder) GetGroupVersion() schema.GroupVersion { + return b.gv +} + +func (b *PlaylistAPIBuilder) InstallSchema(scheme *runtime.Scheme) error { + scheme.AddKnownTypes(b.gv, + &Playlist{}, + &PlaylistList{}, + ) + return nil +} + +func (b *PlaylistAPIBuilder) GetAPIGroupInfo( + scheme *runtime.Scheme, + codecs serializer.CodecFactory, // pointer? + optsGetter generic.RESTOptionsGetter, +) (*genericapiserver.APIGroupInfo, error) { + apiGroupInfo := genericapiserver.NewDefaultAPIGroupInfo(GroupName, scheme, metav1.ParameterCodec, codecs) + storage := map[string]rest.Storage{} + + legacyStore := &legacyStorage{ + service: b.service, + namespacer: b.namespacer, + DefaultQualifiedResource: b.gv.WithResource("playlists").GroupResource(), + SingularQualifiedResource: b.gv.WithResource("playlist").GroupResource(), + } + legacyStore.tableConverter = utils.NewTableConverter( + legacyStore.DefaultQualifiedResource, + []metav1.TableColumnDefinition{ + {Name: "Name", Type: "string", Format: "name"}, + {Name: "Title", Type: "string", Format: "string", Description: "The playlist name"}, + {Name: "Interval", Type: "string", Format: "string", Description: "How often the playlist will update"}, + {Name: "Created At", Type: "date"}, + }, + func(obj runtime.Object) ([]interface{}, error) { + m, ok := obj.(*Playlist) + if !ok { + return nil, fmt.Errorf("expected playlist") + } + return []interface{}{ + m.Name, + m.Spec.Title, + m.Spec.Interval, + m.CreationTimestamp.UTC().Format(time.RFC3339), + }, nil + }, + ) + storage["playlists"] = legacyStore + + // enable dual writes if a RESTOptionsGetter is provided + if optsGetter != nil { + store, err := newStorage(scheme, optsGetter, legacyStore) + if err != nil { + return nil, err + } + storage["playlists"] = grafanarest.NewDualWriter(legacyStore, store) + } + + apiGroupInfo.VersionedResourcesStorageMap["v0alpha1"] = storage + return &apiGroupInfo, nil +} + +func (b *PlaylistAPIBuilder) GetOpenAPIDefinitions() common.GetOpenAPIDefinitions { + return nil // no custom OpenAPI definitions +} + +func (b *PlaylistAPIBuilder) GetAPIRoutes() *grafanaapiserver.APIRoutes { + return nil // no custom API routes +} diff --git a/pkg/apis/playlist/v0alpha1/storage.go b/pkg/apis/playlist/storage.go similarity index 98% rename from pkg/apis/playlist/v0alpha1/storage.go rename to pkg/apis/playlist/storage.go index d9934841468f2..d5d80e18b553d 100644 --- a/pkg/apis/playlist/v0alpha1/storage.go +++ b/pkg/apis/playlist/storage.go @@ -1,4 +1,4 @@ -package v0alpha1 +package playlist import ( "k8s.io/apimachinery/pkg/runtime" diff --git a/pkg/apis/playlist/types.go b/pkg/apis/playlist/types.go new file mode 100644 index 0000000000000..c7c8647086ad0 --- /dev/null +++ b/pkg/apis/playlist/types.go @@ -0,0 +1,65 @@ +package playlist + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +type Playlist struct { + metav1.TypeMeta `json:",inline"` + // Standard object's metadata + // More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata + // +optional + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec Spec `json:"spec,omitempty"` +} + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +type PlaylistList struct { + metav1.TypeMeta `json:",inline"` + // +optional + metav1.ListMeta `json:"metadata,omitempty"` + + Items []Playlist `json:"items,omitempty"` +} + +// Spec defines model for Spec. +type Spec struct { + // Name of the playlist. + Title string `json:"title"` + + // Interval sets the time between switching views in a playlist. + Interval string `json:"interval"` + + // The ordered list of items that the playlist will iterate over. + Items []Item `json:"items,omitempty"` +} + +// Defines values for ItemType. +const ( + ItemTypeDashboardByTag ItemType = "dashboard_by_tag" + ItemTypeDashboardByUid ItemType = "dashboard_by_uid" + + // deprecated -- should use UID + ItemTypeDashboardById ItemType = "dashboard_by_id" +) + +// Item defines model for Item. +type Item struct { + // Type of the item. + Type ItemType `json:"type"` + + // Value depends on type and describes the playlist item. + // + // - dashboard_by_id: The value is an internal numerical identifier set by Grafana. This + // is not portable as the numerical identifier is non-deterministic between different instances. + // Will be replaced by dashboard_by_uid in the future. (deprecated) + // - dashboard_by_tag: The value is a tag which is set on any number of dashboards. All + // dashboards behind the tag will be added to the playlist. + // - dashboard_by_uid: The value is the dashboard UID + Value string `json:"value"` +} + +// Type of the item. +type ItemType string diff --git a/pkg/apis/playlist/v0alpha1/register.go b/pkg/apis/playlist/v0alpha1/register.go index 520f7b5c2d120..cc803ce8fbece 100644 --- a/pkg/apis/playlist/v0alpha1/register.go +++ b/pkg/apis/playlist/v0alpha1/register.go @@ -1,22 +1,16 @@ package v0alpha1 import ( - "fmt" - "time" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/runtime/serializer" "k8s.io/apiserver/pkg/registry/generic" - "k8s.io/apiserver/pkg/registry/rest" genericapiserver "k8s.io/apiserver/pkg/server" common "k8s.io/kube-openapi/pkg/common" grafanaapiserver "github.com/grafana/grafana/pkg/services/grafana-apiserver" "github.com/grafana/grafana/pkg/services/grafana-apiserver/endpoints/request" - grafanarest "github.com/grafana/grafana/pkg/services/grafana-apiserver/rest" - "github.com/grafana/grafana/pkg/services/grafana-apiserver/utils" "github.com/grafana/grafana/pkg/services/playlist" "github.com/grafana/grafana/pkg/setting" ) @@ -56,6 +50,9 @@ func (b *PlaylistAPIBuilder) InstallSchema(scheme *runtime.Scheme) error { &Playlist{}, &PlaylistList{}, ) + if err := RegisterConversions(scheme); err != nil { + return err + } metav1.AddToGroupVersion(scheme, b.gv) return scheme.SetVersionPriority(b.gv) } @@ -65,49 +62,7 @@ func (b *PlaylistAPIBuilder) GetAPIGroupInfo( codecs serializer.CodecFactory, // pointer? optsGetter generic.RESTOptionsGetter, ) (*genericapiserver.APIGroupInfo, error) { - apiGroupInfo := genericapiserver.NewDefaultAPIGroupInfo(GroupName, scheme, metav1.ParameterCodec, codecs) - storage := map[string]rest.Storage{} - - legacyStore := &legacyStorage{ - service: b.service, - namespacer: b.namespacer, - DefaultQualifiedResource: b.gv.WithResource("playlists").GroupResource(), - SingularQualifiedResource: b.gv.WithResource("playlist").GroupResource(), - } - legacyStore.tableConverter = utils.NewTableConverter( - legacyStore.DefaultQualifiedResource, - []metav1.TableColumnDefinition{ - {Name: "Name", Type: "string", Format: "name"}, - {Name: "Title", Type: "string", Format: "string", Description: "The playlist name"}, - {Name: "Interval", Type: "string", Format: "string", Description: "How often the playlist will update"}, - {Name: "Created At", Type: "date"}, - }, - func(obj runtime.Object) ([]interface{}, error) { - m, ok := obj.(*Playlist) - if !ok { - return nil, fmt.Errorf("expected playlist") - } - return []interface{}{ - m.Name, - m.Spec.Title, - m.Spec.Interval, - m.CreationTimestamp.UTC().Format(time.RFC3339), - }, nil - }, - ) - storage["playlists"] = legacyStore - - // enable dual writes if a RESTOptionsGetter is provided - if optsGetter != nil { - store, err := newStorage(scheme, optsGetter, legacyStore) - if err != nil { - return nil, err - } - storage["playlists"] = grafanarest.NewDualWriter(legacyStore, store) - } - - apiGroupInfo.VersionedResourcesStorageMap[VersionID] = storage - return &apiGroupInfo, nil + return nil, nil } func (b *PlaylistAPIBuilder) GetOpenAPIDefinitions() common.GetOpenAPIDefinitions { diff --git a/pkg/apis/playlist/v0alpha1/zz_generated.conversion.go b/pkg/apis/playlist/v0alpha1/zz_generated.conversion.go new file mode 100644 index 0000000000000..d89fef079e011 --- /dev/null +++ b/pkg/apis/playlist/v0alpha1/zz_generated.conversion.go @@ -0,0 +1,170 @@ +//go:build !ignore_autogenerated +// +build !ignore_autogenerated + +/* +Copyright The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by conversion-gen. DO NOT EDIT. + +package v0alpha1 + +import ( + unsafe "unsafe" + + playlist "github.com/grafana/grafana/pkg/apis/playlist" + conversion "k8s.io/apimachinery/pkg/conversion" + runtime "k8s.io/apimachinery/pkg/runtime" +) + +// RegisterConversions adds conversion functions to the given scheme. +// Public to allow building arbitrary schemes. +func RegisterConversions(s *runtime.Scheme) error { + if err := s.AddGeneratedConversionFunc((*Item)(nil), (*playlist.Item)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_v0alpha1_Item_To_playlist_Item(a.(*Item), b.(*playlist.Item), scope) + }); err != nil { + return err + } + if err := s.AddGeneratedConversionFunc((*playlist.Item)(nil), (*Item)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_playlist_Item_To_v0alpha1_Item(a.(*playlist.Item), b.(*Item), scope) + }); err != nil { + return err + } + if err := s.AddGeneratedConversionFunc((*Playlist)(nil), (*playlist.Playlist)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_v0alpha1_Playlist_To_playlist_Playlist(a.(*Playlist), b.(*playlist.Playlist), scope) + }); err != nil { + return err + } + if err := s.AddGeneratedConversionFunc((*playlist.Playlist)(nil), (*Playlist)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_playlist_Playlist_To_v0alpha1_Playlist(a.(*playlist.Playlist), b.(*Playlist), scope) + }); err != nil { + return err + } + if err := s.AddGeneratedConversionFunc((*PlaylistList)(nil), (*playlist.PlaylistList)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_v0alpha1_PlaylistList_To_playlist_PlaylistList(a.(*PlaylistList), b.(*playlist.PlaylistList), scope) + }); err != nil { + return err + } + if err := s.AddGeneratedConversionFunc((*playlist.PlaylistList)(nil), (*PlaylistList)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_playlist_PlaylistList_To_v0alpha1_PlaylistList(a.(*playlist.PlaylistList), b.(*PlaylistList), scope) + }); err != nil { + return err + } + if err := s.AddGeneratedConversionFunc((*Spec)(nil), (*playlist.Spec)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_v0alpha1_Spec_To_playlist_Spec(a.(*Spec), b.(*playlist.Spec), scope) + }); err != nil { + return err + } + if err := s.AddGeneratedConversionFunc((*playlist.Spec)(nil), (*Spec)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_playlist_Spec_To_v0alpha1_Spec(a.(*playlist.Spec), b.(*Spec), scope) + }); err != nil { + return err + } + return nil +} + +func autoConvert_v0alpha1_Item_To_playlist_Item(in *Item, out *playlist.Item, s conversion.Scope) error { + out.Type = playlist.ItemType(in.Type) + out.Value = in.Value + return nil +} + +// Convert_v0alpha1_Item_To_playlist_Item is an autogenerated conversion function. +func Convert_v0alpha1_Item_To_playlist_Item(in *Item, out *playlist.Item, s conversion.Scope) error { + return autoConvert_v0alpha1_Item_To_playlist_Item(in, out, s) +} + +func autoConvert_playlist_Item_To_v0alpha1_Item(in *playlist.Item, out *Item, s conversion.Scope) error { + out.Type = ItemType(in.Type) + out.Value = in.Value + return nil +} + +// Convert_playlist_Item_To_v0alpha1_Item is an autogenerated conversion function. +func Convert_playlist_Item_To_v0alpha1_Item(in *playlist.Item, out *Item, s conversion.Scope) error { + return autoConvert_playlist_Item_To_v0alpha1_Item(in, out, s) +} + +func autoConvert_v0alpha1_Playlist_To_playlist_Playlist(in *Playlist, out *playlist.Playlist, s conversion.Scope) error { + out.ObjectMeta = in.ObjectMeta + if err := Convert_v0alpha1_Spec_To_playlist_Spec(&in.Spec, &out.Spec, s); err != nil { + return err + } + return nil +} + +// Convert_v0alpha1_Playlist_To_playlist_Playlist is an autogenerated conversion function. +func Convert_v0alpha1_Playlist_To_playlist_Playlist(in *Playlist, out *playlist.Playlist, s conversion.Scope) error { + return autoConvert_v0alpha1_Playlist_To_playlist_Playlist(in, out, s) +} + +func autoConvert_playlist_Playlist_To_v0alpha1_Playlist(in *playlist.Playlist, out *Playlist, s conversion.Scope) error { + out.ObjectMeta = in.ObjectMeta + if err := Convert_playlist_Spec_To_v0alpha1_Spec(&in.Spec, &out.Spec, s); err != nil { + return err + } + return nil +} + +// Convert_playlist_Playlist_To_v0alpha1_Playlist is an autogenerated conversion function. +func Convert_playlist_Playlist_To_v0alpha1_Playlist(in *playlist.Playlist, out *Playlist, s conversion.Scope) error { + return autoConvert_playlist_Playlist_To_v0alpha1_Playlist(in, out, s) +} + +func autoConvert_v0alpha1_PlaylistList_To_playlist_PlaylistList(in *PlaylistList, out *playlist.PlaylistList, s conversion.Scope) error { + out.ListMeta = in.ListMeta + out.Items = *(*[]playlist.Playlist)(unsafe.Pointer(&in.Items)) + return nil +} + +// Convert_v0alpha1_PlaylistList_To_playlist_PlaylistList is an autogenerated conversion function. +func Convert_v0alpha1_PlaylistList_To_playlist_PlaylistList(in *PlaylistList, out *playlist.PlaylistList, s conversion.Scope) error { + return autoConvert_v0alpha1_PlaylistList_To_playlist_PlaylistList(in, out, s) +} + +func autoConvert_playlist_PlaylistList_To_v0alpha1_PlaylistList(in *playlist.PlaylistList, out *PlaylistList, s conversion.Scope) error { + out.ListMeta = in.ListMeta + out.Items = *(*[]Playlist)(unsafe.Pointer(&in.Items)) + return nil +} + +// Convert_playlist_PlaylistList_To_v0alpha1_PlaylistList is an autogenerated conversion function. +func Convert_playlist_PlaylistList_To_v0alpha1_PlaylistList(in *playlist.PlaylistList, out *PlaylistList, s conversion.Scope) error { + return autoConvert_playlist_PlaylistList_To_v0alpha1_PlaylistList(in, out, s) +} + +func autoConvert_v0alpha1_Spec_To_playlist_Spec(in *Spec, out *playlist.Spec, s conversion.Scope) error { + out.Title = in.Title + out.Interval = in.Interval + out.Items = *(*[]playlist.Item)(unsafe.Pointer(&in.Items)) + return nil +} + +// Convert_v0alpha1_Spec_To_playlist_Spec is an autogenerated conversion function. +func Convert_v0alpha1_Spec_To_playlist_Spec(in *Spec, out *playlist.Spec, s conversion.Scope) error { + return autoConvert_v0alpha1_Spec_To_playlist_Spec(in, out, s) +} + +func autoConvert_playlist_Spec_To_v0alpha1_Spec(in *playlist.Spec, out *Spec, s conversion.Scope) error { + out.Title = in.Title + out.Interval = in.Interval + out.Items = *(*[]Item)(unsafe.Pointer(&in.Items)) + return nil +} + +// Convert_playlist_Spec_To_v0alpha1_Spec is an autogenerated conversion function. +func Convert_playlist_Spec_To_v0alpha1_Spec(in *playlist.Spec, out *Spec, s conversion.Scope) error { + return autoConvert_playlist_Spec_To_v0alpha1_Spec(in, out, s) +} diff --git a/pkg/apis/playlist/v0alpha1/zz_generated.deepcopy.go b/pkg/apis/playlist/v0alpha1/zz_generated.deepcopy.go index 74637ae2fb4fc..022f71d9be854 100644 --- a/pkg/apis/playlist/v0alpha1/zz_generated.deepcopy.go +++ b/pkg/apis/playlist/v0alpha1/zz_generated.deepcopy.go @@ -1,7 +1,21 @@ //go:build !ignore_autogenerated // +build !ignore_autogenerated -// SPDX-License-Identifier: AGPL-3.0-only +/* +Copyright The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ // Code generated by deepcopy-gen. DO NOT EDIT. diff --git a/pkg/apis/playlist/v0alpha1/zz_generated.defaults.go b/pkg/apis/playlist/v0alpha1/zz_generated.defaults.go new file mode 100644 index 0000000000000..ff019bc601997 --- /dev/null +++ b/pkg/apis/playlist/v0alpha1/zz_generated.defaults.go @@ -0,0 +1,33 @@ +//go:build !ignore_autogenerated +// +build !ignore_autogenerated + +/* +Copyright The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by defaulter-gen. DO NOT EDIT. + +package v0alpha1 + +import ( + runtime "k8s.io/apimachinery/pkg/runtime" +) + +// RegisterDefaults adds defaulters functions to the given scheme. +// Public to allow building arbitrary schemes. +// All generated defaulters are covering - they call all nested defaulters. +func RegisterDefaults(scheme *runtime.Scheme) error { + return nil +} diff --git a/pkg/apis/playlist/zz_generated.deepcopy.go b/pkg/apis/playlist/zz_generated.deepcopy.go new file mode 100644 index 0000000000000..87997bf707926 --- /dev/null +++ b/pkg/apis/playlist/zz_generated.deepcopy.go @@ -0,0 +1,123 @@ +//go:build !ignore_autogenerated +// +build !ignore_autogenerated + +/* +Copyright The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by deepcopy-gen. DO NOT EDIT. + +package playlist + +import ( + runtime "k8s.io/apimachinery/pkg/runtime" +) + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Item) DeepCopyInto(out *Item) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Item. +func (in *Item) DeepCopy() *Item { + if in == nil { + return nil + } + out := new(Item) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Playlist) DeepCopyInto(out *Playlist) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Playlist. +func (in *Playlist) DeepCopy() *Playlist { + if in == nil { + return nil + } + out := new(Playlist) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *Playlist) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PlaylistList) DeepCopyInto(out *PlaylistList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]Playlist, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PlaylistList. +func (in *PlaylistList) DeepCopy() *PlaylistList { + if in == nil { + return nil + } + out := new(PlaylistList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *PlaylistList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Spec) DeepCopyInto(out *Spec) { + *out = *in + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]Item, len(*in)) + copy(*out, *in) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Spec. +func (in *Spec) DeepCopy() *Spec { + if in == nil { + return nil + } + out := new(Spec) + in.DeepCopyInto(out) + return out +} diff --git a/pkg/apis/wireset.go b/pkg/apis/wireset.go index 30b3cbe489ca0..dd7465bf7e5ec 100644 --- a/pkg/apis/wireset.go +++ b/pkg/apis/wireset.go @@ -4,12 +4,14 @@ import ( "github.com/google/wire" examplev0alpha1 "github.com/grafana/grafana/pkg/apis/example/v0alpha1" + "github.com/grafana/grafana/pkg/apis/playlist" playlistsv0alpha1 "github.com/grafana/grafana/pkg/apis/playlist/v0alpha1" ) // WireSet is the list of all services // NOTE: you must also register the service in: pkg/registry/apis/apis.go var WireSet = wire.NewSet( + playlist.RegisterAPIService, playlistsv0alpha1.RegisterAPIService, examplev0alpha1.RegisterAPIService, ) diff --git a/pkg/registry/apis/apis.go b/pkg/registry/apis/apis.go index 546b75f5fa8b7..300d6778f9cb2 100644 --- a/pkg/registry/apis/apis.go +++ b/pkg/registry/apis/apis.go @@ -4,6 +4,7 @@ import ( "context" examplev0alpha1 "github.com/grafana/grafana/pkg/apis/example/v0alpha1" + "github.com/grafana/grafana/pkg/apis/playlist" playlistsv0alpha1 "github.com/grafana/grafana/pkg/apis/playlist/v0alpha1" "github.com/grafana/grafana/pkg/registry" ) @@ -17,6 +18,7 @@ type Service struct{} // ProvideService is an entry point for each service that will force initialization // and give each builder the chance to register itself with the main server func ProvideService( + _ *playlist.PlaylistAPIBuilder, _ *playlistsv0alpha1.PlaylistAPIBuilder, _ *examplev0alpha1.TestingAPIBuilder, ) *Service { diff --git a/pkg/services/grafana-apiserver/service.go b/pkg/services/grafana-apiserver/service.go index 5193b9c89ef04..21eec399f8c0a 100644 --- a/pkg/services/grafana-apiserver/service.go +++ b/pkg/services/grafana-apiserver/service.go @@ -307,6 +307,9 @@ func (s *service) start(ctx context.Context) error { if err != nil { return err } + if g == nil { + continue + } err = server.InstallAPIGroup(g) if err != nil { return err diff --git a/pkg/services/grafana-apiserver/storage/file/file.go b/pkg/services/grafana-apiserver/storage/file/file.go index d04dea3365abe..6b43207e6ce71 100644 --- a/pkg/services/grafana-apiserver/storage/file/file.go +++ b/pkg/services/grafana-apiserver/storage/file/file.go @@ -287,7 +287,10 @@ func (s *Storage) Watch(ctx context.Context, key string, opts storage.ListOption // match 'opts.ResourceVersion' according 'opts.ResourceVersionMatch'. func (s *Storage) Get(ctx context.Context, key string, opts storage.GetOptions, objPtr runtime.Object) error { filename := s.filePath(key) - if !exists(filename) { + obj, err := readFile(s.codec, filename, func() runtime.Object { + return objPtr + }) + if err != nil { if opts.IgnoreNotFound { return runtime.SetZeroValue(objPtr) } @@ -298,13 +301,6 @@ func (s *Storage) Get(ctx context.Context, key string, opts storage.GetOptions, return storage.NewKeyNotFoundError(key, int64(rv)) } - obj, err := readFile(s.codec, filename, func() runtime.Object { - return objPtr - }) - if err != nil { - return err - } - currentVersion, err := s.Versioner().ObjectResourceVersion(obj) if err != nil { return err From 8b43d1b1abe5b3f28a3a6291359ac4fb0974dea4 Mon Sep 17 00:00:00 2001 From: Jack Baldry Date: Wed, 1 Nov 2023 14:02:45 +0000 Subject: [PATCH 027/869] Update workflow that synchronizes the `make docs` procedure (#77123) * Update workflow that synchronizes the `make docs` procedure Signed-off-by: Jack Baldry * Update .github/workflows/update-make-docs.yml * Only try and commit, push, and open a PR if there are changes Signed-off-by: Jack Baldry --------- Signed-off-by: Jack Baldry --- .github/workflows/update-make-docs.yml | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/.github/workflows/update-make-docs.yml b/.github/workflows/update-make-docs.yml index 1bd0f70cb7131..fd223de30687d 100644 --- a/.github/workflows/update-make-docs.yml +++ b/.github/workflows/update-make-docs.yml @@ -12,16 +12,21 @@ jobs: - name: Update procedure run: | + BRANCH=update-make-docs + git checkout -b "${BRANCH}" curl -s -Lo docs/docs.mk https://raw.githubusercontent.com/grafana/writers-toolkit/main/docs/docs.mk curl -s -Lo docs/make-docs https://raw.githubusercontent.com/grafana/writers-toolkit/main/docs/make-docs if git diff --exit-code; then exit 0; fi - BRANCH="$(date +%Y-%m-%d)/update-make-docs" - git checkout -b "${BRANCH}" git add . git config --local user.email "bot@grafana.com" git config --local user.name "grafanabot" - git commit -m "Update \`make docs\` procedure" + git commit --message "Update \`make docs\` procedure" git push -v origin "refs/heads/${BRANCH}" - gh pr create --fill --label no-changelog --label --no-backport --label type/docs + gh pr create --fill \ + --label 'backport v10.0.x' \ + --label 'backport v10.1.x' \ + --label 'backport v10.2.x' \ + --label no-changelog \ + --label type/docs || true env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} From dfc33a70b7b8d0c01f0b1be415f7f6affd386c67 Mon Sep 17 00:00:00 2001 From: Sofia Papagiannaki <1632407+papagian@users.noreply.github.com> Date: Wed, 1 Nov 2023 17:01:54 +0200 Subject: [PATCH 028/869] Dashboards: Fix creating dashboard under folder using deprecated API (#77501) * Dashboards: Add integration tests for creating a dashboard * Fix creating dashboard under folder using deprecated API * Update swagger response * Fix comments --- pkg/api/dashboard.go | 17 +- pkg/services/folder/folderimpl/folder.go | 2 +- pkg/services/folder/service.go | 4 +- .../api/dashboards/api_dashboards_test.go | 163 ++++++++++++++++++ public/api-merged.json | 4 + public/openapi3.json | 4 + 6 files changed, 185 insertions(+), 9 deletions(-) diff --git a/pkg/api/dashboard.go b/pkg/api/dashboard.go index 91b3851833596..008031c85ccf1 100644 --- a/pkg/api/dashboard.go +++ b/pkg/api/dashboard.go @@ -521,12 +521,13 @@ func (hs *HTTPServer) postDashboard(c *contextmodel.ReqContext, cmd dashboards.S c.TimeRequest(metrics.MApiDashboardSave) return response.JSON(http.StatusOK, util.DynMap{ - "status": "success", - "slug": dashboard.Slug, - "version": dashboard.Version, - "id": dashboard.ID, - "uid": dashboard.UID, - "url": dashboard.GetURL(), + "status": "success", + "slug": dashboard.Slug, + "version": dashboard.Version, + "id": dashboard.ID, + "uid": dashboard.UID, + "url": dashboard.GetURL(), + "folderUid": dashboard.FolderUID, }) } @@ -1301,6 +1302,10 @@ type PostDashboardResponse struct { // required: true // example: /d/nHz3SXiiz/my-dashboard URL string `json:"url"` + + // FolderUID The unique identifier (uid) of the folder the dashboard belongs to. + // required: false + FolderUID string `json:"folderUid"` } `json:"body"` } diff --git a/pkg/services/folder/folderimpl/folder.go b/pkg/services/folder/folderimpl/folder.go index 67d38d7fa212c..b943c24fd4423 100644 --- a/pkg/services/folder/folderimpl/folder.go +++ b/pkg/services/folder/folderimpl/folder.go @@ -116,7 +116,7 @@ func (s *Service) Get(ctx context.Context, cmd *folder.GetFolderQuery) (*folder. var dashFolder *folder.Folder var err error switch { - case cmd.UID != nil: + case cmd.UID != nil && *cmd.UID != "": dashFolder, err = s.getFolderByUID(ctx, cmd.OrgID, *cmd.UID) if err != nil { return nil, err diff --git a/pkg/services/folder/service.go b/pkg/services/folder/service.go index 0d7a8218651f4..03d2bc3a77741 100644 --- a/pkg/services/folder/service.go +++ b/pkg/services/folder/service.go @@ -13,9 +13,9 @@ type Service interface { Create(ctx context.Context, cmd *CreateFolderCommand) (*Folder, error) // GetFolder takes a GetFolderCommand and returns a folder matching the - // request. One of ID, UID, or Title must be included. If multiple values + // request. One of UID, ID or Title must be included. If multiple values // are included in the request, Grafana will select one in order of - // specificity (ID, UID, Title). + // specificity (UID, ID, Title). Get(ctx context.Context, cmd *GetFolderQuery) (*Folder, error) // Update is used to update a folder's UID, Title and Description. To change diff --git a/pkg/tests/api/dashboards/api_dashboards_test.go b/pkg/tests/api/dashboards/api_dashboards_test.go index 78d89989fa1c9..e269553c7a52b 100644 --- a/pkg/tests/api/dashboards/api_dashboards_test.go +++ b/pkg/tests/api/dashboards/api_dashboards_test.go @@ -15,9 +15,11 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/services/dashboardimport" "github.com/grafana/grafana/pkg/services/dashboards" + "github.com/grafana/grafana/pkg/services/folder" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/org/orgimpl" "github.com/grafana/grafana/pkg/services/plugindashboards" @@ -28,6 +30,7 @@ import ( "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/services/user/userimpl" "github.com/grafana/grafana/pkg/tests/testinfra" + "github.com/grafana/grafana/pkg/util" ) func TestIntegrationDashboardQuota(t *testing.T) { @@ -272,3 +275,163 @@ providers: }) }) } + +func TestIntegrationCreate(t *testing.T) { + if testing.Short() { + t.Skip("skipping integration test") + } + + // Setup Grafana and its Database + dir, path := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{ + DisableAnonymous: true, + }) + + grafanaListedAddr, store := testinfra.StartGrafana(t, dir, path) + // Create user + createUser(t, store, user.CreateUserCommand{ + DefaultOrgRole: string(org.RoleAdmin), + Password: "admin", + Login: "admin", + }) + + t.Run("create dashboard should succeed", func(t *testing.T) { + dashboardDataOne, err := simplejson.NewJson([]byte(`{"title":"just testing"}`)) + require.NoError(t, err) + buf1 := &bytes.Buffer{} + err = json.NewEncoder(buf1).Encode(dashboards.SaveDashboardCommand{ + Dashboard: dashboardDataOne, + }) + require.NoError(t, err) + u := fmt.Sprintf("http://admin:admin@%s/api/dashboards/db", grafanaListedAddr) + // nolint:gosec + resp, err := http.Post(u, "application/json", buf1) + require.NoError(t, err) + assert.Equal(t, http.StatusOK, resp.StatusCode) + t.Cleanup(func() { + err := resp.Body.Close() + require.NoError(t, err) + }) + b, err := io.ReadAll(resp.Body) + require.NoError(t, err) + var m util.DynMap + err = json.Unmarshal(b, &m) + require.NoError(t, err) + assert.NotEmpty(t, m["id"]) + assert.NotEmpty(t, m["uid"]) + }) + + t.Run("create dashboard under folder should succeed", func(t *testing.T) { + folder := createFolder(t, grafanaListedAddr, "test folder") + + dashboardDataOne, err := simplejson.NewJson([]byte(`{"title":"just testing"}`)) + require.NoError(t, err) + buf1 := &bytes.Buffer{} + err = json.NewEncoder(buf1).Encode(dashboards.SaveDashboardCommand{ + Dashboard: dashboardDataOne, + FolderUID: folder.Uid, + }) + require.NoError(t, err) + u := fmt.Sprintf("http://admin:admin@%s/api/dashboards/db", grafanaListedAddr) + // nolint:gosec + resp, err := http.Post(u, "application/json", buf1) + require.NoError(t, err) + require.Equal(t, http.StatusOK, resp.StatusCode) + t.Cleanup(func() { + err := resp.Body.Close() + require.NoError(t, err) + }) + b, err := io.ReadAll(resp.Body) + require.NoError(t, err) + var m util.DynMap + err = json.Unmarshal(b, &m) + require.NoError(t, err) + assert.NotEmpty(t, m["id"]) + assert.NotEmpty(t, m["uid"]) + assert.Equal(t, folder.Uid, m["folderUid"]) + }) + + t.Run("create dashboard under folder (using deprecated folder sequential ID) should succeed", func(t *testing.T) { + folder := createFolder(t, grafanaListedAddr, "test folder 2") + + dashboardDataOne, err := simplejson.NewJson([]byte(`{"title":"just testing"}`)) + require.NoError(t, err) + buf1 := &bytes.Buffer{} + err = json.NewEncoder(buf1).Encode(dashboards.SaveDashboardCommand{ + Dashboard: dashboardDataOne, + FolderID: folder.Id, + }) + require.NoError(t, err) + u := fmt.Sprintf("http://admin:admin@%s/api/dashboards/db", grafanaListedAddr) + // nolint:gosec + resp, err := http.Post(u, "application/json", buf1) + require.NoError(t, err) + require.Equal(t, http.StatusOK, resp.StatusCode) + t.Cleanup(func() { + err := resp.Body.Close() + require.NoError(t, err) + }) + b, err := io.ReadAll(resp.Body) + require.NoError(t, err) + var m util.DynMap + err = json.Unmarshal(b, &m) + require.NoError(t, err) + assert.NotEmpty(t, m["id"]) + assert.NotEmpty(t, m["uid"]) + assert.Equal(t, folder.Uid, m["folderUid"]) + }) + + t.Run("create dashboard under unknow folder should fail", func(t *testing.T) { + folderUID := "unknown" + // Import dashboard + dashboardDataOne, err := simplejson.NewJson([]byte(`{"title":"just testing"}`)) + require.NoError(t, err) + buf1 := &bytes.Buffer{} + err = json.NewEncoder(buf1).Encode(dashboards.SaveDashboardCommand{ + Dashboard: dashboardDataOne, + FolderUID: folderUID, + }) + require.NoError(t, err) + u := fmt.Sprintf("http://admin:admin@%s/api/dashboards/db", grafanaListedAddr) + // nolint:gosec + resp, err := http.Post(u, "application/json", buf1) + require.NoError(t, err) + assert.Equal(t, http.StatusBadRequest, resp.StatusCode) + t.Cleanup(func() { + err := resp.Body.Close() + require.NoError(t, err) + }) + b, err := io.ReadAll(resp.Body) + require.NoError(t, err) + var m util.DynMap + err = json.Unmarshal(b, &m) + require.NoError(t, err) + assert.Equal(t, "Folder not found", m["message"]) + }) +} + +func createFolder(t *testing.T, grafanaListedAddr string, title string) *dtos.Folder { + t.Helper() + + buf1 := &bytes.Buffer{} + err := json.NewEncoder(buf1).Encode(folder.CreateFolderCommand{ + Title: title, + }) + require.NoError(t, err) + u := fmt.Sprintf("http://admin:admin@%s/api/folders", grafanaListedAddr) + // nolint:gosec + resp, err := http.Post(u, "application/json", buf1) + require.NoError(t, err) + assert.Equal(t, http.StatusOK, resp.StatusCode) + t.Cleanup(func() { + err := resp.Body.Close() + require.NoError(t, err) + }) + b, err := io.ReadAll(resp.Body) + require.NoError(t, err) + require.Equal(t, http.StatusOK, resp.StatusCode) + var f *dtos.Folder + err = json.Unmarshal(b, &f) + require.NoError(t, err) + + return f +} diff --git a/public/api-merged.json b/public/api-merged.json index eb2274d65ac89..b61b4a4aeb089 100644 --- a/public/api-merged.json +++ b/public/api-merged.json @@ -22456,6 +22456,10 @@ "url" ], "properties": { + "folderUid": { + "description": "FolderUID The unique identifier (uid) of the folder the dashboard belongs to.", + "type": "string" + }, "id": { "description": "ID The unique identifier (id) of the created/updated dashboard.", "type": "integer", diff --git a/public/openapi3.json b/public/openapi3.json index ff42e78e8edd7..bb1220726fc33 100644 --- a/public/openapi3.json +++ b/public/openapi3.json @@ -1579,6 +1579,10 @@ "application/json": { "schema": { "properties": { + "folderUid": { + "description": "FolderUID The unique identifier (uid) of the folder the dashboard belongs to.", + "type": "string" + }, "id": { "description": "ID The unique identifier (id) of the created/updated dashboard.", "example": 1, From 7737226786c68cc4c323a717f6cb5dfc56d5ea03 Mon Sep 17 00:00:00 2001 From: Giordano Ricci Date: Wed, 1 Nov 2023 15:49:45 +0000 Subject: [PATCH 029/869] Explore: Fix support for angular based datasource editors (#77486) --- .../explore/hooks/useStateSync/index.ts | 4 +++- .../app/features/explore/state/explorePane.ts | 17 +++++++++++++++-- public/app/features/explore/state/main.ts | 3 ++- 3 files changed, 20 insertions(+), 4 deletions(-) diff --git a/public/app/features/explore/hooks/useStateSync/index.ts b/public/app/features/explore/hooks/useStateSync/index.ts index 22caa8be3ce58..79d5906a96cca 100644 --- a/public/app/features/explore/hooks/useStateSync/index.ts +++ b/public/app/features/explore/hooks/useStateSync/index.ts @@ -1,7 +1,7 @@ import { identity, isEmpty, isEqual, isObject, mapValues, omitBy } from 'lodash'; import { useEffect, useRef } from 'react'; -import { CoreApp, ExploreUrlState, DataSourceApi, toURLRange } from '@grafana/data'; +import { CoreApp, ExploreUrlState, DataSourceApi, toURLRange, EventBusSrv } from '@grafana/data'; import { DataQuery, DataSourceRef } from '@grafana/schema'; import { useGrafana } from 'app/core/context/GrafanaContext'; import { clearQueryKeys, getLastUsedDatasourceUID } from 'app/core/utils/explore'; @@ -151,6 +151,7 @@ export function useStateSync(params: ExploreQueryParams) { range: fromURLRange(range), panelsState, position: i, + eventBridge: new EventBusSrv(), }) ); } @@ -218,6 +219,7 @@ export function useStateSync(params: ExploreQueryParams) { queries, range: fromURLRange(range), panelsState, + eventBridge: new EventBusSrv(), }) ).unwrap(); }) diff --git a/public/app/features/explore/state/explorePane.ts b/public/app/features/explore/state/explorePane.ts index d84551add5920..c426aaf690f3d 100644 --- a/public/app/features/explore/state/explorePane.ts +++ b/public/app/features/explore/state/explorePane.ts @@ -9,6 +9,7 @@ import { PreferredVisualisationType, RawTimeRange, ExploreCorrelationHelperData, + EventBusExtended, } from '@grafana/data'; import { DataQuery, DataSourceRef } from '@grafana/schema'; import { getQueryKeys } from 'app/core/utils/explore'; @@ -98,6 +99,7 @@ interface InitializeExplorePayload { range: TimeRange; history: HistoryItem[]; datasourceInstance?: DataSourceApi; + eventBridge: EventBusExtended; } const initializeExploreAction = createAction('explore/initializeExploreAction'); @@ -128,6 +130,7 @@ export interface InitializeExploreOptions { panelsState?: ExplorePanelsState; correlationHelperData?: ExploreCorrelationHelperData; position?: number; + eventBridge: EventBusExtended; } /** * Initialize Explore state with state from the URL and the React component. @@ -140,7 +143,15 @@ export interface InitializeExploreOptions { export const initializeExplore = createAsyncThunk( 'explore/initializeExplore', async ( - { exploreId, datasource, queries, range, panelsState, correlationHelperData }: InitializeExploreOptions, + { + exploreId, + datasource, + queries, + range, + panelsState, + correlationHelperData, + eventBridge, + }: InitializeExploreOptions, { dispatch, getState, fulfillWithValue } ) => { let instance = undefined; @@ -160,6 +171,7 @@ export const initializeExplore = createAsyncThunk( range: getRange(range, getTimeZone(getState().user)), datasourceInstance: instance, history, + eventBridge, }) ); if (panelsState !== undefined) { @@ -244,13 +256,14 @@ export const paneReducer = (state: ExploreItemState = makeExplorePaneState(), ac } if (initializeExploreAction.match(action)) { - const { queries, range, datasourceInstance, history } = action.payload; + const { queries, range, datasourceInstance, history, eventBridge } = action.payload; return { ...state, range, queries, initialized: true, + eventBridge, queryKeys: getQueryKeys(queries), datasourceInstance, history, diff --git a/public/app/features/explore/state/main.ts b/public/app/features/explore/state/main.ts index 3ed26f8e81621..82b8a90f7b098 100644 --- a/public/app/features/explore/state/main.ts +++ b/public/app/features/explore/state/main.ts @@ -1,7 +1,7 @@ import { createAction } from '@reduxjs/toolkit'; import { AnyAction } from 'redux'; -import { SplitOpenOptions, TimeRange } from '@grafana/data'; +import { SplitOpenOptions, TimeRange, EventBusSrv } from '@grafana/data'; import { locationService } from '@grafana/runtime'; import { generateExploreId, GetExploreUrlArguments } from 'app/core/utils/explore'; import { PanelModel } from 'app/features/dashboard/state'; @@ -84,6 +84,7 @@ export const splitOpen = createAsyncThunk( range: options?.range || originState?.range.raw || DEFAULT_RANGE, panelsState: options?.panelsState || originState?.panelsState, correlationHelperData: options?.correlationHelperData, + eventBridge: new EventBusSrv(), }) ); }, From 17a24c92e832fa0a61f8cb98e89c92ea992c97a0 Mon Sep 17 00:00:00 2001 From: Andreas Christou Date: Wed, 1 Nov 2023 16:15:08 +0000 Subject: [PATCH 030/869] AzureMonitor: Add missing namespaces (#77308) Add missing namespaces and order --- .../azureMetadata/resourceTypes.ts | 239 +++++++++++++----- 1 file changed, 170 insertions(+), 69 deletions(-) diff --git a/public/app/plugins/datasource/azuremonitor/azureMetadata/resourceTypes.ts b/public/app/plugins/datasource/azuremonitor/azureMetadata/resourceTypes.ts index 462d50a87fbbb..ba4b26e27f860 100644 --- a/public/app/plugins/datasource/azuremonitor/azureMetadata/resourceTypes.ts +++ b/public/app/plugins/datasource/azuremonitor/azureMetadata/resourceTypes.ts @@ -424,135 +424,236 @@ export const multiResourceCompatibleTypes: { [ns: string]: boolean } = { }; export const resourceTypes = [ + 'microsoft.aad/domainservices', + 'microsoft.aadiam/azureadmetrics', 'microsoft.analysisservices/servers', 'microsoft.apimanagement/service', - 'microsoft.network/applicationgateways', - 'microsoft.insights/components', - 'microsoft.web/hostingenvironments', - 'microsoft.web/serverfarms', - 'microsoft.web/sites', - 'microsoft.automation/automationaccounts', - 'microsoft.aad/domainservices', - 'microsoft.botservice/botservices', + 'microsoft.app/containerapps', + 'microsoft.app/managedenvironments', + 'microsoft.appconfiguration/configurationstores', 'microsoft.appplatform/spring', - 'microsoft.dataprotection/backupvaults', - 'microsoft.network/bastionhosts', + 'microsoft.automation/automationaccounts', + 'microsoft.avs/privateclouds', + 'microsoft.azuresphere/catalogs', + 'microsoft.azurestackhci/clusters', + 'microsoft.azurestackresourcemonitor/storageaccountmonitor', 'microsoft.batch/batchaccounts', 'microsoft.batchai/workspaces', - 'microsoft.cdn/profiles', + 'microsoft.bing/accounts', + 'microsoft.botservice/botservices', + 'microsoft.botservice/botservices/channels', + 'microsoft.botservice/botservices/connections', + 'microsoft.botservice/checknameavailability', + 'microsoft.botservice/hostsettings', + 'microsoft.botservice/listauthserviceproviders', + 'microsoft.botservice/listqnamakerendpointkeys', + 'microsoft.cache/redis', + 'microsoft.cache/redisenterprise', 'microsoft.cdn/cdnwebapplicationfirewallpolicies', + 'microsoft.cdn/profiles', 'microsoft.classiccompute/domainnames', + 'microsoft.classiccompute/domainnames/slots/roles', 'microsoft.classiccompute/virtualmachines', - 'microsoft.compute/cloudservices', - 'microsoft.vmwarecloudsimple/virtualmachines', + 'microsoft.classicstorage/storageaccounts', + 'microsoft.classicstorage/storageaccounts/blobservices', + 'microsoft.classicstorage/storageaccounts/fileservices', + 'microsoft.classicstorage/storageaccounts/queueservices', + 'microsoft.classicstorage/storageaccounts/tableservices', + 'microsoft.cloudtest/hostedpools', + 'microsoft.cloudtest/pools', + 'microsoft.clusterstor/nodes', 'microsoft.codesigning/codesigningaccounts', 'microsoft.cognitiveservices/accounts', - 'microsoft.voiceservices/communicationsgateways', - 'microsoft.appconfiguration/configurationstores', - 'microsoft.network/connections', + 'microsoft.communication/communicationservices', + 'microsoft.compute/cloudservices', + 'microsoft.compute/cloudservices/roles', + 'microsoft.compute/disks', + 'microsoft.compute/virtualmachines', + 'microsoft.compute/virtualmachinescalesets', + 'microsoft.compute/virtualmachinescalesets/virtualmachines', + 'microsoft.connectedcache/cachenodes', + 'microsoft.connectedcache/enterprisemcccustomers', + 'microsoft.connectedcache/ispcustomers', 'microsoft.connectedvehicle/platformaccounts', 'microsoft.containerinstance/containergroups', + 'microsoft.containerinstance/containerscalesets', 'microsoft.containerregistry/registries', 'microsoft.containerservice/managedclusters', - 'microsoft.documentdb/databaseaccounts', + 'microsoft.customerinsights/hubs', + 'microsoft.customproviders/resourceproviders', + 'microsoft.dashboard/grafana', 'microsoft.databoxedge/databoxedgedevices', + 'microsoft.datacollaboration/workspaces', 'microsoft.datafactory/datafactories', 'microsoft.datafactory/factories', 'microsoft.datalakeanalytics/accounts', 'microsoft.datalakestore/accounts', + 'microsoft.dataprotection/backupvaults', 'microsoft.datashare/accounts', + 'microsoft.dbformariadb/servers', + 'microsoft.dbformysql/flexibleservers', 'microsoft.dbformysql/servers', + 'microsoft.dbforpostgresql/flexibleservers', + 'microsoft.dbforpostgresql/servergroupsv2', + 'microsoft.dbforpostgresql/servers', + 'microsoft.dbforpostgresql/serversv2', + 'microsoft.devices/iothubs', 'microsoft.devices/provisioningservices', - 'microsoft.compute/disks', - 'microsoft.network/dnszones', - 'microsoft.network/dnsresolvers', - 'microsoft.network/dnsforwardingrulesets', + 'microsoft.digitaltwins/digitaltwinsinstances', + 'microsoft.documentdb/cassandraclusters', + 'microsoft.documentdb/databaseaccounts', + 'microsoft.documentdb/mongoclusters', + 'microsoft.edgezones/edgezones', 'microsoft.enterpriseknowledgegraph/services', 'microsoft.eventgrid/domains', - 'microsoft.eventgrid/topics', + 'microsoft.eventgrid/eventsubscriptions', + 'microsoft.eventgrid/extensiontopics', + 'microsoft.eventgrid/namespaces', + 'microsoft.eventgrid/partnernamespaces', + 'microsoft.eventgrid/partnertopics', 'microsoft.eventgrid/systemtopics', - 'microsoft.eventhub/namespaces', + 'microsoft.eventgrid/topics', 'microsoft.eventhub/clusters', - 'microsoft.network/expressroutecircuits', - 'microsoft.network/expressrouteports', - 'microsoft.network/azurefirewalls', - 'microsoft.network/frontdoors', + 'microsoft.eventhub/namespaces', + 'microsoft.fabric.admin/fabriclocations', 'microsoft.hdinsight/clusters', + 'microsoft.healthcareapis/services', + 'microsoft.healthcareapis/workspaces/dicomservices', + 'microsoft.healthcareapis/workspaces/fhirservices', + 'microsoft.healthcareapis/workspaces/iotconnectors', + 'microsoft.healthmodel/healthmodels', + 'microsoft.hybridcontainerservice/provisionedclusters', + 'microsoft.hybridnetwork/networkfunctions', + 'microsoft.hybridnetwork/virtualnetworkfunctions', + 'microsoft.insights/autoscalesettings', + 'microsoft.insights/components', 'microsoft.iotcentral/iotapps', - 'microsoft.devices/iothubs', 'microsoft.iotspaces/graph', + 'microsoft.keyvault/managedhsms', 'microsoft.keyvault/vaults', 'microsoft.kubernetes/connectedclusters', + 'microsoft.kubernetesconfiguration/extensions', 'microsoft.kusto/clusters', - 'microsoft.network/loadbalancers', - 'microsoft.operationalinsights/workspaces', - 'microsoft.logic/workflows', 'microsoft.logic/integrationserviceenvironments', + 'microsoft.logic/workflows', 'microsoft.machinelearningservices/workspaces', + 'microsoft.machinelearningservices/workspaces/onlineendpoints', + 'microsoft.machinelearningservices/workspaces/onlineendpoints/deployments', + 'microsoft.managednetworkfabric/internetgateways', + 'microsoft.managednetworkfabric/l3isolationdomains', 'microsoft.managednetworkfabric/networkdevices', - 'microsoft.dbformariadb/servers', + 'microsoft.maps/accounts', 'microsoft.media/mediaservices', + 'microsoft.media/mediaservices/liveevents', + 'microsoft.media/mediaservices/streamingendpoints', + 'microsoft.media/videoanalyzers', + 'microsoft.mixedreality/remoterenderingaccounts', + 'microsoft.mixedreality/spatialanchorsaccounts', + 'microsoft.mobilenetwork/packetcorecontrolplanes', + 'microsoft.mobilenetwork/packetcorecontrolplanes/packetcoredataplanes', 'microsoft.monitor/accounts', - 'microsoft.dbformysql/flexibleservers', - 'microsoft.network/natgateways', 'microsoft.netapp/netappaccounts/capacitypools', + 'microsoft.netapp/netappaccounts/capacitypools/volumes', + 'microsoft.network/applicationgateways', + 'microsoft.network/azurefirewalls', + 'microsoft.network/bastionhosts', + 'microsoft.network/connections', + 'microsoft.network/dnsforwardingrulesets', + 'microsoft.network/dnsresolvers', + 'microsoft.network/dnszones', + 'microsoft.network/expressroutecircuits', + 'microsoft.network/expressroutecircuits/peerings', + 'microsoft.network/expressroutegateways', + 'microsoft.network/expressrouteports', + 'microsoft.network/frontdoors', + 'microsoft.network/loadbalancers', + 'microsoft.network/natgateways', 'microsoft.network/networkinterfaces', - 'nginx.nginxplus/nginxdeployments', + 'microsoft.network/networkmanagers/ipampools', + 'microsoft.network/networkvirtualappliances', + 'microsoft.network/networkwatchers', + 'microsoft.network/networkwatchers/connectionmonitors', + 'microsoft.network/p2svpngateways', + 'microsoft.network/privatednszones', + 'microsoft.network/privateendpoints', + 'microsoft.network/privatelinkservices', + 'microsoft.network/publicipaddresses', + 'microsoft.network/publicipprefixes', + 'microsoft.network/trafficmanagerprofiles', + 'microsoft.network/virtualhubs', + 'microsoft.network/virtualnetworkgateways', + 'microsoft.network/virtualnetworks', + 'microsoft.network/virtualrouters', + 'microsoft.network/vpngateways', + 'microsoft.networkanalytics/dataconnectors', + 'microsoft.networkcloud/baremetalmachines', + 'microsoft.networkcloud/clusters', + 'microsoft.networkcloud/storageappliances', + 'microsoft.networkfunction/azuretrafficcollectors', 'microsoft.notificationhubs/namespaces/notificationhubs', + 'microsoft.operationalinsights/workspaces', + 'microsoft.operationsmanagement/solutions', + 'microsoft.orbital/contactprofiles', 'microsoft.orbital/l2connections', + 'microsoft.orbital/spacecrafts', + 'microsoft.orbital/terminals', + 'microsoft.peering/peerings', 'microsoft.peering/peeringservices', 'microsoft.playfab/titles', - 'microsoft.dbforpostgresql/servers', - 'microsoft.dbforpostgresql/serversv2', 'microsoft.powerbidedicated/capacities', - 'microsoft.network/privateendpoints', - 'microsoft.network/privatelinkservices', - 'microsoft.hybridcontainerservice/provisionedclusters', - 'microsoft.network/publicipaddresses', + 'microsoft.purview/accounts', 'microsoft.recoveryservices/vaults', - 'microsoft.cache/redis', - 'microsoft.cache/redisenterprise', 'microsoft.relay/namespaces', + 'microsoft.resources/subscriptions', 'microsoft.search/searchservices', - 'microsoft.dbforpostgresql/servergroupsv2', + 'microsoft.securitydetonation/chambers', + 'microsoft.securitydetonation/securitydetonationchambers', 'microsoft.servicebus/namespaces', 'microsoft.servicefabricmesh/applications', + 'microsoft.servicenetworking/trafficcontrollers', 'microsoft.signalrservice/signalr', + 'microsoft.signalrservice/signalr/replicas', 'microsoft.signalrservice/webpubsub', - 'microsoft.operationsmanagement/solutions', + 'microsoft.signalrservice/webpubsub/replicas', + 'microsoft.singularity/accounts', 'microsoft.sql/managedinstances', 'microsoft.sql/servers/databases', 'microsoft.sql/servers/elasticpools', + 'microsoft.sql/servers/jobagents', 'microsoft.storage/storageaccounts', + 'microsoft.storage/storageaccounts/blobservices', + 'microsoft.storage/storageaccounts/fileservices', + 'microsoft.storage/storageaccounts/objectreplicationpolicies', + 'microsoft.storage/storageaccounts/queueservices', + 'microsoft.storage/storageaccounts/storagetasks', + 'microsoft.storage/storageaccounts/tableservices', + 'microsoft.storage/storagetasks', + 'microsoft.storagecache/amlfilesystems', 'microsoft.storagecache/caches', - 'microsoft.classicstorage/storageaccounts', 'microsoft.storagemover/storagemovers', 'microsoft.storagesync/storagesyncservices', + 'microsoft.storagetasks/storagetasks', 'microsoft.streamanalytics/streamingjobs', 'microsoft.synapse/workspaces', + 'microsoft.synapse/workspaces/bigdatapools', + 'microsoft.synapse/workspaces/kustopools', + 'microsoft.synapse/workspaces/scopepools', + 'microsoft.synapse/workspaces/sqlpools', 'microsoft.timeseriesinsights/environments', - 'microsoft.network/trafficmanagerprofiles', - 'microsoft.compute/virtualmachines', - 'microsoft.compute/virtualmachinescalesets', - 'microsoft.network/virtualnetworkgateways', - 'microsoft.web/sites/slots', - 'microsoft.insights/autoscalesettings', - 'microsoft.aadiam/azureadmetrics', - 'microsoft.azurestackresourcemonitor/storageaccountmonitor', - 'microsoft.network/networkwatchers/connectionmonitors', - 'microsoft.app/containerapps', - 'microsoft.customerinsights/hubs', - 'microsoft.network/expressroutegateways', - 'microsoft.fabric.admin/fabriclocations', - 'microsoft.network/networkvirtualappliances', - 'microsoft.network/networkwatchers', - 'microsoft.network/p2svpngateways', - 'microsoft.dbforpostgresql/flexibleservers', - 'microsoft.network/vpngateways', - 'microsoft.network/virtualhubs', + 'microsoft.timeseriesinsights/environments/eventsources', + 'microsoft.vmwarecloudsimple/virtualmachines', + 'microsoft.voiceservices/communicationsgateways', + 'microsoft.web/containerapps', + 'microsoft.web/hostingenvironments', + 'microsoft.web/hostingenvironments/multirolepools', 'microsoft.web/hostingenvironments/workerpools', - 'microsoft.storagecache/amlfilesystems', - 'microsoft.dashboard/grafana', - 'microsoft.orbital/contactprofiles', - 'microsoft.orbital/spacecrafts', + 'microsoft.web/serverfarms', + 'microsoft.web/sites', + 'microsoft.web/sites/slots', + 'microsoft.web/staticsites', + 'nginx.nginxplus/nginxdeployments', + 'wandisco.fusion/migrators', + 'wandisco.fusion/migrators/datatransferagents', + 'wandisco.fusion/migrators/livedatamigrations', + 'wandisco.fusion/migrators/metadatamigrations', ]; From 5d5f8dfc52bad53a146682213ef8fb9dc8174d3d Mon Sep 17 00:00:00 2001 From: Ryan McKinley Date: Wed, 1 Nov 2023 09:17:38 -0700 Subject: [PATCH 031/869] Chore: Upgrade Go to 1.21.3 (#77304) --- .drone.yml | 200 +++++++++--------- .github/CODEOWNERS | 1 - .github/workflows/alerting-swagger-gen.yml | 2 +- .github/workflows/codeql-analysis.yml | 2 +- .github/workflows/ox-code-coverage.yml | 21 -- .github/workflows/pr-codeql-analysis-go.yml | 2 +- .github/workflows/publish-kinds-next.yml | 2 +- .github/workflows/publish-kinds-release.yml | 2 +- .github/workflows/verify-kinds.yml | 2 +- Dockerfile | 2 +- Makefile | 2 +- go.mod | 2 +- go.sum | 25 +++ pkg/api/frontendsettings.go | 3 +- pkg/expr/ml/outlier_test.go | 2 +- pkg/login/social/google_oauth.go | 2 +- pkg/login/social/social.go | 2 +- pkg/services/authn/clients/ext_jwt.go | 2 +- .../oauthserver/oasimpl/service_test.go | 2 +- pkg/services/grafana-apiserver/openapi.go | 3 +- pkg/services/ngalert/notifier/redis_peer.go | 2 +- .../ngalert/state/historian/encode.go | 5 +- pkg/services/ngalert/state/template/funcs.go | 3 +- pkg/services/query/query.go | 2 +- .../sqlstore/permissions/dashboard.go | 3 +- pkg/tsdb/elasticsearch/data_query.go | 2 +- pkg/web/macaron.go | 3 - scripts/build/ci-build/Dockerfile | 2 +- scripts/drone/variables.star | 2 +- 29 files changed, 152 insertions(+), 153 deletions(-) delete mode 100644 .github/workflows/ox-code-coverage.yml diff --git a/.drone.yml b/.drone.yml index 4fe665bf56815..40f20f172b39c 100644 --- a/.drone.yml +++ b/.drone.yml @@ -24,7 +24,7 @@ steps: depends_on: [] environment: CGO_ENABLED: 0 - image: golang:1.20.10-alpine + image: golang:1.21.3-alpine name: compile-build-cmd - commands: - ./bin/build verify-drone @@ -74,14 +74,14 @@ steps: depends_on: [] environment: CGO_ENABLED: 0 - image: golang:1.20.10-alpine + image: golang:1.21.3-alpine name: compile-build-cmd - commands: - go install github.com/bazelbuild/buildtools/buildifier@latest - buildifier --lint=warn -mode=check -r . depends_on: - compile-build-cmd - image: golang:1.20.10-alpine + image: golang:1.21.3-alpine name: lint-starlark trigger: event: @@ -316,7 +316,7 @@ steps: - apk add --update make - CODEGEN_VERIFY=1 make gen-cue depends_on: [] - image: golang:1.20.10-alpine + image: golang:1.21.3-alpine name: verify-gen-cue - commands: - '# It is required that generated jsonnet is committed and in sync with its inputs.' @@ -325,21 +325,21 @@ steps: - apk add --update make - CODEGEN_VERIFY=1 make gen-jsonnet depends_on: [] - image: golang:1.20.10-alpine + image: golang:1.21.3-alpine name: verify-gen-jsonnet - commands: - apk add --update make - make gen-go depends_on: - verify-gen-cue - image: golang:1.20.10-alpine + image: golang:1.21.3-alpine name: wire-install - commands: - apk add --update build-base shared-mime-info shared-mime-info-lang - go test -tags requires_buildifer -short -covermode=atomic -timeout=5m ./pkg/... depends_on: - wire-install - image: golang:1.20.10-alpine + image: golang:1.21.3-alpine name: test-backend - commands: - apk add --update build-base @@ -348,7 +348,7 @@ steps: | grep -o '\(.*\)/' | sort -u) depends_on: - wire-install - image: golang:1.20.10-alpine + image: golang:1.21.3-alpine name: test-backend-integration trigger: event: @@ -397,7 +397,7 @@ steps: depends_on: [] environment: CGO_ENABLED: 0 - image: golang:1.20.10-alpine + image: golang:1.21.3-alpine name: compile-build-cmd - commands: - apk add --update curl jq bash @@ -424,7 +424,7 @@ steps: - apk add --update make - make gen-go depends_on: [] - image: golang:1.20.10-alpine + image: golang:1.21.3-alpine name: wire-install - commands: - apk add --update make build-base @@ -433,16 +433,16 @@ steps: - wire-install environment: CGO_ENABLED: "1" - image: golang:1.20.10-alpine + image: golang:1.21.3-alpine name: lint-backend - commands: - go run scripts/modowners/modowners.go check go.mod - image: golang:1.20.10-alpine + image: golang:1.21.3-alpine name: validate-modfile - commands: - apk add --update make - make swagger-validate - image: golang:1.20.10-alpine + image: golang:1.21.3-alpine name: validate-openapi-spec trigger: event: @@ -498,7 +498,7 @@ steps: depends_on: [] environment: CGO_ENABLED: 0 - image: golang:1.20.10-alpine + image: golang:1.21.3-alpine name: compile-build-cmd - commands: - '# It is required that code generated from Thema/CUE be committed and in sync @@ -508,7 +508,7 @@ steps: - apk add --update make - CODEGEN_VERIFY=1 make gen-cue depends_on: [] - image: golang:1.20.10-alpine + image: golang:1.21.3-alpine name: verify-gen-cue - commands: - '# It is required that generated jsonnet is committed and in sync with its inputs.' @@ -517,14 +517,14 @@ steps: - apk add --update make - CODEGEN_VERIFY=1 make gen-jsonnet depends_on: [] - image: golang:1.20.10-alpine + image: golang:1.21.3-alpine name: verify-gen-jsonnet - commands: - apk add --update make - make gen-go depends_on: - verify-gen-cue - image: golang:1.20.10-alpine + image: golang:1.21.3-alpine name: wire-install - commands: - yarn install --immutable @@ -557,7 +557,7 @@ steps: from_secret: drone_token - commands: - /src/grafana-build artifacts -a targz:grafana:linux/amd64 -a targz:grafana:linux/arm64 - --go-version=1.20.10 --yarn-cache=$$YARN_CACHE_FOLDER --build-id=$$DRONE_BUILD_NUMBER + --go-version=1.21.3 --yarn-cache=$$YARN_CACHE_FOLDER --build-id=$$DRONE_BUILD_NUMBER --grafana-dir=$$PWD > packages.txt depends_on: - yarn-install @@ -842,7 +842,7 @@ steps: depends_on: [] environment: CGO_ENABLED: 0 - image: golang:1.20.10-alpine + image: golang:1.21.3-alpine name: compile-build-cmd - commands: - echo $DRONE_RUNNER_NAME @@ -856,7 +856,7 @@ steps: - apk add --update make - CODEGEN_VERIFY=1 make gen-cue depends_on: [] - image: golang:1.20.10-alpine + image: golang:1.21.3-alpine name: verify-gen-cue - commands: - '# It is required that generated jsonnet is committed and in sync with its inputs.' @@ -865,14 +865,14 @@ steps: - apk add --update make - CODEGEN_VERIFY=1 make gen-jsonnet depends_on: [] - image: golang:1.20.10-alpine + image: golang:1.21.3-alpine name: verify-gen-jsonnet - commands: - apk add --update make - make gen-go depends_on: - verify-gen-cue - image: golang:1.20.10-alpine + image: golang:1.21.3-alpine name: wire-install - commands: - dockerize -wait tcp://postgres:5432 -timeout 120s @@ -893,7 +893,7 @@ steps: GRAFANA_TEST_DB: postgres PGPASSWORD: grafanatest POSTGRES_HOST: postgres - image: golang:1.20.10-alpine + image: golang:1.21.3-alpine name: postgres-integration-tests - commands: - dockerize -wait tcp://mysql57:3306 -timeout 120s @@ -914,7 +914,7 @@ steps: environment: GRAFANA_TEST_DB: mysql MYSQL_HOST: mysql57 - image: golang:1.20.10-alpine + image: golang:1.21.3-alpine name: mysql-5.7-integration-tests - commands: - dockerize -wait tcp://mysql80:3306 -timeout 120s @@ -935,7 +935,7 @@ steps: environment: GRAFANA_TEST_DB: mysql MYSQL_HOST: mysql80 - image: golang:1.20.10-alpine + image: golang:1.21.3-alpine name: mysql-8.0-integration-tests - commands: - dockerize -wait tcp://redis:6379 -timeout 120s @@ -950,7 +950,7 @@ steps: - wait-for-redis environment: REDIS_URL: redis://redis:6379/0 - image: golang:1.20.10-alpine + image: golang:1.21.3-alpine name: redis-integration-tests - commands: - dockerize -wait tcp://memcached:11211 -timeout 120s @@ -965,7 +965,7 @@ steps: - wait-for-memcached environment: MEMCACHED_HOSTS: memcached:11211 - image: golang:1.20.10-alpine + image: golang:1.21.3-alpine name: memcached-integration-tests - commands: - dockerize -wait tcp://mimir_backend:8080 -timeout 120s @@ -982,7 +982,7 @@ steps: AM_PASSWORD: test AM_TENANT_ID: test AM_URL: http://mimir_backend:8080 - image: golang:1.20.10-alpine + image: golang:1.21.3-alpine name: remote-alertmanager-integration-tests trigger: event: @@ -1069,7 +1069,7 @@ steps: - apk add --update make - CODEGEN_VERIFY=1 make gen-cue depends_on: [] - image: golang:1.20.10-alpine + image: golang:1.21.3-alpine name: verify-gen-cue trigger: event: @@ -1109,7 +1109,7 @@ steps: depends_on: [] environment: CGO_ENABLED: 0 - image: golang:1.20.10-alpine + image: golang:1.21.3-alpine name: compile-build-cmd - commands: - apt-get update -yq && apt-get install shellcheck @@ -1176,7 +1176,7 @@ steps: environment: GITHUB_TOKEN: from_secret: github_token - image: golang:1.20.10-alpine + image: golang:1.21.3-alpine name: swagger-gen trigger: event: @@ -1277,7 +1277,7 @@ steps: depends_on: [] environment: CGO_ENABLED: 0 - image: golang:1.20.10-alpine + image: golang:1.21.3-alpine name: compile-build-cmd - commands: - '# It is required that code generated from Thema/CUE be committed and in sync @@ -1288,7 +1288,7 @@ steps: - CODEGEN_VERIFY=1 make gen-cue depends_on: - clone-enterprise - image: golang:1.20.10-alpine + image: golang:1.21.3-alpine name: verify-gen-cue - commands: - '# It is required that generated jsonnet is committed and in sync with its inputs.' @@ -1298,14 +1298,14 @@ steps: - CODEGEN_VERIFY=1 make gen-jsonnet depends_on: - clone-enterprise - image: golang:1.20.10-alpine + image: golang:1.21.3-alpine name: verify-gen-jsonnet - commands: - apk add --update make - make gen-go depends_on: - verify-gen-cue - image: golang:1.20.10-alpine + image: golang:1.21.3-alpine name: wire-install - commands: - apk add --update build-base @@ -1313,7 +1313,7 @@ steps: - go test -v -run=^$ -benchmem -timeout=1h -count=8 -bench=. ${GO_PACKAGES} depends_on: - wire-install - image: golang:1.20.10-alpine + image: golang:1.21.3-alpine name: sqlite-benchmark-integration-tests - commands: - apk add --update build-base @@ -1325,7 +1325,7 @@ steps: GRAFANA_TEST_DB: postgres PGPASSWORD: grafanatest POSTGRES_HOST: postgres - image: golang:1.20.10-alpine + image: golang:1.21.3-alpine name: postgres-benchmark-integration-tests - commands: - apk add --update build-base @@ -1336,7 +1336,7 @@ steps: environment: GRAFANA_TEST_DB: mysql MYSQL_HOST: mysql57 - image: golang:1.20.10-alpine + image: golang:1.21.3-alpine name: mysql-5.7-benchmark-integration-tests - commands: - apk add --update build-base @@ -1347,7 +1347,7 @@ steps: environment: GRAFANA_TEST_DB: mysql MYSQL_HOST: mysql80 - image: golang:1.20.10-alpine + image: golang:1.21.3-alpine name: mysql-8.0-benchmark-integration-tests trigger: event: @@ -1424,7 +1424,7 @@ steps: - apk add --update make - CODEGEN_VERIFY=1 make gen-cue depends_on: [] - image: golang:1.20.10-alpine + image: golang:1.21.3-alpine name: verify-gen-cue trigger: branch: main @@ -1596,7 +1596,7 @@ steps: - apk add --update make - CODEGEN_VERIFY=1 make gen-cue depends_on: [] - image: golang:1.20.10-alpine + image: golang:1.21.3-alpine name: verify-gen-cue - commands: - '# It is required that generated jsonnet is committed and in sync with its inputs.' @@ -1605,21 +1605,21 @@ steps: - apk add --update make - CODEGEN_VERIFY=1 make gen-jsonnet depends_on: [] - image: golang:1.20.10-alpine + image: golang:1.21.3-alpine name: verify-gen-jsonnet - commands: - apk add --update make - make gen-go depends_on: - verify-gen-cue - image: golang:1.20.10-alpine + image: golang:1.21.3-alpine name: wire-install - commands: - apk add --update build-base shared-mime-info shared-mime-info-lang - go test -tags requires_buildifer -short -covermode=atomic -timeout=5m ./pkg/... depends_on: - wire-install - image: golang:1.20.10-alpine + image: golang:1.21.3-alpine name: test-backend - commands: - apk add --update build-base @@ -1628,7 +1628,7 @@ steps: | grep -o '\(.*\)/' | sort -u) depends_on: - wire-install - image: golang:1.20.10-alpine + image: golang:1.21.3-alpine name: test-backend-integration trigger: branch: main @@ -1672,13 +1672,13 @@ steps: depends_on: [] environment: CGO_ENABLED: 0 - image: golang:1.20.10-alpine + image: golang:1.21.3-alpine name: compile-build-cmd - commands: - apk add --update make - make gen-go depends_on: [] - image: golang:1.20.10-alpine + image: golang:1.21.3-alpine name: wire-install - commands: - apk add --update make build-base @@ -1687,16 +1687,16 @@ steps: - wire-install environment: CGO_ENABLED: "1" - image: golang:1.20.10-alpine + image: golang:1.21.3-alpine name: lint-backend - commands: - go run scripts/modowners/modowners.go check go.mod - image: golang:1.20.10-alpine + image: golang:1.21.3-alpine name: validate-modfile - commands: - apk add --update make - make swagger-validate - image: golang:1.20.10-alpine + image: golang:1.21.3-alpine name: validate-openapi-spec - commands: - ./bin/build verify-drone @@ -1752,7 +1752,7 @@ steps: depends_on: [] environment: CGO_ENABLED: 0 - image: golang:1.20.10-alpine + image: golang:1.21.3-alpine name: compile-build-cmd - commands: - '# It is required that code generated from Thema/CUE be committed and in sync @@ -1762,7 +1762,7 @@ steps: - apk add --update make - CODEGEN_VERIFY=1 make gen-cue depends_on: [] - image: golang:1.20.10-alpine + image: golang:1.21.3-alpine name: verify-gen-cue - commands: - '# It is required that generated jsonnet is committed and in sync with its inputs.' @@ -1771,14 +1771,14 @@ steps: - apk add --update make - CODEGEN_VERIFY=1 make gen-jsonnet depends_on: [] - image: golang:1.20.10-alpine + image: golang:1.21.3-alpine name: verify-gen-jsonnet - commands: - apk add --update make - make gen-go depends_on: - verify-gen-cue - image: golang:1.20.10-alpine + image: golang:1.21.3-alpine name: wire-install - commands: - yarn install --immutable @@ -1810,7 +1810,7 @@ steps: name: build-frontend-packages - commands: - /src/grafana-build artifacts -a targz:grafana:linux/amd64 -a targz:grafana:linux/arm64 - --go-version=1.20.10 --yarn-cache=$$YARN_CACHE_FOLDER --build-id=$$DRONE_BUILD_NUMBER + --go-version=1.21.3 --yarn-cache=$$YARN_CACHE_FOLDER --build-id=$$DRONE_BUILD_NUMBER --grafana-dir=$$PWD > packages.txt depends_on: - update-package-json-version @@ -2193,7 +2193,7 @@ steps: depends_on: [] environment: CGO_ENABLED: 0 - image: golang:1.20.10-alpine + image: golang:1.21.3-alpine name: compile-build-cmd - commands: - echo $DRONE_RUNNER_NAME @@ -2207,7 +2207,7 @@ steps: - apk add --update make - CODEGEN_VERIFY=1 make gen-cue depends_on: [] - image: golang:1.20.10-alpine + image: golang:1.21.3-alpine name: verify-gen-cue - commands: - '# It is required that generated jsonnet is committed and in sync with its inputs.' @@ -2216,14 +2216,14 @@ steps: - apk add --update make - CODEGEN_VERIFY=1 make gen-jsonnet depends_on: [] - image: golang:1.20.10-alpine + image: golang:1.21.3-alpine name: verify-gen-jsonnet - commands: - apk add --update make - make gen-go depends_on: - verify-gen-cue - image: golang:1.20.10-alpine + image: golang:1.21.3-alpine name: wire-install - commands: - dockerize -wait tcp://postgres:5432 -timeout 120s @@ -2244,7 +2244,7 @@ steps: GRAFANA_TEST_DB: postgres PGPASSWORD: grafanatest POSTGRES_HOST: postgres - image: golang:1.20.10-alpine + image: golang:1.21.3-alpine name: postgres-integration-tests - commands: - dockerize -wait tcp://mysql57:3306 -timeout 120s @@ -2265,7 +2265,7 @@ steps: environment: GRAFANA_TEST_DB: mysql MYSQL_HOST: mysql57 - image: golang:1.20.10-alpine + image: golang:1.21.3-alpine name: mysql-5.7-integration-tests - commands: - dockerize -wait tcp://mysql80:3306 -timeout 120s @@ -2286,7 +2286,7 @@ steps: environment: GRAFANA_TEST_DB: mysql MYSQL_HOST: mysql80 - image: golang:1.20.10-alpine + image: golang:1.21.3-alpine name: mysql-8.0-integration-tests - commands: - dockerize -wait tcp://redis:6379 -timeout 120s @@ -2301,7 +2301,7 @@ steps: - wait-for-redis environment: REDIS_URL: redis://redis:6379/0 - image: golang:1.20.10-alpine + image: golang:1.21.3-alpine name: redis-integration-tests - commands: - dockerize -wait tcp://memcached:11211 -timeout 120s @@ -2316,7 +2316,7 @@ steps: - wait-for-memcached environment: MEMCACHED_HOSTS: memcached:11211 - image: golang:1.20.10-alpine + image: golang:1.21.3-alpine name: memcached-integration-tests - commands: - dockerize -wait tcp://mimir_backend:8080 -timeout 120s @@ -2333,7 +2333,7 @@ steps: AM_PASSWORD: test AM_TENANT_ID: test AM_URL: http://mimir_backend:8080 - image: golang:1.20.10-alpine + image: golang:1.21.3-alpine name: remote-alertmanager-integration-tests trigger: branch: main @@ -2523,7 +2523,7 @@ steps: depends_on: [] environment: CGO_ENABLED: 0 - image: golang:1.20.10-alpine + image: golang:1.21.3-alpine name: compile-build-cmd - commands: - ./bin/build artifacts docker fetch --edition oss @@ -2619,7 +2619,7 @@ steps: depends_on: [] environment: CGO_ENABLED: 0 - image: golang:1.20.10-alpine + image: golang:1.21.3-alpine name: compile-build-cmd - commands: - ./bin/build artifacts packages --tag $${DRONE_TAG} --src-bucket $${PRERELEASE_BUCKET} @@ -2688,7 +2688,7 @@ steps: depends_on: [] environment: CGO_ENABLED: 0 - image: golang:1.20.10-alpine + image: golang:1.21.3-alpine name: compile-build-cmd - commands: - yarn install --immutable @@ -2753,7 +2753,7 @@ steps: depends_on: [] environment: CGO_ENABLED: 0 - image: golang:1.20.10-alpine + image: golang:1.21.3-alpine name: compile-build-cmd - depends_on: - compile-build-cmd @@ -2859,7 +2859,7 @@ steps: from_secret: gcp_key_base64 GITHUB_TOKEN: from_secret: github_token - GO_VERSION: 1.20.10 + GO_VERSION: 1.21.3 GPG_PASSPHRASE: from_secret: packages_gpg_passphrase GPG_PRIVATE_KEY: @@ -2916,13 +2916,13 @@ steps: depends_on: [] environment: CGO_ENABLED: 0 - image: golang:1.20.10-alpine + image: golang:1.21.3-alpine name: compile-build-cmd - commands: - ./bin/build whatsnew-checker depends_on: - compile-build-cmd - image: golang:1.20.10-alpine + image: golang:1.21.3-alpine name: whats-new-checker trigger: event: @@ -3022,7 +3022,7 @@ steps: - apk add --update make - CODEGEN_VERIFY=1 make gen-cue depends_on: [] - image: golang:1.20.10-alpine + image: golang:1.21.3-alpine name: verify-gen-cue - commands: - '# It is required that generated jsonnet is committed and in sync with its inputs.' @@ -3031,21 +3031,21 @@ steps: - apk add --update make - CODEGEN_VERIFY=1 make gen-jsonnet depends_on: [] - image: golang:1.20.10-alpine + image: golang:1.21.3-alpine name: verify-gen-jsonnet - commands: - apk add --update make - make gen-go depends_on: - verify-gen-cue - image: golang:1.20.10-alpine + image: golang:1.21.3-alpine name: wire-install - commands: - apk add --update build-base shared-mime-info shared-mime-info-lang - go test -tags requires_buildifer -short -covermode=atomic -timeout=5m ./pkg/... depends_on: - wire-install - image: golang:1.20.10-alpine + image: golang:1.21.3-alpine name: test-backend - commands: - apk add --update build-base @@ -3054,7 +3054,7 @@ steps: | grep -o '\(.*\)/' | sort -u) depends_on: - wire-install - image: golang:1.20.10-alpine + image: golang:1.21.3-alpine name: test-backend-integration trigger: event: @@ -3110,7 +3110,7 @@ steps: from_secret: gcp_key_base64 GITHUB_TOKEN: from_secret: github_token - GO_VERSION: 1.20.10 + GO_VERSION: 1.21.3 GPG_PASSPHRASE: from_secret: packages_gpg_passphrase GPG_PRIVATE_KEY: @@ -3290,7 +3290,7 @@ steps: from_secret: gcp_key_base64 GITHUB_TOKEN: from_secret: github_token - GO_VERSION: 1.20.10 + GO_VERSION: 1.21.3 GPG_PASSPHRASE: from_secret: packages_gpg_passphrase GPG_PRIVATE_KEY: @@ -3436,7 +3436,7 @@ steps: - apk add --update make - CODEGEN_VERIFY=1 make gen-cue depends_on: [] - image: golang:1.20.10-alpine + image: golang:1.21.3-alpine name: verify-gen-cue - commands: - '# It is required that generated jsonnet is committed and in sync with its inputs.' @@ -3445,21 +3445,21 @@ steps: - apk add --update make - CODEGEN_VERIFY=1 make gen-jsonnet depends_on: [] - image: golang:1.20.10-alpine + image: golang:1.21.3-alpine name: verify-gen-jsonnet - commands: - apk add --update make - make gen-go depends_on: - verify-gen-cue - image: golang:1.20.10-alpine + image: golang:1.21.3-alpine name: wire-install - commands: - apk add --update build-base shared-mime-info shared-mime-info-lang - go test -tags requires_buildifer -short -covermode=atomic -timeout=5m ./pkg/... depends_on: - wire-install - image: golang:1.20.10-alpine + image: golang:1.21.3-alpine name: test-backend - commands: - apk add --update build-base @@ -3468,7 +3468,7 @@ steps: | grep -o '\(.*\)/' | sort -u) depends_on: - wire-install - image: golang:1.20.10-alpine + image: golang:1.21.3-alpine name: test-backend-integration trigger: cron: @@ -3522,7 +3522,7 @@ steps: from_secret: gcp_key_base64 GITHUB_TOKEN: from_secret: github_token - GO_VERSION: 1.20.10 + GO_VERSION: 1.21.3 GPG_PASSPHRASE: from_secret: packages_gpg_passphrase GPG_PRIVATE_KEY: @@ -3668,7 +3668,7 @@ steps: from_secret: gcp_key_base64 GITHUB_TOKEN: from_secret: github_token - GO_VERSION: 1.20.10 + GO_VERSION: 1.21.3 GPG_PASSPHRASE: from_secret: packages_gpg_passphrase GPG_PRIVATE_KEY: @@ -3765,20 +3765,20 @@ steps: - commands: [] depends_on: - clone - image: golang:1.20.10-windowsservercore-1809 + image: golang:1.21.3-windowsservercore-1809 name: windows-init - commands: - go install github.com/google/wire/cmd/wire@v0.5.0 - wire gen -tags oss ./pkg/server depends_on: - windows-init - image: golang:1.20.10-windowsservercore-1809 + image: golang:1.21.3-windowsservercore-1809 name: wire-install - commands: - go test -tags requires_buildifer -short -covermode=atomic -timeout=5m ./pkg/... depends_on: - wire-install - image: golang:1.20.10-windowsservercore-1809 + image: golang:1.21.3-windowsservercore-1809 name: test-backend trigger: event: @@ -3870,7 +3870,7 @@ steps: - apk add --update make - CODEGEN_VERIFY=1 make gen-cue depends_on: [] - image: golang:1.20.10-alpine + image: golang:1.21.3-alpine name: verify-gen-cue - commands: - '# It is required that generated jsonnet is committed and in sync with its inputs.' @@ -3879,14 +3879,14 @@ steps: - apk add --update make - CODEGEN_VERIFY=1 make gen-jsonnet depends_on: [] - image: golang:1.20.10-alpine + image: golang:1.21.3-alpine name: verify-gen-jsonnet - commands: - apk add --update make - make gen-go depends_on: - verify-gen-cue - image: golang:1.20.10-alpine + image: golang:1.21.3-alpine name: wire-install - commands: - dockerize -wait tcp://postgres:5432 -timeout 120s @@ -3907,7 +3907,7 @@ steps: GRAFANA_TEST_DB: postgres PGPASSWORD: grafanatest POSTGRES_HOST: postgres - image: golang:1.20.10-alpine + image: golang:1.21.3-alpine name: postgres-integration-tests - commands: - dockerize -wait tcp://mysql57:3306 -timeout 120s @@ -3928,7 +3928,7 @@ steps: environment: GRAFANA_TEST_DB: mysql MYSQL_HOST: mysql57 - image: golang:1.20.10-alpine + image: golang:1.21.3-alpine name: mysql-5.7-integration-tests - commands: - dockerize -wait tcp://mysql80:3306 -timeout 120s @@ -3949,7 +3949,7 @@ steps: environment: GRAFANA_TEST_DB: mysql MYSQL_HOST: mysql80 - image: golang:1.20.10-alpine + image: golang:1.21.3-alpine name: mysql-8.0-integration-tests - commands: - dockerize -wait tcp://redis:6379 -timeout 120s @@ -3964,7 +3964,7 @@ steps: - wait-for-redis environment: REDIS_URL: redis://redis:6379/0 - image: golang:1.20.10-alpine + image: golang:1.21.3-alpine name: redis-integration-tests - commands: - dockerize -wait tcp://memcached:11211 -timeout 120s @@ -3979,7 +3979,7 @@ steps: - wait-for-memcached environment: MEMCACHED_HOSTS: memcached:11211 - image: golang:1.20.10-alpine + image: golang:1.21.3-alpine name: memcached-integration-tests - commands: - dockerize -wait tcp://mimir_backend:8080 -timeout 120s @@ -3996,7 +3996,7 @@ steps: AM_PASSWORD: test AM_TENANT_ID: test AM_URL: http://mimir_backend:8080 - image: golang:1.20.10-alpine + image: golang:1.21.3-alpine name: remote-alertmanager-integration-tests trigger: event: @@ -4402,7 +4402,7 @@ steps: path: /root/.docker/ - commands: - trivy --exit-code 0 --severity UNKNOWN,LOW,MEDIUM alpine/git:2.40.1 - - trivy --exit-code 0 --severity UNKNOWN,LOW,MEDIUM golang:1.20.10-alpine + - trivy --exit-code 0 --severity UNKNOWN,LOW,MEDIUM golang:1.21.3-alpine - trivy --exit-code 0 --severity UNKNOWN,LOW,MEDIUM node:20.9.0-alpine - trivy --exit-code 0 --severity UNKNOWN,LOW,MEDIUM google/cloud-sdk:431.0.0 - trivy --exit-code 0 --severity UNKNOWN,LOW,MEDIUM grafana/grafana-ci-deploy:1.3.3 @@ -4436,7 +4436,7 @@ steps: path: /root/.docker/ - commands: - trivy --exit-code 1 --severity HIGH,CRITICAL alpine/git:2.40.1 - - trivy --exit-code 1 --severity HIGH,CRITICAL golang:1.20.10-alpine + - trivy --exit-code 1 --severity HIGH,CRITICAL golang:1.21.3-alpine - trivy --exit-code 1 --severity HIGH,CRITICAL node:20.9.0-alpine - trivy --exit-code 1 --severity HIGH,CRITICAL google/cloud-sdk:431.0.0 - trivy --exit-code 1 --severity HIGH,CRITICAL grafana/grafana-ci-deploy:1.3.3 diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index f363aa6efbfbc..81b5d3e891b7a 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -635,7 +635,6 @@ embed.go @grafana/grafana-as-code /.github/workflows/issue-opened.yml @grafana/grafana-community-support /.github/workflows/metrics-collector.yml @torkelo /.github/workflows/milestone.yml @marefr -/.github/workflows/ox-code-coverage.yml @grafana/explore-squad /.github/workflows/pr-checks.yml @marefr /.github/workflows/pr-codeql-analysis-go.yml @DanCech /.github/workflows/pr-codeql-analysis-javascript.yml @DanCech diff --git a/.github/workflows/alerting-swagger-gen.yml b/.github/workflows/alerting-swagger-gen.yml index f42389ceeed26..3ccb24413f16d 100644 --- a/.github/workflows/alerting-swagger-gen.yml +++ b/.github/workflows/alerting-swagger-gen.yml @@ -16,7 +16,7 @@ jobs: - name: Set go version uses: actions/setup-go@v4 with: - go-version: '1.20.10' + go-version: '1.21.3' - name: Build swagger run: | make -C pkg/services/ngalert/api/tooling post.json api.json diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 60fe93679cf42..77f8d20905d97 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -47,7 +47,7 @@ jobs: name: Set go version uses: actions/setup-go@v4 with: - go-version: '1.20.10' + go-version: '1.21.3' # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL diff --git a/.github/workflows/ox-code-coverage.yml b/.github/workflows/ox-code-coverage.yml deleted file mode 100644 index acc72ed68528e..0000000000000 --- a/.github/workflows/ox-code-coverage.yml +++ /dev/null @@ -1,21 +0,0 @@ -name: Observability Experience test code coverage -on: - pull_request: - paths: - - 'pkg/services/queryhistory/**' - - 'pkg/tsdb/loki/**' - - 'pkg/tsdb/elasticsearch/**' - - 'public/app/features/explore/**' - - 'public/app/features/correlations/**' - - 'public/app/plugins/datasource/loki/**' - - 'public/app/plugins/datasource/elasticsearch/**' - branches-ignore: - - dependabot/** - - backport-* - -jobs: - workflow-call: - uses: grafana/code-coverage/.github/workflows/code-coverage.yml@v0.1.20 - with: - frontend-path-regexp: public\/app\/features\/(explore|correlations)|public\/app\/plugins\/datasource\/(loki|elasticsearch) - backend-path-regexp: pkg\/services\/(queryhistory)|pkg\/tsdb\/(loki|elasticsearch) diff --git a/.github/workflows/pr-codeql-analysis-go.yml b/.github/workflows/pr-codeql-analysis-go.yml index 26b45ad58145f..5a4c4d4c63102 100644 --- a/.github/workflows/pr-codeql-analysis-go.yml +++ b/.github/workflows/pr-codeql-analysis-go.yml @@ -26,7 +26,7 @@ jobs: - name: Set go version uses: actions/setup-go@v4 with: - go-version: '1.20.10' + go-version: '1.21.3' # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL diff --git a/.github/workflows/publish-kinds-next.yml b/.github/workflows/publish-kinds-next.yml index 97c70377ac559..160384c35a8a6 100644 --- a/.github/workflows/publish-kinds-next.yml +++ b/.github/workflows/publish-kinds-next.yml @@ -36,7 +36,7 @@ jobs: - name: "Setup Go" uses: "actions/setup-go@v4" with: - go-version: '1.20.10' + go-version: '1.21.3' - name: "Verify kinds" run: go run .github/workflows/scripts/kinds/verify-kinds.go diff --git a/.github/workflows/publish-kinds-release.yml b/.github/workflows/publish-kinds-release.yml index 30516f062d895..d2c6ea1b904a3 100644 --- a/.github/workflows/publish-kinds-release.yml +++ b/.github/workflows/publish-kinds-release.yml @@ -39,7 +39,7 @@ jobs: - name: "Setup Go" uses: "actions/setup-go@v4" with: - go-version: '1.20.10' + go-version: '1.21.3' - name: "Verify kinds" run: go run .github/workflows/scripts/kinds/verify-kinds.go diff --git a/.github/workflows/verify-kinds.yml b/.github/workflows/verify-kinds.yml index 7ea1bb46a1c38..030d98d9f1052 100644 --- a/.github/workflows/verify-kinds.yml +++ b/.github/workflows/verify-kinds.yml @@ -18,7 +18,7 @@ jobs: - name: "Setup Go" uses: "actions/setup-go@v4" with: - go-version: '1.20.10' + go-version: '1.21.3' - name: "Verify kinds" run: go run .github/workflows/scripts/kinds/verify-kinds.go diff --git a/Dockerfile b/Dockerfile index 3e558c6c6ce91..f39cf43c80fb3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,7 +3,7 @@ ARG BASE_IMAGE=alpine:3.18.3 ARG JS_IMAGE=node:20-alpine3.18 ARG JS_PLATFORM=linux/amd64 -ARG GO_IMAGE=golang:1.20.10-alpine3.18 +ARG GO_IMAGE=golang:1.21.3-alpine3.18 ARG GO_SRC=go-builder ARG JS_SRC=js-builder diff --git a/Makefile b/Makefile index d26b3b4a1a2c8..686e7f55b4d34 100644 --- a/Makefile +++ b/Makefile @@ -261,7 +261,7 @@ build-docker-full-ubuntu: ## Build Docker image based on Ubuntu for development. --build-arg COMMIT_SHA=$$(git rev-parse HEAD) \ --build-arg BUILD_BRANCH=$$(git rev-parse --abbrev-ref HEAD) \ --build-arg BASE_IMAGE=ubuntu:22.04 \ - --build-arg GO_IMAGE=golang:1.20.10 \ + --build-arg GO_IMAGE=golang:1.21.3 \ --tag grafana/grafana$(TAG_SUFFIX):dev-ubuntu \ $(DOCKER_BUILD_ARGS) diff --git a/go.mod b/go.mod index 752461e37df41..14b37ffc1c780 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/grafana/grafana -go 1.20 +go 1.21 // Override docker/docker to avoid: // go: github.com/drone-runners/drone-runner-docker@v1.8.2 requires diff --git a/go.sum b/go.sum index 448cfe5e7aef6..9de11b6e8705a 100644 --- a/go.sum +++ b/go.sum @@ -837,7 +837,9 @@ github.com/boombuler/barcode v1.0.1/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl github.com/bradfitz/gomemcache v0.0.0-20190913173617-a41fca850d0b h1:L/QXpzIa3pOvUGt1D1lA5KjYhPBAN/3iWdP7xeFS9F0= github.com/bradfitz/gomemcache v0.0.0-20190913173617-a41fca850d0b/go.mod h1:H0wQNHz2YrLsuXOZozoeDmnHXkNCRmMW0gwFWDfEZDA= github.com/bsm/ginkgo/v2 v2.5.0 h1:aOAnND1T40wEdAtkGSkvSICWeQ8L3UASX7YVCqQx+eQ= +github.com/bsm/ginkgo/v2 v2.5.0/go.mod h1:AiKlXPm7ItEHNc/2+OkrNG4E0ITzojb9/xWzvQ9XZ9w= github.com/bsm/gomega v1.20.0 h1:JhAwLmtRzXFTx2AkALSLa8ijZafntmhSoU63Ok18Uq8= +github.com/bsm/gomega v1.20.0/go.mod h1:JifAceMQ4crZIWYUKrlGcmbN3bqHogVTADMD2ATsbwk= github.com/bufbuild/connect-go v1.10.0 h1:QAJ3G9A1OYQW2Jbk3DeoJbkCxuKArrvZgDt47mjdTbg= github.com/bufbuild/connect-go v1.10.0/go.mod h1:CAIePUgkDR5pAFaylSMtNK45ANQjp9JvpluG20rhpV8= github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= @@ -902,6 +904,7 @@ github.com/cncf/xds/go v0.0.0-20220314180256-7f1daf1720fc/go.mod h1:eXthEFrGJvWH github.com/cncf/xds/go v0.0.0-20230105202645-06c439db220b/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20230112175826-46e39c7b9b43/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20230607035331-e9ce68804cb4 h1:/inchEIKaYC1Akx+H+gqO04wryn5h75LSazbRlnya1k= +github.com/cncf/xds/go v0.0.0-20230607035331-e9ce68804cb4/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= github.com/cockroachdb/apd/v2 v2.0.2 h1:weh8u7Cneje73dDh+2tEVLUvyBc89iwepWCD8b8034E= github.com/cockroachdb/apd/v2 v2.0.2/go.mod h1:DDxRlzC2lo3/vSlmSoS7JkqbbrARPuFOGr0B9pvN3Gw= @@ -1061,6 +1064,7 @@ github.com/elazarl/goproxy v0.0.0-20230731152917-f99041a5c027 h1:1L0aalTpPz7YlMx github.com/elazarl/goproxy v0.0.0-20230731152917-f99041a5c027/go.mod h1:Ro8st/ElPeALwNFlcTpWmkr6IoMFfkjXAvTHpevnDsM= github.com/elazarl/goproxy/ext v0.0.0-20190711103511-473e67f1d7d2/go.mod h1:gNh8nYJoAm43RfaxurUnxr+N1PwuFV3ZMl/efxlIlY8= github.com/elazarl/goproxy/ext v0.0.0-20220115173737-adb46da277ac h1:9yrT5tmn9Zc0ytWPASlaPwQfQMQYnRf0RSDe1XvHw0Q= +github.com/elazarl/goproxy/ext v0.0.0-20220115173737-adb46da277ac/go.mod h1:gNh8nYJoAm43RfaxurUnxr+N1PwuFV3ZMl/efxlIlY8= github.com/emicklei/go-restful/v3 v3.8.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/emicklei/go-restful/v3 v3.9.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/emicklei/go-restful/v3 v3.10.1 h1:rc42Y5YTp7Am7CS630D7JmhRjq4UlEUuEKfrDac4bSQ= @@ -1082,10 +1086,12 @@ github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go. github.com/envoyproxy/go-control-plane v0.10.3/go.mod h1:fJJn/j26vwOu972OllsvAgJJM//w9BV6Fxbg2LuVd34= github.com/envoyproxy/go-control-plane v0.11.0/go.mod h1:VnHyVMpzcLvCFt9yUz1UnCwHLhwx1WguiVDV7pTG/tI= github.com/envoyproxy/go-control-plane v0.11.1 h1:wSUXTlLfiAQRWs2F+p+EKOY9rUyis1MyGqJ2DIk5HpM= +github.com/envoyproxy/go-control-plane v0.11.1/go.mod h1:uhMcXKCQMEJHiAb0w+YGefQLaTEw+YhGluxZkrTmD0g= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/envoyproxy/protoc-gen-validate v0.6.7/go.mod h1:dyJXwwfPK2VSqiB9Klm1J6romD608Ba7Hij42vrOBCo= github.com/envoyproxy/protoc-gen-validate v0.9.1/go.mod h1:OKNgG7TCp5pF4d6XftA0++PMirau2/yoOwVac3AbF2w= github.com/envoyproxy/protoc-gen-validate v1.0.2 h1:QkIBuU5k+x7/QXPvPPnWXWlCdaBFApVqftFV6k087DA= +github.com/envoyproxy/protoc-gen-validate v1.0.2/go.mod h1:GpiZQP3dDbg4JouG/NNS7QWXpgx6x8QiMKdmN72jogE= github.com/etcd-io/bbolt v1.3.3/go.mod h1:ZF2nL25h33cCyBtcyWeZ2/I3HQOfTP+0PIEvHjkjCrw= github.com/evanphx/json-patch v4.12.0+incompatible h1:4onqiflcdA9EOZ4RxV643DvftH5pOlLGNtQ5lPWQu84= github.com/evanphx/json-patch v4.12.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= @@ -1193,6 +1199,7 @@ github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbV github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-logr/zapr v1.2.3 h1:a9vnzlIBPQBBkeaR9IuMUfmVOrQlkoC4YfPoFkX3T7A= +github.com/go-logr/zapr v1.2.3/go.mod h1:eIauM6P8qSvTw5o2ez6UEAfGjQKrxQTl5EoK+Qa2oG4= github.com/go-martini/martini v0.0.0-20170121215854-22fa46961aab/go.mod h1:/P9AEU963A2AYjv4d1V5eVL1CQbEJq6aCNHDDjibzu8= github.com/go-openapi/analysis v0.0.0-20180825180245-b006789cd277/go.mod h1:k70tL6pCuVxPJOHXQ+wIac1FUrvNkHolPie/cLEU6hI= github.com/go-openapi/analysis v0.17.0/go.mod h1:IowGgpVeD0vNm45So8nr+IcQ3pxVtpRoBWb8PVZO0ik= @@ -1314,7 +1321,9 @@ github.com/go-stack/stack v1.8.1 h1:ntEHSVwIt7PNXNpgPmVfMrNhLtgjlmnZha2kOpuRiDw= github.com/go-stack/stack v1.8.1/go.mod h1:dcoOX6HbPZSZptuspn9bctJ+N/CnF5gGygcUP3XYfe4= github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= +github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM= +github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= github.com/go-xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a h1:9wScpmSP5A3Bk8V3XHWUcJmYTh+ZnlHVyc+A4oZYS3Y= github.com/go-xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a/go.mod h1:56xuuqnHyryaerycW3BfssRdxQstACi0Epw/yC5E2xM= github.com/go-zookeeper/zk v1.0.2/go.mod h1:nOB03cncLtlp4t+UAkGSV+9beXP/akpekBwL+UX1Qcw= @@ -1668,6 +1677,7 @@ github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEW github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golangci/lint-1 v0.0.0-20181222135242-d2cdd8c08219 h1:utua3L2IbQJmauC5IXdEA547bcoU5dozgQAfc8Onsg4= +github.com/golangci/lint-1 v0.0.0-20181222135242-d2cdd8c08219/go.mod h1:/X8TswGSh1pIozq4ZwCfxS0WA5JGXguxk94ar/4c87Y= github.com/gomodule/redigo v1.7.1-0.20190724094224-574c33c3df38/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= @@ -2348,6 +2358,7 @@ github.com/moby/spdystream v0.2.0/go.mod h1:f7i0iNDQJ059oMTcWxx8MA/zKFIuD/lY+0Gq github.com/moby/term v0.0.0-20200915141129-7f0af18e79f2/go.mod h1:TjQg8pa4iejrUrjiz0MCtMV38jdMNW4doKSiBrEvCQQ= github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6/go.mod h1:E2VnQOmVuvZB6UYnnDB0qG5Nq/1tD9acaOpo6xmt0Kw= github.com/moby/term v0.0.0-20221205130635-1aeaba878587 h1:HfkjXDfhgVaN5rmueG8cL8KKeFNecRCXFhaJ2qZ5SKA= +github.com/moby/term v0.0.0-20221205130635-1aeaba878587/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -2405,6 +2416,7 @@ github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4= github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= github.com/oleiade/reflections v1.0.0/go.mod h1:RbATFBbKYkVdqmSFtx13Bb/tVhR0lgOBXunWTZKeL4w= github.com/oleiade/reflections v1.0.1 h1:D1XO3LVEYroYskEsoSiGItp9RUxG6jWnCVvrqH0HHQM= +github.com/oleiade/reflections v1.0.1/go.mod h1:rdFxbxq4QXVZWj0F+e9jqjDkc7dbp97vkRixKo2JR60= github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= @@ -2418,12 +2430,14 @@ github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108 github.com/onsi/ginkgo v1.16.2/go.mod h1:CObGmKUOKaSC0RjmoAK7tKyn4Azo5P2IWuoMnvwxz1E= github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= +github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= github.com/onsi/ginkgo/v2 v2.1.3/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c= github.com/onsi/ginkgo/v2 v2.1.4/go.mod h1:um6tUpWM/cxCK3/FK8BXqEiUMUwRgSM4JXG47RKZmLU= github.com/onsi/ginkgo/v2 v2.1.6/go.mod h1:MEH45j8TBi6u9BMogfbp0stKC5cdGjumZj5Y7AG4VIk= github.com/onsi/ginkgo/v2 v2.3.0/go.mod h1:Eew0uilEqZmIEZr8JrvYlvOM7Rr6xzTmMV8AyFNU9d0= github.com/onsi/ginkgo/v2 v2.4.0/go.mod h1:iHkDK1fKGcBoEHT5W7YBq4RFWaQulw+caOMkAt4OrFo= github.com/onsi/ginkgo/v2 v2.11.0 h1:WgqUCUt/lT6yXoQ8Wef0fsNn5cAuMK7+KT9UFRz2tcU= +github.com/onsi/ginkgo/v2 v2.11.0/go.mod h1:ZhrRA5XmEE3x3rhlzamx/JJvujdZoJ2uvgI7kR0iZvM= github.com/onsi/gomega v1.4.1/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= github.com/onsi/gomega v1.4.2/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= @@ -2440,6 +2454,7 @@ github.com/onsi/gomega v1.21.1/go.mod h1:iYAIXgPSaDHak0LCMA+AWBpIKBr8WZicMxnE8lu github.com/onsi/gomega v1.22.1/go.mod h1:x6n7VNe4hw0vkyYUM4mjIXx3JbLiPaBPNgB7PRQ1tuM= github.com/onsi/gomega v1.23.0/go.mod h1:Z/NWtiqwBrwUt4/2loMmHL63EDLnYHmVbuBpDr2vQAg= github.com/onsi/gomega v1.27.10 h1:naR28SdDFlqrG6kScpT8VWpu1xWY5nJRCF3XaYyBjhI= +github.com/onsi/gomega v1.27.10/go.mod h1:RsS8tutOdbdgzbPtzzATp12yT7kM5I5aElG3evPbQ0M= github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk= github.com/opencontainers/go-digest v1.0.0-rc1/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= @@ -2830,12 +2845,14 @@ github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhV github.com/tidwall/pretty v1.0.2/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= github.com/tidwall/pretty v1.1.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs= +github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/tidwall/sjson v1.0.4/go.mod h1:bURseu1nuBkFpIES5cz6zBtjmYeOQmEESshn7VpF15Y= github.com/tidwall/sjson v1.1.5/go.mod h1:VuJzsZnTowhSxWdOgsAnb886i4AjEyTkk7tNtsL7EYE= github.com/tinylib/msgp v1.1.2/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE= github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/tmc/grpc-websocket-proxy v0.0.0-20220101234140-673ab2c3ae75 h1:6fotK7otjonDflCTK0BCfls4SPy3NcCVb5dqqmbRknE= +github.com/tmc/grpc-websocket-proxy v0.0.0-20220101234140-673ab2c3ae75/go.mod h1:KO6IkyS8Y3j8OdNO85qEYBsRPuteD+YciPomcXdrMnk= github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= github.com/ua-parser/uap-go v0.0.0-20211112212520-00c877edfe0f h1:A+MmlgpvrHLeUP8dkBVn4Pnf5Bp5Yk2OALm7SEJLLE8= github.com/ua-parser/uap-go v0.0.0-20211112212520-00c877edfe0f/go.mod h1:OBcG9bn7sHtXgarhUEb3OfCnNsgtGnkVf41ilSZ3K3E= @@ -2855,6 +2872,7 @@ github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVM github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= github.com/ugorji/go/codec v1.2.7 h1:YPXUKf7fYbp/y8xloBqZOw2qaVggbfwMlI8WM3wZUJ0= +github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY= github.com/unknwon/bra v0.0.0-20200517080246-1e3013ecaff8 h1:aVGB3YnaS/JNfOW3tiHIlmNmTDg618va+eT0mVomgyI= github.com/unknwon/bra v0.0.0-20200517080246-1e3013ecaff8/go.mod h1:fVle4kNr08ydeohzYafr20oZzbAkhQT39gKK/pFQ5M4= github.com/unknwon/com v1.0.1 h1:3d1LTxD+Lnf3soQiD4Cp/0BRB+Rsa/+RTvz8GMMzIXs= @@ -2951,6 +2969,7 @@ go.elastic.co/fastjson v1.0.0/go.mod h1:PmeUOMMtLHQr9ZS9J9owrAVg0FkaZDRZJEFTTGHt go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= go.etcd.io/bbolt v1.3.6 h1:/ecaJf0sk1l4l6V4awd65v2C3ILy7MSj+s/x1ADCIMU= +go.etcd.io/bbolt v1.3.6/go.mod h1:qXsaaIqmgQH0T+OPdb99Bf+PKfBBQVAdyD6TY9G8XM4= go.etcd.io/etcd v0.0.0-20191023171146-3cf2f69b5738 h1:VcrIfasaLFkyjk6KNlXQSzO+B0fZcnECiDrKJsfxka0= go.etcd.io/etcd v0.0.0-20191023171146-3cf2f69b5738/go.mod h1:dnLIgRNXwCJa5e+c6mIZCrds/GIG4ncV9HhK5PX7jPg= go.etcd.io/etcd/api/v3 v3.5.0/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs= @@ -2964,13 +2983,17 @@ go.etcd.io/etcd/client/pkg/v3 v3.5.9/go.mod h1:y+CzeSmkMpWN2Jyu1npecjB9BBnABxGM4 go.etcd.io/etcd/client/v2 v2.305.0/go.mod h1:h9puh54ZTgAKtEbut2oe9P4L/oqKCVB6xsXlzd7alYQ= go.etcd.io/etcd/client/v2 v2.305.4/go.mod h1:Ud+VUwIi9/uQHOMA+4ekToJ12lTxlv0zB/+DHwTGEbU= go.etcd.io/etcd/client/v2 v2.305.7 h1:AELPkjNR3/igjbO7CjyF1fPuVPjrblliiKj+Y6xSGOU= +go.etcd.io/etcd/client/v2 v2.305.7/go.mod h1:GQGT5Z3TBuAQGvgPfhR7VPySu/SudxmEkRq9BgzFU6s= go.etcd.io/etcd/client/v3 v3.5.0/go.mod h1:AIKXXVX/DQXtfTEqBryiLTUXwON+GuvO6Z7lLS/oTh0= go.etcd.io/etcd/client/v3 v3.5.4/go.mod h1:ZaRkVgBZC+L+dLCjTcF1hRXpgZXQPOvnA/Ak/gq3kiY= go.etcd.io/etcd/client/v3 v3.5.9 h1:r5xghnU7CwbUxD/fbUtRyJGaYNfDun8sp/gTr1hew6E= go.etcd.io/etcd/client/v3 v3.5.9/go.mod h1:i/Eo5LrZ5IKqpbtpPDuaUnDOUv471oDg8cjQaUr2MbA= go.etcd.io/etcd/pkg/v3 v3.5.7 h1:obOzeVwerFwZ9trMWapU/VjDcYUJb5OfgC1zqEGWO/0= +go.etcd.io/etcd/pkg/v3 v3.5.7/go.mod h1:kcOfWt3Ov9zgYdOiJ/o1Y9zFfLhQjylTgL4Lru8opRo= go.etcd.io/etcd/raft/v3 v3.5.7 h1:aN79qxLmV3SvIq84aNTliYGmjwsW6NqJSnqmI1HLJKc= +go.etcd.io/etcd/raft/v3 v3.5.7/go.mod h1:TflkAb/8Uy6JFBxcRaH2Fr6Slm9mCPVdI2efzxY96yU= go.etcd.io/etcd/server/v3 v3.5.7 h1:BTBD8IJUV7YFgsczZMHhMTS67XuA4KpRquL0MFOJGRk= +go.etcd.io/etcd/server/v3 v3.5.7/go.mod h1:gxBgT84issUVBRpZ3XkW1T55NjOb4vZZRI4wVvNhf4A= go.mongodb.org/mongo-driver v1.0.3/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM= go.mongodb.org/mongo-driver v1.1.0/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM= go.mongodb.org/mongo-driver v1.1.1/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM= @@ -4225,12 +4248,14 @@ modernc.org/strutil v1.1.3 h1:fNMm+oJklMGYfU9Ylcywl0CO5O6nTfaowNsh2wpPjzY= modernc.org/strutil v1.1.3/go.mod h1:MEHNA7PdEnEwLvspRMtWTNnp2nnyvMfkimT1NKNAGbw= modernc.org/tcl v1.13.1/go.mod h1:XOLfOwzhkljL4itZkK6T72ckMgvj0BDsnKNdZVUOecw= modernc.org/tcl v1.15.0 h1:oY+JeD11qVVSgVvodMJsu7Edf8tr5E/7tuhF5cNYz34= +modernc.org/tcl v1.15.0/go.mod h1:xRoGotBZ6dU+Zo2tca+2EqVEeMmOUBzHnhIwq4YrVnE= modernc.org/token v1.0.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= modernc.org/xc v1.0.0/go.mod h1:mRNCo0bvLjGhHO9WsyuKVU4q0ceiDDDoEeWDJHrNx8I= modernc.org/z v1.5.1/go.mod h1:eWFB510QWW5Th9YGZT81s+LwvaAs3Q2yr4sP0rmLkv8= modernc.org/z v1.7.0 h1:xkDw/KepgEjeizO2sNco+hqYkU12taxQFqPEmgm1GWE= +modernc.org/z v1.7.0/go.mod h1:hVdgNMh8ggTuRG1rGU8x+xGRFfiQUIAw0ZqlPy8+HyQ= nhooyr.io/websocket v1.8.7/go.mod h1:B70DZP8IakI65RVQ51MsWP/8jndNma26DVA/nFSCgW0= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= diff --git a/pkg/api/frontendsettings.go b/pkg/api/frontendsettings.go index cdf4268a0dbe1..f561768200762 100644 --- a/pkg/api/frontendsettings.go +++ b/pkg/api/frontendsettings.go @@ -4,10 +4,9 @@ import ( "context" "fmt" "net/http" + "slices" "strings" - "golang.org/x/exp/slices" - "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/services/accesscontrol" diff --git a/pkg/expr/ml/outlier_test.go b/pkg/expr/ml/outlier_test.go index c383b00e26281..9974b0acfcef8 100644 --- a/pkg/expr/ml/outlier_test.go +++ b/pkg/expr/ml/outlier_test.go @@ -4,13 +4,13 @@ import ( "errors" "fmt" "net/http" + "slices" "testing" "time" "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "golang.org/x/exp/slices" "github.com/grafana/grafana/pkg/api/response" ) diff --git a/pkg/login/social/google_oauth.go b/pkg/login/social/google_oauth.go index f4957eb40964d..1303b12e5c744 100644 --- a/pkg/login/social/google_oauth.go +++ b/pkg/login/social/google_oauth.go @@ -5,9 +5,9 @@ import ( "encoding/json" "fmt" "net/http" + "slices" "strings" - "golang.org/x/exp/slices" "golang.org/x/oauth2" "github.com/grafana/grafana/pkg/services/featuremgmt" diff --git a/pkg/login/social/social.go b/pkg/login/social/social.go index 2d81fd8a97c54..ca04c32d6f7f2 100644 --- a/pkg/login/social/social.go +++ b/pkg/login/social/social.go @@ -14,10 +14,10 @@ import ( "net/http" "os" "regexp" + "slices" "strings" "time" - "golang.org/x/exp/slices" "golang.org/x/oauth2" "golang.org/x/text/cases" "golang.org/x/text/language" diff --git a/pkg/services/authn/clients/ext_jwt.go b/pkg/services/authn/clients/ext_jwt.go index 4d9b88d0de28e..01d8a524bb660 100644 --- a/pkg/services/authn/clients/ext_jwt.go +++ b/pkg/services/authn/clients/ext_jwt.go @@ -4,13 +4,13 @@ import ( "context" "fmt" "net/http" + "slices" "strconv" "strings" "time" "github.com/go-jose/go-jose/v3" "github.com/go-jose/go-jose/v3/jwt" - "golang.org/x/exp/slices" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/services/authn" diff --git a/pkg/services/extsvcauth/oauthserver/oasimpl/service_test.go b/pkg/services/extsvcauth/oauthserver/oasimpl/service_test.go index 761de64a9974d..36568b228a1e0 100644 --- a/pkg/services/extsvcauth/oauthserver/oasimpl/service_test.go +++ b/pkg/services/extsvcauth/oauthserver/oasimpl/service_test.go @@ -6,6 +6,7 @@ import ( "crypto/rsa" "encoding/base64" "fmt" + "slices" "testing" "time" @@ -13,7 +14,6 @@ import ( "github.com/ory/fosite/storage" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" - "golang.org/x/exp/slices" "github.com/grafana/grafana/pkg/infra/localcache" "github.com/grafana/grafana/pkg/infra/log" diff --git a/pkg/services/grafana-apiserver/openapi.go b/pkg/services/grafana-apiserver/openapi.go index 4e27fb03b7fcf..a086f09c45c5c 100644 --- a/pkg/services/grafana-apiserver/openapi.go +++ b/pkg/services/grafana-apiserver/openapi.go @@ -1,7 +1,8 @@ package grafanaapiserver import ( - "golang.org/x/exp/maps" + "maps" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" common "k8s.io/kube-openapi/pkg/common" spec "k8s.io/kube-openapi/pkg/validation/spec" diff --git a/pkg/services/ngalert/notifier/redis_peer.go b/pkg/services/ngalert/notifier/redis_peer.go index 632019411be9e..70fb1dd3845cf 100644 --- a/pkg/services/ngalert/notifier/redis_peer.go +++ b/pkg/services/ngalert/notifier/redis_peer.go @@ -2,6 +2,7 @@ package notifier import ( "context" + "slices" "sort" "strconv" "sync" @@ -12,7 +13,6 @@ import ( "github.com/prometheus/alertmanager/cluster" "github.com/prometheus/alertmanager/cluster/clusterpb" "github.com/prometheus/client_golang/prometheus" - "golang.org/x/exp/slices" "github.com/redis/go-redis/v9" diff --git a/pkg/services/ngalert/state/historian/encode.go b/pkg/services/ngalert/state/historian/encode.go index e2feec2fe09d5..eed07c8f534ed 100644 --- a/pkg/services/ngalert/state/historian/encode.go +++ b/pkg/services/ngalert/state/historian/encode.go @@ -3,14 +3,15 @@ package historian import ( "encoding/json" "fmt" + "slices" "strconv" "strings" "github.com/gogo/protobuf/proto" "github.com/golang/snappy" - "github.com/grafana/grafana/pkg/components/loki/logproto" "github.com/prometheus/common/model" - "golang.org/x/exp/slices" + + "github.com/grafana/grafana/pkg/components/loki/logproto" ) type JsonEncoder struct{} diff --git a/pkg/services/ngalert/state/template/funcs.go b/pkg/services/ngalert/state/template/funcs.go index 348664610109a..29f89d7accbf0 100644 --- a/pkg/services/ngalert/state/template/funcs.go +++ b/pkg/services/ngalert/state/template/funcs.go @@ -5,10 +5,9 @@ import ( "fmt" "net/url" "regexp" + "slices" "strings" "text/template" - - "golang.org/x/exp/slices" ) type query struct { diff --git a/pkg/services/query/query.go b/pkg/services/query/query.go index 4ba73e9135435..5d8b0fd1f464b 100644 --- a/pkg/services/query/query.go +++ b/pkg/services/query/query.go @@ -5,10 +5,10 @@ import ( "fmt" "net/http" "runtime" + "slices" "time" "github.com/grafana/grafana-plugin-sdk-go/backend" - "golang.org/x/exp/slices" "golang.org/x/sync/errgroup" "github.com/grafana/grafana/pkg/api/dtos" diff --git a/pkg/services/sqlstore/permissions/dashboard.go b/pkg/services/sqlstore/permissions/dashboard.go index cf12f5bcd0567..27cc1235e8bc4 100644 --- a/pkg/services/sqlstore/permissions/dashboard.go +++ b/pkg/services/sqlstore/permissions/dashboard.go @@ -3,10 +3,9 @@ package permissions import ( "bytes" "fmt" + "slices" "strings" - "golang.org/x/exp/slices" - "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/auth/identity" "github.com/grafana/grafana/pkg/services/dashboards" diff --git a/pkg/tsdb/elasticsearch/data_query.go b/pkg/tsdb/elasticsearch/data_query.go index f2df4ba0afc48..35fc092d8b71a 100644 --- a/pkg/tsdb/elasticsearch/data_query.go +++ b/pkg/tsdb/elasticsearch/data_query.go @@ -5,11 +5,11 @@ import ( "encoding/json" "fmt" "regexp" + "slices" "strconv" "time" "github.com/grafana/grafana-plugin-sdk-go/backend" - "golang.org/x/exp/slices" "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/infra/log" diff --git a/pkg/web/macaron.go b/pkg/web/macaron.go index 835389675f3c8..ceecf6dc08b89 100644 --- a/pkg/web/macaron.go +++ b/pkg/web/macaron.go @@ -1,6 +1,3 @@ -//go:build go1.3 -// +build go1.3 - // Copyright 2014 The Macaron Authors // // Licensed under the Apache License, Version 2.0 (the "License"): you may diff --git a/scripts/build/ci-build/Dockerfile b/scripts/build/ci-build/Dockerfile index fc44fd9c08aec..3589fb6d0a079 100644 --- a/scripts/build/ci-build/Dockerfile +++ b/scripts/build/ci-build/Dockerfile @@ -108,7 +108,7 @@ RUN rm dockerize-linux-amd64-v${DOCKERIZE_VERSION}.tar.gz # Use old Debian (LTS into 2024) in order to ensure binary compatibility with older glibc's. FROM debian:buster-20220822 -ENV GOVERSION=1.20.10 \ +ENV GOVERSION=1.21.3 \ PATH=/usr/local/go/bin:$PATH \ GOPATH=/go \ NODEVERSION=20.9.0-1nodesource1 \ diff --git a/scripts/drone/variables.star b/scripts/drone/variables.star index 6686f201ece62..8e9e1edd18c3f 100644 --- a/scripts/drone/variables.star +++ b/scripts/drone/variables.star @@ -3,7 +3,7 @@ global variables """ grabpl_version = "v3.0.42" -golang_version = "1.20.10" +golang_version = "1.21.3" # nodejs_version should match what's in ".nvmrc", but without the v prefix. nodejs_version = "20.9.0" From a38c9d4f44f6317d2045baa2a3faa673bf20c823 Mon Sep 17 00:00:00 2001 From: Ivana Huckova <30407135+ivanahuckova@users.noreply.github.com> Date: Wed, 1 Nov 2023 17:22:33 +0100 Subject: [PATCH 032/869] Loki: Add maxLines option to getParserAndLabelKeys (#77460) * Loki: Add logsCount option to getParserAndLabelKeys * Rename logsCount to maxLines --- .../datasource/loki/LanguageProvider.test.ts | 36 ++++++++++++++++++- .../datasource/loki/LanguageProvider.ts | 16 +++++++-- .../app/plugins/datasource/loki/datasource.ts | 4 +-- .../app_plugin_developer_documentation.md | 10 ++++-- 4 files changed, 58 insertions(+), 8 deletions(-) diff --git a/public/app/plugins/datasource/loki/LanguageProvider.test.ts b/public/app/plugins/datasource/loki/LanguageProvider.test.ts index a8bd0b9ed9719..040e2d48e2d7e 100644 --- a/public/app/plugins/datasource/loki/LanguageProvider.test.ts +++ b/public/app/plugins/datasource/loki/LanguageProvider.test.ts @@ -1,7 +1,7 @@ import { AbstractLabelOperator, DataFrame } from '@grafana/data'; import LanguageProvider from './LanguageProvider'; -import { LokiDatasource } from './datasource'; +import { DEFAULT_MAX_LINES_SAMPLE, LokiDatasource } from './datasource'; import { createLokiDatasource, createMetadataRequest } from './mocks'; import { extractLogParserFromDataFrame, @@ -275,6 +275,40 @@ describe('Query imports', () => { }); expect(extractLogParserFromDataFrameMock).not.toHaveBeenCalled(); }); + + it('calls dataSample with correct default maxLines', async () => { + jest.spyOn(datasource, 'getDataSamples').mockResolvedValue([]); + + expect(await languageProvider.getParserAndLabelKeys('{place="luna"}')).toEqual({ + extractedLabelKeys: [], + unwrapLabelKeys: [], + hasJSON: false, + hasLogfmt: false, + hasPack: false, + }); + expect(datasource.getDataSamples).toHaveBeenCalledWith({ + expr: '{place="luna"}', + maxLines: DEFAULT_MAX_LINES_SAMPLE, + refId: 'data-samples', + }); + }); + + it('calls dataSample with correctly set sampleSize', async () => { + jest.spyOn(datasource, 'getDataSamples').mockResolvedValue([]); + + expect(await languageProvider.getParserAndLabelKeys('{place="luna"}', { maxLines: 5 })).toEqual({ + extractedLabelKeys: [], + unwrapLabelKeys: [], + hasJSON: false, + hasLogfmt: false, + hasPack: false, + }); + expect(datasource.getDataSamples).toHaveBeenCalledWith({ + expr: '{place="luna"}', + maxLines: 5, + refId: 'data-samples', + }); + }); }); }); diff --git a/public/app/plugins/datasource/loki/LanguageProvider.ts b/public/app/plugins/datasource/loki/LanguageProvider.ts index f2753668ca9db..070ec78116164 100644 --- a/public/app/plugins/datasource/loki/LanguageProvider.ts +++ b/public/app/plugins/datasource/loki/LanguageProvider.ts @@ -4,7 +4,7 @@ import Prism from 'prismjs'; import { LanguageProvider, AbstractQuery, KeyValue } from '@grafana/data'; import { extractLabelMatchers, processLabels, toPromLikeExpr } from 'app/plugins/datasource/prometheus/language_utils'; -import { LokiDatasource } from './datasource'; +import { DEFAULT_MAX_LINES_SAMPLE, LokiDatasource } from './datasource'; import { extractLabelKeysFromDataFrame, extractLogParserFromDataFrame, @@ -251,11 +251,21 @@ export default class LokiLanguageProvider extends LanguageProvider { * - `unwrapLabelKeys`: An array of label keys that can be used for unwrapping log data. * * @param streamSelector - The selector for the log stream you want to analyze. + * @param {Object} [options] - Optional parameters. + * @param {number} [options.maxLines] - The number of log lines requested when determining parsers and label keys. + * Smaller maxLines is recommended for improved query performance. The default count is 10. * @returns A promise containing an object with parser and label key information. * @throws An error if the fetch operation fails. */ - async getParserAndLabelKeys(streamSelector: string): Promise { - const series = await this.datasource.getDataSamples({ expr: streamSelector, refId: 'data-samples' }); + async getParserAndLabelKeys( + streamSelector: string, + options?: { maxLines?: number } + ): Promise { + const series = await this.datasource.getDataSamples({ + expr: streamSelector, + refId: 'data-samples', + maxLines: options?.maxLines || DEFAULT_MAX_LINES_SAMPLE, + }); if (!series.length) { return { extractedLabelKeys: [], unwrapLabelKeys: [], hasJSON: false, hasLogfmt: false, hasPack: false }; diff --git a/public/app/plugins/datasource/loki/datasource.ts b/public/app/plugins/datasource/loki/datasource.ts index e75462608c215..615bd7e360e22 100644 --- a/public/app/plugins/datasource/loki/datasource.ts +++ b/public/app/plugins/datasource/loki/datasource.ts @@ -99,6 +99,7 @@ import { LokiVariableSupport } from './variables'; export type RangeQueryOptions = DataQueryRequest | AnnotationQueryRequest; export const DEFAULT_MAX_LINES = 1000; +export const DEFAULT_MAX_LINES_SAMPLE = 10; export const LOKI_ENDPOINT = '/loki/api/v1'; export const REF_ID_DATA_SAMPLES = 'loki-data-samples'; export const REF_ID_STARTER_ANNOTATION = 'annotation-'; @@ -761,8 +762,7 @@ export class LokiDatasource expr: query.expr, queryType: LokiQueryType.Range, refId: REF_ID_DATA_SAMPLES, - // For samples we limit the request to 10 lines, so queries are small and fast - maxLines: 10, + maxLines: query.maxLines || DEFAULT_MAX_LINES_SAMPLE, supportingQueryType: SupportingQueryType.DataSample, }; diff --git a/public/app/plugins/datasource/loki/docs/app_plugin_developer_documentation.md b/public/app/plugins/datasource/loki/docs/app_plugin_developer_documentation.md index 04e8f9962921d..7dc09b6f771ee 100644 --- a/public/app/plugins/datasource/loki/docs/app_plugin_developer_documentation.md +++ b/public/app/plugins/datasource/loki/docs/app_plugin_developer_documentation.md @@ -138,10 +138,16 @@ try { * - `unwrapLabelKeys`: An array of label keys that can be used for unwrapping log data. * * @param streamSelector - The selector for the log stream you want to analyze. + * @param {Object} [options] - Optional parameters. + * @param {number} [options.maxLines] - The number of log lines requested when determining parsers and label keys. + * Smaller maxLines is recommended for improved query performance. The default count is 10. * @returns A promise containing an object with parser and label key information. * @throws An error if the fetch operation fails. */ -async function getParserAndLabelKeys(streamSelector: string): Promise<{ +async function getParserAndLabelKeys( + streamSelector: string, + options?: { maxLines?: number } +): Promise<{ extractedLabelKeys: string[]; hasJSON: boolean; hasLogfmt: boolean; @@ -154,7 +160,7 @@ async function getParserAndLabelKeys(streamSelector: string): Promise<{ */ const streamSelector = '{job="grafana"}'; try { - const parserAndLabelKeys = await getParserAndLabelKeys(streamSelector); + const parserAndLabelKeys = await getParserAndLabelKeys(streamSelector, { maxLines: 5 }); console.log(parserAndLabelKeys); } catch (error) { console.error(`Error fetching parser and label keys: ${error.message}`); From 124bf6bfa99d2e0bf2a9b4f677faa9eafb3b1da3 Mon Sep 17 00:00:00 2001 From: Nathan Marrs Date: Wed, 1 Nov 2023 10:25:26 -0600 Subject: [PATCH 033/869] chore: canvas cleanup betterer styles object notation edition (#76315) Co-authored-by: drew08t --- .betterer.results | 94 +-------------- .../features/canvas/elements/droneFront.tsx | 6 +- .../features/canvas/elements/droneSide.tsx | 6 +- .../app/features/canvas/elements/droneTop.tsx | 38 +++--- .../app/features/canvas/elements/ellipse.tsx | 42 +++---- public/app/features/canvas/elements/icon.tsx | 10 +- .../features/canvas/elements/metricValue.tsx | 40 +++--- .../canvas/elements/server/server.tsx | 64 +++++----- public/app/features/canvas/elements/text.tsx | 40 +++--- .../features/canvas/elements/windTurbine.tsx | 25 ++-- .../canvas/components/CanvasContextMenu.tsx | 6 +- .../panel/canvas/components/CanvasTooltip.tsx | 8 +- .../panel/canvas/components/SetBackground.tsx | 10 +- .../connections/ConnectionAnchors.tsx | 63 +++++----- .../components/connections/ConnectionSVG.tsx | 30 ++--- .../panel/canvas/editor/connectionEditor.tsx | 5 +- .../editor/element/ConstraintSelectionBox.tsx | 114 ++++++++++++------ .../editor/element/QuickPositioning.tsx | 18 +-- .../panel/canvas/editor/inline/InlineEdit.tsx | 78 ++++++------ .../canvas/editor/inline/InlineEditBody.tsx | 8 +- .../editor/layer/TreeNavigationEditor.tsx | 8 +- .../canvas/editor/layer/TreeNodeTitle.tsx | 50 ++++---- 22 files changed, 354 insertions(+), 409 deletions(-) diff --git a/.betterer.results b/.betterer.results index 8d5def88db493..d9446d6b110d0 100644 --- a/.betterer.results +++ b/.betterer.results @@ -2765,43 +2765,8 @@ exports[`better eslint`] = { [0, 0, 0, "Unexpected any. Specify a different type.", "3"], [0, 0, 0, "Unexpected any. Specify a different type.", "4"] ], - "public/app/features/canvas/elements/droneFront.tsx:5381": [ - [0, 0, 0, "Styles should be written using objects.", "0"] - ], - "public/app/features/canvas/elements/droneSide.tsx:5381": [ - [0, 0, 0, "Styles should be written using objects.", "0"] - ], - "public/app/features/canvas/elements/droneTop.tsx:5381": [ - [0, 0, 0, "Styles should be written using objects.", "0"], - [0, 0, 0, "Styles should be written using objects.", "1"], - [0, 0, 0, "Styles should be written using objects.", "2"] - ], "public/app/features/canvas/elements/ellipse.tsx:5381": [ - [0, 0, 0, "Styles should be written using objects.", "0"], - [0, 0, 0, "Styles should be written using objects.", "1"] - ], - "public/app/features/canvas/elements/icon.tsx:5381": [ - [0, 0, 0, "Styles should be written using objects.", "0"] - ], - "public/app/features/canvas/elements/metricValue.tsx:5381": [ - [0, 0, 0, "Styles should be written using objects.", "0"], - [0, 0, 0, "Styles should be written using objects.", "1"], - [0, 0, 0, "Styles should be written using objects.", "2"] - ], - "public/app/features/canvas/elements/server/server.tsx:5381": [ - [0, 0, 0, "Styles should be written using objects.", "0"], - [0, 0, 0, "Styles should be written using objects.", "1"], - [0, 0, 0, "Styles should be written using objects.", "2"], - [0, 0, 0, "Styles should be written using objects.", "3"], - [0, 0, 0, "Styles should be written using objects.", "4"] - ], - "public/app/features/canvas/elements/text.tsx:5381": [ - [0, 0, 0, "Styles should be written using objects.", "0"], - [0, 0, 0, "Styles should be written using objects.", "1"], - [0, 0, 0, "Styles should be written using objects.", "2"] - ], - "public/app/features/canvas/elements/windTurbine.tsx:5381": [ - [0, 0, 0, "Styles should be written using objects.", "0"] + [0, 0, 0, "Unexpected any. Specify a different type.", "0"] ], "public/app/features/canvas/runtime/element.tsx:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"], @@ -7005,77 +6970,24 @@ exports[`better eslint`] = { [0, 0, 0, "Do not use any type assertions.", "0"], [0, 0, 0, "Unexpected any. Specify a different type.", "1"] ], - "public/app/plugins/panel/canvas/components/CanvasContextMenu.tsx:5381": [ - [0, 0, 0, "Styles should be written using objects.", "0"] - ], - "public/app/plugins/panel/canvas/components/CanvasTooltip.tsx:5381": [ - [0, 0, 0, "Styles should be written using objects.", "0"] - ], - "public/app/plugins/panel/canvas/components/SetBackground.tsx:5381": [ - [0, 0, 0, "Styles should be written using objects.", "0"] - ], - "public/app/plugins/panel/canvas/components/connections/ConnectionAnchors.tsx:5381": [ - [0, 0, 0, "Styles should be written using objects.", "0"], - [0, 0, 0, "Styles should be written using objects.", "1"], - [0, 0, 0, "Styles should be written using objects.", "2"], - [0, 0, 0, "Styles should be written using objects.", "3"] - ], - "public/app/plugins/panel/canvas/components/connections/ConnectionSVG.tsx:5381": [ - [0, 0, 0, "Styles should be written using objects.", "0"], - [0, 0, 0, "Styles should be written using objects.", "1"] - ], - "public/app/plugins/panel/canvas/editor/connectionEditor.tsx:5381": [ - [0, 0, 0, "Unexpected any. Specify a different type.", "0"] - ], "public/app/plugins/panel/canvas/editor/element/APIEditor.tsx:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"] ], - "public/app/plugins/panel/canvas/editor/element/ConstraintSelectionBox.tsx:5381": [ - [0, 0, 0, "Styles should be written using objects.", "0"], - [0, 0, 0, "Styles should be written using objects.", "1"], - [0, 0, 0, "Styles should be written using objects.", "2"], - [0, 0, 0, "Styles should be written using objects.", "3"], - [0, 0, 0, "Styles should be written using objects.", "4"], - [0, 0, 0, "Styles should be written using objects.", "5"], - [0, 0, 0, "Styles should be written using objects.", "6"], - [0, 0, 0, "Styles should be written using objects.", "7"] - ], "public/app/plugins/panel/canvas/editor/element/PlacementEditor.tsx:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"] ], - "public/app/plugins/panel/canvas/editor/element/QuickPositioning.tsx:5381": [ - [0, 0, 0, "Styles should be written using objects.", "0"], - [0, 0, 0, "Styles should be written using objects.", "1"] - ], "public/app/plugins/panel/canvas/editor/element/elementEditor.tsx:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"], [0, 0, 0, "Unexpected any. Specify a different type.", "1"] ], - "public/app/plugins/panel/canvas/editor/inline/InlineEdit.tsx:5381": [ - [0, 0, 0, "Styles should be written using objects.", "0"], - [0, 0, 0, "Styles should be written using objects.", "1"], - [0, 0, 0, "Styles should be written using objects.", "2"], - [0, 0, 0, "Styles should be written using objects.", "3"], - [0, 0, 0, "Styles should be written using objects.", "4"], - [0, 0, 0, "Styles should be written using objects.", "5"], - [0, 0, 0, "Styles should be written using objects.", "6"] - ], "public/app/plugins/panel/canvas/editor/inline/InlineEditBody.tsx:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"], [0, 0, 0, "Unexpected any. Specify a different type.", "1"], [0, 0, 0, "Do not use any type assertions.", "2"], - [0, 0, 0, "Unexpected any. Specify a different type.", "3"], - [0, 0, 0, "Styles should be written using objects.", "4"] + [0, 0, 0, "Unexpected any. Specify a different type.", "3"] ], "public/app/plugins/panel/canvas/editor/layer/TreeNavigationEditor.tsx:5381": [ - [0, 0, 0, "Unexpected any. Specify a different type.", "0"], - [0, 0, 0, "Styles should be written using objects.", "1"] - ], - "public/app/plugins/panel/canvas/editor/layer/TreeNodeTitle.tsx:5381": [ - [0, 0, 0, "Styles should be written using objects.", "0"], - [0, 0, 0, "Styles should be written using objects.", "1"], - [0, 0, 0, "Styles should be written using objects.", "2"], - [0, 0, 0, "Styles should be written using objects.", "3"] + [0, 0, 0, "Unexpected any. Specify a different type.", "0"] ], "public/app/plugins/panel/canvas/editor/layer/layerEditor.tsx:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"], diff --git a/public/app/features/canvas/elements/droneFront.tsx b/public/app/features/canvas/elements/droneFront.tsx index c91a92fe62f68..9ce31d8f8ace5 100644 --- a/public/app/features/canvas/elements/droneFront.tsx +++ b/public/app/features/canvas/elements/droneFront.tsx @@ -118,7 +118,7 @@ export const droneFrontItem: CanvasElementItem = { }; const getStyles = (theme: GrafanaTheme2) => ({ - droneFront: css` - transition: transform 0.4s; - `, + droneFront: css({ + transition: 'transform 0.4s', + }), }); diff --git a/public/app/features/canvas/elements/droneSide.tsx b/public/app/features/canvas/elements/droneSide.tsx index bc84404b19a1a..8ae6fcc6a218b 100644 --- a/public/app/features/canvas/elements/droneSide.tsx +++ b/public/app/features/canvas/elements/droneSide.tsx @@ -117,7 +117,7 @@ export const droneSideItem: CanvasElementItem = { }; const getStyles = (theme: GrafanaTheme2) => ({ - droneSide: css` - transition: transform 0.4s; - `, + droneSide: css({ + transition: 'transform 0.4s', + }), }); diff --git a/public/app/features/canvas/elements/droneTop.tsx b/public/app/features/canvas/elements/droneTop.tsx index 84a14885b5fcc..d1fb4174cbbbe 100644 --- a/public/app/features/canvas/elements/droneTop.tsx +++ b/public/app/features/canvas/elements/droneTop.tsx @@ -156,23 +156,23 @@ export const droneTopItem: CanvasElementItem = { }; const getStyles = (theme: GrafanaTheme2) => ({ - propeller: css` - transform-origin: 50% 50%; - transform-box: fill-box; - display: block; - @keyframes spin { - from { - transform: rotate(0deg); - } - to { - transform: rotate(360deg); - } - } - `, - propellerCW: css` - animation-direction: normal; - `, - propellerCCW: css` - animation-direction: reverse; - `, + propeller: css({ + transformOrigin: '50% 50%', + transformBox: 'fill-box', + display: 'block', + '@keyframes spin': { + from: { + transform: 'rotate(0deg)', + }, + to: { + transform: 'rotate(360deg)', + }, + }, + }), + propellerCW: css({ + animationDirection: 'normal', + }), + propellerCCW: css({ + animationDirection: 'reverse', + }), }); diff --git a/public/app/features/canvas/elements/ellipse.tsx b/public/app/features/canvas/elements/ellipse.tsx index 00bf1b8c93645..2cac9ba1de4d8 100644 --- a/public/app/features/canvas/elements/ellipse.tsx +++ b/public/app/features/canvas/elements/ellipse.tsx @@ -2,7 +2,6 @@ import { css } from '@emotion/css'; import React, { PureComponent } from 'react'; import { GrafanaTheme2 } from '@grafana/data'; -import { stylesFactory } from '@grafana/ui'; import { config } from 'app/core/config'; import { DimensionContext } from 'app/features/dimensions/context'; import { ColorDimensionEditor } from 'app/features/dimensions/editors/ColorDimensionEditor'; @@ -23,26 +22,27 @@ class EllipseDisplay extends PureComponent ({ - container: css` - display: table; - position: absolute; - top: 50%; - transform: translateY(-50%); - width: 100%; - height: 100%; - background-color: ${data?.backgroundColor}; - border: ${data?.width}px solid ${data?.borderColor}; - border-radius: 50%; - `, - span: css` - display: table-cell; - vertical-align: ${data?.valign}; - text-align: ${data?.align}; - font-size: ${data?.size}px; - color: ${data?.color}; - `, -})); +const getStyles = (theme: GrafanaTheme2, data: any) => ({ + container: css({ + display: 'table', + position: 'absolute', + top: '50%', + transform: 'translateY(-50%)', + width: '100%', + height: '100%', + backgroundColor: data?.backgroundColor, + border: `${data?.width}px solid ${data?.borderColor}`, + // eslint-disable-next-line @grafana/no-border-radius-literal + borderRadius: '50%', + }), + span: css({ + display: 'table-cell', + verticalAlign: data?.valign, + textAlign: data?.align, + fontSize: `${data?.size}px`, + color: data?.color, + }), +}); export const ellipseItem: CanvasElementItem = { id: 'ellipse', diff --git a/public/app/features/canvas/elements/icon.tsx b/public/app/features/canvas/elements/icon.tsx index 8f3ad8adac334..c6c66317887e2 100644 --- a/public/app/features/canvas/elements/icon.tsx +++ b/public/app/features/canvas/elements/icon.tsx @@ -25,11 +25,11 @@ interface IconData { } // When a stoke is defined, we want the path to be in page units -const svgStrokePathClass = css` - path { - vector-effect: non-scaling-stroke; - } -`; +const svgStrokePathClass = css({ + path: { + vectorEffect: 'non-scaling-stroke', + }, +}); export function IconDisplay(props: CanvasElementProps) { const { data } = props; diff --git a/public/app/features/canvas/elements/metricValue.tsx b/public/app/features/canvas/elements/metricValue.tsx index f98dc1979cf50..76ed7a01f1a40 100644 --- a/public/app/features/canvas/elements/metricValue.tsx +++ b/public/app/features/canvas/elements/metricValue.tsx @@ -111,26 +111,26 @@ const MetricValueEdit = (props: CanvasElementProps) => { }; const getStyles = (data: TextData | undefined) => (theme: GrafanaTheme2) => ({ - container: css` - position: absolute; - height: 100%; - width: 100%; - display: table; - `, - inlineEditorContainer: css` - height: 100%; - width: 100%; - display: flex; - align-items: center; - padding: 10px; - `, - span: css` - display: table-cell; - vertical-align: ${data?.valign}; - text-align: ${data?.align}; - font-size: ${data?.size}px; - color: ${data?.color}; - `, + container: css({ + position: 'absolute', + height: '100%', + width: '100%', + display: 'table', + }), + inlineEditorContainer: css({ + height: '100%', + width: '100%', + display: 'flex', + alignItems: 'center', + padding: theme.spacing(1), + }), + span: css({ + display: 'table-cell', + verticalAlign: data?.valign, + textAlign: data?.align, + fontSize: `${data?.size}px`, + color: data?.color, + }), }); export const metricValueItem: CanvasElementItem = { diff --git a/public/app/features/canvas/elements/server/server.tsx b/public/app/features/canvas/elements/server/server.tsx index e56e8aa0285cc..aaee1ea63362a 100644 --- a/public/app/features/canvas/elements/server/server.tsx +++ b/public/app/features/canvas/elements/server/server.tsx @@ -147,36 +147,36 @@ export const serverItem: CanvasElementItem = { }; export const getServerStyles = (data: ServerData | undefined) => (theme: GrafanaTheme2) => ({ - bulb: css` - @keyframes blink { - 0% { - fill-opacity: 0; - } - 50% { - fill-opacity: 1; - } - 100% { - fill-opacity: 0; - } - } - `, - server: css` - fill: ${data?.statusColor ?? 'transparent'}; - `, - circle: css` - animation: blink ${data?.blinkRate ? 1 / data.blinkRate : 0}s infinite step-end; - fill: ${data?.bulbColor}; - stroke: none; - `, - circleBack: css` - fill: ${outlineColor}; - stroke: none; - opacity: 1; - `, - outline: css` - stroke: ${outlineColor}; - stroke-linecap: round; - stroke-linejoin: round; - stroke-width: 4px; - `, + bulb: css({ + '@keyframes blink': { + '0%': { + fillOpacity: 0, + }, + '50%': { + fillOpacity: 1, + }, + '100%': { + fillOpacity: 0, + }, + }, + }), + server: css({ + fill: data?.statusColor ?? 'transparent', + }), + circle: css({ + animation: `blink ${data?.blinkRate ? 1 / data.blinkRate : 0}s infinite step-end`, + fill: data?.bulbColor, + stroke: 'none', + }), + circleBack: css({ + fill: outlineColor, + stroke: 'none', + opacity: 1, + }), + outline: css({ + stroke: outlineColor, + strokeLinecap: 'round', + strokeLinejoin: 'round', + strokeWidth: '4px', + }), }); diff --git a/public/app/features/canvas/elements/text.tsx b/public/app/features/canvas/elements/text.tsx index 92773b5057525..3abd83d8d7c61 100644 --- a/public/app/features/canvas/elements/text.tsx +++ b/public/app/features/canvas/elements/text.tsx @@ -95,26 +95,26 @@ const TextEdit = (props: CanvasElementProps) => { }; const getStyles = (data: TextData | undefined) => (theme: GrafanaTheme2) => ({ - container: css` - position: absolute; - height: 100%; - width: 100%; - display: table; - `, - inlineEditorContainer: css` - height: 100%; - width: 100%; - display: flex; - align-items: center; - padding: 10px; - `, - span: css` - display: table-cell; - vertical-align: ${data?.valign}; - text-align: ${data?.align}; - font-size: ${data?.size}px; - color: ${data?.color}; - `, + container: css({ + position: 'absolute', + height: '100%', + width: '100%', + display: 'table', + }), + inlineEditorContainer: css({ + height: '100%', + width: '100%', + display: 'flex', + alignItems: 'center', + padding: theme.spacing(1), + }), + span: css({ + display: 'table-cell', + verticalAlign: data?.valign, + textAlign: data?.align, + fontSize: `${data?.size}px`, + color: data?.color, + }), }); export const textItem: CanvasElementItem = { diff --git a/public/app/features/canvas/elements/windTurbine.tsx b/public/app/features/canvas/elements/windTurbine.tsx index db593e265e6f1..1d90339e2043f 100644 --- a/public/app/features/canvas/elements/windTurbine.tsx +++ b/public/app/features/canvas/elements/windTurbine.tsx @@ -113,17 +113,16 @@ export const windTurbineItem: CanvasElementItem = { }; const getStyles = (theme: GrafanaTheme2) => ({ - blade: css` - @keyframes spin { - from { - transform: rotate(0deg); - } - to { - transform: rotate(360deg); - } - } - - transform-origin: 94.663px 94.663px; - transform: rotate(15deg); - `, + blade: css({ + transformOrigin: '94.663px 94.663px', + transform: 'rotate(15deg)', + '@keyframes spin': { + from: { + transform: 'rotate(0deg)', + }, + to: { + transform: 'rotate(360deg)', + }, + }, + }), }); diff --git a/public/app/plugins/panel/canvas/components/CanvasContextMenu.tsx b/public/app/plugins/panel/canvas/components/CanvasContextMenu.tsx index c5868fbbcf2a8..5d11484f53f08 100644 --- a/public/app/plugins/panel/canvas/components/CanvasContextMenu.tsx +++ b/public/app/plugins/panel/canvas/components/CanvasContextMenu.tsx @@ -244,7 +244,7 @@ export const CanvasContextMenu = ({ scene, panel }: Props) => { }; const getStyles = () => ({ - menuItem: css` - max-width: 200px; - `, + menuItem: css({ + maxWidth: '200px', + }), }); diff --git a/public/app/plugins/panel/canvas/components/CanvasTooltip.tsx b/public/app/plugins/panel/canvas/components/CanvasTooltip.tsx index 6fb46c975a423..da5dbbfee7ea1 100644 --- a/public/app/plugins/panel/canvas/components/CanvasTooltip.tsx +++ b/public/app/plugins/panel/canvas/components/CanvasTooltip.tsx @@ -73,8 +73,8 @@ export const CanvasTooltip = ({ scene }: Props) => { }; const getStyles = (theme: GrafanaTheme2) => ({ - wrapper: css` - margin-top: 20px; - background: ${theme.colors.background.primary}; - `, + wrapper: css({ + marginTop: '20px', + background: theme.colors.background.primary, + }), }); diff --git a/public/app/plugins/panel/canvas/components/SetBackground.tsx b/public/app/plugins/panel/canvas/components/SetBackground.tsx index 9b353a096f1df..60284cfefee81 100644 --- a/public/app/plugins/panel/canvas/components/SetBackground.tsx +++ b/public/app/plugins/panel/canvas/components/SetBackground.tsx @@ -59,9 +59,9 @@ export function SetBackground({ onClose, scene, anchorPoint }: Props) { } const getStyles = (theme: GrafanaTheme2, anchorPoint: AnchorPoint) => ({ - portalWrapper: css` - width: 315px; - height: 445px; - transform: translate(${anchorPoint.x}px, ${anchorPoint.y - 200}px); - `, + portalWrapper: css({ + width: '315px', + height: '445px', + transform: `translate(${anchorPoint.x}px, ${anchorPoint.y - 200}px)`, + }), }); diff --git a/public/app/plugins/panel/canvas/components/connections/ConnectionAnchors.tsx b/public/app/plugins/panel/canvas/components/connections/ConnectionAnchors.tsx index 944be9d2ffde5..93ff653175664 100644 --- a/public/app/plugins/panel/canvas/components/connections/ConnectionAnchors.tsx +++ b/public/app/plugins/panel/canvas/components/connections/ConnectionAnchors.tsx @@ -114,36 +114,35 @@ export const ConnectionAnchors = ({ setRef, handleMouseLeave }: Props) => { }; const getStyles = (theme: GrafanaTheme2) => ({ - root: css` - position: absolute; - display: none; - `, - mouseoutDiv: css` - position: absolute; - margin: -30px; - width: calc(100% + 60px); - height: calc(100% + 60px); - `, - anchor: css` - padding: ${ANCHOR_PADDING}px; - position: absolute; - cursor: cursor; - width: calc(5px + 2 * ${ANCHOR_PADDING}px); - height: calc(5px + 2 * ${ANCHOR_PADDING}px); - z-index: 100; - pointer-events: auto !important; - `, - highlightElement: css` - background-color: #00ff00; - opacity: 0.3; - position: absolute; - cursor: cursor; - position: absolute; - pointer-events: auto; - width: 16px; - height: 16px; - border-radius: ${theme.shape.radius.circle}; - display: none; - z-index: 110; - `, + root: css({ + position: 'absolute', + display: 'none', + }), + mouseoutDiv: css({ + position: 'absolute', + margin: '-30px', + width: 'calc(100% + 60px)', + height: 'calc(100% + 60px)', + }), + anchor: css({ + padding: `${ANCHOR_PADDING}px`, + position: 'absolute', + cursor: 'cursor', + width: `calc(5px + 2 * ${ANCHOR_PADDING}px)`, + height: `calc(5px + 2 * ${ANCHOR_PADDING}px)`, + zIndex: 100, + pointerEvents: 'auto', + }), + highlightElement: css({ + backgroundColor: '#00ff00', + opacity: 0.3, + position: 'absolute', + cursor: 'cursor', + pointerEvents: 'auto', + width: '16px', + height: '16px', + borderRadius: theme.shape.radius.circle, + display: 'none', + zIndex: 110, + }), }); diff --git a/public/app/plugins/panel/canvas/components/connections/ConnectionSVG.tsx b/public/app/plugins/panel/canvas/components/connections/ConnectionSVG.tsx index ca40af1220030..47a3bde9eb88e 100644 --- a/public/app/plugins/panel/canvas/components/connections/ConnectionSVG.tsx +++ b/public/app/plugins/panel/canvas/components/connections/ConnectionSVG.tsx @@ -231,19 +231,19 @@ export const ConnectionSVG = ({ setSVGRef, setLineRef, scene }: Props) => { }; const getStyles = (theme: GrafanaTheme2) => ({ - editorSVG: css` - position: absolute; - pointer-events: none; - width: 100%; - height: 100%; - z-index: 1000; - display: none; - `, - connection: css` - position: absolute; - width: 100%; - height: 100%; - z-index: 1000; - pointer-events: none; - `, + editorSVG: css({ + position: 'absolute', + pointerEvents: 'none', + width: '100%', + height: '100%', + zIndex: 1000, + display: 'none', + }), + connection: css({ + position: 'absolute', + width: '100%', + height: '100%', + zIndex: 1000, + pointerEvents: 'none', + }), }); diff --git a/public/app/plugins/panel/canvas/editor/connectionEditor.tsx b/public/app/plugins/panel/canvas/editor/connectionEditor.tsx index 49689915cac1c..480d5ac041f13 100644 --- a/public/app/plugins/panel/canvas/editor/connectionEditor.tsx +++ b/public/app/plugins/panel/canvas/editor/connectionEditor.tsx @@ -24,9 +24,8 @@ export function getConnectionEditor(opts: CanvasConnectionEditorOptions): Nested getValue: (path: string) => { return lodashGet(opts.connection.info, path); }, - // TODO: Fix this any (maybe a dimension supplier?) - onChange: (path: string, value: any) => { - console.log(value, typeof value); + // TODO: Fix this unknown (maybe a dimension supplier?) + onChange: (path: string, value: unknown) => { let options = opts.connection.info; options = setOptionImmutably(options, path, value); opts.scene.connections.onChange(opts.connection, options); diff --git a/public/app/plugins/panel/canvas/editor/element/ConstraintSelectionBox.tsx b/public/app/plugins/panel/canvas/editor/element/ConstraintSelectionBox.tsx index 8f8237763dea3..50a895cbae1b3 100644 --- a/public/app/plugins/panel/canvas/editor/element/ConstraintSelectionBox.tsx +++ b/public/app/plugins/panel/canvas/editor/element/ConstraintSelectionBox.tsx @@ -141,48 +141,84 @@ const getStyles = (currentConstraints: Constraint) => (theme: GrafanaTheme2) => const selectionBoxColor = theme.isDark ? '#ffffff' : '#000000'; return { - constraintHover: css` - &:hover { - fill: ${HOVER_COLOR}; - fill-opacity: ${HOVER_OPACITY}; - } - `, - topConstraint: css` - ${currentConstraints.vertical === VerticalConstraint.Top || + constraintHover: css({ + '&:hover': { + fill: HOVER_COLOR, + fillOpacity: HOVER_OPACITY, + }, + }), + topConstraint: css({ + ...(currentConstraints.vertical === VerticalConstraint.Top || currentConstraints.vertical === VerticalConstraint.TopBottom - ? `width: 92pt; x: 1085; fill: ${SELECTED_COLOR};` - : `fill: ${selectionBoxColor};`} - `, - bottomConstraint: css` - ${currentConstraints.vertical === VerticalConstraint.Bottom || + ? { + width: '92pt', + x: '1085', + fill: SELECTED_COLOR, + } + : { + fill: selectionBoxColor, + }), + }), + bottomConstraint: css({ + ...(currentConstraints.vertical === VerticalConstraint.Bottom || currentConstraints.vertical === VerticalConstraint.TopBottom - ? `width: 92pt; x: 1085; fill: ${SELECTED_COLOR};` - : `fill: ${selectionBoxColor};`} - `, - leftConstraint: css` - ${currentConstraints.horizontal === HorizontalConstraint.Left || + ? { + width: '92pt', + x: '1085', + fill: SELECTED_COLOR, + } + : { + fill: selectionBoxColor, + }), + }), + leftConstraint: css({ + ...(currentConstraints.horizontal === HorizontalConstraint.Left || currentConstraints.horizontal === HorizontalConstraint.LeftRight - ? `height: 92pt; y: 1014; fill: ${SELECTED_COLOR};` - : `fill: ${selectionBoxColor};`} - `, - rightConstraint: css` - ${currentConstraints.horizontal === HorizontalConstraint.Right || + ? { + height: '92pt', + y: '1014', + fill: SELECTED_COLOR, + } + : { + fill: selectionBoxColor, + }), + }), + rightConstraint: css({ + ...(currentConstraints.horizontal === HorizontalConstraint.Right || currentConstraints.horizontal === HorizontalConstraint.LeftRight - ? `height: 92pt; y: 1014; fill: ${SELECTED_COLOR};` - : `fill: ${selectionBoxColor};`} - `, - horizontalCenterConstraint: css` - ${currentConstraints.horizontal === HorizontalConstraint.Center - ? `height: 92pt; y: 1014; fill: ${SELECTED_COLOR};` - : `fill: ${selectionBoxColor};`} - `, - verticalCenterConstraint: css` - ${currentConstraints.vertical === VerticalConstraint.Center - ? `width: 92pt; x: 1085; fill: ${SELECTED_COLOR};` - : `fill: ${selectionBoxColor};`} - `, - box: css` - fill: ${selectionBoxColor}; - `, + ? { + height: '92pt', + y: '1014', + fill: SELECTED_COLOR, + } + : { + fill: selectionBoxColor, + }), + }), + horizontalCenterConstraint: css({ + ...(currentConstraints.horizontal === HorizontalConstraint.Center + ? { + height: '92pt', + y: '1014', + fill: SELECTED_COLOR, + } + : { + fill: selectionBoxColor, + }), + }), + verticalCenterConstraint: css({ + ...(currentConstraints.vertical === VerticalConstraint.Center + ? { + width: '92pt', + x: '1085', + fill: SELECTED_COLOR, + } + : { + fill: selectionBoxColor, + }), + }), + box: css({ + fill: selectionBoxColor, + }), }; }; diff --git a/public/app/plugins/panel/canvas/editor/element/QuickPositioning.tsx b/public/app/plugins/panel/canvas/editor/element/QuickPositioning.tsx index d3bc421a698f1..b5772abcc4134 100644 --- a/public/app/plugins/panel/canvas/editor/element/QuickPositioning.tsx +++ b/public/app/plugins/panel/canvas/editor/element/QuickPositioning.tsx @@ -110,13 +110,13 @@ export const QuickPositioning = ({ onPositionChange, element, settings }: Props) }; const getStyles = (theme: GrafanaTheme2) => ({ - buttonGroup: css` - display: flex; - flex-wrap: wrap; - padding: 12px 0 12px 0; - `, - button: css` - margin-left: 5px; - margin-right: 5px; - `, + buttonGroup: css({ + display: 'flex', + flexWrap: 'wrap', + padding: '12px 0 12px 0', + }), + button: css({ + marginLeft: '5px', + marginRight: '5px', + }), }); diff --git a/public/app/plugins/panel/canvas/editor/inline/InlineEdit.tsx b/public/app/plugins/panel/canvas/editor/inline/InlineEdit.tsx index 7149ea5c14e20..636d94031789c 100644 --- a/public/app/plugins/panel/canvas/editor/inline/InlineEdit.tsx +++ b/public/app/plugins/panel/canvas/editor/inline/InlineEdit.tsx @@ -106,43 +106,43 @@ export function InlineEdit({ onClose, id, scene }: Props) { } const getStyles = (theme: GrafanaTheme2) => ({ - inlineEditorContainer: css` - display: flex; - flex-direction: column; - background: ${theme.components.panel.background}; - border: 1px solid ${theme.colors.border.weak}; - box-shadow: ${theme.shadows.z3}; - z-index: 1000; - opacity: 1; - min-width: 400px; - `, - draggableWrapper: css` - width: 0; - height: 0; - `, - inlineEditorHeader: css` - display: flex; - align-items: center; - justify-content: center; - background: ${theme.colors.background.canvas}; - border-bottom: 1px solid ${theme.colors.border.weak}; - height: 40px; - cursor: move; - `, - inlineEditorContent: css` - white-space: pre-wrap; - padding: 10px; - `, - inlineEditorClose: css` - margin-left: auto; - `, - placeholder: css` - width: 24px; - height: 24px; - visibility: hidden; - margin-right: auto; - `, - inlineEditorContentWrapper: css` - overflow: scroll; - `, + inlineEditorContainer: css({ + display: 'flex', + flexDirection: 'column', + background: theme.components.panel.background, + border: `1px solid ${theme.colors.border.weak}`, + boxShadow: theme.shadows.z3, + zIndex: 1000, + opacity: 1, + minWidth: '400px', + }), + draggableWrapper: css({ + width: 0, + height: 0, + }), + inlineEditorHeader: css({ + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + background: theme.colors.background.canvas, + borderBottom: `1px solid ${theme.colors.border.weak}`, + height: '40px', + cursor: 'move', + }), + inlineEditorContent: css({ + whiteSpace: 'pre-wrap', + padding: '10px', + }), + inlineEditorClose: css({ + marginLeft: 'auto', + }), + placeholder: css({ + width: '24px', + height: '24px', + visibility: 'hidden', + marginRight: 'auto', + }), + inlineEditorContentWrapper: css({ + overflow: 'scroll', + }), }); diff --git a/public/app/plugins/panel/canvas/editor/inline/InlineEditBody.tsx b/public/app/plugins/panel/canvas/editor/inline/InlineEditBody.tsx index 44d4954ad18d5..d2b54f88aff3a 100644 --- a/public/app/plugins/panel/canvas/editor/inline/InlineEditBody.tsx +++ b/public/app/plugins/panel/canvas/editor/inline/InlineEditBody.tsx @@ -151,8 +151,8 @@ function getOptionsPaneCategoryDescriptor( } const getStyles = (theme: GrafanaTheme2) => ({ - selectElement: css` - color: ${theme.colors.text.secondary}; - padding: ${theme.spacing(2)}; - `, + selectElement: css({ + color: theme.colors.text.secondary, + padding: theme.spacing(2), + }), }); diff --git a/public/app/plugins/panel/canvas/editor/layer/TreeNavigationEditor.tsx b/public/app/plugins/panel/canvas/editor/layer/TreeNavigationEditor.tsx index 1125634dd1d90..4bb098903a6d4 100644 --- a/public/app/plugins/panel/canvas/editor/layer/TreeNavigationEditor.tsx +++ b/public/app/plugins/panel/canvas/editor/layer/TreeNavigationEditor.tsx @@ -167,8 +167,8 @@ export const TreeNavigationEditor = ({ item }: StandardEditorProps ({ - addLayerButton: css` - margin-left: 18px; - min-width: 150px; - `, + addLayerButton: css({ + marginLeft: '18px', + minWidth: '150px', + }), }); diff --git a/public/app/plugins/panel/canvas/editor/layer/TreeNodeTitle.tsx b/public/app/plugins/panel/canvas/editor/layer/TreeNodeTitle.tsx index 26018ec43bf25..dd983f1cfc086 100644 --- a/public/app/plugins/panel/canvas/editor/layer/TreeNodeTitle.tsx +++ b/public/app/plugins/panel/canvas/editor/layer/TreeNodeTitle.tsx @@ -92,29 +92,29 @@ export const TreeNodeTitle = ({ settings, nodeData, setAllowSelection }: Props) }; const getStyles = (theme: GrafanaTheme2) => ({ - actionButtonsWrapper: css` - display: flex; - align-items: flex-end; - `, - actionIcon: css` - color: ${theme.colors.text.secondary}; - cursor: pointer; - &:hover { - color: ${theme.colors.text.primary}; - } - `, - textWrapper: css` - display: flex; - align-items: center; - flex-grow: 1; - overflow: hidden; - margin-right: ${theme.spacing(1)}; - `, - layerName: css` - font-weight: ${theme.typography.fontWeightMedium}; - color: ${theme.colors.primary.text}; - cursor: pointer; - overflow: hidden; - margin-left: ${theme.spacing(0.5)}; - `, + actionButtonsWrapper: css({ + display: 'flex', + alignItems: 'flex-end', + }), + actionIcon: css({ + color: theme.colors.text.secondary, + cursor: 'pointer', + '&:hover': { + color: theme.colors.text.primary, + }, + }), + textWrapper: css({ + display: 'flex', + alignItems: 'center', + flexGrow: 1, + overflow: 'hidden', + marginRight: theme.spacing(1), + }), + layerName: css({ + fontWeight: theme.typography.fontWeightMedium, + color: theme.colors.primary.text, + cursor: 'pointer', + overflow: 'hidden', + marginLeft: theme.spacing(0.5), + }), }); From f6d323850507749c479e07f0b4621c327bf1a3f5 Mon Sep 17 00:00:00 2001 From: Kevin Minehart Date: Wed, 1 Nov 2023 11:25:49 -0500 Subject: [PATCH 034/869] CI: Fix race condition when building docker on main (#77504) * build docker after packages are updated * use my branch for main pipelines for testing * use my branch for main pipelines for testing * use main instead now * formatting --- .drone.yml | 4 ++-- scripts/drone/pipelines/build.star | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.drone.yml b/.drone.yml index 40f20f172b39c..f5c15812a7365 100644 --- a/.drone.yml +++ b/.drone.yml @@ -1996,7 +1996,7 @@ steps: docker.txt - find ./dist -name '*docker*.tar.gz' -type f | xargs -n1 docker load -i depends_on: - - yarn-install + - update-package-json-version image: grafana/grafana-build:main name: rgm-build-docker pull: always @@ -4684,6 +4684,6 @@ kind: secret name: gcr_credentials --- kind: signature -hmac: 88aa054b297d39ae9757ee3a13d16b51491931fabf7c78ba0d0c125a4088da33 +hmac: 06bd63b82c56b77c48511ab3a41558122abd7df3f82c3d7b1c71f7acb8f27dc6 ... diff --git a/scripts/drone/pipelines/build.star b/scripts/drone/pipelines/build.star index f4d5800242c39..454c684759a0e 100644 --- a/scripts/drone/pipelines/build.star +++ b/scripts/drone/pipelines/build.star @@ -113,6 +113,7 @@ def build_e2e(trigger, ver_mode): rgm_build_docker_step( images["ubuntu"], images["alpine"], + depends_on = ["update-package-json-version"], tag_format = "{{ .version_base }}-{{ .buildID }}-{{ .arch }}", ubuntu_tag_format = "{{ .version_base }}-{{ .buildID }}-ubuntu-{{ .arch }}", ), From 9116043453a2673a748336cce47b940c93ea7ad8 Mon Sep 17 00:00:00 2001 From: Drew Slobodnjak <60050885+drew08t@users.noreply.github.com> Date: Wed, 1 Nov 2023 09:42:24 -0700 Subject: [PATCH 035/869] Storage: Add maxFiles to list functions (#76414) * Storage: Add maxFiles to list functions * Add maxDataPoints argument to listFiles function * Add maxFiles to ResourceDimensionEditor * Update pkg/services/store/http.go * rename First to Limit --------- Co-authored-by: jennyfana <110450222+jennyfana@users.noreply.github.com> Co-authored-by: nmarrs Co-authored-by: Ryan McKinley --- pkg/infra/filestorage/api.go | 4 ++- pkg/infra/filestorage/cdk_blob_filestorage.go | 2 +- pkg/infra/filestorage/db_filestorage.go | 2 +- pkg/infra/filestorage/fs_integration_test.go | 28 +++++++++---------- pkg/infra/filestorage/test_utils.go | 2 +- pkg/infra/filestorage/wrapper.go | 8 +++--- pkg/services/store/http.go | 3 +- pkg/services/store/service.go | 6 ++-- pkg/services/store/service_test.go | 20 ++++++------- pkg/services/store/tree.go | 18 +++++++----- pkg/services/store/types.go | 2 +- pkg/tsdb/grafanads/grafana.go | 3 +- public/app/features/canvas/elements/icon.tsx | 1 + .../dimensions/editors/FolderPickerTab.tsx | 7 +++-- .../editors/ResourceDimensionEditor.tsx | 2 ++ .../dimensions/editors/ResourcePicker.tsx | 11 ++++++-- .../editors/ResourcePickerPopover.tsx | 4 ++- public/app/features/dimensions/types.ts | 1 + .../plugins/datasource/grafana/datasource.ts | 3 +- .../panel/geomap/editor/StyleEditor.tsx | 3 ++ 20 files changed, 78 insertions(+), 52 deletions(-) diff --git a/pkg/infra/filestorage/api.go b/pkg/infra/filestorage/api.go index 184881a5b7df0..775e8686dd30e 100644 --- a/pkg/infra/filestorage/api.go +++ b/pkg/infra/filestorage/api.go @@ -93,8 +93,10 @@ type FileMetadata struct { } type Paging struct { + // The number of items to return + Limit int + // Starting after the key After string - First int } type UpsertFileCommand struct { diff --git a/pkg/infra/filestorage/cdk_blob_filestorage.go b/pkg/infra/filestorage/cdk_blob_filestorage.go index b3c14bb5305a4..81c76c629be00 100644 --- a/pkg/infra/filestorage/cdk_blob_filestorage.go +++ b/pkg/infra/filestorage/cdk_blob_filestorage.go @@ -292,7 +292,7 @@ func (c cdkBlobStorage) list(ctx context.Context, folderPath string, paging *Pag })} recursive := options.Recursive - pageSize := paging.First + pageSize := paging.Limit foundCursor := true if paging.After != "" { diff --git a/pkg/infra/filestorage/db_filestorage.go b/pkg/infra/filestorage/db_filestorage.go index 50d0ffddde4fd..47cb64e427349 100644 --- a/pkg/infra/filestorage/db_filestorage.go +++ b/pkg/infra/filestorage/db_filestorage.go @@ -345,7 +345,7 @@ func (s dbFileStorage) List(ctx context.Context, folderPath string, paging *Pagi sess.OrderBy("path") - pageSize := paging.First + pageSize := paging.Limit sess.Limit(pageSize + 1) if cursor != "" { diff --git a/pkg/infra/filestorage/fs_integration_test.go b/pkg/infra/filestorage/fs_integration_test.go index 6cecfe89a9df9..9336636ab237f 100644 --- a/pkg/infra/filestorage/fs_integration_test.go +++ b/pkg/infra/filestorage/fs_integration_test.go @@ -467,7 +467,7 @@ func TestIntegrationFsStorage(t *testing.T) { }, }, queryListFiles{ - input: queryListFilesInput{path: "/", options: &ListOptions{Recursive: true}, paging: &Paging{First: 2, After: ""}}, + input: queryListFilesInput{path: "/", options: &ListOptions{Recursive: true}, paging: &Paging{Limit: 2, After: ""}}, list: checks(listSize(2), listHasMore(true), listLastPath("/folder1/b")), files: [][]any{ checks(fPath("/folder1/a")), @@ -475,7 +475,7 @@ func TestIntegrationFsStorage(t *testing.T) { }, }, queryListFiles{ - input: queryListFilesInput{path: "/", options: &ListOptions{Recursive: true, WithFiles: true, WithFolders: true}, paging: &Paging{First: 2, After: ""}}, + input: queryListFilesInput{path: "/", options: &ListOptions{Recursive: true, WithFiles: true, WithFolders: true}, paging: &Paging{Limit: 2, After: ""}}, list: checks(listSize(2), listHasMore(true), listLastPath("/folder1/a")), files: [][]any{ checks(fPath("/folder1"), fMimeType(DirectoryMimeType)), @@ -483,49 +483,49 @@ func TestIntegrationFsStorage(t *testing.T) { }, }, queryListFiles{ - input: queryListFilesInput{path: "/", options: &ListOptions{Recursive: true, WithFiles: true, WithFolders: true}, paging: &Paging{First: 1, After: "/folder1"}}, + input: queryListFilesInput{path: "/", options: &ListOptions{Recursive: true, WithFiles: true, WithFolders: true}, paging: &Paging{Limit: 1, After: "/folder1"}}, list: checks(listSize(1), listHasMore(true), listLastPath("/folder1/a")), files: [][]any{ checks(fPath("/folder1/a")), }, }, queryListFiles{ - input: queryListFilesInput{path: "/", options: &ListOptions{Recursive: true}, paging: &Paging{First: 1, After: "/folder1/a"}}, + input: queryListFilesInput{path: "/", options: &ListOptions{Recursive: true}, paging: &Paging{Limit: 1, After: "/folder1/a"}}, list: checks(listSize(1), listHasMore(true), listLastPath("/folder1/b")), files: [][]any{ checks(fPath("/folder1/b")), }, }, queryListFiles{ - input: queryListFilesInput{path: "/", options: &ListOptions{Recursive: true, WithFiles: true, WithFolders: true}, paging: &Paging{First: 1, After: "/folder1/a"}}, + input: queryListFilesInput{path: "/", options: &ListOptions{Recursive: true, WithFiles: true, WithFolders: true}, paging: &Paging{Limit: 1, After: "/folder1/a"}}, list: checks(listSize(1), listHasMore(true), listLastPath("/folder1/b")), files: [][]any{ checks(fPath("/folder1/b")), }, }, queryListFiles{ - input: queryListFilesInput{path: "/", options: &ListOptions{Recursive: true}, paging: &Paging{First: 1, After: "/folder1/b"}}, + input: queryListFilesInput{path: "/", options: &ListOptions{Recursive: true}, paging: &Paging{Limit: 1, After: "/folder1/b"}}, list: checks(listSize(1), listHasMore(false), listLastPath("/folder2/c")), files: [][]any{ checks(fPath("/folder2/c")), }, }, queryListFiles{ - input: queryListFilesInput{path: "/", options: &ListOptions{Recursive: true, WithFiles: true, WithFolders: true}, paging: &Paging{First: 1, After: "/folder1/b"}}, + input: queryListFilesInput{path: "/", options: &ListOptions{Recursive: true, WithFiles: true, WithFolders: true}, paging: &Paging{Limit: 1, After: "/folder1/b"}}, list: checks(listSize(1), listHasMore(true), listLastPath("/folder2")), files: [][]any{ checks(fPath("/folder2"), fMimeType(DirectoryMimeType)), }, }, queryListFiles{ - input: queryListFilesInput{path: "/", options: &ListOptions{Recursive: true, WithFiles: true, WithFolders: true}, paging: &Paging{First: 1, After: "/folder2"}}, + input: queryListFilesInput{path: "/", options: &ListOptions{Recursive: true, WithFiles: true, WithFolders: true}, paging: &Paging{Limit: 1, After: "/folder2"}}, list: checks(listSize(1), listHasMore(false), listLastPath("/folder2/c")), files: [][]any{ checks(fPath("/folder2/c")), }, }, queryListFiles{ - input: queryListFilesInput{path: "/", options: &ListOptions{Recursive: true}, paging: &Paging{First: 5, After: ""}}, + input: queryListFilesInput{path: "/", options: &ListOptions{Recursive: true}, paging: &Paging{Limit: 5, After: ""}}, list: checks(listSize(3), listHasMore(false), listLastPath("/folder2/c")), files: [][]any{ checks(fPath("/folder1/a")), @@ -534,7 +534,7 @@ func TestIntegrationFsStorage(t *testing.T) { }, }, queryListFiles{ - input: queryListFilesInput{path: "/", options: &ListOptions{Recursive: true, WithFiles: true, WithFolders: true}, paging: &Paging{First: 5, After: ""}}, + input: queryListFilesInput{path: "/", options: &ListOptions{Recursive: true, WithFiles: true, WithFolders: true}, paging: &Paging{Limit: 5, After: ""}}, list: checks(listSize(5), listHasMore(false), listLastPath("/folder2/c")), files: [][]any{ checks(fPath("/folder1"), fMimeType(DirectoryMimeType)), @@ -545,19 +545,19 @@ func TestIntegrationFsStorage(t *testing.T) { }, }, queryListFiles{ - input: queryListFilesInput{path: "/", options: &ListOptions{Recursive: true}, paging: &Paging{First: 5, After: "/folder2"}}, + input: queryListFilesInput{path: "/", options: &ListOptions{Recursive: true}, paging: &Paging{Limit: 5, After: "/folder2"}}, list: checks(listSize(1), listHasMore(false)), }, queryListFiles{ - input: queryListFilesInput{path: "/", options: &ListOptions{Recursive: true, WithFiles: true, WithFolders: true}, paging: &Paging{First: 5, After: "/folder2"}}, + input: queryListFilesInput{path: "/", options: &ListOptions{Recursive: true, WithFiles: true, WithFolders: true}, paging: &Paging{Limit: 5, After: "/folder2"}}, list: checks(listSize(1), listHasMore(false)), }, queryListFiles{ - input: queryListFilesInput{path: "/", options: &ListOptions{Recursive: true}, paging: &Paging{First: 5, After: "/folder2/c"}}, + input: queryListFilesInput{path: "/", options: &ListOptions{Recursive: true}, paging: &Paging{Limit: 5, After: "/folder2/c"}}, list: checks(listSize(0), listHasMore(false)), }, queryListFiles{ - input: queryListFilesInput{path: "/", options: &ListOptions{Recursive: true, WithFiles: true, WithFolders: true}, paging: &Paging{First: 5, After: "/folder2/c"}}, + input: queryListFilesInput{path: "/", options: &ListOptions{Recursive: true, WithFiles: true, WithFolders: true}, paging: &Paging{Limit: 5, After: "/folder2/c"}}, list: checks(listSize(0), listHasMore(false)), }, }, diff --git a/pkg/infra/filestorage/test_utils.go b/pkg/infra/filestorage/test_utils.go index f63cba35caf13..0445807a094ae 100644 --- a/pkg/infra/filestorage/test_utils.go +++ b/pkg/infra/filestorage/test_utils.go @@ -321,7 +321,7 @@ func handleQuery(t *testing.T, ctx context.Context, query interface{}, queryName } resp, err := fs.List(ctx, inputPath, &Paging{ After: "", - First: 100000, + Limit: 100000, }, opts) require.NotNil(t, resp) require.NoError(t, err, "%s: should be able to list folders in %s", queryName, inputPath) diff --git a/pkg/infra/filestorage/wrapper.go b/pkg/infra/filestorage/wrapper.go index 62ddef6c0fbf5..aa236d9cbe5a4 100644 --- a/pkg/infra/filestorage/wrapper.go +++ b/pkg/infra/filestorage/wrapper.go @@ -200,12 +200,12 @@ func (b wrapper) Upsert(ctx context.Context, file *UpsertFileCommand) error { func (b wrapper) pagingOptionsWithDefaults(paging *Paging) *Paging { if paging == nil { return &Paging{ - First: 100, + Limit: 100, } } - if paging.First <= 0 { - paging.First = 100 + if paging.Limit <= 0 { + paging.Limit = 100 } if paging.After != "" { paging.After = b.addRoot(paging.After) @@ -381,7 +381,7 @@ func (b wrapper) List(ctx context.Context, folderPath string, paging *Paging, op } func (b wrapper) isFolderEmpty(ctx context.Context, path string) (bool, error) { - resp, err := b.List(ctx, path, &Paging{First: 1}, &ListOptions{Recursive: true, WithFolders: true, WithFiles: true}) + resp, err := b.List(ctx, path, &Paging{Limit: 1}, &ListOptions{Recursive: true, WithFolders: true, WithFiles: true}) if err != nil { return false, err } diff --git a/pkg/services/store/http.go b/pkg/services/store/http.go index 939995011e829..ad748a4a07b96 100644 --- a/pkg/services/store/http.go +++ b/pkg/services/store/http.go @@ -260,7 +260,8 @@ func (s *standardStorageService) doCreateFolder(c *contextmodel.ReqContext) resp func (s *standardStorageService) list(c *contextmodel.ReqContext) response.Response { params := web.Params(c.Req) path := params["*"] - frame, err := s.List(c.Req.Context(), c.SignedInUser, path) + // maxFiles of 0 will result in default behaviour from wrapper + frame, err := s.List(c.Req.Context(), c.SignedInUser, path, 0) if err != nil { return response.Error(400, "error reading path", err) } diff --git a/pkg/services/store/service.go b/pkg/services/store/service.go index 61bb61a50f012..1ad4ad85558a5 100644 --- a/pkg/services/store/service.go +++ b/pkg/services/store/service.go @@ -64,7 +64,7 @@ type StorageService interface { RegisterHTTPRoutes(routing.RouteRegister) // List folder contents - List(ctx context.Context, user *user.SignedInUser, path string) (*StorageListFrame, error) + List(ctx context.Context, user *user.SignedInUser, path string, maxFiles int) (*StorageListFrame, error) // Read raw file contents out of the store Read(ctx context.Context, user *user.SignedInUser, path string) (*filestorage.File, error) @@ -340,9 +340,9 @@ func getOrgId(user *user.SignedInUser) int64 { return user.OrgID } -func (s *standardStorageService) List(ctx context.Context, user *user.SignedInUser, path string) (*StorageListFrame, error) { +func (s *standardStorageService) List(ctx context.Context, user *user.SignedInUser, path string, maxFiles int) (*StorageListFrame, error) { guardian := s.authService.newGuardian(ctx, user, getFirstSegment(path)) - return s.tree.ListFolder(ctx, getOrgId(user), path, guardian.getPathFilter(ActionFilesRead)) + return s.tree.ListFolder(ctx, getOrgId(user), path, maxFiles, guardian.getPathFilter(ActionFilesRead)) } func (s *standardStorageService) Read(ctx context.Context, user *user.SignedInUser, path string) (*filestorage.File, error) { diff --git a/pkg/services/store/service_test.go b/pkg/services/store/service_test.go index 05fe50066d9e2..0546461787ad8 100644 --- a/pkg/services/store/service_test.go +++ b/pkg/services/store/service_test.go @@ -74,7 +74,7 @@ func TestListFiles(t *testing.T) { store := newStandardStorageService(db.InitTestDB(t), roots, func(orgId int64) []storageRuntime { return make([]storageRuntime, 0) }, allowAllAuthService, cfg, nil) - frame, err := store.List(context.Background(), dummyUser, "public/maps") + frame, err := store.List(context.Background(), dummyUser, "public/maps", 0) require.NoError(t, err) experimental.CheckGoldenJSONFrame(t, "testdata", "public_testdata.golden", frame.Frame, true) @@ -95,7 +95,7 @@ func TestListFilesWithoutPermissions(t *testing.T) { store := newStandardStorageService(db.InitTestDB(t), roots, func(orgId int64) []storageRuntime { return make([]storageRuntime, 0) }, denyAllAuthService, cfg, nil) - frame, err := store.List(context.Background(), dummyUser, "public/maps") + frame, err := store.List(context.Background(), dummyUser, "public/maps", 0) require.NoError(t, err) rowLen, err := frame.RowLen() require.NoError(t, err) @@ -371,7 +371,7 @@ func TestContentRootWithNestedStorage(t *testing.T) { Files: []*filestorage.File{}, }, nil) - _, err := store.List(context.Background(), test.user, RootContent+"/"+test.nestedRoot) + _, err := store.List(context.Background(), test.user, RootContent+"/"+test.nestedRoot, 0) require.NoError(t, err) }) @@ -387,7 +387,7 @@ func TestContentRootWithNestedStorage(t *testing.T) { Files: []*filestorage.File{}, }, nil) - _, err := store.List(context.Background(), test.user, strings.Join([]string{RootContent, test.nestedRoot, "folder1", "folder2"}, "/")) + _, err := store.List(context.Background(), test.user, strings.Join([]string{RootContent, test.nestedRoot, "folder1", "folder2"}, "/"), 0) require.NoError(t, err) }) @@ -434,16 +434,16 @@ func TestContentRootWithNestedStorage(t *testing.T) { Files: []*filestorage.File{}, }, nil) - _, err := store.List(context.Background(), test.user, strings.Join([]string{RootContent, "not-nested-content"}, "/")) + _, err := store.List(context.Background(), test.user, strings.Join([]string{RootContent, "not-nested-content"}, "/"), 0) require.NoError(t, err) - _, err = store.List(context.Background(), test.user, strings.Join([]string{RootContent, "a", "b", "c"}, "/")) + _, err = store.List(context.Background(), test.user, strings.Join([]string{RootContent, "a", "b", "c"}, "/"), 0) require.NoError(t, err) - _, err = store.List(context.Background(), test.user, strings.Join([]string{RootContent, test.nestedRoot + "a"}, "/")) + _, err = store.List(context.Background(), test.user, strings.Join([]string{RootContent, test.nestedRoot + "a"}, "/"), 0) require.NoError(t, err) - _, err = store.List(context.Background(), test.user, strings.Join([]string{RootContent, test.nestedRoot + "a", "b"}, "/")) + _, err = store.List(context.Background(), test.user, strings.Join([]string{RootContent, test.nestedRoot + "a", "b"}, "/"), 0) require.NoError(t, err) }) @@ -536,7 +536,7 @@ func TestShadowingExistingFolderByNestedContentRoot(t *testing.T) { AllowUnsanitizedSvgUpload: true, } - resp, err := store.List(ctx, globalUser, "content/nested") + resp, err := store.List(ctx, globalUser, "content/nested", 0) require.NoError(t, err) require.NotNil(t, resp) @@ -544,7 +544,7 @@ func TestShadowingExistingFolderByNestedContentRoot(t *testing.T) { require.NoError(t, err) require.Equal(t, 0, rowLen) // nested storage is empty - resp, err = store.List(ctx, globalUser, "content") + resp, err = store.List(ctx, globalUser, "content", 0) require.NoError(t, err) require.NotNil(t, resp) diff --git a/pkg/services/store/tree.go b/pkg/services/store/tree.go index 5f88d48eb341d..fe276c2c1268c 100644 --- a/pkg/services/store/tree.go +++ b/pkg/services/store/tree.go @@ -165,7 +165,7 @@ func (t *nestedTree) getStorages(orgId int64) []storageRuntime { return storages } -func (t *nestedTree) ListFolder(ctx context.Context, orgId int64, path string, accessFilter filestorage.PathFilter) (*StorageListFrame, error) { +func (t *nestedTree) ListFolder(ctx context.Context, orgId int64, path string, maxFiles int, accessFilter filestorage.PathFilter) (*StorageListFrame, error) { if path == "" || path == "/" { t.assureOrgIsInitialized(orgId) @@ -224,12 +224,16 @@ func (t *nestedTree) ListFolder(ctx context.Context, orgId int64, path string, a ) } - listResponse, err := store.List(ctx, path, nil, &filestorage.ListOptions{ - Recursive: false, - WithFolders: true, - WithFiles: true, - Filter: pathFilter, - }) + listResponse, err := store.List(ctx, path, + &filestorage.Paging{ + Limit: maxFiles, + }, + &filestorage.ListOptions{ + Recursive: false, + WithFolders: true, + WithFiles: true, + Filter: pathFilter, + }) if err != nil { return nil, err diff --git a/pkg/services/store/types.go b/pkg/services/store/types.go index 861ddfd54237d..0cde74107b90d 100644 --- a/pkg/services/store/types.go +++ b/pkg/services/store/types.go @@ -40,7 +40,7 @@ type WriteValueResponse struct { type storageTree interface { GetFile(ctx context.Context, orgId int64, path string) (*filestorage.File, error) - ListFolder(ctx context.Context, orgId int64, path string, accessFilter filestorage.PathFilter) (*StorageListFrame, error) + ListFolder(ctx context.Context, orgId int64, path string, maxFiles int, accessFilter filestorage.PathFilter) (*StorageListFrame, error) } //------------------------------------------- diff --git a/pkg/tsdb/grafanads/grafana.go b/pkg/tsdb/grafanads/grafana.go index f35a7cfb96a78..e3eb5c7f8bd17 100644 --- a/pkg/tsdb/grafanads/grafana.go +++ b/pkg/tsdb/grafanads/grafana.go @@ -124,7 +124,8 @@ func (s *Service) doListQuery(ctx context.Context, query backend.DataQuery) back } path := store.RootPublicStatic + "/" + q.Path - listFrame, err := s.store.List(ctx, nil, path) + maxFiles := int(query.MaxDataPoints) + listFrame, err := s.store.List(ctx, nil, path, maxFiles) response.Error = err if listFrame != nil { response.Frames = data.Frames{listFrame.Frame} diff --git a/public/app/features/canvas/elements/icon.tsx b/public/app/features/canvas/elements/icon.tsx index c6c66317887e2..2f5eec04394ec 100644 --- a/public/app/features/canvas/elements/icon.tsx +++ b/public/app/features/canvas/elements/icon.tsx @@ -115,6 +115,7 @@ export const iconItem: CanvasElementItem = { editor: ResourceDimensionEditor, settings: { resourceType: 'icon', + maxFiles: 2000, }, }) .addCustomEditor({ diff --git a/public/app/features/dimensions/editors/FolderPickerTab.tsx b/public/app/features/dimensions/editors/FolderPickerTab.tsx index cad3463ef84f6..ec7c225502235 100644 --- a/public/app/features/dimensions/editors/FolderPickerTab.tsx +++ b/public/app/features/dimensions/editors/FolderPickerTab.tsx @@ -35,10 +35,11 @@ interface Props { folderName: ResourceFolderName; newValue: string; setNewValue: Dispatch>; + maxFiles?: number; } export const FolderPickerTab = (props: Props) => { - const { value, mediaType, folderName, newValue, setNewValue } = props; + const { value, mediaType, folderName, newValue, setNewValue, maxFiles } = props; const styles = useStyles2(getStyles); const folders = getFolders(mediaType).map((v) => ({ @@ -75,7 +76,7 @@ export const FolderPickerTab = (props: Props) => { getDatasourceSrv() .get('-- Grafana --') .then((ds) => { - (ds as GrafanaDatasource).listFiles(folder).subscribe({ + (ds as GrafanaDatasource).listFiles(folder, maxFiles).subscribe({ next: (frame) => { const cards: ResourceItem[] = []; frame.forEach((item) => { @@ -95,7 +96,7 @@ export const FolderPickerTab = (props: Props) => { }); }); } - }, [mediaType, currentFolder]); + }, [mediaType, currentFolder, maxFiles]); return ( <> diff --git a/public/app/features/dimensions/editors/ResourceDimensionEditor.tsx b/public/app/features/dimensions/editors/ResourceDimensionEditor.tsx index 4141fe73a798d..925ac4fdbb781 100644 --- a/public/app/features/dimensions/editors/ResourceDimensionEditor.tsx +++ b/public/app/features/dimensions/editors/ResourceDimensionEditor.tsx @@ -65,6 +65,7 @@ export const ResourceDimensionEditor = ( const showSourceRadio = item.settings?.showSourceRadio ?? true; const mediaType = item.settings?.resourceType ?? MediaType.Icon; const folderName = item.settings?.folderName ?? ResourceFolderName.Icon; + const maxFiles = item.settings?.maxFiles; // undefined leads to backend default let srcPath = ''; if (mediaType === MediaType.Icon) { if (value?.fixed) { @@ -106,6 +107,7 @@ export const ResourceDimensionEditor = ( mediaType={mediaType} folderName={folderName} size={ResourcePickerSize.NORMAL} + maxFiles={maxFiles} /> )} {mode === ResourceDimensionMode.Mapping && ( diff --git a/public/app/features/dimensions/editors/ResourcePicker.tsx b/public/app/features/dimensions/editors/ResourcePicker.tsx index 488c32c897016..19af481161dd3 100644 --- a/public/app/features/dimensions/editors/ResourcePicker.tsx +++ b/public/app/features/dimensions/editors/ResourcePicker.tsx @@ -32,17 +32,24 @@ interface Props { name?: string; placeholder?: string; color?: string; + maxFiles?: number; } export const ResourcePicker = (props: Props) => { - const { value, src, name, placeholder, onChange, onClear, mediaType, folderName, size, color } = props; + const { value, src, name, placeholder, onChange, onClear, mediaType, folderName, size, color, maxFiles } = props; const styles = useStyles2(getStyles); const theme = useTheme2(); const pickerTriggerRef = createRef(); const popoverElement = ( - + ); let sanitizedSrc = src; diff --git a/public/app/features/dimensions/editors/ResourcePickerPopover.tsx b/public/app/features/dimensions/editors/ResourcePickerPopover.tsx index 50df16fdc2e14..2a72c20a9e24b 100644 --- a/public/app/features/dimensions/editors/ResourcePickerPopover.tsx +++ b/public/app/features/dimensions/editors/ResourcePickerPopover.tsx @@ -20,13 +20,14 @@ interface Props { onChange: (value?: string) => void; mediaType: MediaType; folderName: ResourceFolderName; + maxFiles?: number; } interface ErrorResponse { message: string; } export const ResourcePickerPopover = (props: Props) => { - const { value, onChange, mediaType, folderName } = props; + const { value, onChange, mediaType, folderName, maxFiles } = props; const styles = useStyles2(getStyles); const onClose = () => { @@ -55,6 +56,7 @@ export const ResourcePickerPopover = (props: Props) => { folderName={folderName} newValue={newValue} setNewValue={setNewValue} + maxFiles={maxFiles} /> ); diff --git a/public/app/features/dimensions/types.ts b/public/app/features/dimensions/types.ts index 9e6720f4b9be0..b1c1d7ec3b053 100644 --- a/public/app/features/dimensions/types.ts +++ b/public/app/features/dimensions/types.ts @@ -59,6 +59,7 @@ export interface ResourceDimensionOptions { placeholderValue?: string; // If you want your icon to be driven by value of a field showSourceRadio?: boolean; + maxFiles?: number; } export enum ResourceFolderName { diff --git a/public/app/plugins/datasource/grafana/datasource.ts b/public/app/plugins/datasource/grafana/datasource.ts index 7b0d886eb11e1..1f9cb331962f6 100644 --- a/public/app/plugins/datasource/grafana/datasource.ts +++ b/public/app/plugins/datasource/grafana/datasource.ts @@ -170,7 +170,7 @@ export class GrafanaDatasource extends DataSourceWithBackend { return of(); // nothing } - listFiles(path: string): Observable> { + listFiles(path: string, maxDataPoints?: number): Observable> { return this.query({ targets: [ { @@ -179,6 +179,7 @@ export class GrafanaDatasource extends DataSourceWithBackend { path, }, ], + maxDataPoints, } as any).pipe( map((v) => { const frame = v.data[0] ?? new MutableDataFrame(); diff --git a/public/app/plugins/panel/geomap/editor/StyleEditor.tsx b/public/app/plugins/panel/geomap/editor/StyleEditor.tsx index 7fb29cc507ff2..e11b5d895e603 100644 --- a/public/app/plugins/panel/geomap/editor/StyleEditor.tsx +++ b/public/app/plugins/panel/geomap/editor/StyleEditor.tsx @@ -120,6 +120,7 @@ export const StyleEditor = (props: Props) => { const propertyOptions = useObservable(settings?.layerInfo ?? of()); const featuresHavePoints = propertyOptions?.geometryType === GeometryTypeId.Point; const hasTextLabel = styleUsesText(value); + const maxFiles = 2000; // Simple fixed value display if (settings?.simpleFixedValues) { @@ -141,6 +142,7 @@ export const StyleEditor = (props: Props) => { placeholderText: hasTextLabel ? 'Select a symbol' : 'Select a symbol or add a text label', placeholderValue: defaultStyleConfig.symbol.fixed, showSourceRadio: false, + maxFiles, }, } as StandardEditorsRegistryItem } @@ -230,6 +232,7 @@ export const StyleEditor = (props: Props) => { placeholderText: hasTextLabel ? 'Select a symbol' : 'Select a symbol or add a text label', placeholderValue: defaultStyleConfig.symbol.fixed, showSourceRadio: false, + maxFiles, }, } as StandardEditorsRegistryItem } From 0189908798a2ac783e230335b64d7f5fe5ad8ec3 Mon Sep 17 00:00:00 2001 From: Kevin Minehart Date: Wed, 1 Nov 2023 11:51:44 -0500 Subject: [PATCH 036/869] CI: Sign drone yaml (#77512) Sign drone yaml --- .drone.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.drone.yml b/.drone.yml index f5c15812a7365..2123ec00ccd50 100644 --- a/.drone.yml +++ b/.drone.yml @@ -4684,6 +4684,6 @@ kind: secret name: gcr_credentials --- kind: signature -hmac: 06bd63b82c56b77c48511ab3a41558122abd7df3f82c3d7b1c71f7acb8f27dc6 +hmac: b3a77df22aa83cf39c768ab7efcd8724d242a431298ddf53ca1f4402f3e7d8a7 ... From 402f6e7ed6a22d4a999933c7a85f89c8f41cb0ad Mon Sep 17 00:00:00 2001 From: Carl Bergquist Date: Wed, 1 Nov 2023 17:58:03 +0100 Subject: [PATCH 037/869] Policies: Adds deprecation policy (#68439) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: bergquist Co-authored-by: Dan Cech Co-authored-by: Torkel Ödegaard --- contribute/deprecation-policy.md | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 contribute/deprecation-policy.md diff --git a/contribute/deprecation-policy.md b/contribute/deprecation-policy.md new file mode 100644 index 0000000000000..a49016588a147 --- /dev/null +++ b/contribute/deprecation-policy.md @@ -0,0 +1,31 @@ +# Deprecation policy + +We do our best to limit breaking changes and the deprecation of features to major releases. We always do our best **not** to introduce breaking changes in order to make upgrading Grafana as easy and reliable as possible. However, at times we have to introduce a breaking change by changing behavior or by removing a feature. + +To minimize the negative effects of removing a feature we require a deprecation plan that includes: + +- Determine usage levels of the feature. +- Find alternative solutions and possible migration paths. +- Announce deprecation of the feature. +- Migrate users if possible +- Give users time to adjust to the deprecation. +- Disable the feature by default. +- Remove the feature from the code base. + +Depending on the size and importance of the feature this can be a design doc or an issue. We want this to be written communication for all parties so we know it's intentional and that did a reasonable attempt to avoid breaking changes unless needed. The size of the feature also means different notice times between Depreciation and disabling as well as disabling and removal. The actual duration will depend on releases of Grafana and the table below should be used as a guide. + +Grafana employees can find more details in our internal docs. + +## Grace period between announcement and disabling feature by default + +| Size | Duration | Example | +| ------ | ---------- | ---------------------------------------------------------------- | +| Large | 1-2 years | Classic alerting, scripted dashboards, AngularJS | +| Medium | 6 months | Supported Database for Grafana's backend | +| Small | 1-3 months | Refresh OAuth access_token automatically using the refresh_token | + +## Announced deprecations. + +| Name | Annoucement Date | Disabling date | Removal Date | Description | Status | +| ------------------------------------------------------------------------ | ---------------- | -------------- | ------------ | ----------------------------------------------------------------------------------------------------------------------- | ------- | +| [Support for Mysql 5.7](https://github.com/grafana/grafana/issues/68446) | 2023-05-15 | October 2023 | | MySQL 5.7 is being deprecated in October 2023 and Grafana's policy is to test against the officially supported version. | Planned | From e913160beb8a836dd54ba4ab52bdcb169401c8f0 Mon Sep 17 00:00:00 2001 From: Jonathan Davies Date: Wed, 1 Nov 2023 17:06:38 +0000 Subject: [PATCH 038/869] docs: provisioning: Added NixOS module link. (#77273) --- docs/sources/administration/provisioning/index.md | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/docs/sources/administration/provisioning/index.md b/docs/sources/administration/provisioning/index.md index 1de8c823454ab..0f3c5f1e846da 100644 --- a/docs/sources/administration/provisioning/index.md +++ b/docs/sources/administration/provisioning/index.md @@ -59,13 +59,14 @@ If you have a literal `$` in your value and want to avoid interpolation, `$$` ca Currently we do not provide any scripts/manifests for configuring Grafana. Rather than spending time learning and creating scripts/manifests for each tool, we think our time is better spent making Grafana easier to provision. Therefore, we heavily rely on the expertise of the community. -| Tool | Project | -| --------- | -------------------------------------------------------------------------------------------------------------- | -| Puppet | [https://forge.puppet.com/puppet/grafana](https://forge.puppet.com/puppet/grafana) | -| Ansible | [https://github.com/grafana/grafana-ansible-collection](https://github.com/grafana/grafana-ansible-collection) | -| Chef | [https://github.com/sous-chefs/chef-grafana](https://github.com/sous-chefs/chef-grafana) | -| Saltstack | [https://github.com/salt-formulas/salt-formula-grafana](https://github.com/salt-formulas/salt-formula-grafana) | -| Jsonnet | [https://github.com/grafana/grafonnet-lib/](https://github.com/grafana/grafonnet-lib/) | +| Tool | Project | +| --------- | ------------------------------------------------------------------------------------------------------------------------------- | +| Puppet | [https://forge.puppet.com/puppet/grafana](https://forge.puppet.com/puppet/grafana) | +| Ansible | [https://github.com/grafana/grafana-ansible-collection](https://github.com/grafana/grafana-ansible-collection) | +| Chef | [https://github.com/sous-chefs/chef-grafana](https://github.com/sous-chefs/chef-grafana) | +| Saltstack | [https://github.com/salt-formulas/salt-formula-grafana](https://github.com/salt-formulas/salt-formula-grafana) | +| Jsonnet | [https://github.com/grafana/grafonnet-lib/](https://github.com/grafana/grafonnet-lib/) | +| NixOS | [services.grafana.provision module](https://github.com/NixOS/nixpkgs/blob/master/nixos/modules/services/monitoring/grafana.nix) | ## Data sources From d5932760d9aa0e8b550eba9b5062b7f148d94856 Mon Sep 17 00:00:00 2001 From: Jev Forsberg <46619047+baldm0mma@users.noreply.github.com> Date: Wed, 1 Nov 2023 11:20:22 -0600 Subject: [PATCH 039/869] Docs: Remove reliance on oneshell multiline feature for building transformation docs (#77514) * baldm0mma/rem_oneshell_feat/ remove reliance on oneshell multiline feature * Update docs/Makefile update whitespace Co-authored-by: Jack Baldry --------- Co-authored-by: Jack Baldry --- docs/Makefile | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/docs/Makefile b/docs/Makefile index e1c0e26cb3a0c..38997200b0de9 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -10,7 +10,6 @@ include docs.mk .PHONY: sources/panels-visualizations/query-transform-data/transform-data/index.md sources/panels-visualizations/query-transform-data/transform-data/index.md: ## Generate the Transform Data page source. sources/panels-visualizations/query-transform-data/transform-data/index.md: - cd $(CURDIR)/.. - npx tsc ./scripts/docs/generate-transformations.ts - node ./scripts/docs/generate-transformations.js > $(CURDIR)/$@ + cd $(CURDIR)/.. && npx tsc ./scripts/docs/generate-transformations.ts && \ + node ./scripts/docs/generate-transformations.js > $(CURDIR)/$@ && \ npx prettier -w $(CURDIR)/$@ From a59588a62ec85e482ce5662cc84ab88cc98660e8 Mon Sep 17 00:00:00 2001 From: Shabeeb Khalid Date: Wed, 1 Nov 2023 21:06:06 +0200 Subject: [PATCH 040/869] Cloudwatch: Use context in aws DescribeLogGroupsWithContext (#77176) --- pkg/tsdb/cloudwatch/cloudwatch.go | 2 +- pkg/tsdb/cloudwatch/cloudwatch_test.go | 4 +- pkg/tsdb/cloudwatch/metric_find_query.go | 2 +- pkg/tsdb/cloudwatch/mocks/logs.go | 4 +- pkg/tsdb/cloudwatch/models/api.go | 4 +- pkg/tsdb/cloudwatch/routes/log_groups.go | 2 +- pkg/tsdb/cloudwatch/routes/log_groups_test.go | 32 ++++---- pkg/tsdb/cloudwatch/services/log_groups.go | 4 +- .../cloudwatch/services/log_groups_test.go | 74 +++++++++---------- pkg/tsdb/cloudwatch/test_utils.go | 10 +-- 10 files changed, 66 insertions(+), 72 deletions(-) diff --git a/pkg/tsdb/cloudwatch/cloudwatch.go b/pkg/tsdb/cloudwatch/cloudwatch.go index 2807d7372cd18..e5e0d590bd870 100644 --- a/pkg/tsdb/cloudwatch/cloudwatch.go +++ b/pkg/tsdb/cloudwatch/cloudwatch.go @@ -236,7 +236,7 @@ func (e *cloudWatchExecutor) checkHealthLogs(ctx context.Context, pluginCtx back return err } logsClient := NewLogsAPI(session) - _, err = logsClient.DescribeLogGroups(&cloudwatchlogs.DescribeLogGroupsInput{Limit: aws.Int64(1)}) + _, err = logsClient.DescribeLogGroupsWithContext(ctx, &cloudwatchlogs.DescribeLogGroupsInput{Limit: aws.Int64(1)}) return err } diff --git a/pkg/tsdb/cloudwatch/cloudwatch_test.go b/pkg/tsdb/cloudwatch/cloudwatch_test.go index 7f05039d6dc11..3e4992a20a84b 100644 --- a/pkg/tsdb/cloudwatch/cloudwatch_test.go +++ b/pkg/tsdb/cloudwatch/cloudwatch_test.go @@ -220,7 +220,7 @@ func TestQuery_ResourceRequest_DescribeLogGroups_with_CrossAccountQuerying(t *te t.Run("maps log group api response to resource response of log-groups", func(t *testing.T) { logsApi = mocks.LogsAPI{} - logsApi.On("DescribeLogGroups", mock.Anything).Return(&cloudwatchlogs.DescribeLogGroupsOutput{ + logsApi.On("DescribeLogGroupsWithContext", mock.Anything).Return(&cloudwatchlogs.DescribeLogGroupsOutput{ LogGroups: []*cloudwatchlogs.LogGroup{ {Arn: aws.String("arn:aws:logs:us-east-1:111:log-group:group_a"), LogGroupName: aws.String("group_a")}, }, @@ -248,7 +248,7 @@ func TestQuery_ResourceRequest_DescribeLogGroups_with_CrossAccountQuerying(t *te } ]`, string(sender.Response.Body)) - logsApi.AssertCalled(t, "DescribeLogGroups", + logsApi.AssertCalled(t, "DescribeLogGroupsWithContext", &cloudwatchlogs.DescribeLogGroupsInput{ AccountIdentifiers: []*string{utils.Pointer("some-account-id")}, IncludeLinkedAccounts: utils.Pointer(true), diff --git a/pkg/tsdb/cloudwatch/metric_find_query.go b/pkg/tsdb/cloudwatch/metric_find_query.go index 5f91f118cad81..2960d78de8043 100644 --- a/pkg/tsdb/cloudwatch/metric_find_query.go +++ b/pkg/tsdb/cloudwatch/metric_find_query.go @@ -307,7 +307,7 @@ func (e *cloudWatchExecutor) handleGetLogGroups(ctx context.Context, pluginCtx b input.LogGroupNamePrefix = aws.String(logGroupNamePrefix) } var response *cloudwatchlogs.DescribeLogGroupsOutput - response, err = logsClient.DescribeLogGroups(input) + response, err = logsClient.DescribeLogGroupsWithContext(ctx, input) if err != nil || response == nil { return nil, err } diff --git a/pkg/tsdb/cloudwatch/mocks/logs.go b/pkg/tsdb/cloudwatch/mocks/logs.go index 51d838cf73c21..6b3962703b017 100644 --- a/pkg/tsdb/cloudwatch/mocks/logs.go +++ b/pkg/tsdb/cloudwatch/mocks/logs.go @@ -15,7 +15,7 @@ type LogsAPI struct { mock.Mock } -func (l *LogsAPI) DescribeLogGroups(input *cloudwatchlogs.DescribeLogGroupsInput) (*cloudwatchlogs.DescribeLogGroupsOutput, error) { +func (l *LogsAPI) DescribeLogGroupsWithContext(ctx context.Context, input *cloudwatchlogs.DescribeLogGroupsInput, option ...request.Option) (*cloudwatchlogs.DescribeLogGroupsOutput, error) { args := l.Called(input) return args.Get(0).(*cloudwatchlogs.DescribeLogGroupsOutput), args.Error(1) @@ -31,7 +31,7 @@ type LogsService struct { mock.Mock } -func (l *LogsService) GetLogGroups(request resources.LogGroupsRequest) ([]resources.ResourceResponse[resources.LogGroup], error) { +func (l *LogsService) GetLogGroupsWithContext(ctx context.Context, request resources.LogGroupsRequest) ([]resources.ResourceResponse[resources.LogGroup], error) { args := l.Called(request) return args.Get(0).([]resources.ResourceResponse[resources.LogGroup]), args.Error(1) diff --git a/pkg/tsdb/cloudwatch/models/api.go b/pkg/tsdb/cloudwatch/models/api.go index fc1af9c4d53e7..f950d750ffd37 100644 --- a/pkg/tsdb/cloudwatch/models/api.go +++ b/pkg/tsdb/cloudwatch/models/api.go @@ -37,7 +37,7 @@ type ListMetricsProvider interface { } type LogGroupsProvider interface { - GetLogGroups(request resources.LogGroupsRequest) ([]resources.ResourceResponse[resources.LogGroup], error) + GetLogGroupsWithContext(ctx context.Context, request resources.LogGroupsRequest) ([]resources.ResourceResponse[resources.LogGroup], error) GetLogGroupFieldsWithContext(ctx context.Context, request resources.LogGroupFieldsRequest, option ...request.Option) ([]resources.ResourceResponse[resources.LogGroupField], error) } @@ -60,7 +60,7 @@ type CloudWatchMetricsAPIProvider interface { } type CloudWatchLogsAPIProvider interface { - DescribeLogGroups(*cloudwatchlogs.DescribeLogGroupsInput) (*cloudwatchlogs.DescribeLogGroupsOutput, error) + DescribeLogGroupsWithContext(ctx context.Context, in *cloudwatchlogs.DescribeLogGroupsInput, opts ...request.Option) (*cloudwatchlogs.DescribeLogGroupsOutput, error) GetLogGroupFieldsWithContext(ctx context.Context, in *cloudwatchlogs.GetLogGroupFieldsInput, option ...request.Option) (*cloudwatchlogs.GetLogGroupFieldsOutput, error) } diff --git a/pkg/tsdb/cloudwatch/routes/log_groups.go b/pkg/tsdb/cloudwatch/routes/log_groups.go index 42b2c2d817dd1..0ed15c70180ff 100644 --- a/pkg/tsdb/cloudwatch/routes/log_groups.go +++ b/pkg/tsdb/cloudwatch/routes/log_groups.go @@ -24,7 +24,7 @@ func LogGroupsHandler(ctx context.Context, pluginCtx backend.PluginContext, reqC return nil, models.NewHttpError("newLogGroupsService error", http.StatusInternalServerError, err) } - logGroups, err := service.GetLogGroups(request) + logGroups, err := service.GetLogGroupsWithContext(ctx, request) if err != nil { return nil, models.NewHttpError("GetLogGroups error", http.StatusInternalServerError, err) } diff --git a/pkg/tsdb/cloudwatch/routes/log_groups_test.go b/pkg/tsdb/cloudwatch/routes/log_groups_test.go index 5f478f6cc432e..8a6d88613d64e 100644 --- a/pkg/tsdb/cloudwatch/routes/log_groups_test.go +++ b/pkg/tsdb/cloudwatch/routes/log_groups_test.go @@ -31,7 +31,7 @@ func TestLogGroupsRoute(t *testing.T) { t.Run("successfully returns 1 log group with account id", func(t *testing.T) { mockLogsService := mocks.LogsService{} - mockLogsService.On("GetLogGroups", mock.Anything).Return([]resources.ResourceResponse[resources.LogGroup]{{ + mockLogsService.On("GetLogGroupsWithContext", mock.Anything).Return([]resources.ResourceResponse[resources.LogGroup]{{ Value: resources.LogGroup{ Arn: "some arn", Name: "some name", @@ -53,7 +53,7 @@ func TestLogGroupsRoute(t *testing.T) { t.Run("successfully returns multiple log groups with account id", func(t *testing.T) { mockLogsService := mocks.LogsService{} - mockLogsService.On("GetLogGroups", mock.Anything).Return( + mockLogsService.On("GetLogGroupsWithContext", mock.Anything).Return( []resources.ResourceResponse[resources.LogGroup]{ { Value: resources.LogGroup{ @@ -99,7 +99,7 @@ func TestLogGroupsRoute(t *testing.T) { t.Run("returns error when both logGroupPrefix and logGroup Pattern are provided", func(t *testing.T) { mockLogsService := mocks.LogsService{} - mockLogsService.On("GetLogGroups", mock.Anything).Return([]resources.ResourceResponse[resources.LogGroup]{}, nil) + mockLogsService.On("GetLogGroupsWithContext", mock.Anything).Return([]resources.ResourceResponse[resources.LogGroup]{}, nil) newLogGroupsService = func(_ context.Context, pluginCtx backend.PluginContext, reqCtxFactory models.RequestContextFactoryFunc, region string) (models.LogGroupsProvider, error) { return &mockLogsService, nil } @@ -115,7 +115,7 @@ func TestLogGroupsRoute(t *testing.T) { t.Run("passes default log group limit and nil for logGroupNamePrefix, accountId, and logGroupPattern", func(t *testing.T) { mockLogsService := mocks.LogsService{} - mockLogsService.On("GetLogGroups", mock.Anything).Return([]resources.ResourceResponse[resources.LogGroup]{}, nil) + mockLogsService.On("GetLogGroupsWithContext", mock.Anything).Return([]resources.ResourceResponse[resources.LogGroup]{}, nil) newLogGroupsService = func(_ context.Context, pluginCtx backend.PluginContext, reqCtxFactory models.RequestContextFactoryFunc, region string) (models.LogGroupsProvider, error) { return &mockLogsService, nil } @@ -125,7 +125,7 @@ func TestLogGroupsRoute(t *testing.T) { handler := http.HandlerFunc(ResourceRequestMiddleware(LogGroupsHandler, logger, reqCtxFunc)) handler.ServeHTTP(rr, req) - mockLogsService.AssertCalled(t, "GetLogGroups", resources.LogGroupsRequest{ + mockLogsService.AssertCalled(t, "GetLogGroupsWithContext", resources.LogGroupsRequest{ Limit: 50, ResourceRequest: resources.ResourceRequest{}, LogGroupNamePrefix: nil, @@ -135,7 +135,7 @@ func TestLogGroupsRoute(t *testing.T) { t.Run("passes default log group limit and nil for logGroupNamePrefix when both are absent", func(t *testing.T) { mockLogsService := mocks.LogsService{} - mockLogsService.On("GetLogGroups", mock.Anything).Return([]resources.ResourceResponse[resources.LogGroup]{}, nil) + mockLogsService.On("GetLogGroupsWithContext", mock.Anything).Return([]resources.ResourceResponse[resources.LogGroup]{}, nil) newLogGroupsService = func(_ context.Context, pluginCtx backend.PluginContext, reqCtxFactory models.RequestContextFactoryFunc, region string) (models.LogGroupsProvider, error) { return &mockLogsService, nil } @@ -145,7 +145,7 @@ func TestLogGroupsRoute(t *testing.T) { handler := http.HandlerFunc(ResourceRequestMiddleware(LogGroupsHandler, logger, reqCtxFunc)) handler.ServeHTTP(rr, req) - mockLogsService.AssertCalled(t, "GetLogGroups", resources.LogGroupsRequest{ + mockLogsService.AssertCalled(t, "GetLogGroupsWithContext", resources.LogGroupsRequest{ Limit: 50, LogGroupNamePrefix: nil, }) @@ -153,7 +153,7 @@ func TestLogGroupsRoute(t *testing.T) { t.Run("passes log group limit from query parameter", func(t *testing.T) { mockLogsService := mocks.LogsService{} - mockLogsService.On("GetLogGroups", mock.Anything).Return([]resources.ResourceResponse[resources.LogGroup]{}, nil) + mockLogsService.On("GetLogGroupsWithContext", mock.Anything).Return([]resources.ResourceResponse[resources.LogGroup]{}, nil) newLogGroupsService = func(_ context.Context, pluginCtx backend.PluginContext, reqCtxFactory models.RequestContextFactoryFunc, region string) (models.LogGroupsProvider, error) { return &mockLogsService, nil } @@ -163,14 +163,14 @@ func TestLogGroupsRoute(t *testing.T) { handler := http.HandlerFunc(ResourceRequestMiddleware(LogGroupsHandler, logger, reqCtxFunc)) handler.ServeHTTP(rr, req) - mockLogsService.AssertCalled(t, "GetLogGroups", resources.LogGroupsRequest{ + mockLogsService.AssertCalled(t, "GetLogGroupsWithContext", resources.LogGroupsRequest{ Limit: 2, }) }) t.Run("passes logGroupPrefix from query parameter", func(t *testing.T) { mockLogsService := mocks.LogsService{} - mockLogsService.On("GetLogGroups", mock.Anything).Return([]resources.ResourceResponse[resources.LogGroup]{}, nil) + mockLogsService.On("GetLogGroupsWithContext", mock.Anything).Return([]resources.ResourceResponse[resources.LogGroup]{}, nil) newLogGroupsService = func(_ context.Context, pluginCtx backend.PluginContext, reqCtxFactory models.RequestContextFactoryFunc, region string) (models.LogGroupsProvider, error) { return &mockLogsService, nil } @@ -180,7 +180,7 @@ func TestLogGroupsRoute(t *testing.T) { handler := http.HandlerFunc(ResourceRequestMiddleware(LogGroupsHandler, logger, reqCtxFunc)) handler.ServeHTTP(rr, req) - mockLogsService.AssertCalled(t, "GetLogGroups", resources.LogGroupsRequest{ + mockLogsService.AssertCalled(t, "GetLogGroupsWithContext", resources.LogGroupsRequest{ Limit: 50, LogGroupNamePrefix: utils.Pointer("some-prefix"), }) @@ -188,7 +188,7 @@ func TestLogGroupsRoute(t *testing.T) { t.Run("passes logGroupPattern from query parameter", func(t *testing.T) { mockLogsService := mocks.LogsService{} - mockLogsService.On("GetLogGroups", mock.Anything).Return([]resources.ResourceResponse[resources.LogGroup]{}, nil) + mockLogsService.On("GetLogGroupsWithContext", mock.Anything).Return([]resources.ResourceResponse[resources.LogGroup]{}, nil) newLogGroupsService = func(_ context.Context, pluginCtx backend.PluginContext, reqCtxFactory models.RequestContextFactoryFunc, region string) (models.LogGroupsProvider, error) { return &mockLogsService, nil } @@ -198,7 +198,7 @@ func TestLogGroupsRoute(t *testing.T) { handler := http.HandlerFunc(ResourceRequestMiddleware(LogGroupsHandler, logger, reqCtxFunc)) handler.ServeHTTP(rr, req) - mockLogsService.AssertCalled(t, "GetLogGroups", resources.LogGroupsRequest{ + mockLogsService.AssertCalled(t, "GetLogGroupsWithContext", resources.LogGroupsRequest{ Limit: 50, LogGroupNamePattern: utils.Pointer("some-pattern"), }) @@ -206,7 +206,7 @@ func TestLogGroupsRoute(t *testing.T) { t.Run("passes logGroupPattern from query parameter", func(t *testing.T) { mockLogsService := mocks.LogsService{} - mockLogsService.On("GetLogGroups", mock.Anything).Return([]resources.ResourceResponse[resources.LogGroup]{}, nil) + mockLogsService.On("GetLogGroupsWithContext", mock.Anything).Return([]resources.ResourceResponse[resources.LogGroup]{}, nil) newLogGroupsService = func(_ context.Context, pluginCtx backend.PluginContext, reqCtxFactory models.RequestContextFactoryFunc, region string) (models.LogGroupsProvider, error) { return &mockLogsService, nil } @@ -216,7 +216,7 @@ func TestLogGroupsRoute(t *testing.T) { handler := http.HandlerFunc(ResourceRequestMiddleware(LogGroupsHandler, logger, reqCtxFunc)) handler.ServeHTTP(rr, req) - mockLogsService.AssertCalled(t, "GetLogGroups", resources.LogGroupsRequest{ + mockLogsService.AssertCalled(t, "GetLogGroupsWithContext", resources.LogGroupsRequest{ Limit: 50, ResourceRequest: resources.ResourceRequest{AccountId: utils.Pointer("some-account-id")}, }) @@ -224,7 +224,7 @@ func TestLogGroupsRoute(t *testing.T) { t.Run("returns error if service returns error", func(t *testing.T) { mockLogsService := mocks.LogsService{} - mockLogsService.On("GetLogGroups", mock.Anything). + mockLogsService.On("GetLogGroupsWithContext", mock.Anything). Return([]resources.ResourceResponse[resources.LogGroup]{}, fmt.Errorf("some error")) newLogGroupsService = func(_ context.Context, pluginCtx backend.PluginContext, reqCtxFactory models.RequestContextFactoryFunc, region string) (models.LogGroupsProvider, error) { return &mockLogsService, nil diff --git a/pkg/tsdb/cloudwatch/services/log_groups.go b/pkg/tsdb/cloudwatch/services/log_groups.go index ee146786f4737..1bbc99a67bd02 100644 --- a/pkg/tsdb/cloudwatch/services/log_groups.go +++ b/pkg/tsdb/cloudwatch/services/log_groups.go @@ -20,7 +20,7 @@ func NewLogGroupsService(logsClient models.CloudWatchLogsAPIProvider, isCrossAcc return &LogGroupsService{logGroupsAPI: logsClient, isCrossAccountEnabled: isCrossAccountEnabled} } -func (s *LogGroupsService) GetLogGroups(req resources.LogGroupsRequest) ([]resources.ResourceResponse[resources.LogGroup], error) { +func (s *LogGroupsService) GetLogGroupsWithContext(ctx context.Context, req resources.LogGroupsRequest) ([]resources.ResourceResponse[resources.LogGroup], error) { input := &cloudwatchlogs.DescribeLogGroupsInput{ Limit: aws.Int64(req.Limit), LogGroupNamePrefix: req.LogGroupNamePrefix, @@ -39,7 +39,7 @@ func (s *LogGroupsService) GetLogGroups(req resources.LogGroupsRequest) ([]resou result := []resources.ResourceResponse[resources.LogGroup]{} for { - response, err := s.logGroupsAPI.DescribeLogGroups(input) + response, err := s.logGroupsAPI.DescribeLogGroupsWithContext(ctx, input) if err != nil || response == nil { return nil, err } diff --git a/pkg/tsdb/cloudwatch/services/log_groups_test.go b/pkg/tsdb/cloudwatch/services/log_groups_test.go index 9c2c5c0f8d532..6b9a09170c3a1 100644 --- a/pkg/tsdb/cloudwatch/services/log_groups_test.go +++ b/pkg/tsdb/cloudwatch/services/log_groups_test.go @@ -17,7 +17,7 @@ import ( func TestGetLogGroups(t *testing.T) { t.Run("Should map log groups response", func(t *testing.T) { mockLogsAPI := &mocks.LogsAPI{} - mockLogsAPI.On("DescribeLogGroups", mock.Anything).Return( + mockLogsAPI.On("DescribeLogGroupsWithContext", mock.Anything).Return( &cloudwatchlogs.DescribeLogGroupsOutput{ LogGroups: []*cloudwatchlogs.LogGroup{ {Arn: utils.Pointer("arn:aws:logs:us-east-1:111:log-group:group_a"), LogGroupName: utils.Pointer("group_a")}, @@ -27,7 +27,7 @@ func TestGetLogGroups(t *testing.T) { }, nil) service := NewLogGroupsService(mockLogsAPI, false) - resp, err := service.GetLogGroups(resources.LogGroupsRequest{}) + resp, err := service.GetLogGroupsWithContext(context.Background(), resources.LogGroupsRequest{}) assert.NoError(t, err) assert.Equal(t, []resources.ResourceResponse[resources.LogGroup]{ @@ -48,10 +48,10 @@ func TestGetLogGroups(t *testing.T) { t.Run("Should return an empty error if api doesn't return any data", func(t *testing.T) { mockLogsAPI := &mocks.LogsAPI{} - mockLogsAPI.On("DescribeLogGroups", mock.Anything).Return(&cloudwatchlogs.DescribeLogGroupsOutput{}, nil) + mockLogsAPI.On("DescribeLogGroupsWithContext", mock.Anything).Return(&cloudwatchlogs.DescribeLogGroupsOutput{}, nil) service := NewLogGroupsService(mockLogsAPI, false) - resp, err := service.GetLogGroups(resources.LogGroupsRequest{}) + resp, err := service.GetLogGroupsWithContext(context.Background(), resources.LogGroupsRequest{}) assert.NoError(t, err) assert.Equal(t, []resources.ResourceResponse[resources.LogGroup]{}, resp) @@ -60,16 +60,16 @@ func TestGetLogGroups(t *testing.T) { t.Run("Should only use LogGroupNamePrefix even if LogGroupNamePattern passed in resource call", func(t *testing.T) { // TODO: use LogGroupNamePattern when we have accounted for its behavior, still a little unexpected at the moment mockLogsAPI := &mocks.LogsAPI{} - mockLogsAPI.On("DescribeLogGroups", mock.Anything).Return(&cloudwatchlogs.DescribeLogGroupsOutput{}, nil) + mockLogsAPI.On("DescribeLogGroupsWithContext", mock.Anything).Return(&cloudwatchlogs.DescribeLogGroupsOutput{}, nil) service := NewLogGroupsService(mockLogsAPI, false) - _, err := service.GetLogGroups(resources.LogGroupsRequest{ + _, err := service.GetLogGroupsWithContext(context.Background(), resources.LogGroupsRequest{ Limit: 0, LogGroupNamePrefix: utils.Pointer("test"), }) assert.NoError(t, err) - mockLogsAPI.AssertCalled(t, "DescribeLogGroups", &cloudwatchlogs.DescribeLogGroupsInput{ + mockLogsAPI.AssertCalled(t, "DescribeLogGroupsWithContext", &cloudwatchlogs.DescribeLogGroupsInput{ Limit: utils.Pointer(int64(0)), LogGroupNamePrefix: utils.Pointer("test"), }) @@ -77,24 +77,24 @@ func TestGetLogGroups(t *testing.T) { t.Run("Should call api without LogGroupNamePrefix nor LogGroupNamePattern if not passed in resource call", func(t *testing.T) { mockLogsAPI := &mocks.LogsAPI{} - mockLogsAPI.On("DescribeLogGroups", mock.Anything).Return(&cloudwatchlogs.DescribeLogGroupsOutput{}, nil) + mockLogsAPI.On("DescribeLogGroupsWithContext", mock.Anything).Return(&cloudwatchlogs.DescribeLogGroupsOutput{}, nil) service := NewLogGroupsService(mockLogsAPI, false) - _, err := service.GetLogGroups(resources.LogGroupsRequest{}) + _, err := service.GetLogGroupsWithContext(context.Background(), resources.LogGroupsRequest{}) assert.NoError(t, err) - mockLogsAPI.AssertCalled(t, "DescribeLogGroups", &cloudwatchlogs.DescribeLogGroupsInput{ + mockLogsAPI.AssertCalled(t, "DescribeLogGroupsWithContext", &cloudwatchlogs.DescribeLogGroupsInput{ Limit: utils.Pointer(int64(0)), }) }) t.Run("Should return an error when API returns error", func(t *testing.T) { mockLogsAPI := &mocks.LogsAPI{} - mockLogsAPI.On("DescribeLogGroups", mock.Anything).Return(&cloudwatchlogs.DescribeLogGroupsOutput{}, + mockLogsAPI.On("DescribeLogGroupsWithContext", mock.Anything).Return(&cloudwatchlogs.DescribeLogGroupsOutput{}, fmt.Errorf("some error")) service := NewLogGroupsService(mockLogsAPI, false) - _, err := service.GetLogGroups(resources.LogGroupsRequest{}) + _, err := service.GetLogGroupsWithContext(context.Background(), resources.LogGroupsRequest{}) assert.Error(t, err) assert.Equal(t, "some error", err.Error()) @@ -108,7 +108,7 @@ func TestGetLogGroups(t *testing.T) { ListAllLogGroups: false, } - mockLogsAPI.On("DescribeLogGroups", &cloudwatchlogs.DescribeLogGroupsInput{ + mockLogsAPI.On("DescribeLogGroupsWithContext", &cloudwatchlogs.DescribeLogGroupsInput{ Limit: aws.Int64(req.Limit), LogGroupNamePrefix: req.LogGroupNamePrefix, }).Return(&cloudwatchlogs.DescribeLogGroupsOutput{ @@ -119,10 +119,10 @@ func TestGetLogGroups(t *testing.T) { }, nil) service := NewLogGroupsService(mockLogsAPI, false) - resp, err := service.GetLogGroups(req) + resp, err := service.GetLogGroupsWithContext(context.Background(), req) assert.NoError(t, err) - mockLogsAPI.AssertNumberOfCalls(t, "DescribeLogGroups", 1) + mockLogsAPI.AssertNumberOfCalls(t, "DescribeLogGroupsWithContext", 1) assert.Equal(t, []resources.ResourceResponse[resources.LogGroup]{ { AccountId: utils.Pointer("111"), @@ -140,7 +140,7 @@ func TestGetLogGroups(t *testing.T) { } // first call - mockLogsAPI.On("DescribeLogGroups", &cloudwatchlogs.DescribeLogGroupsInput{ + mockLogsAPI.On("DescribeLogGroupsWithContext", &cloudwatchlogs.DescribeLogGroupsInput{ Limit: aws.Int64(req.Limit), LogGroupNamePrefix: req.LogGroupNamePrefix, }).Return(&cloudwatchlogs.DescribeLogGroupsOutput{ @@ -151,7 +151,7 @@ func TestGetLogGroups(t *testing.T) { }, nil) // second call - mockLogsAPI.On("DescribeLogGroups", &cloudwatchlogs.DescribeLogGroupsInput{ + mockLogsAPI.On("DescribeLogGroupsWithContext", &cloudwatchlogs.DescribeLogGroupsInput{ Limit: aws.Int64(req.Limit), LogGroupNamePrefix: req.LogGroupNamePrefix, NextToken: utils.Pointer("token"), @@ -161,9 +161,9 @@ func TestGetLogGroups(t *testing.T) { }, }, nil) service := NewLogGroupsService(mockLogsAPI, false) - resp, err := service.GetLogGroups(req) + resp, err := service.GetLogGroupsWithContext(context.Background(), req) assert.NoError(t, err) - mockLogsAPI.AssertNumberOfCalls(t, "DescribeLogGroups", 2) + mockLogsAPI.AssertNumberOfCalls(t, "DescribeLogGroupsWithContext", 2) assert.Equal(t, []resources.ResourceResponse[resources.LogGroup]{ { AccountId: utils.Pointer("111"), @@ -180,16 +180,16 @@ func TestGetLogGroups(t *testing.T) { func TestGetLogGroupsCrossAccountQuerying(t *testing.T) { t.Run("Should not includeLinkedAccounts or accountId if isCrossAccountEnabled is set to false", func(t *testing.T) { mockLogsAPI := &mocks.LogsAPI{} - mockLogsAPI.On("DescribeLogGroups", mock.Anything).Return(&cloudwatchlogs.DescribeLogGroupsOutput{}, nil) + mockLogsAPI.On("DescribeLogGroupsWithContext", mock.Anything).Return(&cloudwatchlogs.DescribeLogGroupsOutput{}, nil) service := NewLogGroupsService(mockLogsAPI, false) - _, err := service.GetLogGroups(resources.LogGroupsRequest{ + _, err := service.GetLogGroupsWithContext(context.Background(), resources.LogGroupsRequest{ ResourceRequest: resources.ResourceRequest{AccountId: utils.Pointer("accountId")}, LogGroupNamePrefix: utils.Pointer("prefix"), }) assert.NoError(t, err) - mockLogsAPI.AssertCalled(t, "DescribeLogGroups", &cloudwatchlogs.DescribeLogGroupsInput{ + mockLogsAPI.AssertCalled(t, "DescribeLogGroupsWithContext", &cloudwatchlogs.DescribeLogGroupsInput{ Limit: utils.Pointer(int64(0)), LogGroupNamePrefix: utils.Pointer("prefix"), }) @@ -197,17 +197,17 @@ func TestGetLogGroupsCrossAccountQuerying(t *testing.T) { t.Run("Should replace LogGroupNamePrefix if LogGroupNamePattern passed in resource call", func(t *testing.T) { mockLogsAPI := &mocks.LogsAPI{} - mockLogsAPI.On("DescribeLogGroups", mock.Anything).Return(&cloudwatchlogs.DescribeLogGroupsOutput{}, nil) + mockLogsAPI.On("DescribeLogGroupsWithContext", mock.Anything).Return(&cloudwatchlogs.DescribeLogGroupsOutput{}, nil) service := NewLogGroupsService(mockLogsAPI, true) - _, err := service.GetLogGroups(resources.LogGroupsRequest{ + _, err := service.GetLogGroupsWithContext(context.Background(), resources.LogGroupsRequest{ ResourceRequest: resources.ResourceRequest{AccountId: utils.Pointer("accountId")}, LogGroupNamePrefix: utils.Pointer("prefix"), LogGroupNamePattern: utils.Pointer("pattern"), }) assert.NoError(t, err) - mockLogsAPI.AssertCalled(t, "DescribeLogGroups", &cloudwatchlogs.DescribeLogGroupsInput{ + mockLogsAPI.AssertCalled(t, "DescribeLogGroupsWithContext", &cloudwatchlogs.DescribeLogGroupsInput{ AccountIdentifiers: []*string{utils.Pointer("accountId")}, Limit: utils.Pointer(int64(0)), LogGroupNamePrefix: utils.Pointer("pattern"), @@ -217,15 +217,15 @@ func TestGetLogGroupsCrossAccountQuerying(t *testing.T) { t.Run("Should includeLinkedAccounts,and accountId if isCrossAccountEnabled is set to true", func(t *testing.T) { mockLogsAPI := &mocks.LogsAPI{} - mockLogsAPI.On("DescribeLogGroups", mock.Anything).Return(&cloudwatchlogs.DescribeLogGroupsOutput{}, nil) + mockLogsAPI.On("DescribeLogGroupsWithContext", mock.Anything).Return(&cloudwatchlogs.DescribeLogGroupsOutput{}, nil) service := NewLogGroupsService(mockLogsAPI, true) - _, err := service.GetLogGroups(resources.LogGroupsRequest{ + _, err := service.GetLogGroupsWithContext(context.Background(), resources.LogGroupsRequest{ ResourceRequest: resources.ResourceRequest{AccountId: utils.Pointer("accountId")}, }) assert.NoError(t, err) - mockLogsAPI.AssertCalled(t, "DescribeLogGroups", &cloudwatchlogs.DescribeLogGroupsInput{ + mockLogsAPI.AssertCalled(t, "DescribeLogGroupsWithContext", &cloudwatchlogs.DescribeLogGroupsInput{ Limit: utils.Pointer(int64(0)), IncludeLinkedAccounts: utils.Pointer(true), AccountIdentifiers: []*string{utils.Pointer("accountId")}, @@ -234,15 +234,15 @@ func TestGetLogGroupsCrossAccountQuerying(t *testing.T) { t.Run("Should should not override prefix is there is no logGroupNamePattern", func(t *testing.T) { mockLogsAPI := &mocks.LogsAPI{} - mockLogsAPI.On("DescribeLogGroups", mock.Anything).Return(&cloudwatchlogs.DescribeLogGroupsOutput{}, nil) + mockLogsAPI.On("DescribeLogGroupsWithContext", mock.Anything).Return(&cloudwatchlogs.DescribeLogGroupsOutput{}, nil) service := NewLogGroupsService(mockLogsAPI, true) - _, err := service.GetLogGroups(resources.LogGroupsRequest{ + _, err := service.GetLogGroupsWithContext(context.Background(), resources.LogGroupsRequest{ ResourceRequest: resources.ResourceRequest{AccountId: utils.Pointer("accountId")}, LogGroupNamePrefix: utils.Pointer("prefix"), }) assert.NoError(t, err) - mockLogsAPI.AssertCalled(t, "DescribeLogGroups", &cloudwatchlogs.DescribeLogGroupsInput{ + mockLogsAPI.AssertCalled(t, "DescribeLogGroupsWithContext", &cloudwatchlogs.DescribeLogGroupsInput{ AccountIdentifiers: []*string{utils.Pointer("accountId")}, Limit: utils.Pointer(int64(0)), LogGroupNamePrefix: utils.Pointer("prefix"), @@ -252,15 +252,15 @@ func TestGetLogGroupsCrossAccountQuerying(t *testing.T) { t.Run("Should not includeLinkedAccounts, or accountId if accountId is nil", func(t *testing.T) { mockLogsAPI := &mocks.LogsAPI{} - mockLogsAPI.On("DescribeLogGroups", mock.Anything).Return(&cloudwatchlogs.DescribeLogGroupsOutput{}, nil) + mockLogsAPI.On("DescribeLogGroupsWithContext", mock.Anything).Return(&cloudwatchlogs.DescribeLogGroupsOutput{}, nil) service := NewLogGroupsService(mockLogsAPI, true) - _, err := service.GetLogGroups(resources.LogGroupsRequest{ + _, err := service.GetLogGroupsWithContext(context.Background(), resources.LogGroupsRequest{ LogGroupNamePrefix: utils.Pointer("prefix"), }) assert.NoError(t, err) - mockLogsAPI.AssertCalled(t, "DescribeLogGroups", &cloudwatchlogs.DescribeLogGroupsInput{ + mockLogsAPI.AssertCalled(t, "DescribeLogGroupsWithContext", &cloudwatchlogs.DescribeLogGroupsInput{ Limit: utils.Pointer(int64(0)), LogGroupNamePrefix: utils.Pointer("prefix"), }) @@ -268,10 +268,10 @@ func TestGetLogGroupsCrossAccountQuerying(t *testing.T) { t.Run("Should should not override prefix is there is no logGroupNamePattern", func(t *testing.T) { mockLogsAPI := &mocks.LogsAPI{} - mockLogsAPI.On("DescribeLogGroups", mock.Anything).Return(&cloudwatchlogs.DescribeLogGroupsOutput{}, nil) + mockLogsAPI.On("DescribeLogGroupsWithContext", mock.Anything).Return(&cloudwatchlogs.DescribeLogGroupsOutput{}, nil) service := NewLogGroupsService(mockLogsAPI, true) - _, err := service.GetLogGroups(resources.LogGroupsRequest{ + _, err := service.GetLogGroupsWithContext(context.Background(), resources.LogGroupsRequest{ ResourceRequest: resources.ResourceRequest{ AccountId: utils.Pointer("accountId"), }, @@ -279,7 +279,7 @@ func TestGetLogGroupsCrossAccountQuerying(t *testing.T) { }) assert.NoError(t, err) - mockLogsAPI.AssertCalled(t, "DescribeLogGroups", &cloudwatchlogs.DescribeLogGroupsInput{ + mockLogsAPI.AssertCalled(t, "DescribeLogGroupsWithContext", &cloudwatchlogs.DescribeLogGroupsInput{ AccountIdentifiers: []*string{utils.Pointer("accountId")}, IncludeLinkedAccounts: utils.Pointer(true), Limit: utils.Pointer(int64(0)), diff --git a/pkg/tsdb/cloudwatch/test_utils.go b/pkg/tsdb/cloudwatch/test_utils.go index a54972305a15b..fe0cd907fb3bd 100644 --- a/pkg/tsdb/cloudwatch/test_utils.go +++ b/pkg/tsdb/cloudwatch/test_utils.go @@ -71,14 +71,8 @@ func (m *mockLogsSyncClient) StartQueryWithContext(ctx context.Context, input *c return args.Get(0).(*cloudwatchlogs.StartQueryOutput), args.Error(1) } -func (m *fakeCWLogsClient) DescribeLogGroups(input *cloudwatchlogs.DescribeLogGroupsInput) (*cloudwatchlogs.DescribeLogGroupsOutput, error) { - m.calls.describeLogGroups = append(m.calls.describeLogGroups, input) - output := &m.logGroups[m.logGroupsIndex] - m.logGroupsIndex++ - return output, nil -} - func (m *fakeCWLogsClient) DescribeLogGroupsWithContext(ctx context.Context, input *cloudwatchlogs.DescribeLogGroupsInput, option ...request.Option) (*cloudwatchlogs.DescribeLogGroupsOutput, error) { + m.calls.describeLogGroups = append(m.calls.describeLogGroups, input) output := &m.logGroups[m.logGroupsIndex] m.logGroupsIndex++ return output, nil @@ -207,7 +201,7 @@ func (c fakeCheckHealthClient) ListMetricsPagesWithContext(ctx aws.Context, inpu return nil } -func (c fakeCheckHealthClient) DescribeLogGroups(input *cloudwatchlogs.DescribeLogGroupsInput) (*cloudwatchlogs.DescribeLogGroupsOutput, error) { +func (c fakeCheckHealthClient) DescribeLogGroupsWithContext(ctx context.Context, input *cloudwatchlogs.DescribeLogGroupsInput, option ...request.Option) (*cloudwatchlogs.DescribeLogGroupsOutput, error) { if c.describeLogGroups != nil { return c.describeLogGroups(input) } From e3641d925c809c5a39d322246f3112d29fc19fd1 Mon Sep 17 00:00:00 2001 From: Ryan McKinley Date: Wed, 1 Nov 2023 12:32:24 -0700 Subject: [PATCH 041/869] K8s/Playlist: Support full CRUD from k8s to existing storage (#75709) --- pkg/apis/playlist/conversions.go | 20 ++ pkg/apis/playlist/legacy_storage.go | 97 ++++++++++ .../registry/generic/strategy.go | 4 +- pkg/tests/apis/helper.go | 12 +- pkg/tests/apis/playlist/playlist_test.go | 181 +++++++++++++++++- .../playlist/testdata/playlist-generate.yaml | 12 ++ .../testdata/playlist-test-apply.yaml | 6 + .../testdata/playlist-test-create.yaml | 12 ++ .../testdata/playlist-test-replace.yaml | 10 + public/app/features/playlist/api.ts | 14 -- 10 files changed, 349 insertions(+), 19 deletions(-) create mode 100644 pkg/tests/apis/playlist/testdata/playlist-generate.yaml create mode 100644 pkg/tests/apis/playlist/testdata/playlist-test-apply.yaml create mode 100644 pkg/tests/apis/playlist/testdata/playlist-test-create.yaml create mode 100644 pkg/tests/apis/playlist/testdata/playlist-test-replace.yaml diff --git a/pkg/apis/playlist/conversions.go b/pkg/apis/playlist/conversions.go index bbe77f689fa26..adffda00343a7 100644 --- a/pkg/apis/playlist/conversions.go +++ b/pkg/apis/playlist/conversions.go @@ -76,6 +76,26 @@ func convertToK8sResource(v *playlist.PlaylistDTO, namespacer request.NamespaceM } } +func convertToLegacyUpdateCommand(p *Playlist, orgId int64) (*playlist.UpdatePlaylistCommand, error) { + spec := p.Spec + cmd := &playlist.UpdatePlaylistCommand{ + UID: p.Name, + Name: spec.Title, + Interval: spec.Interval, + OrgId: orgId, + } + for _, item := range spec.Items { + if item.Type == ItemTypeDashboardById { + return nil, fmt.Errorf("unsupported item type: %s", item.Type) + } + cmd.Items = append(cmd.Items, playlist.PlaylistItem{ + Type: string(item.Type), + Value: item.Value, + }) + } + return cmd, nil +} + // Read legacy ID from metadata annotations func getLegacyID(item *unstructured.Unstructured) int64 { meta := kinds.GrafanaResourceMetadata{ diff --git a/pkg/apis/playlist/legacy_storage.go b/pkg/apis/playlist/legacy_storage.go index dab3ce7f8133e..7bad9013a473e 100644 --- a/pkg/apis/playlist/legacy_storage.go +++ b/pkg/apis/playlist/legacy_storage.go @@ -3,6 +3,7 @@ package playlist import ( "context" "errors" + "fmt" k8serrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/apis/meta/internalversion" @@ -21,6 +22,9 @@ var ( _ rest.Getter = (*legacyStorage)(nil) _ rest.Lister = (*legacyStorage)(nil) _ rest.Storage = (*legacyStorage)(nil) + _ rest.Creater = (*legacyStorage)(nil) + _ rest.Updater = (*legacyStorage)(nil) + _ rest.GracefulDeleter = (*legacyStorage)(nil) ) type legacyStorage struct { @@ -110,3 +114,96 @@ func (s *legacyStorage) Get(ctx context.Context, name string, options *metav1.Ge return convertToK8sResource(dto, s.namespacer), nil } + +func (s *legacyStorage) Create(ctx context.Context, + obj runtime.Object, + createValidation rest.ValidateObjectFunc, + options *metav1.CreateOptions, +) (runtime.Object, error) { + info, err := request.NamespaceInfoFrom(ctx, true) + if err != nil { + return nil, err + } + + p, ok := obj.(*Playlist) + if !ok { + return nil, fmt.Errorf("expected playlist?") + } + cmd, err := convertToLegacyUpdateCommand(p, info.OrgID) + if err != nil { + return nil, err + } + out, err := s.service.Create(ctx, &playlist.CreatePlaylistCommand{ + UID: p.Name, + Name: cmd.Name, + Interval: cmd.Interval, + Items: cmd.Items, + OrgId: cmd.OrgId, + }) + if err != nil { + return nil, err + } + return s.Get(ctx, out.UID, nil) +} + +func (s *legacyStorage) Update(ctx context.Context, + name string, + objInfo rest.UpdatedObjectInfo, + createValidation rest.ValidateObjectFunc, + updateValidation rest.ValidateObjectUpdateFunc, + forceAllowCreate bool, + options *metav1.UpdateOptions, +) (runtime.Object, bool, error) { + info, err := request.NamespaceInfoFrom(ctx, true) + if err != nil { + return nil, false, err + } + + created := false + old, err := s.Get(ctx, name, nil) + if err != nil { + return old, created, err + } + + obj, err := objInfo.UpdatedObject(ctx, old) + if err != nil { + return old, created, err + } + p, ok := obj.(*Playlist) + if !ok { + return nil, created, fmt.Errorf("expected playlist after update") + } + + cmd, err := convertToLegacyUpdateCommand(p, info.OrgID) + if err != nil { + return old, created, err + } + _, err = s.service.Update(ctx, cmd) + if err != nil { + return nil, false, err + } + + r, err := s.Get(ctx, name, nil) + return r, created, err +} + +// GracefulDeleter +func (s *legacyStorage) Delete(ctx context.Context, name string, deleteValidation rest.ValidateObjectFunc, options *metav1.DeleteOptions) (runtime.Object, bool, error) { + v, err := s.Get(ctx, name, &metav1.GetOptions{}) + if err != nil { + return v, false, err // includes the not-found error + } + info, err := request.NamespaceInfoFrom(ctx, true) + if err != nil { + return nil, false, err + } + p, ok := v.(*Playlist) + if !ok { + return v, false, fmt.Errorf("expected a playlist response from Get") + } + err = s.service.Delete(ctx, &playlist.DeletePlaylistCommand{ + UID: name, + OrgId: info.OrgID, + }) + return p, true, err // true is instant delete +} diff --git a/pkg/services/grafana-apiserver/registry/generic/strategy.go b/pkg/services/grafana-apiserver/registry/generic/strategy.go index 797f2acc4fb09..95bf1424e04ed 100644 --- a/pkg/services/grafana-apiserver/registry/generic/strategy.go +++ b/pkg/services/grafana-apiserver/registry/generic/strategy.go @@ -39,11 +39,11 @@ func (genericStrategy) Validate(ctx context.Context, obj runtime.Object) field.E func (genericStrategy) WarningsOnCreate(ctx context.Context, obj runtime.Object) []string { return nil } func (genericStrategy) AllowCreateOnUpdate() bool { - return false + return true } func (genericStrategy) AllowUnconditionalUpdate() bool { - return false + return true } func (genericStrategy) Canonicalize(obj runtime.Object) {} diff --git a/pkg/tests/apis/helper.go b/pkg/tests/apis/helper.go index 52fc57bd57491..b24fa49900d04 100644 --- a/pkg/tests/apis/helper.go +++ b/pkg/tests/apis/helper.go @@ -126,7 +126,16 @@ func (c *K8sTestHelper) AsStatusError(err error) *errors.StatusError { func (c *K8sResourceClient) SanitizeJSON(v *unstructured.Unstructured) string { c.t.Helper() - copy := v.DeepCopy().Object + deep := v.DeepCopy() + anno := deep.GetAnnotations() + if anno["grafana.app/originKey"] != "" { + anno["grafana.app/originKey"] = "${originKey}" + } + if anno["grafana.app/updatedTimestamp"] != "" { + anno["grafana.app/updatedTimestamp"] = "${updatedTimestamp}" + } + deep.SetAnnotations(anno) + copy := deep.Object meta, ok := copy["metadata"].(map[string]any) require.True(c.t, ok) @@ -139,6 +148,7 @@ func (c *K8sResourceClient) SanitizeJSON(v *unstructured.Unstructured) string { } out, err := json.MarshalIndent(copy, "", " ") + //fmt.Printf("%s", out) require.NoError(c.t, err) return string(out) } diff --git a/pkg/tests/apis/playlist/playlist_test.go b/pkg/tests/apis/playlist/playlist_test.go index 9d475d82d5a4e..59a47e8561231 100644 --- a/pkg/tests/apis/playlist/playlist_test.go +++ b/pkg/tests/apis/playlist/playlist_test.go @@ -1,13 +1,17 @@ package playlist import ( + "cmp" "context" + "encoding/json" "net/http" + "slices" "strings" "testing" "github.com/stretchr/testify/require" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime/schema" "github.com/grafana/grafana/pkg/services/playlist" @@ -127,6 +131,11 @@ func TestPlaylist(t *testing.T) { "apiVersion": "playlist.grafana.app/v0alpha1", "kind": "Playlist", "metadata": { + "annotations": { + "grafana.app/originKey": "${originKey}", + "grafana.app/originName": "SQL", + "grafana.app/updatedTimestamp": "${updatedTimestamp}" + }, "creationTimestamp": "${creationTimestamp}", "name": "` + uid + `", "namespace": "default", @@ -134,7 +143,6 @@ func TestPlaylist(t *testing.T) { "uid": "${uid}" }, "spec": { - "title": "Test", "interval": "20s", "items": [ { @@ -145,7 +153,8 @@ func TestPlaylist(t *testing.T) { "type": "dashboard_by_tag", "value": "graph-ng" } - ] + ], + "title": "Test" } }` @@ -191,4 +200,172 @@ func TestPlaylist(t *testing.T) { require.Nil(t, found) require.Equal(t, metav1.StatusReasonNotFound, statusError.Status().Reason) }) + + t.Run("Do CRUD via k8s (and check that legacy api still works)", func(t *testing.T) { + client := helper.GetResourceClient(apis.ResourceClientArgs{ + User: helper.Org1.Editor, + GVR: gvr, + }) + + // Create the playlist "test" + first, err := client.Resource.Create(context.Background(), + helper.LoadYAMLOrJSONFile("testdata/playlist-test-create.yaml"), + metav1.CreateOptions{}, + ) + require.NoError(t, err) + require.Equal(t, "test", first.GetName()) + uids := []string{first.GetName()} + + // Create (with name generation) two playlists + for i := 0; i < 2; i++ { + out, err := client.Resource.Create(context.Background(), + helper.LoadYAMLOrJSONFile("testdata/playlist-generate.yaml"), + metav1.CreateOptions{}, + ) + require.NoError(t, err) + uids = append(uids, out.GetName()) + } + slices.Sort(uids) // make list compare stable + + // Check that everything is returned from the List command + list, err := client.Resource.List(context.Background(), metav1.ListOptions{}) + require.NoError(t, err) + require.Equal(t, uids, SortSlice(Map(list.Items, func(item unstructured.Unstructured) string { + return item.GetName() + }))) + + // The legacy endpoint has the same results + searchResponse := apis.DoRequest(helper, apis.RequestParams{ + User: client.Args.User, + Method: http.MethodGet, + Path: "/api/playlists", + }, &playlist.Playlists{}) + require.NotNil(t, searchResponse.Result) + require.Equal(t, uids, SortSlice(Map(*searchResponse.Result, func(item *playlist.Playlist) string { + return item.UID + }))) + + // Check all playlists + for _, uid := range uids { + getFromBothAPIs(t, helper, client, uid, nil) + } + + // PUT :: Update the title (full payload) + updated, err := client.Resource.Update(context.Background(), + helper.LoadYAMLOrJSONFile("testdata/playlist-test-replace.yaml"), + metav1.UpdateOptions{}, + ) + require.NoError(t, err) + require.Equal(t, first.GetName(), updated.GetName()) + require.Equal(t, first.GetUID(), updated.GetUID()) + require.Less(t, first.GetResourceVersion(), updated.GetResourceVersion()) + out := getFromBothAPIs(t, helper, client, "test", &playlist.PlaylistDTO{ + Name: "Test playlist (replaced from k8s; 22m; 1 items; PUT)", + Interval: "22m", + }) + require.Equal(t, updated.GetResourceVersion(), out.GetResourceVersion()) + + // PATCH :: apply only some fields + updated, err = client.Resource.Apply(context.Background(), "test", + helper.LoadYAMLOrJSONFile("testdata/playlist-test-apply.yaml"), + metav1.ApplyOptions{ + Force: true, + FieldManager: "testing", + }, + ) + require.NoError(t, err) + require.Equal(t, first.GetName(), updated.GetName()) + require.Equal(t, first.GetUID(), updated.GetUID()) + require.Less(t, first.GetResourceVersion(), updated.GetResourceVersion()) + getFromBothAPIs(t, helper, client, "test", &playlist.PlaylistDTO{ + Name: "Test playlist (apply from k8s; ??m; ?? items; PATCH)", + Interval: "22m", // has not changed from previous update + }) + + // Now delete all playlist (three) + for _, uid := range uids { + err := client.Resource.Delete(context.Background(), uid, metav1.DeleteOptions{}) + require.NoError(t, err) + + // Second call is not found! + err = client.Resource.Delete(context.Background(), uid, metav1.DeleteOptions{}) + statusError := helper.AsStatusError(err) + require.Equal(t, metav1.StatusReasonNotFound, statusError.Status().Reason) + + // Not found from k8s getter + _, err = client.Resource.Get(context.Background(), uid, metav1.GetOptions{}) + statusError = helper.AsStatusError(err) + require.Equal(t, metav1.StatusReasonNotFound, statusError.Status().Reason) + } + + // Check that they are all gone + list, err = client.Resource.List(context.Background(), metav1.ListOptions{}) + require.NoError(t, err) + require.Empty(t, list.Items) + }) +} + +// typescript style map function +func Map[A any, B any](input []A, m func(A) B) []B { + output := make([]B, len(input)) + for i, element := range input { + output[i] = m(element) + } + return output +} + +func SortSlice[A cmp.Ordered](input []A) []A { + slices.Sort(input) + return input +} + +// This does a get with both k8s and legacy API, and verifies the results are the same +func getFromBothAPIs(t *testing.T, + helper *apis.K8sTestHelper, + client *apis.K8sResourceClient, + uid string, + // Optionally match some expect some values + expect *playlist.PlaylistDTO, +) *unstructured.Unstructured { + t.Helper() + + found, err := client.Resource.Get(context.Background(), uid, metav1.GetOptions{}) + require.NoError(t, err) + require.Equal(t, uid, found.GetName()) + + dto := apis.DoRequest(helper, apis.RequestParams{ + User: client.Args.User, + Method: http.MethodGet, + Path: "/api/playlists/" + uid, + }, &playlist.PlaylistDTO{}).Result + require.NotNil(t, dto) + require.Equal(t, uid, dto.Uid) + + spec, ok := found.Object["spec"].(map[string]any) + require.True(t, ok) + require.Equal(t, dto.Uid, found.GetName()) + require.Equal(t, dto.Name, spec["title"]) + require.Equal(t, dto.Interval, spec["interval"]) + + a, errA := json.Marshal(spec["items"]) + b, errB := json.Marshal(dto.Items) + require.NoError(t, errA) + require.NoError(t, errB) + require.JSONEq(t, string(a), string(b)) + + if expect != nil { + if expect.Name != "" { + require.Equal(t, expect.Name, dto.Name) + require.Equal(t, expect.Name, spec["title"]) + } + if expect.Interval != "" { + require.Equal(t, expect.Interval, dto.Interval) + require.Equal(t, expect.Interval, spec["interval"]) + } + if expect.Uid != "" { + require.Equal(t, expect.Uid, dto.Uid) + require.Equal(t, expect.Uid, found.GetName()) + } + } + return found } diff --git a/pkg/tests/apis/playlist/testdata/playlist-generate.yaml b/pkg/tests/apis/playlist/testdata/playlist-generate.yaml new file mode 100644 index 0000000000000..07dbe1efe320e --- /dev/null +++ b/pkg/tests/apis/playlist/testdata/playlist-generate.yaml @@ -0,0 +1,12 @@ +apiVersion: playlist.grafana.app/v0alpha1 +kind: Playlist +metadata: + generateName: x # anything is ok here... except yes or true -- they become boolean! +spec: + title: Playlist with auto generated UID + interval: 5m + items: + - type: dashboard_by_tag + value: panel-tests + - type: dashboard_by_uid + value: vmie2cmWz # dashboard from devenv diff --git a/pkg/tests/apis/playlist/testdata/playlist-test-apply.yaml b/pkg/tests/apis/playlist/testdata/playlist-test-apply.yaml new file mode 100644 index 0000000000000..098dc08e86ad2 --- /dev/null +++ b/pkg/tests/apis/playlist/testdata/playlist-test-apply.yaml @@ -0,0 +1,6 @@ +apiVersion: playlist.grafana.app/v0alpha1 +kind: Playlist +metadata: + name: test +spec: + title: Test playlist (apply from k8s; ??m; ?? items; PATCH) diff --git a/pkg/tests/apis/playlist/testdata/playlist-test-create.yaml b/pkg/tests/apis/playlist/testdata/playlist-test-create.yaml new file mode 100644 index 0000000000000..4839fa07669e7 --- /dev/null +++ b/pkg/tests/apis/playlist/testdata/playlist-test-create.yaml @@ -0,0 +1,12 @@ +apiVersion: playlist.grafana.app/v0alpha1 +kind: Playlist +metadata: + name: test +spec: + title: Test playlist (created from k8s; 2 items; POST) + interval: 5m + items: + - type: dashboard_by_tag + value: panel-tests + - type: dashboard_by_uid + value: vmie2cmWz # dashboard from devenv diff --git a/pkg/tests/apis/playlist/testdata/playlist-test-replace.yaml b/pkg/tests/apis/playlist/testdata/playlist-test-replace.yaml new file mode 100644 index 0000000000000..1984ef2c1dfde --- /dev/null +++ b/pkg/tests/apis/playlist/testdata/playlist-test-replace.yaml @@ -0,0 +1,10 @@ +apiVersion: playlist.grafana.app/v0alpha1 +kind: Playlist +metadata: + name: test +spec: + title: Test playlist (replaced from k8s; 22m; 1 items; PUT) + interval: 22m + items: + - type: dashboard_by_tag + value: panel-tests diff --git a/public/app/features/playlist/api.ts b/public/app/features/playlist/api.ts index 27ea44fdf967a..d8a40b649aea5 100644 --- a/public/app/features/playlist/api.ts +++ b/public/app/features/playlist/api.ts @@ -57,15 +57,10 @@ interface K8sPlaylist { class K8sAPI implements PlaylistAPI { readonly apiVersion = 'playlist.grafana.app/v0alpha1'; readonly url: string; - readonly legacy: PlaylistAPI | undefined; constructor() { const ns = contextSrv.user.orgId === 1 ? 'default' : `org-${contextSrv.user.orgId}`; this.url = `/apis/${this.apiVersion}/namespaces/${ns}/playlists`; - - // When undefined, this will use k8s for all CRUD features - // if (!config.featureToggles.grafanaAPIServerWithExperimentalAPIs) { - this.legacy = new LegacyAPI(); } async getAllPlaylist(): Promise { @@ -81,25 +76,16 @@ class K8sAPI implements PlaylistAPI { } async createPlaylist(playlist: Playlist): Promise { - if (this.legacy) { - return this.legacy.createPlaylist(playlist); - } const body = this.playlistAsK8sResource(playlist); await withErrorHandling(() => getBackendSrv().post(this.url, body)); } async updatePlaylist(playlist: Playlist): Promise { - if (this.legacy) { - return this.legacy.updatePlaylist(playlist); - } const body = this.playlistAsK8sResource(playlist); await withErrorHandling(() => getBackendSrv().put(`${this.url}/${playlist.uid}`, body)); } async deletePlaylist(uid: string): Promise { - if (this.legacy) { - return this.legacy.deletePlaylist(uid); - } await withErrorHandling(() => getBackendSrv().delete(`${this.url}/${uid}`), 'Playlist deleted'); } From 85425b219427567d85a22f144073f0fe3d2b398e Mon Sep 17 00:00:00 2001 From: Yuri Tseretyan Date: Wed, 1 Nov 2023 15:35:04 -0400 Subject: [PATCH 042/869] Alerting: Fix flaky test TestExportRules (#77519) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix test to correclty mock data store * Update pkg/services/ngalert/api/api_ruler_export_test.go Co-authored-by: Jean-Philippe Quéméner * Update pkg/services/ngalert/api/api_ruler_export_test.go --------- Co-authored-by: Jean-Philippe Quéméner --- pkg/services/ngalert/api/api_ruler_export_test.go | 11 +++++++---- pkg/services/ngalert/models/testing.go | 13 +++++++++++++ 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/pkg/services/ngalert/api/api_ruler_export_test.go b/pkg/services/ngalert/api/api_ruler_export_test.go index 6cbeb65c893f9..32d254d2dd266 100644 --- a/pkg/services/ngalert/api/api_ruler_export_test.go +++ b/pkg/services/ngalert/api/api_ruler_export_test.go @@ -202,7 +202,6 @@ func TestExportRules(t *testing.T) { f2 := randFolder() ruleStore := fakes.NewRuleStore(t) - ruleStore.Folders[orgID] = append(ruleStore.Folders[orgID], f1, f2) hasAccessKey1 := ngmodels.AlertRuleGroupKey{ OrgID: orgID, @@ -253,12 +252,16 @@ func TestExportRules(t *testing.T) { )) ruleStore.PutRule(context.Background(), hasAccess2...) - _, noAccess2 := ngmodels.GenerateUniqueAlertRules(10, + _, noAccessByFolder := ngmodels.GenerateUniqueAlertRules(10, ngmodels.AlertRuleGen( ngmodels.WithUniqueUID(&uids), ngmodels.WithQuery(accessQuery), // no access because of folder + ngmodels.WithNamespaceUIDNotIn(f1.UID, f2.UID), )) - ruleStore.PutRule(context.Background(), noAccess2...) + + ruleStore.PutRule(context.Background(), noAccessByFolder...) + // overwrite the folders visible to user because PutRule automatically creates folders in the fake store. + ruleStore.Folders[orgID] = []*folder2.Folder{f1, f2} srv := createService(ruleStore) @@ -351,7 +354,7 @@ func TestExportRules(t *testing.T) { { title: "unauthorized if folders are not accessible", params: url.Values{ - "folderUid": []string{noAccess2[0].NamespaceUID}, + "folderUid": []string{noAccessByFolder[0].NamespaceUID}, }, expectedStatus: 401, expectedRules: nil, diff --git a/pkg/services/ngalert/models/testing.go b/pkg/services/ngalert/models/testing.go index 7f7746adcf0bd..280dfa3026962 100644 --- a/pkg/services/ngalert/models/testing.go +++ b/pkg/services/ngalert/models/testing.go @@ -4,6 +4,7 @@ import ( "encoding/json" "fmt" "math/rand" + "slices" "sync" "time" @@ -158,6 +159,18 @@ func WithUniqueOrgID() AlertRuleMutator { } } +// WithNamespaceUIDNotIn generates a random namespace UID if it is among excluded +func WithNamespaceUIDNotIn(exclude ...string) AlertRuleMutator { + return func(rule *AlertRule) { + for { + if !slices.Contains(exclude, rule.NamespaceUID) { + return + } + rule.NamespaceUID = uuid.NewString() + } + } +} + func WithNamespace(namespace *folder.Folder) AlertRuleMutator { return func(rule *AlertRule) { rule.NamespaceUID = namespace.UID From fb9732e319749f7586472093ed21b54905d717f7 Mon Sep 17 00:00:00 2001 From: Ryan McKinley Date: Wed, 1 Nov 2023 14:13:48 -0700 Subject: [PATCH 043/869] Chore: Prepare to remove from @grafana/ui (#77522) --- .../grafana-ui/src/components/Graph/Graph.tsx | 3 +- .../GraphTooltip/MultiModeGraphTooltip.tsx | 2 +- .../grafana-ui/src/components/Graph/types.ts | 10 ----- .../src/components/VizTooltip/VizTooltip.tsx | 10 ++++- public/app/plugins/panel/graph/graph.ts | 7 ++- public/app/plugins/panel/graph/utils.test.ts | 12 ++++- public/app/plugins/panel/graph/utils.ts | 44 +++++++++++++++++++ 7 files changed, 70 insertions(+), 18 deletions(-) diff --git a/packages/grafana-ui/src/components/Graph/Graph.tsx b/packages/grafana-ui/src/components/Graph/Graph.tsx index 4b1d4c8e5425c..9c61e8691fd00 100644 --- a/packages/grafana-ui/src/components/Graph/Graph.tsx +++ b/packages/grafana-ui/src/components/Graph/Graph.tsx @@ -8,11 +8,12 @@ import { TimeRange, GraphSeriesXY, TimeZone, createDimension } from '@grafana/da import { TooltipDisplayMode } from '@grafana/schema'; import { VizTooltipProps, VizTooltipContentProps, ActiveDimensions, VizTooltip } from '../VizTooltip'; +import { FlotPosition } from '../VizTooltip/VizTooltip'; import { GraphContextMenu, GraphContextMenuProps, ContextDimensions } from './GraphContextMenu'; import { GraphTooltip } from './GraphTooltip/GraphTooltip'; import { GraphDimensions } from './GraphTooltip/types'; -import { FlotPosition, FlotItem } from './types'; +import { FlotItem } from './types'; import { graphTimeFormat, graphTickFormatter } from './utils'; /** @deprecated */ diff --git a/packages/grafana-ui/src/components/Graph/GraphTooltip/MultiModeGraphTooltip.tsx b/packages/grafana-ui/src/components/Graph/GraphTooltip/MultiModeGraphTooltip.tsx index b41351e60b156..b6810da3e9363 100644 --- a/packages/grafana-ui/src/components/Graph/GraphTooltip/MultiModeGraphTooltip.tsx +++ b/packages/grafana-ui/src/components/Graph/GraphTooltip/MultiModeGraphTooltip.tsx @@ -3,7 +3,7 @@ import React from 'react'; import { getValueFromDimension } from '@grafana/data'; import { SeriesTable } from '../../VizTooltip'; -import { FlotPosition } from '../types'; +import { FlotPosition } from '../../VizTooltip/VizTooltip'; import { getMultiSeriesGraphHoverInfo } from '../utils'; import { GraphTooltipContentProps } from './types'; diff --git a/packages/grafana-ui/src/components/Graph/types.ts b/packages/grafana-ui/src/components/Graph/types.ts index 24d5f59588d08..60cdb44b983f8 100644 --- a/packages/grafana-ui/src/components/Graph/types.ts +++ b/packages/grafana-ui/src/components/Graph/types.ts @@ -1,13 +1,3 @@ -/** @deprecated */ -export interface FlotPosition { - pageX: number; - pageY: number; - x: number; - x1: number; - y: number; - y1: number; -} - /** @deprecated */ export interface FlotItem { datapoint: [number, number]; diff --git a/packages/grafana-ui/src/components/VizTooltip/VizTooltip.tsx b/packages/grafana-ui/src/components/VizTooltip/VizTooltip.tsx index 62b8448c64311..5f7c4cf551e1b 100644 --- a/packages/grafana-ui/src/components/VizTooltip/VizTooltip.tsx +++ b/packages/grafana-ui/src/components/VizTooltip/VizTooltip.tsx @@ -5,11 +5,19 @@ import { Dimensions, TimeZone } from '@grafana/data'; import { TooltipDisplayMode } from '@grafana/schema'; import { useStyles2 } from '../../themes'; -import { FlotPosition } from '../Graph/types'; import { Portal } from '../Portal/Portal'; import { VizTooltipContainer } from './VizTooltipContainer'; +export interface FlotPosition { + pageX: number; + pageY: number; + x: number; + x1: number; + y: number; + y1: number; +} + // Describes active dimensions user interacts with // It's a key-value pair where: // - key is the name of the dimension diff --git a/public/app/plugins/panel/graph/graph.ts b/public/app/plugins/panel/graph/graph.ts index eccc354ac755d..ec4d93dfe5cf3 100644 --- a/public/app/plugins/panel/graph/graph.ts +++ b/public/app/plugins/panel/graph/graph.ts @@ -36,7 +36,7 @@ import { PanelEvents, toUtc, } from '@grafana/data'; -import { graphTickFormatter, graphTimeFormat, MenuItemProps, MenuItemsGroup } from '@grafana/ui'; +import { MenuItemProps, MenuItemsGroup } from '@grafana/ui'; import { coreModule } from 'app/angular/core_module'; import config from 'app/core/config'; import { updateLegendValues } from 'app/core/core'; @@ -44,10 +44,9 @@ import { ContextSrv } from 'app/core/services/context_srv'; import { provideTheme } from 'app/core/utils/ConfigProvider'; import { tickStep } from 'app/core/utils/ticks'; import { TimeSrv } from 'app/features/dashboard/services/TimeSrv'; +import { DashboardModel } from 'app/features/dashboard/state'; import { getFieldLinksSupplier } from 'app/features/panel/panellinks/linkSuppliers'; -import { DashboardModel } from '../../../features/dashboard/state'; - import { GraphContextMenuCtrl } from './GraphContextMenuCtrl'; import { GraphLegendProps, Legend } from './Legend/Legend'; import { alignYLevel } from './align_yaxes'; @@ -57,7 +56,7 @@ import { convertToHistogramData } from './histogram'; import { GraphCtrl } from './module'; import { ThresholdManager } from './threshold_manager'; import { TimeRegionManager } from './time_region_manager'; -import { isLegacyGraphHoverEvent } from './utils'; +import { isLegacyGraphHoverEvent, graphTickFormatter, graphTimeFormat } from './utils'; const LegendWithThemeProvider = provideTheme(Legend, config.theme2); diff --git a/public/app/plugins/panel/graph/utils.test.ts b/public/app/plugins/panel/graph/utils.test.ts index 071ecc5d132e9..d96dad86ce15f 100644 --- a/public/app/plugins/panel/graph/utils.test.ts +++ b/public/app/plugins/panel/graph/utils.test.ts @@ -1,6 +1,6 @@ import { toDataFrame, FieldType } from '@grafana/data'; -import { getDataTimeRange } from './utils'; +import { getDataTimeRange, graphTimeFormat } from './utils'; describe('DataFrame utility functions', () => { const frame = toDataFrame({ @@ -15,4 +15,14 @@ describe('DataFrame utility functions', () => { expect(range!.from).toEqual(2); expect(range!.to).toEqual(9); }); + + describe('graphTimeFormat', () => { + it('graphTimeFormat', () => { + expect(graphTimeFormat(5, 1, 45 * 5 * 1000)).toBe('HH:mm:ss'); + expect(graphTimeFormat(5, 1, 7200 * 5 * 1000)).toBe('HH:mm'); + expect(graphTimeFormat(5, 1, 80000 * 5 * 1000)).toBe('MM/DD HH:mm'); + expect(graphTimeFormat(5, 1, 2419200 * 5 * 1000)).toBe('MM/DD'); + expect(graphTimeFormat(5, 1, 12419200 * 5 * 1000)).toBe('YYYY-MM'); + }); + }); }); diff --git a/public/app/plugins/panel/graph/utils.ts b/public/app/plugins/panel/graph/utils.ts index d637391a216ed..aa2a7e15f78c0 100644 --- a/public/app/plugins/panel/graph/utils.ts +++ b/public/app/plugins/panel/graph/utils.ts @@ -5,6 +5,8 @@ import { LegacyGraphHoverEventPayload, reduceField, ReducerID, + dateTimeFormat, + systemDateFormats, } from '@grafana/data'; /** @@ -34,3 +36,45 @@ export function getDataTimeRange(frames: DataFrame[]): AbsoluteTimeRange | undef export function isLegacyGraphHoverEvent(event: unknown): event is LegacyGraphHoverEventPayload { return Boolean(event && typeof event === 'object' && event.hasOwnProperty('pos')); } + +/** @deprecated */ +export const graphTickFormatter = (epoch: number, axis: any) => { + return dateTimeFormat(epoch, { + format: axis?.options?.timeformat, + timeZone: axis?.options?.timezone, + }); +}; + +/** @deprecated */ +export const graphTimeFormat = (ticks: number | null, min: number | null, max: number | null): string => { + if (min && max && ticks) { + const range = max - min; + const secPerTick = range / ticks / 1000; + // Need have 10 millisecond margin on the day range + // As sometimes last 24 hour dashboard evaluates to more than 86400000 + const oneDay = 86400010; + const oneYear = 31536000000; + + if (secPerTick <= 10) { + return systemDateFormats.interval.millisecond; + } + if (secPerTick <= 45) { + return systemDateFormats.interval.second; + } + if (range <= oneDay) { + return systemDateFormats.interval.minute; + } + if (secPerTick <= 80000) { + return systemDateFormats.interval.hour; + } + if (range <= oneYear) { + return systemDateFormats.interval.day; + } + if (secPerTick <= 31536000) { + return systemDateFormats.interval.month; + } + return systemDateFormats.interval.year; + } + + return systemDateFormats.interval.minute; +}; From c6e27e00b4f1320c06bf1bfb8e7a631c46cb1ba8 Mon Sep 17 00:00:00 2001 From: Ryan McKinley Date: Wed, 1 Nov 2023 21:59:55 -0700 Subject: [PATCH 044/869] Chore: Move internal GraphNG+Timeseries components into core (#77525) * move to core where possible * update imports * ignore import order for now * add graveyard files * update codeowners --- .betterer.results | 99 ++- .github/CODEOWNERS | 6 +- .../grafana-data/src/transformations/index.ts | 3 + .../nulls}/nullInsertThreshold.test.ts | 0 .../transformers/nulls/nullInsertThreshold.ts | 186 +++++ .../nulls}/nullToUndefThreshold.ts | 0 .../transformers/nulls}/nullToValue.test.ts | 0 .../transformers/nulls/nullToValue.ts | 27 + .../src/components/Sparkline/utils.ts | 3 +- packages/grafana-ui/src/components/index.ts | 13 +- .../src/components/uPlot/internal.ts | 40 ++ .../grafana-ui/src/components/uPlot/types.ts | 5 + .../src/components/uPlot/utils.test.ts | 2 +- .../grafana-ui/src/components/uPlot/utils.ts | 3 +- .../src/graveyard/GraphNG/GraphNG.tsx | 275 +++++++ .../GraphNG/__snapshots__/utils.test.ts.snap | 0 .../grafana-ui/src/graveyard/GraphNG/hooks.ts | 41 ++ .../GraphNG/nullInsertThreshold.test.ts | 334 +++++++++ .../GraphNG/nullInsertThreshold.ts | 1 + .../graveyard/GraphNG/nullToUndefThreshold.ts | 33 + .../src/graveyard/GraphNG/nullToValue.test.ts | 94 +++ .../GraphNG/nullToValue.ts | 1 + .../grafana-ui/src/graveyard/GraphNG/types.ts | 18 + .../GraphNG/utils.test.ts | 0 .../GraphNG/utils.ts | 3 +- packages/grafana-ui/src/graveyard/README.md | 1 + .../TimeSeries/TimeSeries.tsx | 6 +- .../TimeSeries/utils.test.ts | 0 .../TimeSeries/utils.ts | 6 +- .../app/core}/components/GraphNG/GraphNG.tsx | 20 +- .../GraphNG/__snapshots__/utils.test.ts.snap | 245 +++++++ .../app/core}/components/GraphNG/hooks.ts | 0 .../app/core}/components/GraphNG/types.ts | 3 +- .../app/core/components/GraphNG/utils.test.ts | 522 ++++++++++++++ public/app/core/components/GraphNG/utils.ts | 140 ++++ .../core/components/TimeSeries/TimeSeries.tsx | 63 ++ .../core/components/TimeSeries/utils.test.ts | 274 +++++++ .../app/core/components/TimeSeries/utils.ts | 669 ++++++++++++++++++ .../TimelineChart/TimelineChart.tsx | 13 +- .../core/components/TimelineChart/timeline.ts | 2 +- .../core/components/TimelineChart/utils.ts | 4 +- .../plugins/panel/barchart/BarChartPanel.tsx | 4 +- .../panel/candlestick/CandlestickPanel.tsx | 3 +- .../app/plugins/panel/graph/data_processor.ts | 2 +- .../panel/timeseries/TimeSeriesPanel.tsx | 3 +- .../timeseries/plugins/ExemplarsPlugin.tsx | 2 +- .../plugins/ThresholdControlsPlugin.tsx | 3 +- public/app/plugins/panel/timeseries/utils.ts | 6 +- public/app/plugins/panel/trend/TrendPanel.tsx | 13 +- public/app/plugins/panel/xychart/dims.ts | 2 +- 50 files changed, 3091 insertions(+), 102 deletions(-) rename packages/{grafana-ui/src/components/GraphNG => grafana-data/src/transformations/transformers/nulls}/nullInsertThreshold.test.ts (100%) create mode 100644 packages/grafana-data/src/transformations/transformers/nulls/nullInsertThreshold.ts rename packages/{grafana-ui/src/components/GraphNG => grafana-data/src/transformations/transformers/nulls}/nullToUndefThreshold.ts (100%) rename packages/{grafana-ui/src/components/GraphNG => grafana-data/src/transformations/transformers/nulls}/nullToValue.test.ts (100%) create mode 100644 packages/grafana-data/src/transformations/transformers/nulls/nullToValue.ts create mode 100644 packages/grafana-ui/src/components/uPlot/internal.ts create mode 100644 packages/grafana-ui/src/graveyard/GraphNG/GraphNG.tsx rename packages/grafana-ui/src/{components => graveyard}/GraphNG/__snapshots__/utils.test.ts.snap (100%) create mode 100644 packages/grafana-ui/src/graveyard/GraphNG/hooks.ts create mode 100644 packages/grafana-ui/src/graveyard/GraphNG/nullInsertThreshold.test.ts rename packages/grafana-ui/src/{components => graveyard}/GraphNG/nullInsertThreshold.ts (99%) create mode 100644 packages/grafana-ui/src/graveyard/GraphNG/nullToUndefThreshold.ts create mode 100644 packages/grafana-ui/src/graveyard/GraphNG/nullToValue.test.ts rename packages/grafana-ui/src/{components => graveyard}/GraphNG/nullToValue.ts (96%) create mode 100644 packages/grafana-ui/src/graveyard/GraphNG/types.ts rename packages/grafana-ui/src/{components => graveyard}/GraphNG/utils.test.ts (100%) rename packages/grafana-ui/src/{components => graveyard}/GraphNG/utils.ts (98%) create mode 100644 packages/grafana-ui/src/graveyard/README.md rename packages/grafana-ui/src/{components => graveyard}/TimeSeries/TimeSeries.tsx (87%) rename packages/grafana-ui/src/{components => graveyard}/TimeSeries/utils.test.ts (100%) rename packages/grafana-ui/src/{components => graveyard}/TimeSeries/utils.ts (98%) rename {packages/grafana-ui/src => public/app/core}/components/GraphNG/GraphNG.tsx (92%) create mode 100644 public/app/core/components/GraphNG/__snapshots__/utils.test.ts.snap rename {packages/grafana-ui/src => public/app/core}/components/GraphNG/hooks.ts (100%) rename {packages/grafana-ui/src => public/app/core}/components/GraphNG/types.ts (85%) create mode 100644 public/app/core/components/GraphNG/utils.test.ts create mode 100644 public/app/core/components/GraphNG/utils.ts create mode 100644 public/app/core/components/TimeSeries/TimeSeries.tsx create mode 100644 public/app/core/components/TimeSeries/utils.test.ts create mode 100644 public/app/core/components/TimeSeries/utils.ts diff --git a/.betterer.results b/.betterer.results index d9446d6b110d0..27b2fd82df330 100644 --- a/.betterer.results +++ b/.betterer.results @@ -256,6 +256,17 @@ exports[`better eslint`] = { [0, 0, 0, "Unexpected any. Specify a different type.", "3"], [0, 0, 0, "Unexpected any. Specify a different type.", "4"] ], + "packages/grafana-data/src/transformations/transformers/nulls/nullInsertThreshold.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, "Unexpected any. Specify a different type.", "2"], + [0, 0, 0, "Unexpected any. Specify a different type.", "3"] + ], + "packages/grafana-data/src/transformations/transformers/nulls/nullToUndefThreshold.ts:5381": [ + [0, 0, 0, "Unexpected any. Specify a different type.", "0"], + [0, 0, 0, "Unexpected any. Specify a different type.", "1"], + [0, 0, 0, "Do not use any type assertions.", "2"] + ], "packages/grafana-data/src/transformations/transformers/reduce.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"], [0, 0, 0, "Unexpected any. Specify a different type.", "1"] @@ -802,34 +813,6 @@ exports[`better eslint`] = { "packages/grafana-ui/src/components/Graph/utils.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"] ], - "packages/grafana-ui/src/components/GraphNG/GraphNG.tsx:5381": [ - [0, 0, 0, "Unexpected any. Specify a different type.", "0"], - [0, 0, 0, "Unexpected any. Specify a different type.", "1"], - [0, 0, 0, "Unexpected any. Specify a different type.", "2"], - [0, 0, 0, "Unexpected any. Specify a different type.", "3"], - [0, 0, 0, "Unexpected any. Specify a different type.", "4"], - [0, 0, 0, "Do not use any type assertions.", "5"], - [0, 0, 0, "Do not use any type assertions.", "6"], - [0, 0, 0, "Do not use any type assertions.", "7"], - [0, 0, 0, "Unexpected any. Specify a different type.", "8"], - [0, 0, 0, "Do not use any type assertions.", "9"], - [0, 0, 0, "Do not use any type assertions.", "10"], - [0, 0, 0, "Do not use any type assertions.", "11"] - ], - "packages/grafana-ui/src/components/GraphNG/hooks.ts:5381": [ - [0, 0, 0, "Do not use any type assertions.", "0"] - ], - "packages/grafana-ui/src/components/GraphNG/nullInsertThreshold.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, "Unexpected any. Specify a different type.", "2"], - [0, 0, 0, "Unexpected any. Specify a different type.", "3"] - ], - "packages/grafana-ui/src/components/GraphNG/nullToUndefThreshold.ts:5381": [ - [0, 0, 0, "Unexpected any. Specify a different type.", "0"], - [0, 0, 0, "Unexpected any. Specify a different type.", "1"], - [0, 0, 0, "Do not use any type assertions.", "2"] - ], "packages/grafana-ui/src/components/InfoBox/InfoBox.tsx:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"] ], @@ -981,11 +964,6 @@ exports[`better eslint`] = { "packages/grafana-ui/src/components/Tags/Tag.tsx:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"] ], - "packages/grafana-ui/src/components/TimeSeries/utils.ts:5381": [ - [0, 0, 0, "Unexpected any. Specify a different type.", "0"], - [0, 0, 0, "Do not use any type assertions.", "1"], - [0, 0, 0, "Unexpected any. Specify a different type.", "2"] - ], "packages/grafana-ui/src/components/ValuePicker/ValuePicker.tsx:5381": [ [0, 0, 0, "Use data-testid for E2E selectors instead of aria-label", "0"], [0, 0, 0, "Use data-testid for E2E selectors instead of aria-label", "1"] @@ -1032,6 +1010,39 @@ exports[`better eslint`] = { [0, 0, 0, "Do not use any type assertions.", "0"], [0, 0, 0, "Do not use any type assertions.", "1"] ], + "packages/grafana-ui/src/graveyard/GraphNG/GraphNG.tsx:5381": [ + [0, 0, 0, "Unexpected any. Specify a different type.", "0"], + [0, 0, 0, "Unexpected any. Specify a different type.", "1"], + [0, 0, 0, "Unexpected any. Specify a different type.", "2"], + [0, 0, 0, "Unexpected any. Specify a different type.", "3"], + [0, 0, 0, "Unexpected any. Specify a different type.", "4"], + [0, 0, 0, "Do not use any type assertions.", "5"], + [0, 0, 0, "Do not use any type assertions.", "6"], + [0, 0, 0, "Do not use any type assertions.", "7"], + [0, 0, 0, "Unexpected any. Specify a different type.", "8"], + [0, 0, 0, "Do not use any type assertions.", "9"], + [0, 0, 0, "Do not use any type assertions.", "10"], + [0, 0, 0, "Do not use any type assertions.", "11"] + ], + "packages/grafana-ui/src/graveyard/GraphNG/hooks.ts:5381": [ + [0, 0, 0, "Do not use any type assertions.", "0"] + ], + "packages/grafana-ui/src/graveyard/GraphNG/nullInsertThreshold.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, "Unexpected any. Specify a different type.", "2"], + [0, 0, 0, "Unexpected any. Specify a different type.", "3"] + ], + "packages/grafana-ui/src/graveyard/GraphNG/nullToUndefThreshold.ts:5381": [ + [0, 0, 0, "Unexpected any. Specify a different type.", "0"], + [0, 0, 0, "Unexpected any. Specify a different type.", "1"], + [0, 0, 0, "Do not use any type assertions.", "2"] + ], + "packages/grafana-ui/src/graveyard/TimeSeries/utils.ts:5381": [ + [0, 0, 0, "Unexpected any. Specify a different type.", "0"], + [0, 0, 0, "Do not use any type assertions.", "1"], + [0, 0, 0, "Unexpected any. Specify a different type.", "2"] + ], "packages/grafana-ui/src/options/builder/axis.tsx:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"], [0, 0, 0, "Unexpected any. Specify a different type.", "1"], @@ -1177,6 +1188,23 @@ exports[`better eslint`] = { "public/app/core/components/ForgottenPassword/ForgottenPassword.tsx:5381": [ [0, 0, 0, "Styles should be written using objects.", "0"] ], + "public/app/core/components/GraphNG/GraphNG.tsx:5381": [ + [0, 0, 0, "Unexpected any. Specify a different type.", "0"], + [0, 0, 0, "Unexpected any. Specify a different type.", "1"], + [0, 0, 0, "Unexpected any. Specify a different type.", "2"], + [0, 0, 0, "Unexpected any. Specify a different type.", "3"], + [0, 0, 0, "Unexpected any. Specify a different type.", "4"], + [0, 0, 0, "Do not use any type assertions.", "5"], + [0, 0, 0, "Do not use any type assertions.", "6"], + [0, 0, 0, "Do not use any type assertions.", "7"], + [0, 0, 0, "Unexpected any. Specify a different type.", "8"], + [0, 0, 0, "Do not use any type assertions.", "9"], + [0, 0, 0, "Do not use any type assertions.", "10"], + [0, 0, 0, "Do not use any type assertions.", "11"] + ], + "public/app/core/components/GraphNG/hooks.ts:5381": [ + [0, 0, 0, "Do not use any type assertions.", "0"] + ], "public/app/core/components/Layers/LayerDragDropList.tsx:5381": [ [0, 0, 0, "Styles should be written using objects.", "0"], [0, 0, 0, "Styles should be written using objects.", "1"], @@ -1411,6 +1439,11 @@ exports[`better eslint`] = { [0, 0, 0, "Styles should be written using objects.", "1"], [0, 0, 0, "Styles should be written using objects.", "2"] ], + "public/app/core/components/TimeSeries/utils.ts:5381": [ + [0, 0, 0, "Unexpected any. Specify a different type.", "0"], + [0, 0, 0, "Do not use any type assertions.", "1"], + [0, 0, 0, "Unexpected any. Specify a different type.", "2"] + ], "public/app/core/components/TraceToLogs/TagMappingInput.tsx:5381": [ [0, 0, 0, "Styles should be written using objects.", "0"], [0, 0, 0, "Styles should be written using objects.", "1"], diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 81b5d3e891b7a..fbbf20a345b03 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -312,9 +312,7 @@ /packages/grafana-ui/src/components/Table/ @grafana/grafana-bi-squad /packages/grafana-ui/src/components/Gauge/ @grafana/dataviz-squad /packages/grafana-ui/src/components/BarGauge/ @grafana/dataviz-squad -/packages/grafana-ui/src/components/GraphNG/ @grafana/dataviz-squad /packages/grafana-ui/src/components/Graph/ @grafana/dataviz-squad -/packages/grafana-ui/src/components/TimeSeries/ @grafana/dataviz-squad /packages/grafana-ui/src/components/uPlot/ @grafana/dataviz-squad /packages/grafana-ui/src/components/DataLinks/ @grafana/dataviz-squad /packages/grafana-ui/src/components/ValuePicker/ @grafana/dataviz-squad @@ -322,6 +320,8 @@ /packages/grafana-ui/src/components/VizLegend/ @grafana/dataviz-squad /packages/grafana-ui/src/components/VizRepeater/ @grafana/dataviz-squad /packages/grafana-ui/src/components/VizTooltip/ @grafana/dataviz-squad +/packages/grafana-ui/src/graveyard/GraphNG/ @grafana/dataviz-squad +/packages/grafana-ui/src/graveyard/TimeSeries/ @grafana/dataviz-squad /packages/grafana-ui/src/utils/storybook/ @grafana/plugins-platform-frontend /packages/grafana-data/src/transformations/ @grafana/grafana-bi-squad /packages/grafana-data/src/**/*logs* @grafana/observability-logs @@ -366,6 +366,8 @@ cypress.config.js @grafana/grafana-frontend-platform /public/app/core/components/TimePicker/ @grafana/grafana-frontend-platform /public/app/core/components/Layers/ @grafana/dataviz-squad /public/app/core/components/TraceToLogs @grafana/observability-traces-and-profiling +/public/app/core/components/GraphNG/ @grafana/dataviz-squad +/public/app/core/components/TimeSeries/ @grafana/dataviz-squad /public/app/features/all.ts @grafana/grafana-frontend-platform /public/app/features/admin/ @grafana/grafana-authnz-team /public/app/features/auth-config/ @grafana/grafana-authnz-team diff --git a/packages/grafana-data/src/transformations/index.ts b/packages/grafana-data/src/transformations/index.ts index 1fd3229f897d0..4cdd4ea2f8bfb 100644 --- a/packages/grafana-data/src/transformations/index.ts +++ b/packages/grafana-data/src/transformations/index.ts @@ -20,3 +20,6 @@ export type { RenameByRegexTransformerOptions } from './transformers/renameByReg export { joinDataFrames as outerJoinDataFrames, isLikelyAscendingVector } from './transformers/joinDataFrames'; export * from './transformers/histogram'; export { ensureTimeField } from './transformers/convertFieldType'; + +// Required for Sparklines util to work in @grafana/data, but ideally kept internal +export { applyNullInsertThreshold } from './transformers/nulls/nullInsertThreshold'; diff --git a/packages/grafana-ui/src/components/GraphNG/nullInsertThreshold.test.ts b/packages/grafana-data/src/transformations/transformers/nulls/nullInsertThreshold.test.ts similarity index 100% rename from packages/grafana-ui/src/components/GraphNG/nullInsertThreshold.test.ts rename to packages/grafana-data/src/transformations/transformers/nulls/nullInsertThreshold.test.ts diff --git a/packages/grafana-data/src/transformations/transformers/nulls/nullInsertThreshold.ts b/packages/grafana-data/src/transformations/transformers/nulls/nullInsertThreshold.ts new file mode 100644 index 0000000000000..94d8dd2e3c0ce --- /dev/null +++ b/packages/grafana-data/src/transformations/transformers/nulls/nullInsertThreshold.ts @@ -0,0 +1,186 @@ +import { DataFrame, FieldType } from '../../../types'; + +type InsertMode = (prev: number, next: number, threshold: number) => number; + +const INSERT_MODES = { + threshold: (prev: number, next: number, threshold: number) => prev + threshold, + midpoint: (prev: number, next: number, threshold: number) => (prev + next) / 2, + // previous time + 1ms to prevent StateTimeline from forward-interpolating prior state + plusone: (prev: number, next: number, threshold: number) => prev + 1, +}; + +interface NullInsertOptions { + frame: DataFrame; + refFieldName?: string | null; + refFieldPseudoMax?: number; + refFieldPseudoMin?: number; + insertMode?: InsertMode; +} + +function getRefField(frame: DataFrame, refFieldName?: string | null) { + return frame.fields.find((field) => { + // note: getFieldDisplayName() would require full DF[] + return refFieldName != null ? field.name === refFieldName : field.type === FieldType.time; + }); +} + +/** @internal */ +export function applyNullInsertThreshold(opts: NullInsertOptions): DataFrame { + if (opts.frame.length === 0) { + return opts.frame; + } + + let thorough = true; + let { frame, refFieldName, refFieldPseudoMax, refFieldPseudoMin, insertMode } = opts; + + if (!insertMode) { + insertMode = INSERT_MODES.threshold; + } + + const refField = getRefField(frame, refFieldName); + + if (refField == null) { + return frame; + } + + refField.state = { + ...refField.state, + nullThresholdApplied: true, + }; + + const thresholds = frame.fields.map((field) => field.config.custom?.insertNulls || refField.config.interval || null); + + const uniqueThresholds = new Set(thresholds); + + uniqueThresholds.delete(null as any); + + if (uniqueThresholds.size === 0) { + return frame; + } + + if (uniqueThresholds.size === 1) { + const threshold = uniqueThresholds.values().next().value; + + if (threshold <= 0) { + return frame; + } + + const refValues = refField.values; + + const frameValues = frame.fields.map((field) => field.values); + + const filledFieldValues = nullInsertThreshold( + refValues, + frameValues, + threshold, + refFieldPseudoMin, + refFieldPseudoMax, + insertMode, + thorough + ); + + if (filledFieldValues === frameValues) { + return frame; + } + + return { + ...frame, + length: filledFieldValues[0].length, + fields: frame.fields.map((field, i) => ({ + ...field, + values: filledFieldValues[i], + })), + }; + } + + // TODO: unique threshold-per-field (via overrides) is unimplemented + // should be done by processing each (refField + thresholdA-field1 + thresholdA-field2...) + // as a separate nullInsertThreshold() dataset, then re-join into single dataset via join() + return frame; +} + +function nullInsertThreshold( + refValues: number[], + frameValues: any[][], + threshold: number, + refFieldPseudoMin: number | null = null, + // will insert a trailing null when refFieldPseudoMax > last datapoint + threshold + refFieldPseudoMax: number | null = null, + getInsertValue: InsertMode, + // will insert the value at every missing interval + thorough: boolean +) { + const len = refValues.length; + const refValuesNew: number[] = []; + + // Continuously subtract the threshold from the first data point, filling in insert values accordingly + if (refFieldPseudoMin != null && refFieldPseudoMin < refValues[0]) { + let preFillCount = Math.ceil((refValues[0] - refFieldPseudoMin) / threshold); + // this will be 0 or 1 threshold increment left of visible range + let prevSlot = refValues[0] - preFillCount * threshold; + + while (prevSlot < refValues[0]) { + // (prevSlot - threshold) is used to simulate the previous 'real' data point, as getInsertValue expects + refValuesNew.push(getInsertValue(prevSlot - threshold, prevSlot, threshold)); + prevSlot += threshold; + } + } + + // Insert initial value + refValuesNew.push(refValues[0]); + + let prevValue: number = refValues[0]; + + // Fill nulls when a value is greater than the threshold value + for (let i = 1; i < len; i++) { + const curValue = refValues[i]; + + while (curValue - prevValue > threshold) { + refValuesNew.push(getInsertValue(prevValue, curValue, threshold)); + + prevValue += threshold; + + if (!thorough) { + break; + } + } + + refValuesNew.push(curValue); + + prevValue = curValue; + } + + // At the end of the sequence + if (refFieldPseudoMax != null && refFieldPseudoMax > prevValue) { + while (prevValue + threshold < refFieldPseudoMax) { + refValuesNew.push(getInsertValue(prevValue, refFieldPseudoMax, threshold)); + prevValue += threshold; + } + } + + const filledLen = refValuesNew.length; + + if (filledLen === len) { + return frameValues; + } + + const filledFieldValues: any[][] = []; + + for (let fieldValues of frameValues) { + let filledValues; + + if (fieldValues !== refValues) { + filledValues = Array(filledLen); + + for (let i = 0, j = 0; i < filledLen; i++) { + filledValues[i] = refValues[j] === refValuesNew[i] ? fieldValues[j++] : null; + } + } else { + filledValues = refValuesNew; + } + + filledFieldValues.push(filledValues); + } + + return filledFieldValues; +} diff --git a/packages/grafana-ui/src/components/GraphNG/nullToUndefThreshold.ts b/packages/grafana-data/src/transformations/transformers/nulls/nullToUndefThreshold.ts similarity index 100% rename from packages/grafana-ui/src/components/GraphNG/nullToUndefThreshold.ts rename to packages/grafana-data/src/transformations/transformers/nulls/nullToUndefThreshold.ts diff --git a/packages/grafana-ui/src/components/GraphNG/nullToValue.test.ts b/packages/grafana-data/src/transformations/transformers/nulls/nullToValue.test.ts similarity index 100% rename from packages/grafana-ui/src/components/GraphNG/nullToValue.test.ts rename to packages/grafana-data/src/transformations/transformers/nulls/nullToValue.test.ts diff --git a/packages/grafana-data/src/transformations/transformers/nulls/nullToValue.ts b/packages/grafana-data/src/transformations/transformers/nulls/nullToValue.ts new file mode 100644 index 0000000000000..4c3ba5a81e683 --- /dev/null +++ b/packages/grafana-data/src/transformations/transformers/nulls/nullToValue.ts @@ -0,0 +1,27 @@ +import { DataFrame } from '../../../types'; + +export function nullToValue(frame: DataFrame) { + return { + ...frame, + fields: frame.fields.map((field) => { + const noValue = +field.config?.noValue!; + + if (!Number.isNaN(noValue)) { + const transformedVals = field.values.slice(); + + for (let i = 0; i < transformedVals.length; i++) { + if (transformedVals[i] === null) { + transformedVals[i] = noValue; + } + } + + return { + ...field, + values: transformedVals, + }; + } else { + return field; + } + }), + }; +} diff --git a/packages/grafana-ui/src/components/Sparkline/utils.ts b/packages/grafana-ui/src/components/Sparkline/utils.ts index c643d375a370d..aa77fd64dc48e 100644 --- a/packages/grafana-ui/src/components/Sparkline/utils.ts +++ b/packages/grafana-ui/src/components/Sparkline/utils.ts @@ -5,11 +5,10 @@ import { FieldType, isLikelyAscendingVector, sortDataFrame, + applyNullInsertThreshold, } from '@grafana/data'; import { GraphFieldConfig } from '@grafana/schema'; -import { applyNullInsertThreshold } from '../GraphNG/nullInsertThreshold'; - /** @internal * Given a sparkline config returns a DataFrame ready to be turned into Plot data set **/ diff --git a/packages/grafana-ui/src/components/index.ts b/packages/grafana-ui/src/components/index.ts index 0f5dd38ea0215..78cd9b5321e58 100644 --- a/packages/grafana-ui/src/components/index.ts +++ b/packages/grafana-ui/src/components/index.ts @@ -289,13 +289,14 @@ export { UPlotChart } from './uPlot/Plot'; export { PlotLegend } from './uPlot/PlotLegend'; export * from './uPlot/geometries'; export * from './uPlot/plugins'; -export { type PlotTooltipInterpolator, type PlotSelection } from './uPlot/types'; +export { type PlotTooltipInterpolator, type PlotSelection, FIXED_UNIT } from './uPlot/types'; export { type UPlotConfigPrepFn } from './uPlot/config/UPlotConfigBuilder'; -export { GraphNG, type GraphNGProps, FIXED_UNIT } from './GraphNG/GraphNG'; -export { TimeSeries } from './TimeSeries/TimeSeries'; -export { useGraphNGContext } from './GraphNG/hooks'; -export { preparePlotFrame, buildScaleKey } from './GraphNG/utils'; -export { type GraphNGLegendEvent } from './GraphNG/types'; export * from './PanelChrome/types'; export { Label as BrowserLabel } from './BrowserLabel/Label'; export { PanelContainer } from './PanelContainer/PanelContainer'; + +export { GraphNG, type GraphNGProps } from '../graveyard/GraphNG/GraphNG'; +export { TimeSeries } from '../graveyard/TimeSeries/TimeSeries'; +export { useGraphNGContext } from '../graveyard/GraphNG/hooks'; +export { preparePlotFrame, buildScaleKey } from '../graveyard/GraphNG/utils'; +export { type GraphNGLegendEvent } from '../graveyard/GraphNG/types'; diff --git a/packages/grafana-ui/src/components/uPlot/internal.ts b/packages/grafana-ui/src/components/uPlot/internal.ts new file mode 100644 index 0000000000000..e7a9c6a14d126 --- /dev/null +++ b/packages/grafana-ui/src/components/uPlot/internal.ts @@ -0,0 +1,40 @@ +import { FieldConfig, FieldType } from '@grafana/data'; +import { AxisPlacement, GraphFieldConfig, ScaleDistribution, ScaleDistributionConfig } from '@grafana/schema'; + +import { FIXED_UNIT } from './types'; + +/** + * @internal -- not a public API + */ +export function buildScaleKey(config: FieldConfig, fieldType: FieldType) { + const defaultPart = 'na'; + + const scaleRange = `${config.min !== undefined ? config.min : defaultPart}-${ + config.max !== undefined ? config.max : defaultPart + }`; + + const scaleSoftRange = `${config.custom?.axisSoftMin !== undefined ? config.custom.axisSoftMin : defaultPart}-${ + config.custom?.axisSoftMax !== undefined ? config.custom.axisSoftMax : defaultPart + }`; + + const scalePlacement = `${ + config.custom?.axisPlacement !== undefined ? config.custom?.axisPlacement : AxisPlacement.Auto + }`; + + const scaleUnit = config.unit ?? FIXED_UNIT; + + const scaleDistribution = config.custom?.scaleDistribution + ? getScaleDistributionPart(config.custom.scaleDistribution) + : ScaleDistribution.Linear; + + const scaleLabel = Boolean(config.custom?.axisLabel) ? config.custom!.axisLabel : defaultPart; + + return `${scaleUnit}/${scaleRange}/${scaleSoftRange}/${scalePlacement}/${scaleDistribution}/${scaleLabel}/${fieldType}`; +} + +function getScaleDistributionPart(config: ScaleDistributionConfig) { + if (config.type === ScaleDistribution.Log) { + return `${config.type}${config.log}`; + } + return config.type; +} diff --git a/packages/grafana-ui/src/components/uPlot/types.ts b/packages/grafana-ui/src/components/uPlot/types.ts index 959787608d738..86f3c99c277c7 100644 --- a/packages/grafana-ui/src/components/uPlot/types.ts +++ b/packages/grafana-ui/src/components/uPlot/types.ts @@ -3,6 +3,11 @@ import uPlot, { Options, AlignedData } from 'uplot'; import { UPlotConfigBuilder } from './config/UPlotConfigBuilder'; +/** + * @internal -- not a public API + */ +export const FIXED_UNIT = '__fixed'; + export type PlotConfig = Pick< Options, 'mode' | 'series' | 'scales' | 'axes' | 'cursor' | 'bands' | 'hooks' | 'select' | 'tzDate' | 'padding' diff --git a/packages/grafana-ui/src/components/uPlot/utils.test.ts b/packages/grafana-ui/src/components/uPlot/utils.test.ts index 605c8649e63e9..fc6224ac5963d 100644 --- a/packages/grafana-ui/src/components/uPlot/utils.test.ts +++ b/packages/grafana-ui/src/components/uPlot/utils.test.ts @@ -1,7 +1,7 @@ import { FieldMatcherID, fieldMatchers, FieldType, MutableDataFrame } from '@grafana/data'; import { BarAlignment, GraphDrawStyle, GraphTransform, LineInterpolation, StackingMode } from '@grafana/schema'; -import { preparePlotFrame } from '../GraphNG/utils'; +import { preparePlotFrame } from '../../../../../public/app/core/components/GraphNG/utils'; import { getStackingGroups, preparePlotData2, timeFormatToTemplate } from './utils'; diff --git a/packages/grafana-ui/src/components/uPlot/utils.ts b/packages/grafana-ui/src/components/uPlot/utils.ts index b9312097b661b..d5d1dd7356b6e 100644 --- a/packages/grafana-ui/src/components/uPlot/utils.ts +++ b/packages/grafana-ui/src/components/uPlot/utils.ts @@ -5,7 +5,8 @@ import { BarAlignment, GraphDrawStyle, GraphTransform, LineInterpolation, Stacki import { attachDebugger } from '../../utils'; import { createLogger } from '../../utils/logger'; -import { buildScaleKey } from '../GraphNG/utils'; + +import { buildScaleKey } from './internal'; const ALLOWED_FORMAT_STRINGS_REGEX = /\b(YYYY|YY|MMMM|MMM|MM|M|DD|D|WWWW|WWW|HH|H|h|AA|aa|a|mm|m|ss|s|fff)\b/g; diff --git a/packages/grafana-ui/src/graveyard/GraphNG/GraphNG.tsx b/packages/grafana-ui/src/graveyard/GraphNG/GraphNG.tsx new file mode 100644 index 0000000000000..52dfadb27a6cd --- /dev/null +++ b/packages/grafana-ui/src/graveyard/GraphNG/GraphNG.tsx @@ -0,0 +1,275 @@ +import React, { Component } from 'react'; +import { Subscription } from 'rxjs'; +import { throttleTime } from 'rxjs/operators'; +import uPlot, { AlignedData } from 'uplot'; + +import { + DataFrame, + DataHoverClearEvent, + DataHoverEvent, + Field, + FieldMatcherID, + fieldMatchers, + FieldType, + LegacyGraphHoverEvent, + TimeRange, + TimeZone, +} from '@grafana/data'; +import { VizLegendOptions } from '@grafana/schema'; + +import { PanelContext, PanelContextRoot } from '../../components/PanelChrome/PanelContext'; +import { VizLayout } from '../../components/VizLayout/VizLayout'; +import { UPlotChart } from '../../components/uPlot/Plot'; +import { AxisProps } from '../../components/uPlot/config/UPlotAxisBuilder'; +import { Renderers, UPlotConfigBuilder } from '../../components/uPlot/config/UPlotConfigBuilder'; +import { ScaleProps } from '../../components/uPlot/config/UPlotScaleBuilder'; +import { findMidPointYPosition, pluginLog } from '../../components/uPlot/utils'; +import { Themeable2 } from '../../types'; + +import { GraphNGLegendEvent, XYFieldMatchers } from './types'; +import { preparePlotFrame as defaultPreparePlotFrame } from './utils'; + +/** + * @deprecated + * @internal -- not a public API + */ +export type PropDiffFn = (prev: T, next: T) => boolean; + +/** @deprecated */ +export interface GraphNGProps extends Themeable2 { + frames: DataFrame[]; + structureRev?: number; // a number that will change when the frames[] structure changes + width: number; + height: number; + timeRange: TimeRange; + timeZone: TimeZone[] | TimeZone; + legend: VizLegendOptions; + fields?: XYFieldMatchers; // default will assume timeseries data + renderers?: Renderers; + tweakScale?: (opts: ScaleProps, forField: Field) => ScaleProps; + tweakAxis?: (opts: AxisProps, forField: Field) => AxisProps; + onLegendClick?: (event: GraphNGLegendEvent) => void; + children?: (builder: UPlotConfigBuilder, alignedFrame: DataFrame) => React.ReactNode; + prepConfig: (alignedFrame: DataFrame, allFrames: DataFrame[], getTimeRange: () => TimeRange) => UPlotConfigBuilder; + propsToDiff?: Array; + preparePlotFrame?: (frames: DataFrame[], dimFields: XYFieldMatchers) => DataFrame | null; + renderLegend: (config: UPlotConfigBuilder) => React.ReactElement | null; + + /** + * needed for propsToDiff to re-init the plot & config + * this is a generic approach to plot re-init, without having to specify which panel-level options + * should cause invalidation. we can drop this in favor of something like panelOptionsRev that gets passed in + * similar to structureRev. then we can drop propsToDiff entirely. + */ + options?: Record; +} + +function sameProps(prevProps: any, nextProps: any, propsToDiff: Array = []) { + for (const propName of propsToDiff) { + if (typeof propName === 'function') { + if (!propName(prevProps, nextProps)) { + return false; + } + } else if (nextProps[propName] !== prevProps[propName]) { + return false; + } + } + + return true; +} + +/** + * @internal -- not a public API + * @deprecated + */ +export interface GraphNGState { + alignedFrame: DataFrame; + alignedData?: AlignedData; + config?: UPlotConfigBuilder; +} + +/** + * "Time as X" core component, expects ascending x + * @deprecated + */ +export class GraphNG extends Component { + static contextType = PanelContextRoot; + panelContext: PanelContext = {} as PanelContext; + private plotInstance: React.RefObject; + + private subscription = new Subscription(); + + constructor(props: GraphNGProps) { + super(props); + let state = this.prepState(props); + state.alignedData = state.config!.prepData!([state.alignedFrame]) as AlignedData; + this.state = state; + this.plotInstance = React.createRef(); + } + + getTimeRange = () => this.props.timeRange; + + prepState(props: GraphNGProps, withConfig = true) { + let state: GraphNGState = null as any; + + const { frames, fields, preparePlotFrame } = props; + + const preparePlotFrameFn = preparePlotFrame || defaultPreparePlotFrame; + + const alignedFrame = preparePlotFrameFn( + frames, + fields || { + x: fieldMatchers.get(FieldMatcherID.firstTimeField).get({}), + y: fieldMatchers.get(FieldMatcherID.byTypes).get(new Set([FieldType.number, FieldType.enum])), + }, + props.timeRange + ); + pluginLog('GraphNG', false, 'data aligned', alignedFrame); + + if (alignedFrame) { + let config = this.state?.config; + + if (withConfig) { + config = props.prepConfig(alignedFrame, this.props.frames, this.getTimeRange); + pluginLog('GraphNG', false, 'config prepared', config); + } + + state = { + alignedFrame, + config, + }; + + pluginLog('GraphNG', false, 'data prepared', state.alignedData); + } + + return state; + } + + handleCursorUpdate(evt: DataHoverEvent | LegacyGraphHoverEvent) { + const time = evt.payload?.point?.time; + const u = this.plotInstance.current; + if (u && time) { + // Try finding left position on time axis + const left = u.valToPos(time, 'x'); + let top; + if (left) { + // find midpoint between points at current idx + top = findMidPointYPosition(u, u.posToIdx(left)); + } + + if (!top || !left) { + return; + } + + u.setCursor({ + left, + top, + }); + } + } + + componentDidMount() { + this.panelContext = this.context as PanelContext; + const { eventBus } = this.panelContext; + + this.subscription.add( + eventBus + .getStream(DataHoverEvent) + .pipe(throttleTime(50)) + .subscribe({ + next: (evt) => { + if (eventBus === evt.origin) { + return; + } + this.handleCursorUpdate(evt); + }, + }) + ); + + // Legacy events (from flot graph) + this.subscription.add( + eventBus + .getStream(LegacyGraphHoverEvent) + .pipe(throttleTime(50)) + .subscribe({ + next: (evt) => this.handleCursorUpdate(evt), + }) + ); + + this.subscription.add( + eventBus + .getStream(DataHoverClearEvent) + .pipe(throttleTime(50)) + .subscribe({ + next: () => { + const u = this.plotInstance?.current; + + // @ts-ignore + if (u && !u.cursor._lock) { + u.setCursor({ + left: -10, + top: -10, + }); + } + }, + }) + ); + } + + componentDidUpdate(prevProps: GraphNGProps) { + const { frames, structureRev, timeZone, propsToDiff } = this.props; + + const propsChanged = !sameProps(prevProps, this.props, propsToDiff); + + if (frames !== prevProps.frames || propsChanged || timeZone !== prevProps.timeZone) { + let newState = this.prepState(this.props, false); + + if (newState) { + const shouldReconfig = + this.state.config === undefined || + timeZone !== prevProps.timeZone || + structureRev !== prevProps.structureRev || + !structureRev || + propsChanged; + + if (shouldReconfig) { + newState.config = this.props.prepConfig(newState.alignedFrame, this.props.frames, this.getTimeRange); + pluginLog('GraphNG', false, 'config recreated', newState.config); + } + + newState.alignedData = newState.config!.prepData!([newState.alignedFrame]) as AlignedData; + + this.setState(newState); + } + } + } + + componentWillUnmount() { + this.subscription.unsubscribe(); + } + + render() { + const { width, height, children, renderLegend } = this.props; + const { config, alignedFrame, alignedData } = this.state; + + if (!config) { + return null; + } + + return ( + + {(vizWidth: number, vizHeight: number) => ( + ((this.plotInstance as React.MutableRefObject).current = u)} + > + {children ? children(config, alignedFrame) : null} + + )} + + ); + } +} diff --git a/packages/grafana-ui/src/components/GraphNG/__snapshots__/utils.test.ts.snap b/packages/grafana-ui/src/graveyard/GraphNG/__snapshots__/utils.test.ts.snap similarity index 100% rename from packages/grafana-ui/src/components/GraphNG/__snapshots__/utils.test.ts.snap rename to packages/grafana-ui/src/graveyard/GraphNG/__snapshots__/utils.test.ts.snap diff --git a/packages/grafana-ui/src/graveyard/GraphNG/hooks.ts b/packages/grafana-ui/src/graveyard/GraphNG/hooks.ts new file mode 100644 index 0000000000000..e4f1d46550a20 --- /dev/null +++ b/packages/grafana-ui/src/graveyard/GraphNG/hooks.ts @@ -0,0 +1,41 @@ +import React, { useCallback, useContext } from 'react'; + +import { DataFrame, DataFrameFieldIndex, Field } from '@grafana/data'; + +import { XYFieldMatchers } from './types'; + +/** @deprecated */ +interface GraphNGContextType { + mapSeriesIndexToDataFrameFieldIndex: (index: number) => DataFrameFieldIndex; + dimFields: XYFieldMatchers; + data: DataFrame; +} + +/** @deprecated */ +export const GraphNGContext = React.createContext({} as GraphNGContextType); + +/** @deprecated */ +export const useGraphNGContext = () => { + const { data, dimFields, mapSeriesIndexToDataFrameFieldIndex } = useContext(GraphNGContext); + + const getXAxisField = useCallback(() => { + const xFieldMatcher = dimFields.x; + let xField: Field | null = null; + + for (let j = 0; j < data.fields.length; j++) { + if (xFieldMatcher(data.fields[j], data, [data])) { + xField = data.fields[j]; + break; + } + } + + return xField; + }, [data, dimFields]); + + return { + dimFields, + mapSeriesIndexToDataFrameFieldIndex, + getXAxisField, + alignedData: data, + }; +}; diff --git a/packages/grafana-ui/src/graveyard/GraphNG/nullInsertThreshold.test.ts b/packages/grafana-ui/src/graveyard/GraphNG/nullInsertThreshold.test.ts new file mode 100644 index 0000000000000..7a2ceb15e8dbd --- /dev/null +++ b/packages/grafana-ui/src/graveyard/GraphNG/nullInsertThreshold.test.ts @@ -0,0 +1,334 @@ +import { FieldType, createDataFrame } from '@grafana/data'; + +import { applyNullInsertThreshold } from './nullInsertThreshold'; + +function randInt(min: number, max: number) { + return Math.floor(Math.random() * (max - min + 1) + min); +} + +function genFrame() { + let fieldCount = 10; + let valueCount = 3000; + let step = 1000; + let skipProb = 0.5; + let skipSteps = [1, 5]; // min, max + + let allValues = Array(fieldCount); + + allValues[0] = Array(valueCount); + + for (let i = 0, curStep = Date.now(); i < valueCount; i++) { + curStep = allValues[0][i] = curStep + step * (Math.random() < skipProb ? randInt(skipSteps[0], skipSteps[1]) : 1); + } + + for (let fi = 1; fi < fieldCount; fi++) { + let values = Array(valueCount); + + for (let i = 0; i < valueCount; i++) { + values[i] = Math.random() * 100; + } + + allValues[fi] = values; + } + + return { + length: valueCount, + fields: allValues.map((values, i) => { + return { + name: 'A-' + i, + type: i === 0 ? FieldType.time : FieldType.number, + config: { + interval: i === 0 ? step : null, + }, + values: values, + }; + }), + }; +} + +describe('nullInsertThreshold Transformer', () => { + test('should insert nulls at +threshold between adjacent > threshold: 1', () => { + const df = createDataFrame({ + refId: 'A', + fields: [ + { name: 'Time', type: FieldType.time, values: [1, 3, 10] }, + { name: 'One', type: FieldType.number, config: { custom: { insertNulls: 1 } }, values: [4, 6, 8] }, + { name: 'Two', type: FieldType.string, config: { custom: { insertNulls: 1 } }, values: ['a', 'b', 'c'] }, + ], + }); + + const result = applyNullInsertThreshold({ frame: df }); + + expect(result.fields[0].values).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]); + expect(result.fields[1].values).toEqual([4, null, 6, null, null, null, null, null, null, 8]); + expect(result.fields[2].values).toEqual(['a', null, 'b', null, null, null, null, null, null, 'c']); + }); + + test('should insert nulls at +threshold between adjacent > threshold: 2', () => { + const df = createDataFrame({ + refId: 'A', + fields: [ + { name: 'Time', type: FieldType.time, values: [5, 7, 11] }, + { name: 'One', type: FieldType.number, config: { custom: { insertNulls: 2 } }, values: [4, 6, 8] }, + { name: 'Two', type: FieldType.string, config: { custom: { insertNulls: 2 } }, values: ['a', 'b', 'c'] }, + ], + }); + + const result = applyNullInsertThreshold({ frame: df }); + + expect(result.fields[0].values).toEqual([5, 7, 9, 11]); + expect(result.fields[1].values).toEqual([4, 6, null, 8]); + expect(result.fields[2].values).toEqual(['a', 'b', null, 'c']); + }); + + test('should insert nulls at +interval between adjacent > interval: 1', () => { + const df = createDataFrame({ + refId: 'A', + fields: [ + { name: 'Time', type: FieldType.time, config: { interval: 1 }, values: [1, 3, 10] }, + { name: 'One', type: FieldType.number, values: [4, 6, 8] }, + { name: 'Two', type: FieldType.string, values: ['a', 'b', 'c'] }, + ], + }); + + const result = applyNullInsertThreshold({ frame: df }); + + expect(result.fields[0].values).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]); + expect(result.fields[1].values).toEqual([4, null, 6, null, null, null, null, null, null, 8]); + expect(result.fields[2].values).toEqual(['a', null, 'b', null, null, null, null, null, null, 'c']); + }); + + test('should insert leading null at beginning +interval when timeRange.from.valueOf() exceeds threshold', () => { + const df = createDataFrame({ + refId: 'A', + fields: [ + { name: 'Time', type: FieldType.time, config: { interval: 1 }, values: [4, 6, 13] }, + { name: 'One', type: FieldType.number, values: [4, 6, 8] }, + { name: 'Two', type: FieldType.string, values: ['a', 'b', 'c'] }, + ], + }); + + const result = applyNullInsertThreshold({ + frame: df, + refFieldName: null, + refFieldPseudoMin: -0.5, + refFieldPseudoMax: 13, + }); + + expect(result.fields[0].values).toEqual([-1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13]); + expect(result.fields[1].values).toEqual([ + null, + null, + null, + null, + null, + 4, + null, + 6, + null, + null, + null, + null, + null, + null, + 8, + ]); + expect(result.fields[2].values).toEqual([ + null, + null, + null, + null, + null, + 'a', + null, + 'b', + null, + null, + null, + null, + null, + null, + 'c', + ]); + }); + + // this tests that intervals at 24hr but starting not at 12am UTC are not always snapped to 12am UTC + test('should insert leading null at beginning +interval when timeRange.from.valueOf() exceeds threshold 11PM UTC', () => { + const df = createDataFrame({ + refId: 'A', + fields: [ + { + name: 'Time', + type: FieldType.time, + config: { interval: 86400000 }, + values: [1679439600000, 1679526000000, 1679612400000, 1679698800000, 1679785200000], + }, + { name: 'One', type: FieldType.number, values: [0, 1, 2, 3, 4] }, + ], + }); + + const result = applyNullInsertThreshold({ + frame: df, + refFieldName: null, + refFieldPseudoMin: 1679320395828, + refFieldPseudoMax: 1679815217157, + }); + + expect(result.fields[0].values).toEqual([ + 1679266800000, 1679353200000, 1679439600000, 1679526000000, 1679612400000, 1679698800000, 1679785200000, + ]); + expect(result.fields[1].values).toEqual([null, null, 0, 1, 2, 3, 4]); + }); + + test('should insert trailing null at end +interval when timeRange.to.valueOf() exceeds threshold', () => { + const df = createDataFrame({ + refId: 'A', + fields: [ + { name: 'Time', type: FieldType.time, config: { interval: 1 }, values: [1, 3, 10] }, + { name: 'One', type: FieldType.number, values: [4, 6, 8] }, + { name: 'Two', type: FieldType.string, values: ['a', 'b', 'c'] }, + ], + }); + + const result = applyNullInsertThreshold({ frame: df, refFieldName: null, refFieldPseudoMax: 13 }); + + expect(result.fields[0].values).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]); + expect(result.fields[1].values).toEqual([4, null, 6, null, null, null, null, null, null, 8, null, null]); + expect(result.fields[2].values).toEqual(['a', null, 'b', null, null, null, null, null, null, 'c', null, null]); + + // should work for frames with 1 datapoint + const df2 = createDataFrame({ + refId: 'A', + fields: [ + { name: 'Time', type: FieldType.time, config: { interval: 1 }, values: [1] }, + { name: 'One', type: FieldType.number, values: [1] }, + { name: 'Two', type: FieldType.string, values: ['a'] }, + ], + }); + + // Max is 2.5 as opposed to the above 13 otherwise + // we get 12 nulls instead of the additional 1 + const result2 = applyNullInsertThreshold({ frame: df2, refFieldName: null, refFieldPseudoMax: 2.5 }); + + expect(result2.fields[0].values).toEqual([1, 2]); + expect(result2.fields[1].values).toEqual([1, null]); + expect(result2.fields[2].values).toEqual(['a', null]); + }); + + test('should not insert trailing null at end +interval when timeRange.to.valueOf() equals threshold', () => { + const df = createDataFrame({ + refId: 'A', + fields: [ + { name: 'Time', type: FieldType.time, config: { interval: 1 }, values: [1] }, + { name: 'One', type: FieldType.number, values: [1] }, + { name: 'Two', type: FieldType.string, values: ['a'] }, + ], + }); + + const result = applyNullInsertThreshold({ frame: df, refFieldName: null, refFieldPseudoMax: 2 }); + + expect(result.fields[0].values).toEqual([1]); + expect(result.fields[1].values).toEqual([1]); + expect(result.fields[2].values).toEqual(['a']); + }); + + // TODO: make this work + test.skip('should insert nulls at +threshold (when defined) instead of +interval', () => { + const df = createDataFrame({ + refId: 'A', + fields: [ + { name: 'Time', type: FieldType.time, config: { interval: 2 }, values: [5, 7, 11] }, + { name: 'One', type: FieldType.number, config: { custom: { insertNulls: 1 } }, values: [4, 6, 8] }, + { name: 'Two', type: FieldType.string, config: { custom: { insertNulls: 1 } }, values: ['a', 'b', 'c'] }, + ], + }); + + const result = applyNullInsertThreshold({ frame: df }); + + expect(result.fields[0].values).toEqual([5, 6, 7, 8, 11]); + expect(result.fields[1].values).toEqual([4, null, 6, null, 8]); + expect(result.fields[2].values).toEqual(['a', null, 'b', null, 'c']); + }); + + test('should noop on 0 datapoints', () => { + const df = createDataFrame({ + refId: 'A', + fields: [ + { name: 'Time', type: FieldType.time, config: { interval: 1 }, values: [] }, + { name: 'Value', type: FieldType.number, values: [] }, + ], + }); + + const result = applyNullInsertThreshold({ frame: df }); + + expect(result).toBe(df); + }); + + test('should noop on invalid threshold', () => { + const df = createDataFrame({ + refId: 'A', + fields: [ + { name: 'Time', type: FieldType.time, values: [1, 2, 4] }, + { name: 'Value', type: FieldType.number, config: { custom: { insertNulls: -1 } }, values: [1, 1, 1] }, + ], + }); + + const result = applyNullInsertThreshold({ frame: df }); + + expect(result).toBe(df); + }); + + test('should noop on invalid interval', () => { + const df = createDataFrame({ + refId: 'A', + fields: [ + { name: 'Time', type: FieldType.time, config: { interval: -1 }, values: [1, 2, 4] }, + { name: 'Value', type: FieldType.number, values: [1, 1, 1] }, + ], + }); + + const result = applyNullInsertThreshold({ frame: df }); + + expect(result).toBe(df); + }); + + test('should noop when no missing steps', () => { + const df = createDataFrame({ + refId: 'A', + fields: [ + { name: 'Time', type: FieldType.time, config: { interval: 1 }, values: [1, 2, 3] }, + { name: 'Value', type: FieldType.number, values: [1, 1, 1] }, + ], + }); + + const result = applyNullInsertThreshold({ frame: df }); + + expect(result).toBe(df); + }); + + test('should noop when refFieldName not found', () => { + const df = createDataFrame({ + refId: 'A', + fields: [ + { name: 'Time', type: FieldType.time, config: { interval: 1 }, values: [1, 2, 5] }, + { name: 'Value', type: FieldType.number, values: [1, 1, 1] }, + ], + }); + + const result = applyNullInsertThreshold({ frame: df, refFieldName: 'Time2' }); + + expect(result).toBe(df); + }); + + // Leave this test skipped - it should be run manually + test.skip('perf stress test should be <= 10ms', () => { + // 10 fields x 3,000 values with 50% skip (output = 10 fields x 6,000 values) + let bigFrameA = genFrame(); + + // eslint-disable-next-line no-console + console.time('insertValues-10x3k'); + applyNullInsertThreshold({ frame: bigFrameA }); + // eslint-disable-next-line no-console + console.timeEnd('insertValues-10x3k'); + }); +}); diff --git a/packages/grafana-ui/src/components/GraphNG/nullInsertThreshold.ts b/packages/grafana-ui/src/graveyard/GraphNG/nullInsertThreshold.ts similarity index 99% rename from packages/grafana-ui/src/components/GraphNG/nullInsertThreshold.ts rename to packages/grafana-ui/src/graveyard/GraphNG/nullInsertThreshold.ts index 352f2f0660bc1..9361a6bab8dcc 100644 --- a/packages/grafana-ui/src/components/GraphNG/nullInsertThreshold.ts +++ b/packages/grafana-ui/src/graveyard/GraphNG/nullInsertThreshold.ts @@ -19,6 +19,7 @@ interface NullInsertOptions { insertMode?: InsertMode; } +/** @deprecated */ export function applyNullInsertThreshold(opts: NullInsertOptions): DataFrame { if (opts.frame.length === 0) { return opts.frame; diff --git a/packages/grafana-ui/src/graveyard/GraphNG/nullToUndefThreshold.ts b/packages/grafana-ui/src/graveyard/GraphNG/nullToUndefThreshold.ts new file mode 100644 index 0000000000000..bcc52e5e1b984 --- /dev/null +++ b/packages/grafana-ui/src/graveyard/GraphNG/nullToUndefThreshold.ts @@ -0,0 +1,33 @@ +/** + * mutates all nulls -> undefineds in the fieldValues array for value-less refValues ranges below maxThreshold + * refValues is typically a time array and maxThreshold is the allowable distance between in time + * @deprecated + */ +export function nullToUndefThreshold(refValues: number[], fieldValues: any[], maxThreshold: number): any[] { + let prevRef; + let nullIdx; + + for (let i = 0; i < fieldValues.length; i++) { + let fieldVal = fieldValues[i]; + + if (fieldVal == null) { + if (nullIdx == null && prevRef != null) { + nullIdx = i; + } + } else { + if (nullIdx != null) { + if (refValues[i] - (prevRef as number) < maxThreshold) { + while (nullIdx < i) { + fieldValues[nullIdx++] = undefined; + } + } + + nullIdx = null; + } + + prevRef = refValues[i]; + } + } + + return fieldValues; +} diff --git a/packages/grafana-ui/src/graveyard/GraphNG/nullToValue.test.ts b/packages/grafana-ui/src/graveyard/GraphNG/nullToValue.test.ts new file mode 100644 index 0000000000000..e14f64e17109c --- /dev/null +++ b/packages/grafana-ui/src/graveyard/GraphNG/nullToValue.test.ts @@ -0,0 +1,94 @@ +import { FieldType, createDataFrame } from '@grafana/data'; + +import { applyNullInsertThreshold } from './nullInsertThreshold'; +import { nullToValue } from './nullToValue'; + +describe('nullToValue Transformer', () => { + test('should change all nulls to configured zero value', () => { + const df = createDataFrame({ + refId: 'A', + fields: [ + { name: 'Time', type: FieldType.time, values: [1, 3, 10] }, + { + name: 'One', + type: FieldType.number, + config: { custom: { insertNulls: 1 }, noValue: '0' }, + values: [4, 6, 8], + }, + { + name: 'Two', + type: FieldType.string, + config: { custom: { insertNulls: 1 }, noValue: '0' }, + values: ['a', 'b', 'c'], + }, + ], + }); + + const result = nullToValue(applyNullInsertThreshold({ frame: df })); + + expect(result.fields[0].values).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]); + expect(result.fields[1].values).toEqual([4, 0, 6, 0, 0, 0, 0, 0, 0, 8]); + expect(result.fields[2].values).toEqual(['a', 0, 'b', 0, 0, 0, 0, 0, 0, 'c']); + }); + + test('should change all nulls to configured positive value', () => { + const df = createDataFrame({ + refId: 'A', + fields: [ + { name: 'Time', type: FieldType.time, values: [5, 7, 11] }, + { + name: 'One', + type: FieldType.number, + config: { custom: { insertNulls: 2 }, noValue: '1' }, + values: [4, 6, 8], + }, + { + name: 'Two', + type: FieldType.string, + config: { custom: { insertNulls: 2 }, noValue: '1' }, + values: ['a', 'b', 'c'], + }, + ], + }); + + const result = nullToValue(applyNullInsertThreshold({ frame: df })); + + expect(result.fields[0].values).toEqual([5, 7, 9, 11]); + expect(result.fields[1].values).toEqual([4, 6, 1, 8]); + expect(result.fields[2].values).toEqual(['a', 'b', 1, 'c']); + }); + + test('should change all nulls to configured negative value', () => { + const df = createDataFrame({ + refId: 'A', + fields: [ + { name: 'Time', type: FieldType.time, config: { interval: 1 }, values: [1, 3, 10] }, + { name: 'One', type: FieldType.number, config: { noValue: '-1' }, values: [4, 6, 8] }, + { name: 'Two', type: FieldType.string, config: { noValue: '-1' }, values: ['a', 'b', 'c'] }, + ], + }); + + const result = nullToValue(applyNullInsertThreshold({ frame: df })); + + expect(result.fields[0].values).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]); + expect(result.fields[1].values).toEqual([4, -1, 6, -1, -1, -1, -1, -1, -1, 8]); + expect(result.fields[2].values).toEqual(['a', -1, 'b', -1, -1, -1, -1, -1, -1, 'c']); + }); + + test('should have no effect without nulls', () => { + const df = createDataFrame({ + refId: 'A', + fields: [ + { name: 'Time', type: FieldType.time, config: { interval: 1 }, values: [1, 2, 3] }, + { name: 'One', type: FieldType.number, values: [4, 6, 8] }, + { name: 'Two', type: FieldType.string, values: ['a', 'b', 'c'] }, + ], + }); + + const result = nullToValue(applyNullInsertThreshold({ frame: df, refFieldName: null })); + + expect(result.fields[0].values).toEqual([1, 2, 3]); + expect(result.fields[1].values).toEqual([4, 6, 8]); + expect(result.fields[2].values).toEqual(['a', 'b', 'c']); + }); +}); diff --git a/packages/grafana-ui/src/components/GraphNG/nullToValue.ts b/packages/grafana-ui/src/graveyard/GraphNG/nullToValue.ts similarity index 96% rename from packages/grafana-ui/src/components/GraphNG/nullToValue.ts rename to packages/grafana-ui/src/graveyard/GraphNG/nullToValue.ts index 62aea31b8371c..7ad8e3705aaf9 100644 --- a/packages/grafana-ui/src/components/GraphNG/nullToValue.ts +++ b/packages/grafana-ui/src/graveyard/GraphNG/nullToValue.ts @@ -1,5 +1,6 @@ import { DataFrame } from '@grafana/data'; +/** @deprecated */ export function nullToValue(frame: DataFrame) { return { ...frame, diff --git a/packages/grafana-ui/src/graveyard/GraphNG/types.ts b/packages/grafana-ui/src/graveyard/GraphNG/types.ts new file mode 100644 index 0000000000000..91dd83fb5a8fd --- /dev/null +++ b/packages/grafana-ui/src/graveyard/GraphNG/types.ts @@ -0,0 +1,18 @@ +import { DataFrameFieldIndex, FieldMatcher } from '@grafana/data'; + +import { SeriesVisibilityChangeMode } from '../../components/PanelChrome'; + +/** + * Event being triggered when the user interact with the Graph legend. + * @deprecated + */ +export interface GraphNGLegendEvent { + fieldIndex: DataFrameFieldIndex; + mode: SeriesVisibilityChangeMode; +} + +/** @deprecated */ +export interface XYFieldMatchers { + x: FieldMatcher; // first match + y: FieldMatcher; +} diff --git a/packages/grafana-ui/src/components/GraphNG/utils.test.ts b/packages/grafana-ui/src/graveyard/GraphNG/utils.test.ts similarity index 100% rename from packages/grafana-ui/src/components/GraphNG/utils.test.ts rename to packages/grafana-ui/src/graveyard/GraphNG/utils.test.ts diff --git a/packages/grafana-ui/src/components/GraphNG/utils.ts b/packages/grafana-ui/src/graveyard/GraphNG/utils.ts similarity index 98% rename from packages/grafana-ui/src/components/GraphNG/utils.ts rename to packages/grafana-ui/src/graveyard/GraphNG/utils.ts index 9922b85d9df02..742031446acd3 100644 --- a/packages/grafana-ui/src/components/GraphNG/utils.ts +++ b/packages/grafana-ui/src/graveyard/GraphNG/utils.ts @@ -7,7 +7,8 @@ import { ScaleDistributionConfig, } from '@grafana/schema'; -import { FIXED_UNIT } from './GraphNG'; +import { FIXED_UNIT } from '../../components/uPlot/types'; + import { applyNullInsertThreshold } from './nullInsertThreshold'; import { nullToUndefThreshold } from './nullToUndefThreshold'; import { XYFieldMatchers } from './types'; diff --git a/packages/grafana-ui/src/graveyard/README.md b/packages/grafana-ui/src/graveyard/README.md new file mode 100644 index 0000000000000..2715ecec38a5e --- /dev/null +++ b/packages/grafana-ui/src/graveyard/README.md @@ -0,0 +1 @@ +Items in this folder are all deprecated and will be removed in the future diff --git a/packages/grafana-ui/src/components/TimeSeries/TimeSeries.tsx b/packages/grafana-ui/src/graveyard/TimeSeries/TimeSeries.tsx similarity index 87% rename from packages/grafana-ui/src/components/TimeSeries/TimeSeries.tsx rename to packages/grafana-ui/src/graveyard/TimeSeries/TimeSeries.tsx index 019671e2b6ae5..9a748236d779e 100644 --- a/packages/grafana-ui/src/components/TimeSeries/TimeSeries.tsx +++ b/packages/grafana-ui/src/graveyard/TimeSeries/TimeSeries.tsx @@ -2,11 +2,11 @@ import React, { Component } from 'react'; import { DataFrame, TimeRange } from '@grafana/data'; +import { PanelContextRoot } from '../../components/PanelChrome/PanelContext'; +import { hasVisibleLegendSeries, PlotLegend } from '../../components/uPlot/PlotLegend'; +import { UPlotConfigBuilder } from '../../components/uPlot/config/UPlotConfigBuilder'; import { withTheme2 } from '../../themes/ThemeContext'; import { GraphNG, GraphNGProps, PropDiffFn } from '../GraphNG/GraphNG'; -import { PanelContextRoot } from '../PanelChrome/PanelContext'; -import { hasVisibleLegendSeries, PlotLegend } from '../uPlot/PlotLegend'; -import { UPlotConfigBuilder } from '../uPlot/config/UPlotConfigBuilder'; import { preparePlotConfigBuilder } from './utils'; diff --git a/packages/grafana-ui/src/components/TimeSeries/utils.test.ts b/packages/grafana-ui/src/graveyard/TimeSeries/utils.test.ts similarity index 100% rename from packages/grafana-ui/src/components/TimeSeries/utils.test.ts rename to packages/grafana-ui/src/graveyard/TimeSeries/utils.test.ts diff --git a/packages/grafana-ui/src/components/TimeSeries/utils.ts b/packages/grafana-ui/src/graveyard/TimeSeries/utils.ts similarity index 98% rename from packages/grafana-ui/src/components/TimeSeries/utils.ts rename to packages/grafana-ui/src/graveyard/TimeSeries/utils.ts index 9d2d373b33d8c..ad029097ba55e 100644 --- a/packages/grafana-ui/src/components/TimeSeries/utils.ts +++ b/packages/grafana-ui/src/graveyard/TimeSeries/utils.ts @@ -61,10 +61,10 @@ for (let i = 0; i < BIN_INCRS.length; i++) { BIN_INCRS[i] = 2 ** i; } +import { UPlotConfigBuilder, UPlotConfigPrepFn } from '../../components/uPlot/config/UPlotConfigBuilder'; +import { getScaleGradientFn } from '../../components/uPlot/config/gradientFills'; +import { getStackingGroups, preparePlotData2 } from '../../components/uPlot/utils'; import { buildScaleKey } from '../GraphNG/utils'; -import { UPlotConfigBuilder, UPlotConfigPrepFn } from '../uPlot/config/UPlotConfigBuilder'; -import { getScaleGradientFn } from '../uPlot/config/gradientFills'; -import { getStackingGroups, preparePlotData2 } from '../uPlot/utils'; const defaultFormatter = (v: any, decimals: DecimalCount = 1) => (v == null ? '-' : v.toFixed(decimals)); diff --git a/packages/grafana-ui/src/components/GraphNG/GraphNG.tsx b/public/app/core/components/GraphNG/GraphNG.tsx similarity index 92% rename from packages/grafana-ui/src/components/GraphNG/GraphNG.tsx rename to public/app/core/components/GraphNG/GraphNG.tsx index d75ab59252b6e..9ebfa499c819c 100644 --- a/packages/grafana-ui/src/components/GraphNG/GraphNG.tsx +++ b/public/app/core/components/GraphNG/GraphNG.tsx @@ -16,24 +16,16 @@ import { TimeZone, } from '@grafana/data'; import { VizLegendOptions } from '@grafana/schema'; - -import { Themeable2 } from '../../types'; -import { PanelContext, PanelContextRoot } from '../PanelChrome/PanelContext'; -import { VizLayout } from '../VizLayout/VizLayout'; -import { UPlotChart } from '../uPlot/Plot'; -import { AxisProps } from '../uPlot/config/UPlotAxisBuilder'; -import { Renderers, UPlotConfigBuilder } from '../uPlot/config/UPlotConfigBuilder'; -import { ScaleProps } from '../uPlot/config/UPlotScaleBuilder'; -import { findMidPointYPosition, pluginLog } from '../uPlot/utils'; +import { Themeable2, PanelContext, PanelContextRoot, VizLayout } from '@grafana/ui'; +import { UPlotChart } from '@grafana/ui/src/components/uPlot/Plot'; +import { AxisProps } from '@grafana/ui/src/components/uPlot/config/UPlotAxisBuilder'; +import { Renderers, UPlotConfigBuilder } from '@grafana/ui/src/components/uPlot/config/UPlotConfigBuilder'; +import { ScaleProps } from '@grafana/ui/src/components/uPlot/config/UPlotScaleBuilder'; +import { findMidPointYPosition, pluginLog } from '@grafana/ui/src/components/uPlot/utils'; import { GraphNGLegendEvent, XYFieldMatchers } from './types'; import { preparePlotFrame as defaultPreparePlotFrame } from './utils'; -/** - * @internal -- not a public API - */ -export const FIXED_UNIT = '__fixed'; - /** * @internal -- not a public API */ diff --git a/public/app/core/components/GraphNG/__snapshots__/utils.test.ts.snap b/public/app/core/components/GraphNG/__snapshots__/utils.test.ts.snap new file mode 100644 index 0000000000000..09f70e81c444d --- /dev/null +++ b/public/app/core/components/GraphNG/__snapshots__/utils.test.ts.snap @@ -0,0 +1,245 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`GraphNG utils preparePlotConfigBuilder 1`] = ` +{ + "axes": [ + { + "filter": undefined, + "font": "12px "Inter", "Helvetica", "Arial", sans-serif", + "gap": 5, + "grid": { + "show": true, + "stroke": "rgba(240, 250, 255, 0.09)", + "width": 1, + }, + "incrs": undefined, + "labelGap": 0, + "rotate": undefined, + "scale": "x", + "show": true, + "side": 2, + "size": [Function], + "space": [Function], + "splits": undefined, + "stroke": "rgb(204, 204, 220)", + "ticks": { + "show": true, + "size": 4, + "stroke": "rgba(240, 250, 255, 0.09)", + "width": 1, + }, + "timeZone": "utc", + "values": [Function], + }, + { + "filter": undefined, + "font": "12px "Inter", "Helvetica", "Arial", sans-serif", + "gap": 5, + "grid": { + "show": true, + "stroke": "rgba(240, 250, 255, 0.09)", + "width": 1, + }, + "incrs": undefined, + "labelGap": 0, + "rotate": undefined, + "scale": "__fixed/na-na/na-na/auto/linear/na/number", + "show": true, + "side": 3, + "size": [Function], + "space": [Function], + "splits": undefined, + "stroke": "rgb(204, 204, 220)", + "ticks": { + "show": false, + "size": 4, + "stroke": "rgb(204, 204, 220)", + "width": 1, + }, + "timeZone": undefined, + "values": [Function], + }, + ], + "cursor": { + "dataIdx": [Function], + "drag": { + "setScale": false, + }, + "focus": { + "prox": 30, + }, + "points": { + "fill": [Function], + "size": [Function], + "stroke": [Function], + "width": [Function], + }, + "sync": { + "filters": { + "pub": [Function], + }, + "key": "__global_", + "scales": [ + "x", + "__fixed/na-na/na-na/auto/linear/na/number", + ], + }, + }, + "focus": { + "alpha": 1, + }, + "hooks": {}, + "legend": { + "show": false, + }, + "mode": 1, + "ms": 1, + "padding": [ + [Function], + [Function], + [Function], + [Function], + ], + "scales": { + "__fixed/na-na/na-na/auto/linear/na/number": { + "asinh": undefined, + "auto": true, + "dir": 1, + "distr": 1, + "log": undefined, + "ori": 1, + "range": [Function], + "time": undefined, + }, + "x": { + "auto": false, + "dir": 1, + "ori": 0, + "range": [Function], + "time": true, + }, + }, + "select": undefined, + "series": [ + { + "value": [Function], + }, + { + "dash": [ + 1, + 2, + ], + "facets": undefined, + "fill": [Function], + "paths": [Function], + "points": { + "fill": "#ff0000", + "filter": [Function], + "show": true, + "size": undefined, + "stroke": "#ff0000", + }, + "pxAlign": undefined, + "scale": "__fixed/na-na/na-na/auto/linear/na/number", + "show": true, + "spanGaps": false, + "stroke": "#ff0000", + "value": [Function], + "width": 2, + }, + { + "dash": [ + 1, + 2, + ], + "facets": undefined, + "fill": [Function], + "paths": [Function], + "points": { + "fill": "#ff0000", + "filter": [Function], + "show": true, + "size": undefined, + "stroke": "#ff0000", + }, + "pxAlign": undefined, + "scale": "__fixed/na-na/na-na/auto/linear/na/number", + "show": true, + "spanGaps": false, + "stroke": "#ff0000", + "value": [Function], + "width": 2, + }, + { + "dash": [ + 1, + 2, + ], + "facets": undefined, + "fill": [Function], + "paths": [Function], + "points": { + "fill": "#ff0000", + "filter": [Function], + "show": true, + "size": undefined, + "stroke": "#ff0000", + }, + "pxAlign": undefined, + "scale": "__fixed/na-na/na-na/auto/linear/na/number", + "show": true, + "spanGaps": false, + "stroke": "#ff0000", + "value": [Function], + "width": 2, + }, + { + "dash": [ + 1, + 2, + ], + "facets": undefined, + "fill": [Function], + "paths": [Function], + "points": { + "fill": "#ff0000", + "filter": [Function], + "show": true, + "size": undefined, + "stroke": "#ff0000", + }, + "pxAlign": undefined, + "scale": "__fixed/na-na/na-na/auto/linear/na/number", + "show": true, + "spanGaps": false, + "stroke": "#ff0000", + "value": [Function], + "width": 2, + }, + { + "dash": [ + 1, + 2, + ], + "facets": undefined, + "fill": [Function], + "paths": [Function], + "points": { + "fill": "#ff0000", + "filter": [Function], + "show": true, + "size": undefined, + "stroke": "#ff0000", + }, + "pxAlign": undefined, + "scale": "__fixed/na-na/na-na/auto/linear/na/number", + "show": true, + "spanGaps": false, + "stroke": "#ff0000", + "value": [Function], + "width": 2, + }, + ], + "tzDate": [Function], +} +`; diff --git a/packages/grafana-ui/src/components/GraphNG/hooks.ts b/public/app/core/components/GraphNG/hooks.ts similarity index 100% rename from packages/grafana-ui/src/components/GraphNG/hooks.ts rename to public/app/core/components/GraphNG/hooks.ts diff --git a/packages/grafana-ui/src/components/GraphNG/types.ts b/public/app/core/components/GraphNG/types.ts similarity index 85% rename from packages/grafana-ui/src/components/GraphNG/types.ts rename to public/app/core/components/GraphNG/types.ts index 534cd9edf4ded..e642fbd8aac52 100644 --- a/packages/grafana-ui/src/components/GraphNG/types.ts +++ b/public/app/core/components/GraphNG/types.ts @@ -1,6 +1,5 @@ import { DataFrameFieldIndex, FieldMatcher } from '@grafana/data'; - -import { SeriesVisibilityChangeMode } from '../PanelChrome'; +import { SeriesVisibilityChangeMode } from '@grafana/ui'; /** * Event being triggered when the user interact with the Graph legend. diff --git a/public/app/core/components/GraphNG/utils.test.ts b/public/app/core/components/GraphNG/utils.test.ts new file mode 100644 index 0000000000000..9675cd7ca562c --- /dev/null +++ b/public/app/core/components/GraphNG/utils.test.ts @@ -0,0 +1,522 @@ +import { + createTheme, + DashboardCursorSync, + DataFrame, + DefaultTimeZone, + EventBusSrv, + FieldColorModeId, + FieldConfig, + FieldMatcherID, + fieldMatchers, + FieldType, + getDefaultTimeRange, + MutableDataFrame, +} from '@grafana/data'; +import { + BarAlignment, + GraphDrawStyle, + GraphFieldConfig, + GraphGradientMode, + LineInterpolation, + VisibilityMode, + StackingMode, +} from '@grafana/schema'; + +import { preparePlotConfigBuilder } from '../TimeSeries/utils'; + +import { preparePlotFrame } from './utils'; + +function mockDataFrame() { + const df1 = new MutableDataFrame({ + refId: 'A', + fields: [{ name: 'ts', type: FieldType.time, values: [1, 2, 3] }], + }); + const df2 = new MutableDataFrame({ + refId: 'B', + fields: [{ name: 'ts', type: FieldType.time, values: [1, 2, 4] }], + }); + + const f1Config: FieldConfig = { + displayName: 'Metric 1', + color: { + mode: FieldColorModeId.Fixed, + }, + decimals: 2, + custom: { + drawStyle: GraphDrawStyle.Line, + gradientMode: GraphGradientMode.Opacity, + lineColor: '#ff0000', + lineWidth: 2, + lineInterpolation: LineInterpolation.Linear, + lineStyle: { + fill: 'dash', + dash: [1, 2], + }, + spanNulls: false, + fillColor: '#ff0000', + fillOpacity: 0.1, + showPoints: VisibilityMode.Always, + stacking: { + group: 'A', + mode: StackingMode.Normal, + }, + }, + }; + + const f2Config: FieldConfig = { + displayName: 'Metric 2', + color: { + mode: FieldColorModeId.Fixed, + }, + decimals: 2, + custom: { + drawStyle: GraphDrawStyle.Bars, + gradientMode: GraphGradientMode.Hue, + lineColor: '#ff0000', + lineWidth: 2, + lineInterpolation: LineInterpolation.Linear, + lineStyle: { + fill: 'dash', + dash: [1, 2], + }, + barAlignment: BarAlignment.Before, + fillColor: '#ff0000', + fillOpacity: 0.1, + showPoints: VisibilityMode.Always, + stacking: { + group: 'A', + mode: StackingMode.Normal, + }, + }, + }; + + const f3Config: FieldConfig = { + displayName: 'Metric 3', + decimals: 2, + color: { + mode: FieldColorModeId.Fixed, + }, + custom: { + drawStyle: GraphDrawStyle.Line, + gradientMode: GraphGradientMode.Opacity, + lineColor: '#ff0000', + lineWidth: 2, + lineInterpolation: LineInterpolation.Linear, + lineStyle: { + fill: 'dash', + dash: [1, 2], + }, + spanNulls: false, + fillColor: '#ff0000', + fillOpacity: 0.1, + showPoints: VisibilityMode.Always, + stacking: { + group: 'B', + mode: StackingMode.Normal, + }, + }, + }; + const f4Config: FieldConfig = { + displayName: 'Metric 4', + decimals: 2, + color: { + mode: FieldColorModeId.Fixed, + }, + custom: { + drawStyle: GraphDrawStyle.Bars, + gradientMode: GraphGradientMode.Hue, + lineColor: '#ff0000', + lineWidth: 2, + lineInterpolation: LineInterpolation.Linear, + lineStyle: { + fill: 'dash', + dash: [1, 2], + }, + barAlignment: BarAlignment.Before, + fillColor: '#ff0000', + fillOpacity: 0.1, + showPoints: VisibilityMode.Always, + stacking: { + group: 'B', + mode: StackingMode.Normal, + }, + }, + }; + const f5Config: FieldConfig = { + displayName: 'Metric 4', + decimals: 2, + color: { + mode: FieldColorModeId.Fixed, + }, + custom: { + drawStyle: GraphDrawStyle.Bars, + gradientMode: GraphGradientMode.Hue, + lineColor: '#ff0000', + lineWidth: 2, + lineInterpolation: LineInterpolation.Linear, + lineStyle: { + fill: 'dash', + dash: [1, 2], + }, + barAlignment: BarAlignment.Before, + fillColor: '#ff0000', + fillOpacity: 0.1, + showPoints: VisibilityMode.Always, + stacking: { + group: 'B', + mode: StackingMode.None, + }, + }, + }; + + df1.addField({ + name: 'metric1', + type: FieldType.number, + config: f1Config, + }); + + df2.addField({ + name: 'metric2', + type: FieldType.number, + config: f2Config, + }); + df2.addField({ + name: 'metric3', + type: FieldType.number, + config: f3Config, + }); + df2.addField({ + name: 'metric4', + type: FieldType.number, + config: f4Config, + }); + df2.addField({ + name: 'metric5', + type: FieldType.number, + config: f5Config, + }); + + return preparePlotFrame([df1, df2], { + x: fieldMatchers.get(FieldMatcherID.firstTimeField).get({}), + y: fieldMatchers.get(FieldMatcherID.numeric).get({}), + }); +} + +jest.mock('@grafana/data', () => ({ + ...jest.requireActual('@grafana/data'), + DefaultTimeZone: 'utc', +})); + +describe('GraphNG utils', () => { + test('preparePlotConfigBuilder', () => { + const frame = mockDataFrame(); + const result = preparePlotConfigBuilder({ + frame: frame!, + theme: createTheme(), + timeZones: [DefaultTimeZone], + getTimeRange: getDefaultTimeRange, + eventBus: new EventBusSrv(), + sync: () => DashboardCursorSync.Tooltip, + allFrames: [frame!], + }).getConfig(); + expect(result).toMatchSnapshot(); + }); + + test('preparePlotFrame appends min bar spaced nulls when > 1 bar series', () => { + const df1: DataFrame = { + name: 'A', + length: 5, + fields: [ + { + name: 'time', + type: FieldType.time, + config: {}, + values: [1, 2, 4, 6, 100], // should find smallest delta === 1 from here + }, + { + name: 'value', + type: FieldType.number, + config: { + custom: { + drawStyle: GraphDrawStyle.Bars, + }, + }, + values: [1, 1, 1, 1, 1], + }, + ], + }; + + const df2: DataFrame = { + name: 'B', + length: 5, + fields: [ + { + name: 'time', + type: FieldType.time, + config: {}, + values: [30, 40, 50, 90, 100], // should be appended with two smallest-delta increments + }, + { + name: 'value', + type: FieldType.number, + config: { + custom: { + drawStyle: GraphDrawStyle.Bars, + }, + }, + values: [2, 2, 2, 2, 2], // bar series should be appended with nulls + }, + { + name: 'value', + type: FieldType.number, + config: { + custom: { + drawStyle: GraphDrawStyle.Line, + }, + }, + values: [3, 3, 3, 3, 3], // line series should be appended with undefineds + }, + ], + }; + + const df3: DataFrame = { + name: 'C', + length: 2, + fields: [ + { + name: 'time', + type: FieldType.time, + config: {}, + values: [1, 1.1], // should not trip up on smaller deltas of non-bars + }, + { + name: 'value', + type: FieldType.number, + config: { + custom: { + drawStyle: GraphDrawStyle.Line, + }, + }, + values: [4, 4], + }, + { + name: 'value', + type: FieldType.number, + config: { + custom: { + drawStyle: GraphDrawStyle.Bars, + hideFrom: { + viz: true, // should ignore hidden bar series + }, + }, + }, + values: [4, 4], + }, + ], + }; + + let aligndFrame = preparePlotFrame([df1, df2, df3], { + x: fieldMatchers.get(FieldMatcherID.firstTimeField).get({}), + y: fieldMatchers.get(FieldMatcherID.numeric).get({}), + }); + + expect(aligndFrame).toMatchInlineSnapshot(` + { + "fields": [ + { + "config": {}, + "name": "time", + "state": { + "nullThresholdApplied": true, + "origin": { + "fieldIndex": 0, + "frameIndex": 0, + }, + }, + "type": "time", + "values": [ + 1, + 1.1, + 2, + 4, + 6, + 30, + 40, + 50, + 90, + 100, + 101, + 102, + ], + }, + { + "config": { + "custom": { + "drawStyle": "bars", + "spanNulls": -1, + }, + }, + "labels": { + "name": "A", + }, + "name": "value", + "state": { + "origin": { + "fieldIndex": 1, + "frameIndex": 0, + }, + }, + "type": "number", + "values": [ + 1, + undefined, + 1, + 1, + 1, + undefined, + undefined, + undefined, + undefined, + 1, + null, + null, + ], + }, + { + "config": { + "custom": { + "drawStyle": "bars", + "spanNulls": -1, + }, + }, + "labels": { + "name": "B", + }, + "name": "value", + "state": { + "origin": { + "fieldIndex": 1, + "frameIndex": 1, + }, + }, + "type": "number", + "values": [ + undefined, + undefined, + undefined, + undefined, + undefined, + 2, + 2, + 2, + 2, + 2, + null, + null, + ], + }, + { + "config": { + "custom": { + "drawStyle": "line", + }, + }, + "labels": { + "name": "B", + }, + "name": "value", + "state": { + "origin": { + "fieldIndex": 2, + "frameIndex": 1, + }, + }, + "type": "number", + "values": [ + undefined, + undefined, + undefined, + undefined, + undefined, + 3, + 3, + 3, + 3, + 3, + undefined, + undefined, + ], + }, + { + "config": { + "custom": { + "drawStyle": "line", + }, + }, + "labels": { + "name": "C", + }, + "name": "value", + "state": { + "origin": { + "fieldIndex": 1, + "frameIndex": 2, + }, + }, + "type": "number", + "values": [ + 4, + 4, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + ], + }, + { + "config": { + "custom": { + "drawStyle": "bars", + "hideFrom": { + "viz": true, + }, + }, + }, + "labels": { + "name": "C", + }, + "name": "value", + "state": { + "origin": { + "fieldIndex": 2, + "frameIndex": 2, + }, + }, + "type": "number", + "values": [ + 4, + 4, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + ], + }, + ], + "length": 12, + } + `); + }); +}); diff --git a/public/app/core/components/GraphNG/utils.ts b/public/app/core/components/GraphNG/utils.ts new file mode 100644 index 0000000000000..030ea9722dbb2 --- /dev/null +++ b/public/app/core/components/GraphNG/utils.ts @@ -0,0 +1,140 @@ +import { DataFrame, Field, FieldType, outerJoinDataFrames, TimeRange } from '@grafana/data'; +import { applyNullInsertThreshold } from '@grafana/data/src/transformations/transformers/nulls/nullInsertThreshold'; +import { nullToUndefThreshold } from '@grafana/data/src/transformations/transformers/nulls/nullToUndefThreshold'; +import { GraphDrawStyle } from '@grafana/schema'; + +import { XYFieldMatchers } from './types'; + +function isVisibleBarField(f: Field) { + return ( + f.type === FieldType.number && f.config.custom?.drawStyle === GraphDrawStyle.Bars && !f.config.custom?.hideFrom?.viz + ); +} + +export function getRefField(frame: DataFrame, refFieldName?: string | null) { + return frame.fields.find((field) => { + // note: getFieldDisplayName() would require full DF[] + return refFieldName != null ? field.name === refFieldName : field.type === FieldType.time; + }); +} + +// will mutate the DataFrame's fields' values +function applySpanNullsThresholds(frame: DataFrame, refFieldName?: string | null) { + const refField = getRefField(frame, refFieldName); + + let refValues = refField?.values; + + for (let i = 0; i < frame.fields.length; i++) { + let field = frame.fields[i]; + + if (field === refField || isVisibleBarField(field)) { + continue; + } + + let spanNulls = field.config.custom?.spanNulls; + + if (typeof spanNulls === 'number') { + if (spanNulls !== -1 && refValues) { + field.values = nullToUndefThreshold(refValues, field.values, spanNulls); + } + } + } + + return frame; +} + +export function preparePlotFrame(frames: DataFrame[], dimFields: XYFieldMatchers, timeRange?: TimeRange | null) { + let xField: Field; + loop: for (let frame of frames) { + for (let field of frame.fields) { + if (dimFields.x(field, frame, frames)) { + xField = field; + break loop; + } + } + } + + // apply null insertions at interval + frames = frames.map((frame) => { + if (!xField?.state?.nullThresholdApplied) { + return applyNullInsertThreshold({ + frame, + refFieldName: xField.name, + refFieldPseudoMin: timeRange?.from.valueOf(), + refFieldPseudoMax: timeRange?.to.valueOf(), + }); + } else { + return frame; + } + }); + + let numBarSeries = 0; + + frames.forEach((frame) => { + frame.fields.forEach((f) => { + if (isVisibleBarField(f)) { + // prevent minesweeper-expansion of nulls (gaps) when joining bars + // since bar width is determined from the minimum distance between non-undefined values + // (this strategy will still retain any original pre-join nulls, though) + f.config.custom = { + ...f.config.custom, + spanNulls: -1, + }; + + numBarSeries++; + } + }); + }); + + // to make bar widths of all series uniform (equal to narrowest bar series), find smallest distance between x points + let minXDelta = Infinity; + + if (numBarSeries > 1) { + frames.forEach((frame) => { + if (!frame.fields.some(isVisibleBarField)) { + return; + } + + const xVals = xField.values; + + for (let i = 0; i < xVals.length; i++) { + if (i > 0) { + minXDelta = Math.min(minXDelta, xVals[i] - xVals[i - 1]); + } + } + }); + } + + let alignedFrame = outerJoinDataFrames({ + frames, + joinBy: dimFields.x, + keep: dimFields.y, + keepOriginIndices: true, + }); + + if (alignedFrame) { + alignedFrame = applySpanNullsThresholds(alignedFrame, xField!.name); + + // append 2 null vals at minXDelta to bar series + if (minXDelta !== Infinity) { + alignedFrame.fields.forEach((f, fi) => { + let vals = f.values; + + if (fi === 0) { + let lastVal = vals[vals.length - 1]; + vals.push(lastVal + minXDelta, lastVal + 2 * minXDelta); + } else if (isVisibleBarField(f)) { + vals.push(null, null); + } else { + vals.push(undefined, undefined); + } + }); + + alignedFrame.length += 2; + } + + return alignedFrame; + } + + return null; +} diff --git a/public/app/core/components/TimeSeries/TimeSeries.tsx b/public/app/core/components/TimeSeries/TimeSeries.tsx new file mode 100644 index 0000000000000..4441bca98f37e --- /dev/null +++ b/public/app/core/components/TimeSeries/TimeSeries.tsx @@ -0,0 +1,63 @@ +import React, { Component } from 'react'; + +import { DataFrame, TimeRange } from '@grafana/data'; +import { PanelContextRoot } from '@grafana/ui/src/components/PanelChrome/PanelContext'; +import { hasVisibleLegendSeries, PlotLegend } from '@grafana/ui/src/components/uPlot/PlotLegend'; +import { UPlotConfigBuilder } from '@grafana/ui/src/components/uPlot/config/UPlotConfigBuilder'; +import { withTheme2 } from '@grafana/ui/src/themes/ThemeContext'; + +import { GraphNG, GraphNGProps, PropDiffFn } from '../GraphNG/GraphNG'; + +import { preparePlotConfigBuilder } from './utils'; + +const propsToDiff: Array = ['legend', 'options', 'theme']; + +type TimeSeriesProps = Omit; + +export class UnthemedTimeSeries extends Component { + static contextType = PanelContextRoot; + declare context: React.ContextType; + + prepConfig = (alignedFrame: DataFrame, allFrames: DataFrame[], getTimeRange: () => TimeRange) => { + const { eventBus, eventsScope, sync } = this.context; + const { theme, timeZone, renderers, tweakAxis, tweakScale } = this.props; + + return preparePlotConfigBuilder({ + frame: alignedFrame, + theme, + timeZones: Array.isArray(timeZone) ? timeZone : [timeZone], + getTimeRange, + eventBus, + sync, + allFrames, + renderers, + tweakScale, + tweakAxis, + eventsScope, + }); + }; + + renderLegend = (config: UPlotConfigBuilder) => { + const { legend, frames } = this.props; + + if (!config || (legend && !legend.showLegend) || !hasVisibleLegendSeries(config, frames)) { + return null; + } + + return ; + }; + + render() { + return ( + + ); + } +} + +export const TimeSeries = withTheme2(UnthemedTimeSeries); +TimeSeries.displayName = 'TimeSeries'; diff --git a/public/app/core/components/TimeSeries/utils.test.ts b/public/app/core/components/TimeSeries/utils.test.ts new file mode 100644 index 0000000000000..583358c7c4c7f --- /dev/null +++ b/public/app/core/components/TimeSeries/utils.test.ts @@ -0,0 +1,274 @@ +import { EventBus, FieldType } from '@grafana/data'; +import { getTheme } from '@grafana/ui'; + +import { preparePlotConfigBuilder } from './utils'; + +describe('when fill below to option is used', () => { + let eventBus: EventBus; + // eslint-disable-next-line + let renderers: any[]; + // eslint-disable-next-line + let tests: any; + + beforeEach(() => { + eventBus = { + publish: jest.fn(), + getStream: jest.fn(), + subscribe: jest.fn(), + removeAllListeners: jest.fn(), + newScopedBus: jest.fn(), + }; + renderers = []; + + tests = [ + { + alignedFrame: { + fields: [ + { + config: {}, + values: [1667406900000, 1667407170000, 1667407185000], + name: 'Time', + state: { multipleFrames: true, displayName: 'Time', origin: { fieldIndex: 0, frameIndex: 0 } }, + type: FieldType.time, + }, + { + config: { displayNameFromDS: 'Test1', custom: { fillBelowTo: 'Test2' }, min: 0, max: 100 }, + values: [1, 2, 3], + name: 'Value', + state: { multipleFrames: true, displayName: 'Test1', origin: { fieldIndex: 1, frameIndex: 0 } }, + type: FieldType.number, + }, + { + config: { displayNameFromDS: 'Test2', min: 0, max: 100 }, + values: [4, 5, 6], + name: 'Value', + state: { multipleFrames: true, displayName: 'Test2', origin: { fieldIndex: 1, frameIndex: 1 } }, + type: FieldType.number, + }, + ], + length: 3, + }, + allFrames: [ + { + name: 'Test1', + refId: 'A', + fields: [ + { + config: {}, + values: [1667406900000, 1667407170000, 1667407185000], + name: 'Time', + state: { multipleFrames: true, displayName: 'Time', origin: { fieldIndex: 0, frameIndex: 0 } }, + type: FieldType.time, + }, + { + config: { displayNameFromDS: 'Test1', custom: { fillBelowTo: 'Test2' }, min: 0, max: 100 }, + values: [1, 2, 3], + name: 'Value', + state: { multipleFrames: true, displayName: 'Test1', origin: { fieldIndex: 1, frameIndex: 0 } }, + type: FieldType.number, + }, + ], + length: 2, + }, + { + name: 'Test2', + refId: 'B', + fields: [ + { + config: {}, + values: [1667406900000, 1667407170000, 1667407185000], + name: 'Time', + state: { multipleFrames: true, displayName: 'Time', origin: { fieldIndex: 0, frameIndex: 1 } }, + type: FieldType.time, + }, + { + config: { displayNameFromDS: 'Test2', min: 0, max: 100 }, + values: [1, 2, 3], + name: 'Value', + state: { multipleFrames: true, displayName: 'Test2', origin: { fieldIndex: 1, frameIndex: 1 } }, + type: FieldType.number, + }, + ], + length: 2, + }, + ], + expectedResult: 1, + }, + { + alignedFrame: { + fields: [ + { + config: {}, + values: [1667406900000, 1667407170000, 1667407185000], + name: 'time', + state: { multipleFrames: true, displayName: 'time', origin: { fieldIndex: 0, frameIndex: 0 } }, + type: FieldType.time, + }, + { + config: { custom: { fillBelowTo: 'below_value1' } }, + values: [1, 2, 3], + name: 'value1', + state: { multipleFrames: true, displayName: 'value1', origin: { fieldIndex: 1, frameIndex: 0 } }, + type: FieldType.number, + }, + { + config: { custom: { fillBelowTo: 'below_value2' } }, + values: [4, 5, 6], + name: 'value2', + state: { multipleFrames: true, displayName: 'value2', origin: { fieldIndex: 2, frameIndex: 0 } }, + type: FieldType.number, + }, + { + config: {}, + values: [4, 5, 6], + name: 'below_value1', + state: { multipleFrames: true, displayName: 'below_value1', origin: { fieldIndex: 1, frameIndex: 1 } }, + type: FieldType.number, + }, + { + config: {}, + values: [4, 5, 6], + name: 'below_value2', + state: { multipleFrames: true, displayName: 'below_value2', origin: { fieldIndex: 2, frameIndex: 1 } }, + type: FieldType.number, + }, + ], + length: 5, + }, + allFrames: [ + { + refId: 'A', + fields: [ + { + config: {}, + values: [1667406900000, 1667407170000, 1667407185000], + name: 'time', + state: { multipleFrames: true, displayName: 'time', origin: { fieldIndex: 0, frameIndex: 0 } }, + type: FieldType.time, + }, + { + config: { custom: { fillBelowTo: 'below_value1' } }, + values: [1, 2, 3], + name: 'value1', + state: { multipleFrames: true, displayName: 'value1', origin: { fieldIndex: 1, frameIndex: 0 } }, + type: FieldType.number, + }, + { + config: { custom: { fillBelowTo: 'below_value2' } }, + values: [4, 5, 6], + name: 'value2', + state: { multipleFrames: true, displayName: 'value2', origin: { fieldIndex: 2, frameIndex: 0 } }, + type: FieldType.number, + }, + ], + length: 3, + }, + { + refId: 'B', + fields: [ + { + config: {}, + values: [1667406900000, 1667407170000, 1667407185000], + name: 'time', + state: { multipleFrames: true, displayName: 'time', origin: { fieldIndex: 0, frameIndex: 1 } }, + type: FieldType.time, + }, + { + config: {}, + values: [4, 5, 6], + name: 'below_value1', + state: { multipleFrames: true, displayName: 'below_value1', origin: { fieldIndex: 1, frameIndex: 1 } }, + type: FieldType.number, + }, + { + config: {}, + values: [4, 5, 6], + name: 'below_value2', + state: { multipleFrames: true, displayName: 'below_value2', origin: { fieldIndex: 2, frameIndex: 1 } }, + type: FieldType.number, + }, + ], + length: 3, + }, + ], + expectedResult: 2, + }, + ]; + }); + + it('should verify if fill below to is set then builder bands are set', () => { + for (const test of tests) { + const builder = preparePlotConfigBuilder({ + frame: test.alignedFrame, + //@ts-ignore + theme: getTheme(), + timeZones: ['browser'], + getTimeRange: jest.fn(), + eventBus, + sync: jest.fn(), + allFrames: test.allFrames, + renderers, + }); + + //@ts-ignore + expect(builder.bands.length).toBe(test.expectedResult); + } + }); + + it('should verify if fill below to is not set then builder bands are empty', () => { + tests[0].alignedFrame.fields[1].config.custom.fillBelowTo = undefined; + tests[0].allFrames[0].fields[1].config.custom.fillBelowTo = undefined; + tests[1].alignedFrame.fields[1].config.custom.fillBelowTo = undefined; + tests[1].alignedFrame.fields[2].config.custom.fillBelowTo = undefined; + tests[1].allFrames[0].fields[1].config.custom.fillBelowTo = undefined; + tests[1].allFrames[0].fields[2].config.custom.fillBelowTo = undefined; + tests[0].expectedResult = 0; + tests[1].expectedResult = 0; + + for (const test of tests) { + const builder = preparePlotConfigBuilder({ + frame: test.alignedFrame, + //@ts-ignore + theme: getTheme(), + timeZones: ['browser'], + getTimeRange: jest.fn(), + eventBus, + sync: jest.fn(), + allFrames: test.allFrames, + renderers, + }); + + //@ts-ignore + expect(builder.bands.length).toBe(test.expectedResult); + } + }); + + it('should verify if fill below to is set and field name is overriden then builder bands are set', () => { + tests[0].alignedFrame.fields[2].config.displayName = 'newName'; + tests[0].alignedFrame.fields[2].state.displayName = 'newName'; + tests[0].allFrames[1].fields[1].config.displayName = 'newName'; + tests[0].allFrames[1].fields[1].state.displayName = 'newName'; + + tests[1].alignedFrame.fields[3].config.displayName = 'newName'; + tests[1].alignedFrame.fields[3].state.displayName = 'newName'; + tests[1].allFrames[1].fields[1].config.displayName = 'newName'; + tests[1].allFrames[1].fields[1].state.displayName = 'newName'; + + for (const test of tests) { + const builder = preparePlotConfigBuilder({ + frame: test.alignedFrame, + //@ts-ignore + theme: getTheme(), + timeZones: ['browser'], + getTimeRange: jest.fn(), + eventBus, + sync: jest.fn(), + allFrames: test.allFrames, + renderers, + }); + + //@ts-ignore + expect(builder.bands.length).toBe(test.expectedResult); + } + }); +}); diff --git a/public/app/core/components/TimeSeries/utils.ts b/public/app/core/components/TimeSeries/utils.ts new file mode 100644 index 0000000000000..36da06299da16 --- /dev/null +++ b/public/app/core/components/TimeSeries/utils.ts @@ -0,0 +1,669 @@ +import { isNumber } from 'lodash'; +import uPlot from 'uplot'; + +import { + DashboardCursorSync, + DataFrame, + DataHoverClearEvent, + DataHoverEvent, + DataHoverPayload, + FieldConfig, + FieldType, + formattedValueToString, + getFieldColorModeForField, + getFieldSeriesColor, + getFieldDisplayName, + getDisplayProcessor, + FieldColorModeId, + DecimalCount, +} from '@grafana/data'; +// eslint-disable-next-line import/order +import { + AxisPlacement, + GraphDrawStyle, + GraphFieldConfig, + GraphTresholdsStyleMode, + VisibilityMode, + ScaleDirection, + ScaleOrientation, + StackingMode, + GraphTransform, + AxisColorMode, + GraphGradientMode, +} from '@grafana/schema'; + +// unit lookup needed to determine if we want power-of-2 or power-of-10 axis ticks +// see categories.ts is @grafana/data +const IEC_UNITS = new Set([ + 'bytes', + 'bits', + 'kbytes', + 'mbytes', + 'gbytes', + 'tbytes', + 'pbytes', + 'binBps', + 'binbps', + 'KiBs', + 'Kibits', + 'MiBs', + 'Mibits', + 'GiBs', + 'Gibits', + 'TiBs', + 'Tibits', + 'PiBs', + 'Pibits', +]); + +const BIN_INCRS = Array(53); + +for (let i = 0; i < BIN_INCRS.length; i++) { + BIN_INCRS[i] = 2 ** i; +} + +import { UPlotConfigBuilder, UPlotConfigPrepFn } from '@grafana/ui/src/components/uPlot/config/UPlotConfigBuilder'; +import { getScaleGradientFn } from '@grafana/ui/src/components/uPlot/config/gradientFills'; +import { buildScaleKey } from '@grafana/ui/src/components/uPlot/internal'; +import { getStackingGroups, preparePlotData2 } from '@grafana/ui/src/components/uPlot/utils'; + +const defaultFormatter = (v: any, decimals: DecimalCount = 1) => (v == null ? '-' : v.toFixed(decimals)); + +const defaultConfig: GraphFieldConfig = { + drawStyle: GraphDrawStyle.Line, + showPoints: VisibilityMode.Auto, + axisPlacement: AxisPlacement.Auto, +}; + +export const preparePlotConfigBuilder: UPlotConfigPrepFn<{ + sync?: () => DashboardCursorSync; +}> = ({ + frame, + theme, + timeZones, + getTimeRange, + eventBus, + sync, + allFrames, + renderers, + tweakScale = (opts) => opts, + tweakAxis = (opts) => opts, + eventsScope = '__global_', +}) => { + const builder = new UPlotConfigBuilder(timeZones[0]); + + let alignedFrame: DataFrame; + + builder.setPrepData((frames) => { + // cache alignedFrame + alignedFrame = frames[0]; + + return preparePlotData2(frames[0], builder.getStackingGroups()); + }); + + // X is the first field in the aligned frame + const xField = frame.fields[0]; + if (!xField) { + return builder; // empty frame with no options + } + + const xScaleKey = 'x'; + let xScaleUnit = '_x'; + let yScaleKey = ''; + + const xFieldAxisPlacement = + xField.config.custom?.axisPlacement !== AxisPlacement.Hidden ? AxisPlacement.Bottom : AxisPlacement.Hidden; + const xFieldAxisShow = xField.config.custom?.axisPlacement !== AxisPlacement.Hidden; + + if (xField.type === FieldType.time) { + xScaleUnit = 'time'; + builder.addScale({ + scaleKey: xScaleKey, + orientation: ScaleOrientation.Horizontal, + direction: ScaleDirection.Right, + isTime: true, + range: () => { + const r = getTimeRange(); + return [r.from.valueOf(), r.to.valueOf()]; + }, + }); + + // filters first 2 ticks to make space for timezone labels + const filterTicks: uPlot.Axis.Filter | undefined = + timeZones.length > 1 + ? (u, splits) => { + return splits.map((v, i) => (i < 2 ? null : v)); + } + : undefined; + + for (let i = 0; i < timeZones.length; i++) { + const timeZone = timeZones[i]; + builder.addAxis({ + scaleKey: xScaleKey, + isTime: true, + placement: xFieldAxisPlacement, + show: xFieldAxisShow, + label: xField.config.custom?.axisLabel, + timeZone, + theme, + grid: { show: i === 0 && xField.config.custom?.axisGridShow }, + filter: filterTicks, + }); + } + + // render timezone labels + if (timeZones.length > 1) { + builder.addHook('drawAxes', (u: uPlot) => { + u.ctx.save(); + + u.ctx.fillStyle = theme.colors.text.primary; + u.ctx.textAlign = 'left'; + u.ctx.textBaseline = 'bottom'; + + let i = 0; + u.axes.forEach((a) => { + if (a.side === 2) { + //@ts-ignore + let cssBaseline: number = a._pos + a._size; + u.ctx.fillText(timeZones[i], u.bbox.left, cssBaseline * uPlot.pxRatio); + i++; + } + }); + + u.ctx.restore(); + }); + } + } else { + // Not time! + if (xField.config.unit) { + xScaleUnit = xField.config.unit; + } + + builder.addScale({ + scaleKey: xScaleKey, + orientation: ScaleOrientation.Horizontal, + direction: ScaleDirection.Right, + range: (u, dataMin, dataMax) => [xField.config.min ?? dataMin, xField.config.max ?? dataMax], + }); + + builder.addAxis({ + scaleKey: xScaleKey, + placement: xFieldAxisPlacement, + show: xFieldAxisShow, + label: xField.config.custom?.axisLabel, + theme, + grid: { show: xField.config.custom?.axisGridShow }, + formatValue: (v, decimals) => formattedValueToString(xField.display!(v, decimals)), + }); + } + + let customRenderedFields = + renderers?.flatMap((r) => Object.values(r.fieldMap).filter((name) => r.indicesOnly.indexOf(name) === -1)) ?? []; + + let indexByName: Map | undefined; + + for (let i = 1; i < frame.fields.length; i++) { + const field = frame.fields[i]; + + const config: FieldConfig = { + ...field.config, + custom: { + ...defaultConfig, + ...field.config.custom, + }, + }; + + const customConfig: GraphFieldConfig = config.custom!; + + if (field === xField || (field.type !== FieldType.number && field.type !== FieldType.enum)) { + continue; + } + + let fmt = field.display ?? defaultFormatter; + if (field.config.custom?.stacking?.mode === StackingMode.Percent) { + fmt = getDisplayProcessor({ + field: { + ...field, + config: { + ...field.config, + unit: 'percentunit', + }, + }, + theme, + }); + } + const scaleKey = buildScaleKey(config, field.type); + const colorMode = getFieldColorModeForField(field); + const scaleColor = getFieldSeriesColor(field, theme); + const seriesColor = scaleColor.color; + + // The builder will manage unique scaleKeys and combine where appropriate + builder.addScale( + tweakScale( + { + scaleKey, + orientation: ScaleOrientation.Vertical, + direction: ScaleDirection.Up, + distribution: customConfig.scaleDistribution?.type, + log: customConfig.scaleDistribution?.log, + linearThreshold: customConfig.scaleDistribution?.linearThreshold, + min: field.config.min, + max: field.config.max, + softMin: customConfig.axisSoftMin, + softMax: customConfig.axisSoftMax, + centeredZero: customConfig.axisCenteredZero, + range: + customConfig.stacking?.mode === StackingMode.Percent + ? (u: uPlot, dataMin: number, dataMax: number) => { + dataMin = dataMin < 0 ? -1 : 0; + dataMax = dataMax > 0 ? 1 : 0; + return [dataMin, dataMax]; + } + : field.type === FieldType.enum + ? (u: uPlot, dataMin: number, dataMax: number) => { + // this is the exhaustive enum (stable) + let len = field.config.type!.enum!.text!.length; + + return [-1, len]; + + // these are only values that are present + // return [dataMin - 1, dataMax + 1] + } + : undefined, + decimals: field.config.decimals, + }, + field + ) + ); + + if (!yScaleKey) { + yScaleKey = scaleKey; + } + + if (customConfig.axisPlacement !== AxisPlacement.Hidden) { + let axisColor: uPlot.Axis.Stroke | undefined; + + if (customConfig.axisColorMode === AxisColorMode.Series) { + if ( + colorMode.isByValue && + field.config.custom?.gradientMode === GraphGradientMode.Scheme && + colorMode.id === FieldColorModeId.Thresholds + ) { + axisColor = getScaleGradientFn(1, theme, colorMode, field.config.thresholds); + } else { + axisColor = seriesColor; + } + } + + const axisDisplayOptions = { + border: { + show: customConfig.axisBorderShow || false, + width: 1 / devicePixelRatio, + stroke: axisColor || theme.colors.text.primary, + }, + ticks: { + show: customConfig.axisBorderShow || false, + stroke: axisColor || theme.colors.text.primary, + }, + color: axisColor || theme.colors.text.primary, + }; + + let incrs: uPlot.Axis.Incrs | undefined; + + // TODO: these will be dynamic with frame updates, so need to accept getYTickLabels() + let values: uPlot.Axis.Values | undefined; + let splits: uPlot.Axis.Splits | undefined; + + if (IEC_UNITS.has(config.unit!)) { + incrs = BIN_INCRS; + } else if (field.type === FieldType.enum) { + let text = field.config.type!.enum!.text!; + splits = text.map((v: string, i: number) => i); + values = text; + } + + builder.addAxis( + tweakAxis( + { + scaleKey, + label: customConfig.axisLabel, + size: customConfig.axisWidth, + placement: customConfig.axisPlacement ?? AxisPlacement.Auto, + formatValue: (v, decimals) => formattedValueToString(fmt(v, decimals)), + theme, + grid: { show: customConfig.axisGridShow }, + decimals: field.config.decimals, + distr: customConfig.scaleDistribution?.type, + splits, + values, + incrs, + ...axisDisplayOptions, + }, + field + ) + ); + } + + const showPoints = + customConfig.drawStyle === GraphDrawStyle.Points ? VisibilityMode.Always : customConfig.showPoints; + + let pointsFilter: uPlot.Series.Points.Filter = () => null; + + if (customConfig.spanNulls !== true) { + pointsFilter = (u, seriesIdx, show, gaps) => { + let filtered = []; + + let series = u.series[seriesIdx]; + + if (!show && gaps && gaps.length) { + const [firstIdx, lastIdx] = series.idxs!; + const xData = u.data[0]; + const yData = u.data[seriesIdx]; + const firstPos = Math.round(u.valToPos(xData[firstIdx], 'x', true)); + const lastPos = Math.round(u.valToPos(xData[lastIdx], 'x', true)); + + if (gaps[0][0] === firstPos) { + filtered.push(firstIdx); + } + + // show single points between consecutive gaps that share end/start + for (let i = 0; i < gaps.length; i++) { + let thisGap = gaps[i]; + let nextGap = gaps[i + 1]; + + if (nextGap && thisGap[1] === nextGap[0]) { + // approx when data density is > 1pt/px, since gap start/end pixels are rounded + let approxIdx = u.posToIdx(thisGap[1], true); + + if (yData[approxIdx] == null) { + // scan left/right alternating to find closest index with non-null value + for (let j = 1; j < 100; j++) { + if (yData[approxIdx + j] != null) { + approxIdx += j; + break; + } + if (yData[approxIdx - j] != null) { + approxIdx -= j; + break; + } + } + } + + filtered.push(approxIdx); + } + } + + if (gaps[gaps.length - 1][1] === lastPos) { + filtered.push(lastIdx); + } + } + + return filtered.length ? filtered : null; + }; + } + + let { fillOpacity } = customConfig; + + let pathBuilder: uPlot.Series.PathBuilder | null = null; + let pointsBuilder: uPlot.Series.Points.Show | null = null; + + if (field.state?.origin) { + if (!indexByName) { + indexByName = getNamesToFieldIndex(frame, allFrames); + } + + const originFrame = allFrames[field.state.origin.frameIndex]; + const originField = originFrame?.fields[field.state.origin.fieldIndex]; + + const dispName = getFieldDisplayName(originField ?? field, originFrame, allFrames); + + // disable default renderers + if (customRenderedFields.indexOf(dispName) >= 0) { + pathBuilder = () => null; + pointsBuilder = () => undefined; + } else if (customConfig.transform === GraphTransform.Constant) { + // patch some monkeys! + const defaultBuilder = uPlot.paths!.linear!(); + + pathBuilder = (u, seriesIdx) => { + //eslint-disable-next-line + const _data: any[] = (u as any)._data; // uplot.AlignedData not exposed in types + + // the data we want the line renderer to pull is x at each plot edge with paired flat y values + + const r = getTimeRange(); + let xData = [r.from.valueOf(), r.to.valueOf()]; + let firstY = _data[seriesIdx].find((v: number | null | undefined) => v != null); + let yData = [firstY, firstY]; + let fauxData = _data.slice(); + fauxData[0] = xData; + fauxData[seriesIdx] = yData; + + //eslint-disable-next-line + return defaultBuilder( + { + ...u, + _data: fauxData, + } as any, + seriesIdx, + 0, + 1 + ); + }; + } + + if (customConfig.fillBelowTo) { + const fillBelowToField = frame.fields.find( + (f) => + customConfig.fillBelowTo === f.name || + customConfig.fillBelowTo === f.config?.displayNameFromDS || + customConfig.fillBelowTo === getFieldDisplayName(f, frame, allFrames) + ); + + const fillBelowDispName = fillBelowToField + ? getFieldDisplayName(fillBelowToField, frame, allFrames) + : customConfig.fillBelowTo; + + const t = indexByName.get(dispName); + const b = indexByName.get(fillBelowDispName); + if (isNumber(b) && isNumber(t)) { + builder.addBand({ + series: [t, b], + fill: undefined, // using null will have the band use fill options from `t` + }); + + if (!fillOpacity) { + fillOpacity = 35; // default from flot + } + } else { + fillOpacity = 0; + } + } + } + + let dynamicSeriesColor: ((seriesIdx: number) => string | undefined) | undefined = undefined; + + if (colorMode.id === FieldColorModeId.Thresholds) { + dynamicSeriesColor = (seriesIdx) => getFieldSeriesColor(alignedFrame.fields[seriesIdx], theme).color; + } + + builder.addSeries({ + pathBuilder, + pointsBuilder, + scaleKey, + showPoints, + pointsFilter, + colorMode, + fillOpacity, + theme, + dynamicSeriesColor, + drawStyle: customConfig.drawStyle!, + lineColor: customConfig.lineColor ?? seriesColor, + lineWidth: customConfig.lineWidth, + lineInterpolation: customConfig.lineInterpolation, + lineStyle: customConfig.lineStyle, + barAlignment: customConfig.barAlignment, + barWidthFactor: customConfig.barWidthFactor, + barMaxWidth: customConfig.barMaxWidth, + pointSize: customConfig.pointSize, + spanNulls: customConfig.spanNulls || false, + show: !customConfig.hideFrom?.viz, + gradientMode: customConfig.gradientMode, + thresholds: config.thresholds, + hardMin: field.config.min, + hardMax: field.config.max, + softMin: customConfig.axisSoftMin, + softMax: customConfig.axisSoftMax, + // The following properties are not used in the uPlot config, but are utilized as transport for legend config + dataFrameFieldIndex: field.state?.origin, + }); + + // Render thresholds in graph + if (customConfig.thresholdsStyle && config.thresholds) { + const thresholdDisplay = customConfig.thresholdsStyle.mode ?? GraphTresholdsStyleMode.Off; + if (thresholdDisplay !== GraphTresholdsStyleMode.Off) { + builder.addThresholds({ + config: customConfig.thresholdsStyle, + thresholds: config.thresholds, + scaleKey, + theme, + hardMin: field.config.min, + hardMax: field.config.max, + softMin: customConfig.axisSoftMin, + softMax: customConfig.axisSoftMax, + }); + } + } + } + + let stackingGroups = getStackingGroups(frame); + + builder.setStackingGroups(stackingGroups); + + // hook up custom/composite renderers + renderers?.forEach((r) => { + if (!indexByName) { + indexByName = getNamesToFieldIndex(frame, allFrames); + } + let fieldIndices: Record = {}; + + for (let key in r.fieldMap) { + let dispName = r.fieldMap[key]; + fieldIndices[key] = indexByName.get(dispName)!; + } + + r.init(builder, fieldIndices); + }); + + builder.scaleKeys = [xScaleKey, yScaleKey]; + + // if hovered value is null, how far we may scan left/right to hover nearest non-null + const hoverProximityPx = 15; + + let cursor: Partial = { + // this scans left and right from cursor position to find nearest data index with value != null + // TODO: do we want to only scan past undefined values, but halt at explicit null values? + dataIdx: (self, seriesIdx, hoveredIdx, cursorXVal) => { + let seriesData = self.data[seriesIdx]; + + if (seriesData[hoveredIdx] == null) { + let nonNullLft = null, + nonNullRgt = null, + i; + + i = hoveredIdx; + while (nonNullLft == null && i-- > 0) { + if (seriesData[i] != null) { + nonNullLft = i; + } + } + + i = hoveredIdx; + while (nonNullRgt == null && i++ < seriesData.length) { + if (seriesData[i] != null) { + nonNullRgt = i; + } + } + + let xVals = self.data[0]; + + let curPos = self.valToPos(cursorXVal, 'x'); + let rgtPos = nonNullRgt == null ? Infinity : self.valToPos(xVals[nonNullRgt], 'x'); + let lftPos = nonNullLft == null ? -Infinity : self.valToPos(xVals[nonNullLft], 'x'); + + let lftDelta = curPos - lftPos; + let rgtDelta = rgtPos - curPos; + + if (lftDelta <= rgtDelta) { + if (lftDelta <= hoverProximityPx) { + hoveredIdx = nonNullLft!; + } + } else { + if (rgtDelta <= hoverProximityPx) { + hoveredIdx = nonNullRgt!; + } + } + } + + return hoveredIdx; + }, + }; + + if (sync && sync() !== DashboardCursorSync.Off) { + const payload: DataHoverPayload = { + point: { + [xScaleKey]: null, + [yScaleKey]: null, + }, + data: frame, + }; + + const hoverEvent = new DataHoverEvent(payload); + cursor.sync = { + key: eventsScope, + filters: { + pub: (type: string, src: uPlot, x: number, y: number, w: number, h: number, dataIdx: number) => { + if (sync && sync() === DashboardCursorSync.Off) { + return false; + } + + payload.rowIndex = dataIdx; + if (x < 0 && y < 0) { + payload.point[xScaleUnit] = null; + payload.point[yScaleKey] = null; + eventBus.publish(new DataHoverClearEvent()); + } else { + // convert the points + payload.point[xScaleUnit] = src.posToVal(x, xScaleKey); + payload.point[yScaleKey] = src.posToVal(y, yScaleKey); + payload.point.panelRelY = y > 0 ? y / h : 1; // used by old graph panel to position tooltip + eventBus.publish(hoverEvent); + hoverEvent.payload.down = undefined; + } + return true; + }, + }, + scales: [xScaleKey, yScaleKey], + // match: [() => true, (a, b) => a === b], + }; + } + + builder.setSync(); + builder.setCursor(cursor); + + return builder; +}; + +export function getNamesToFieldIndex(frame: DataFrame, allFrames: DataFrame[]): Map { + const originNames = new Map(); + frame.fields.forEach((field, i) => { + const origin = field.state?.origin; + if (origin) { + const origField = allFrames[origin.frameIndex]?.fields[origin.fieldIndex]; + if (origField) { + originNames.set(getFieldDisplayName(origField, allFrames[origin.frameIndex], allFrames), i); + } + } + }); + return originNames; +} diff --git a/public/app/core/components/TimelineChart/TimelineChart.tsx b/public/app/core/components/TimelineChart/TimelineChart.tsx index ac5e2e627692e..e904530a82e93 100644 --- a/public/app/core/components/TimelineChart/TimelineChart.tsx +++ b/public/app/core/components/TimelineChart/TimelineChart.tsx @@ -2,16 +2,9 @@ import React from 'react'; import { DataFrame, FALLBACK_COLOR, FieldType, TimeRange } from '@grafana/data'; import { VisibilityMode, TimelineValueAlignment } from '@grafana/schema'; -import { - PanelContext, - PanelContextRoot, - GraphNG, - GraphNGProps, - UPlotConfigBuilder, - VizLayout, - VizLegend, - VizLegendItem, -} from '@grafana/ui'; +import { PanelContext, PanelContextRoot, UPlotConfigBuilder, VizLayout, VizLegend, VizLegendItem } from '@grafana/ui'; + +import { GraphNG, GraphNGProps } from '../GraphNG/GraphNG'; import { preparePlotConfigBuilder, TimelineMode } from './utils'; diff --git a/public/app/core/components/TimelineChart/timeline.ts b/public/app/core/components/TimelineChart/timeline.ts index 18ecfdbf33de3..13fdd76fb4d6c 100644 --- a/public/app/core/components/TimelineChart/timeline.ts +++ b/public/app/core/components/TimelineChart/timeline.ts @@ -3,7 +3,7 @@ import uPlot, { Series } from 'uplot'; import { GrafanaTheme2, TimeRange } from '@grafana/data'; import { alpha } from '@grafana/data/src/themes/colorManipulator'; import { VisibilityMode, TimelineValueAlignment } from '@grafana/schema'; -import { FIXED_UNIT } from '@grafana/ui/src/components/GraphNG/GraphNG'; +import { FIXED_UNIT } from '@grafana/ui'; import { distribute, SPACE_BETWEEN } from 'app/plugins/panel/barchart/distribute'; import { pointWithin, Quadtree, Rect } from 'app/plugins/panel/barchart/quadtree'; import { FieldConfig as StateTimeLineFieldConfig } from 'app/plugins/panel/state-timeline/panelcfg.gen'; diff --git a/public/app/core/components/TimelineChart/utils.ts b/public/app/core/components/TimelineChart/utils.ts index 02048cca457d7..a372190209b74 100644 --- a/public/app/core/components/TimelineChart/utils.ts +++ b/public/app/core/components/TimelineChart/utils.ts @@ -23,6 +23,8 @@ import { TimeRange, } from '@grafana/data'; import { maybeSortFrame } from '@grafana/data/src/transformations/transformers/joinDataFrames'; +import { applyNullInsertThreshold } from '@grafana/data/src/transformations/transformers/nulls/nullInsertThreshold'; +import { nullToValue } from '@grafana/data/src/transformations/transformers/nulls/nullToValue'; import { VizLegendOptions, AxisPlacement, @@ -40,8 +42,6 @@ import { UPlotConfigPrepFn, VizLegendItem, } from '@grafana/ui'; -import { applyNullInsertThreshold } from '@grafana/ui/src/components/GraphNG/nullInsertThreshold'; -import { nullToValue } from '@grafana/ui/src/components/GraphNG/nullToValue'; import { PlotTooltipInterpolator } from '@grafana/ui/src/components/uPlot/types'; import { preparePlotData2, getStackingGroups } from '@grafana/ui/src/components/uPlot/utils'; diff --git a/public/app/plugins/panel/barchart/BarChartPanel.tsx b/public/app/plugins/panel/barchart/BarChartPanel.tsx index c3ea72f4b2167..27d7a366e17c7 100644 --- a/public/app/plugins/panel/barchart/BarChartPanel.tsx +++ b/public/app/plugins/panel/barchart/BarChartPanel.tsx @@ -16,8 +16,6 @@ import { PanelDataErrorView } from '@grafana/runtime'; import { SortOrder } from '@grafana/schema'; import { GraphGradientMode, - GraphNG, - GraphNGProps, measureText, PlotLegend, Portal, @@ -31,9 +29,9 @@ import { VizLegend, VizTooltipContainer, } from '@grafana/ui'; -import { PropDiffFn } from '@grafana/ui/src/components/GraphNG/GraphNG'; import { HoverEvent, addTooltipSupport } from '@grafana/ui/src/components/uPlot/config/addTooltipSupport'; import { CloseButton } from 'app/core/components/CloseButton/CloseButton'; +import { GraphNG, GraphNGProps, PropDiffFn } from 'app/core/components/GraphNG/GraphNG'; import { getFieldLegendItem } from 'app/core/components/TimelineChart/utils'; import { DataHoverView } from 'app/features/visualization/data-hover/DataHoverView'; diff --git a/public/app/plugins/panel/candlestick/CandlestickPanel.tsx b/public/app/plugins/panel/candlestick/CandlestickPanel.tsx index e0707a3820f4f..a0396c4a01b39 100644 --- a/public/app/plugins/panel/candlestick/CandlestickPanel.tsx +++ b/public/app/plugins/panel/candlestick/CandlestickPanel.tsx @@ -7,9 +7,10 @@ import uPlot from 'uplot'; import { Field, getDisplayProcessor, getLinksSupplier, PanelProps } from '@grafana/data'; import { PanelDataErrorView } from '@grafana/runtime'; import { TooltipDisplayMode } from '@grafana/schema'; -import { TimeSeries, TooltipPlugin, UPlotConfigBuilder, usePanelContext, useTheme2, ZoomPlugin } from '@grafana/ui'; +import { TooltipPlugin, UPlotConfigBuilder, usePanelContext, useTheme2, ZoomPlugin } from '@grafana/ui'; import { AxisProps } from '@grafana/ui/src/components/uPlot/config/UPlotAxisBuilder'; import { ScaleProps } from '@grafana/ui/src/components/uPlot/config/UPlotScaleBuilder'; +import { TimeSeries } from 'app/core/components/TimeSeries/TimeSeries'; import { config } from 'app/core/config'; import { AnnotationEditorPlugin } from '../timeseries/plugins/AnnotationEditorPlugin'; diff --git a/public/app/plugins/panel/graph/data_processor.ts b/public/app/plugins/panel/graph/data_processor.ts index 8f67e3abb5cb0..21afccf65d3fd 100644 --- a/public/app/plugins/panel/graph/data_processor.ts +++ b/public/app/plugins/panel/graph/data_processor.ts @@ -1,8 +1,8 @@ import { find } from 'lodash'; import { DataFrame, dateTime, Field, FieldType, getFieldDisplayName, getTimeField, TimeRange } from '@grafana/data'; +import { applyNullInsertThreshold } from '@grafana/data/src/transformations/transformers/nulls/nullInsertThreshold'; import { colors } from '@grafana/ui'; -import { applyNullInsertThreshold } from '@grafana/ui/src/components/GraphNG/nullInsertThreshold'; import config from 'app/core/config'; import TimeSeries from 'app/core/time_series2'; diff --git a/public/app/plugins/panel/timeseries/TimeSeriesPanel.tsx b/public/app/plugins/panel/timeseries/TimeSeriesPanel.tsx index 752fc49ba3f27..f9d57f0457850 100644 --- a/public/app/plugins/panel/timeseries/TimeSeriesPanel.tsx +++ b/public/app/plugins/panel/timeseries/TimeSeriesPanel.tsx @@ -3,7 +3,8 @@ import React, { useMemo } from 'react'; import { PanelProps, DataFrameType } from '@grafana/data'; import { PanelDataErrorView } from '@grafana/runtime'; import { TooltipDisplayMode } from '@grafana/schema'; -import { KeyboardPlugin, TimeSeries, TooltipPlugin, usePanelContext, ZoomPlugin } from '@grafana/ui'; +import { KeyboardPlugin, TooltipPlugin, usePanelContext, ZoomPlugin } from '@grafana/ui'; +import { TimeSeries } from 'app/core/components/TimeSeries/TimeSeries'; import { config } from 'app/core/config'; import { Options } from './panelcfg.gen'; diff --git a/public/app/plugins/panel/timeseries/plugins/ExemplarsPlugin.tsx b/public/app/plugins/panel/timeseries/plugins/ExemplarsPlugin.tsx index 321dbfb1cec61..754e052c7b958 100644 --- a/public/app/plugins/panel/timeseries/plugins/ExemplarsPlugin.tsx +++ b/public/app/plugins/panel/timeseries/plugins/ExemplarsPlugin.tsx @@ -9,7 +9,7 @@ import { TIME_SERIES_VALUE_FIELD_NAME, TimeZone, } from '@grafana/data'; -import { EventsCanvas, FIXED_UNIT, UPlotConfigBuilder } from '@grafana/ui'; +import { FIXED_UNIT, EventsCanvas, UPlotConfigBuilder } from '@grafana/ui'; import { ExemplarMarker } from './ExemplarMarker'; diff --git a/public/app/plugins/panel/timeseries/plugins/ThresholdControlsPlugin.tsx b/public/app/plugins/panel/timeseries/plugins/ThresholdControlsPlugin.tsx index 3db8c039e8d9a..a286ce7612f5d 100644 --- a/public/app/plugins/panel/timeseries/plugins/ThresholdControlsPlugin.tsx +++ b/public/app/plugins/panel/timeseries/plugins/ThresholdControlsPlugin.tsx @@ -2,7 +2,8 @@ import React, { useState, useLayoutEffect, useMemo, useRef } from 'react'; import uPlot from 'uplot'; import { FieldConfigSource, ThresholdsConfig, getValueFormat, FieldType } from '@grafana/data'; -import { UPlotConfigBuilder, buildScaleKey } from '@grafana/ui'; +import { UPlotConfigBuilder } from '@grafana/ui'; +import { buildScaleKey } from '@grafana/ui/src/components/uPlot/internal'; import { ThresholdDragHandle } from './ThresholdDragHandle'; diff --git a/public/app/plugins/panel/timeseries/utils.ts b/public/app/plugins/panel/timeseries/utils.ts index 27cb4845a6a55..871cf62b5999b 100644 --- a/public/app/plugins/panel/timeseries/utils.ts +++ b/public/app/plugins/panel/timeseries/utils.ts @@ -13,10 +13,10 @@ import { TimeRange, } from '@grafana/data'; import { convertFieldType } from '@grafana/data/src/transformations/transformers/convertFieldType'; +import { applyNullInsertThreshold } from '@grafana/data/src/transformations/transformers/nulls/nullInsertThreshold'; +import { nullToValue } from '@grafana/data/src/transformations/transformers/nulls/nullToValue'; import { GraphFieldConfig, LineInterpolation } from '@grafana/schema'; -import { applyNullInsertThreshold } from '@grafana/ui/src/components/GraphNG/nullInsertThreshold'; -import { nullToValue } from '@grafana/ui/src/components/GraphNG/nullToValue'; -import { buildScaleKey } from '@grafana/ui/src/components/GraphNG/utils'; +import { buildScaleKey } from '@grafana/ui/src/components/uPlot/internal'; type ScaleKey = string; diff --git a/public/app/plugins/panel/trend/TrendPanel.tsx b/public/app/plugins/panel/trend/TrendPanel.tsx index 371b0679a6521..e5371dca26325 100644 --- a/public/app/plugins/panel/trend/TrendPanel.tsx +++ b/public/app/plugins/panel/trend/TrendPanel.tsx @@ -3,15 +3,10 @@ import React, { useMemo } from 'react'; import { DataFrame, FieldMatcherID, fieldMatchers, FieldType, PanelProps, TimeRange } from '@grafana/data'; import { isLikelyAscendingVector } from '@grafana/data/src/transformations/transformers/joinDataFrames'; import { config, PanelDataErrorView } from '@grafana/runtime'; -import { - KeyboardPlugin, - preparePlotFrame, - TimeSeries, - TooltipDisplayMode, - TooltipPlugin, - usePanelContext, -} from '@grafana/ui'; -import { XYFieldMatchers } from '@grafana/ui/src/components/GraphNG/types'; +import { KeyboardPlugin, TooltipDisplayMode, TooltipPlugin, usePanelContext } from '@grafana/ui'; +import { XYFieldMatchers } from 'app/core/components/GraphNG/types'; +import { preparePlotFrame } from 'app/core/components/GraphNG/utils'; +import { TimeSeries } from 'app/core/components/TimeSeries/TimeSeries'; import { findFieldIndex } from 'app/features/dimensions'; import { prepareGraphableFields, regenerateLinksSupplier } from '../timeseries/utils'; diff --git a/public/app/plugins/panel/xychart/dims.ts b/public/app/plugins/panel/xychart/dims.ts index 30320448330d6..9b2619b64ab3e 100644 --- a/public/app/plugins/panel/xychart/dims.ts +++ b/public/app/plugins/panel/xychart/dims.ts @@ -1,5 +1,5 @@ import { DataFrame, Field, FieldMatcher, FieldType, getFieldDisplayName } from '@grafana/data'; -import { XYFieldMatchers } from '@grafana/ui/src/components/GraphNG/types'; +import { XYFieldMatchers } from 'app/core/components/GraphNG/types'; import { XYDimensionConfig } from './panelcfg.gen'; From 41572238ac6e84057c9caffb11c983ab66a9960f Mon Sep 17 00:00:00 2001 From: Fabrizio <135109076+fabrizio-grafana@users.noreply.github.com> Date: Thu, 2 Nov 2023 10:46:43 +0100 Subject: [PATCH 045/869] Tempo: Fix support for `statusMessage` (#77438) --- package.json | 2 +- yarn.lock | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index 1745d51f78c41..646f1927e510c 100644 --- a/package.json +++ b/package.json @@ -252,7 +252,7 @@ "@grafana/flamegraph": "workspace:*", "@grafana/google-sdk": "0.1.1", "@grafana/lezer-logql": "0.2.1", - "@grafana/lezer-traceql": "0.0.8", + "@grafana/lezer-traceql": "0.0.9", "@grafana/monaco-logql": "^0.0.7", "@grafana/runtime": "workspace:*", "@grafana/scenes": "^1.20.1", diff --git a/yarn.lock b/yarn.lock index 3adbcf5516636..e111cff3ffb2a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3242,12 +3242,12 @@ __metadata: languageName: node linkType: hard -"@grafana/lezer-traceql@npm:0.0.8": - version: 0.0.8 - resolution: "@grafana/lezer-traceql@npm:0.0.8" +"@grafana/lezer-traceql@npm:0.0.9": + version: 0.0.9 + resolution: "@grafana/lezer-traceql@npm:0.0.9" peerDependencies: "@lezer/lr": ^1.3.0 - checksum: 9a85e4e6a3d77c2075643d179727855f3b7d49e76e93053048b56c86bcfaec040e654012f7ab5f8cf00cd0464220b9afbb1166a1f927b0ce9d2bb724af8f5133 + checksum: 1511e34d47466a9bd4880cd04f817d263dc01c0d4cd4dea0ad1bd7829d59e3d6b15c723e9ba4b240d812fc5d288ec0c4726928a2da031ee3d60573a8861d21a3 languageName: node linkType: hard @@ -17099,7 +17099,7 @@ __metadata: "@grafana/flamegraph": "workspace:*" "@grafana/google-sdk": "npm:0.1.1" "@grafana/lezer-logql": "npm:0.2.1" - "@grafana/lezer-traceql": "npm:0.0.8" + "@grafana/lezer-traceql": "npm:0.0.9" "@grafana/monaco-logql": "npm:^0.0.7" "@grafana/runtime": "workspace:*" "@grafana/scenes": "npm:^1.20.1" From 1e065580ac07df747d421e46090a23de81cd02c1 Mon Sep 17 00:00:00 2001 From: Ivana Huckova <30407135+ivanahuckova@users.noreply.github.com> Date: Thu, 2 Nov 2023 11:02:00 +0100 Subject: [PATCH 046/869] Loki: Remove duplicated methods in languageProvider (#77456) Loki: Remove fuplicated methods in languageProvider --- .../datasource/loki/LanguageProvider.ts | 21 ------------------- .../loki/components/LokiCheatSheet.tsx | 2 +- .../loki/components/LokiLabelBrowser.test.tsx | 2 +- .../loki/components/LokiLabelBrowser.tsx | 2 +- .../CompletionDataProvider.test.ts | 4 ++-- .../CompletionDataProvider.ts | 4 ++-- 6 files changed, 7 insertions(+), 28 deletions(-) diff --git a/public/app/plugins/datasource/loki/LanguageProvider.ts b/public/app/plugins/datasource/loki/LanguageProvider.ts index 070ec78116164..960dfcbfa894c 100644 --- a/public/app/plugins/datasource/loki/LanguageProvider.ts +++ b/public/app/plugins/datasource/loki/LanguageProvider.ts @@ -96,20 +96,6 @@ export default class LokiLanguageProvider extends LanguageProvider { }; } - /** - * Wrapper method over fetchSeriesLabels to retrieve series labels and handle errors. - * @todo remove this in favor of fetchSeriesLabels as we already in this.request do the same thing - */ - async getSeriesLabels(selector: string) { - try { - return await this.fetchSeriesLabels(selector); - } catch (error) { - // TODO: better error handling - console.error(error); - return undefined; - } - } - /** * Fetch all label keys * This asynchronous function returns all available label keys from the data source. @@ -186,13 +172,6 @@ export default class LokiLanguageProvider extends LanguageProvider { return nanoseconds ? Math.floor(nanoseconds / NS_IN_MS / 1000 / 60 / 5) : 0; } - /** - * @todo remove this in favor of fetchLabelValues as it is the same thing - */ - async getLabelValues(key: string): Promise { - return await this.fetchLabelValues(key); - } - /** * Fetch label values * diff --git a/public/app/plugins/datasource/loki/components/LokiCheatSheet.tsx b/public/app/plugins/datasource/loki/components/LokiCheatSheet.tsx index b6c75f526a47b..0f7a0b99dd1d1 100644 --- a/public/app/plugins/datasource/loki/components/LokiCheatSheet.tsx +++ b/public/app/plugins/datasource/loki/components/LokiCheatSheet.tsx @@ -62,7 +62,7 @@ export default class LokiCheatSheet extends PureComponent labels.includes(l)); if (preferredLabel) { - const values = await provider.getLabelValues(preferredLabel); + const values = await provider.fetchLabelValues(preferredLabel); const userExamples = shuffle(values) .slice(0, EXAMPLES_LIMIT) .map((value) => `{${preferredLabel}="${value}"}`); diff --git a/public/app/plugins/datasource/loki/components/LokiLabelBrowser.test.tsx b/public/app/plugins/datasource/loki/components/LokiLabelBrowser.test.tsx index fc158203f6576..5090172f64efd 100644 --- a/public/app/plugins/datasource/loki/components/LokiLabelBrowser.test.tsx +++ b/public/app/plugins/datasource/loki/components/LokiLabelBrowser.test.tsx @@ -85,7 +85,7 @@ describe('LokiLabelBrowser', () => { const setupProps = (): BrowserProps => { const mockLanguageProvider = { start: () => Promise.resolve(), - getLabelValues: (name: string) => { + fetchLabelValues: (name: string) => { switch (name) { case 'label1': return ['value1-1', 'value1-2']; diff --git a/public/app/plugins/datasource/loki/components/LokiLabelBrowser.tsx b/public/app/plugins/datasource/loki/components/LokiLabelBrowser.tsx index 936255b4a90e6..2c32e36549dd6 100644 --- a/public/app/plugins/datasource/loki/components/LokiLabelBrowser.tsx +++ b/public/app/plugins/datasource/loki/components/LokiLabelBrowser.tsx @@ -350,7 +350,7 @@ export class UnthemedLokiLabelBrowser extends React.Component { completionProvider = new CompletionDataProvider(languageProvider, historyRef); jest.spyOn(languageProvider, 'getLabelKeys').mockReturnValue(labelKeys); - jest.spyOn(languageProvider, 'getLabelValues').mockResolvedValue(labelValues); - jest.spyOn(languageProvider, 'getSeriesLabels').mockResolvedValue(seriesLabels); + jest.spyOn(languageProvider, 'fetchLabelValues').mockResolvedValue(labelValues); + jest.spyOn(languageProvider, 'fetchSeriesLabels').mockResolvedValue(seriesLabels); jest.spyOn(languageProvider, 'getParserAndLabelKeys').mockResolvedValue(parserAndLabelKeys); }); diff --git a/public/app/plugins/datasource/loki/components/monaco-query-field/monaco-completion-provider/CompletionDataProvider.ts b/public/app/plugins/datasource/loki/components/monaco-query-field/monaco-completion-provider/CompletionDataProvider.ts index 03064d5a74a84..f8934ff1fc2c9 100644 --- a/public/app/plugins/datasource/loki/components/monaco-query-field/monaco-completion-provider/CompletionDataProvider.ts +++ b/public/app/plugins/datasource/loki/components/monaco-query-field/monaco-completion-provider/CompletionDataProvider.ts @@ -51,7 +51,7 @@ export class CompletionDataProvider { async getLabelValues(labelName: string, otherLabels: Label[]) { if (otherLabels.length === 0) { // if there is no filtering, we have to use a special endpoint - return await this.languageProvider.getLabelValues(labelName); + return await this.languageProvider.fetchLabelValues(labelName); } const data = await this.getSeriesLabels(otherLabels); @@ -90,6 +90,6 @@ export class CompletionDataProvider { } async getSeriesLabels(labels: Label[]) { - return await this.languageProvider.getSeriesLabels(this.buildSelector(labels)).then((data) => data ?? {}); + return await this.languageProvider.fetchSeriesLabels(this.buildSelector(labels)).then((data) => data ?? {}); } } From d62170e4ce24ce2def6ca531ca183a91e1cc4de7 Mon Sep 17 00:00:00 2001 From: Alex Khomenko Date: Thu, 2 Nov 2023 11:22:57 +0100 Subject: [PATCH 047/869] Grafana/ui: Move the Stack component out of unstable (#77495) * grafana/ui: Move Stack out of unstable * grafana/ui: Replace imports --- packages/grafana-ui/src/components/index.ts | 1 + packages/grafana-ui/src/unstable.ts | 1 - .../AppChrome/DockedMegaMenu/MegaMenu.tsx | 3 +-- public/app/features/admin/Users/OrgUnits.tsx | 3 +-- .../app/features/admin/Users/OrgUsersTable.tsx | 16 ++++++++-------- public/app/features/admin/Users/UsersTable.tsx | 14 +++++++------- .../dashboard/dashgrid/DashboardEmpty.tsx | 3 +-- .../serviceaccounts/ServiceAccountsListPage.tsx | 2 +- public/app/features/teams/TeamList.tsx | 16 ++++++++-------- 9 files changed, 28 insertions(+), 31 deletions(-) diff --git a/packages/grafana-ui/src/components/index.ts b/packages/grafana-ui/src/components/index.ts index 78cd9b5321e58..d5abea2d4c114 100644 --- a/packages/grafana-ui/src/components/index.ts +++ b/packages/grafana-ui/src/components/index.ts @@ -215,6 +215,7 @@ export { Link } from './Link/Link'; export { TextLink } from './Link/TextLink'; export { Text } from './Text/Text'; export { Box } from './Layout/Box/Box'; +export { Stack } from './Layout/Stack/Stack'; export { Label } from './Forms/Label'; export { Field, type FieldProps } from './Forms/Field'; diff --git a/packages/grafana-ui/src/unstable.ts b/packages/grafana-ui/src/unstable.ts index f90909a1517b4..2e08df7264249 100644 --- a/packages/grafana-ui/src/unstable.ts +++ b/packages/grafana-ui/src/unstable.ts @@ -10,4 +10,3 @@ */ export { Grid } from './components/Layout/Grid/Grid'; -export { Stack } from './components/Layout/Stack/Stack'; diff --git a/public/app/core/components/AppChrome/DockedMegaMenu/MegaMenu.tsx b/public/app/core/components/AppChrome/DockedMegaMenu/MegaMenu.tsx index dff065c4242f3..fae86e4e5761c 100644 --- a/public/app/core/components/AppChrome/DockedMegaMenu/MegaMenu.tsx +++ b/public/app/core/components/AppChrome/DockedMegaMenu/MegaMenu.tsx @@ -5,8 +5,7 @@ import { useLocation } from 'react-router-dom'; import { GrafanaTheme2 } from '@grafana/data'; import { selectors } from '@grafana/e2e-selectors'; -import { CustomScrollbar, Icon, IconButton, useStyles2 } from '@grafana/ui'; -import { Stack } from '@grafana/ui/src/unstable'; +import { CustomScrollbar, Icon, IconButton, useStyles2, Stack } from '@grafana/ui'; import { useGrafana } from 'app/core/context/GrafanaContext'; import { t } from 'app/core/internationalization'; import { useSelector } from 'app/types'; diff --git a/public/app/features/admin/Users/OrgUnits.tsx b/public/app/features/admin/Users/OrgUnits.tsx index 08a58be885454..5da20ac22c371 100644 --- a/public/app/features/admin/Users/OrgUnits.tsx +++ b/public/app/features/admin/Users/OrgUnits.tsx @@ -1,8 +1,7 @@ import React, { forwardRef, PropsWithChildren } from 'react'; import { IconName } from '@grafana/data'; -import { Icon, Tooltip, Box } from '@grafana/ui'; -import { Stack } from '@grafana/ui/src/unstable'; +import { Icon, Tooltip, Box, Stack } from '@grafana/ui'; import { Unit } from 'app/types'; type OrgUnitProps = { units?: Unit[]; icon: IconName }; diff --git a/public/app/features/admin/Users/OrgUsersTable.tsx b/public/app/features/admin/Users/OrgUsersTable.tsx index 0e3df7d60a710..34a58210fab0b 100644 --- a/public/app/features/admin/Users/OrgUsersTable.tsx +++ b/public/app/features/admin/Users/OrgUsersTable.tsx @@ -3,20 +3,20 @@ import React, { useEffect, useMemo, useState } from 'react'; import { OrgRole } from '@grafana/data'; import { selectors as e2eSelectors } from '@grafana/e2e-selectors'; import { + Avatar, + Box, Button, - ConfirmModal, - Icon, - Tooltip, CellProps, - Tag, - InteractiveTable, Column, + ConfirmModal, FetchDataFunc, + Icon, + InteractiveTable, Pagination, - Avatar, - Box, + Stack, + Tag, + Tooltip, } from '@grafana/ui'; -import { Stack } from '@grafana/ui/src/unstable'; import { UserRolePicker } from 'app/core/components/RolePicker/UserRolePicker'; import { fetchRoleOptions } from 'app/core/components/RolePicker/api'; import { TagBadge } from 'app/core/components/TagFilter/TagBadge'; diff --git a/public/app/features/admin/Users/UsersTable.tsx b/public/app/features/admin/Users/UsersTable.tsx index 7713855e4e195..745efbd925ae9 100644 --- a/public/app/features/admin/Users/UsersTable.tsx +++ b/public/app/features/admin/Users/UsersTable.tsx @@ -1,18 +1,18 @@ import React, { useMemo } from 'react'; import { - InteractiveTable, + Avatar, CellProps, - Tooltip, - Icon, - Tag, - Pagination, Column, FetchDataFunc, + Icon, + InteractiveTable, + Pagination, + Stack, + Tag, Text, - Avatar, + Tooltip, } from '@grafana/ui'; -import { Stack } from '@grafana/ui/src/unstable'; import { TagBadge } from 'app/core/components/TagFilter/TagBadge'; import { UserDTO } from 'app/types'; diff --git a/public/app/features/dashboard/dashgrid/DashboardEmpty.tsx b/public/app/features/dashboard/dashgrid/DashboardEmpty.tsx index 0a521767cc667..2d087b41b0c68 100644 --- a/public/app/features/dashboard/dashgrid/DashboardEmpty.tsx +++ b/public/app/features/dashboard/dashgrid/DashboardEmpty.tsx @@ -4,8 +4,7 @@ import React from 'react'; import { GrafanaTheme2 } from '@grafana/data'; import { selectors } from '@grafana/e2e-selectors'; import { config, locationService, reportInteraction } from '@grafana/runtime'; -import { Button, useStyles2, Text, Box } from '@grafana/ui'; -import { Stack } from '@grafana/ui/src/unstable'; +import { Button, useStyles2, Text, Box, Stack } from '@grafana/ui'; import { Trans } from 'app/core/internationalization'; import { DashboardModel } from 'app/features/dashboard/state'; import { onAddLibraryPanel, onCreateNewPanel, onImportDashboard } from 'app/features/dashboard/utils/dashboard'; diff --git a/public/app/features/serviceaccounts/ServiceAccountsListPage.tsx b/public/app/features/serviceaccounts/ServiceAccountsListPage.tsx index b943e841cb1b5..d6aedc26be695 100644 --- a/public/app/features/serviceaccounts/ServiceAccountsListPage.tsx +++ b/public/app/features/serviceaccounts/ServiceAccountsListPage.tsx @@ -12,8 +12,8 @@ import { useStyles2, InlineField, Pagination, + Stack, } from '@grafana/ui'; -import { Stack } from '@grafana/ui/src/unstable'; import EmptyListCTA from 'app/core/components/EmptyListCTA/EmptyListCTA'; import { Page } from 'app/core/components/Page/Page'; import PageLoader from 'app/core/components/PageLoader/PageLoader'; diff --git a/public/app/features/teams/TeamList.tsx b/public/app/features/teams/TeamList.tsx index bd762baf1d049..4211a462ec18f 100644 --- a/public/app/features/teams/TeamList.tsx +++ b/public/app/features/teams/TeamList.tsx @@ -2,19 +2,19 @@ import React, { useEffect, useMemo, useState } from 'react'; import { connect, ConnectedProps } from 'react-redux'; import { - LinkButton, - FilterInput, - InlineField, + Avatar, CellProps, + Column, DeleteButton, - InteractiveTable, + FilterInput, Icon, - Tooltip, - Column, + InlineField, + InteractiveTable, + LinkButton, Pagination, - Avatar, + Stack, + Tooltip, } from '@grafana/ui'; -import { Stack } from '@grafana/ui/src/unstable'; import EmptyListCTA from 'app/core/components/EmptyListCTA/EmptyListCTA'; import { Page } from 'app/core/components/Page/Page'; import { fetchRoleOptions } from 'app/core/components/RolePicker/api'; From b13395afbccde52d02ac926919234032237794e1 Mon Sep 17 00:00:00 2001 From: Fabrizio <135109076+fabrizio-grafana@users.noreply.github.com> Date: Thu, 2 Nov 2023 11:23:18 +0100 Subject: [PATCH 048/869] Tempo: Handle empty responses in ServiceGraph (#77539) --- .../datasource/tempo/graphTransform.test.ts | 21 +++++++++++++++++++ .../datasource/tempo/graphTransform.ts | 2 +- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/public/app/plugins/datasource/tempo/graphTransform.test.ts b/public/app/plugins/datasource/tempo/graphTransform.test.ts index e6acd91ddd11c..efebee60cd6d2 100644 --- a/public/app/plugins/datasource/tempo/graphTransform.test.ts +++ b/public/app/plugins/datasource/tempo/graphTransform.test.ts @@ -80,6 +80,27 @@ it('assigns correct field type even if values are numbers', async () => { ]); }); +it('do not fail on response with empty list', async () => { + const range = { + from: dateTime('2000-01-01T00:00:00'), + to: dateTime('2000-01-01T00:01:00'), + }; + const { nodes } = mapPromMetricsToServiceMap([], { + ...range, + raw: range, + }); + + expect(nodes.fields).toMatchObject([ + { name: 'id', values: [], type: FieldType.string }, + { name: 'title', values: [], type: FieldType.string }, + { name: 'subtitle', type: FieldType.string, values: [] }, + { name: 'mainstat', values: [], type: FieldType.number }, + { name: 'secondarystat', values: [], type: FieldType.number }, + { name: 'arc__success', values: [], type: FieldType.number }, + { name: 'arc__failed', values: [], type: FieldType.number }, + ]); +}); + describe('mapPromMetricsToServiceMap', () => { it('transforms prom metrics to service graph', async () => { const range = { diff --git a/public/app/plugins/datasource/tempo/graphTransform.ts b/public/app/plugins/datasource/tempo/graphTransform.ts index 4b7b4b56feddd..08cbbf19c6bb1 100644 --- a/public/app/plugins/datasource/tempo/graphTransform.ts +++ b/public/app/plugins/datasource/tempo/graphTransform.ts @@ -249,7 +249,7 @@ function createServiceMapDataFrames() { * @param responses */ function getMetricFrames(responses: DataQueryResponse[]): Record { - return responses[0].data.reduce>((acc, frameDTO) => { + return (responses[0]?.data || []).reduce>((acc, frameDTO) => { const frame = toDataFrame(frameDTO); acc[frame.refId ?? 'A'] = new DataFrameView(frame); return acc; From 82a7e1229aace381487645256ad8cee4f26bb0d3 Mon Sep 17 00:00:00 2001 From: Andres Martinez Gotor Date: Thu, 2 Nov 2023 11:27:17 +0100 Subject: [PATCH 049/869] Bug Fix: Respect data source version when provisioning (#77428) --- docs/sources/administration/provisioning/index.md | 2 ++ pkg/services/provisioning/datasources/datasources.go | 6 +++++- pkg/services/provisioning/datasources/types.go | 1 + pkg/services/provisioning/datasources/types_test.go | 8 ++++++++ 4 files changed, 16 insertions(+), 1 deletion(-) diff --git a/docs/sources/administration/provisioning/index.md b/docs/sources/administration/provisioning/index.md index 0f3c5f1e846da..3a96f6e31117b 100644 --- a/docs/sources/administration/provisioning/index.md +++ b/docs/sources/administration/provisioning/index.md @@ -157,6 +157,8 @@ datasources: password: # Sets the basic authorization password. basicAuthPassword: + # Sets the version. Used to compare versions when + # updating. Ignored when creating a new data source. version: 1 # Allows users to edit data sources from the # Grafana UI. diff --git a/pkg/services/provisioning/datasources/datasources.go b/pkg/services/provisioning/datasources/datasources.go index 3093e39db1b82..83d1288b4aba2 100644 --- a/pkg/services/provisioning/datasources/datasources.go +++ b/pkg/services/provisioning/datasources/datasources.go @@ -80,7 +80,11 @@ func (dc *DatasourceProvisioner) provisionDataSources(ctx context.Context, cfg * updateCmd := createUpdateCommand(ds, dataSource.ID) dc.log.Debug("updating datasource from configuration", "name", updateCmd.Name, "uid", updateCmd.UID) if _, err := dc.store.UpdateDataSource(ctx, updateCmd); err != nil { - return err + if errors.Is(err, datasources.ErrDataSourceUpdatingOldVersion) { + dc.log.Debug("ignoring old version of datasource", "name", updateCmd.Name, "uid", updateCmd.UID) + } else { + return err + } } } } diff --git a/pkg/services/provisioning/datasources/types.go b/pkg/services/provisioning/datasources/types.go index 2e58df3a01684..5088278cc8536 100644 --- a/pkg/services/provisioning/datasources/types.go +++ b/pkg/services/provisioning/datasources/types.go @@ -243,6 +243,7 @@ func createUpdateCommand(ds *upsertDataSourceFromConfig, id int64) *datasources. return &datasources.UpdateDataSourceCommand{ ID: id, + Version: ds.Version, UID: ds.UID, OrgID: ds.OrgID, Name: ds.Name, diff --git a/pkg/services/provisioning/datasources/types_test.go b/pkg/services/provisioning/datasources/types_test.go index 75820d6b12890..5cf0592f6b749 100644 --- a/pkg/services/provisioning/datasources/types_test.go +++ b/pkg/services/provisioning/datasources/types_test.go @@ -13,3 +13,11 @@ func TestUIDFromNames(t *testing.T) { require.Equal(t, safeUIDFromName("AAA"), "PCB1AD2119D8FAFB6") }) } + +func TestCreateUpdateCommand(t *testing.T) { + t.Run("includes the version in the command", func(t *testing.T) { + ds := &upsertDataSourceFromConfig{OrgID: 1, Version: 1, Name: "test"} + cmd := createUpdateCommand(ds, 1) + require.Equal(t, 1, cmd.Version) + }) +} From 774a8a889a95c299a7c452d4dfddf8cae19a8ffd Mon Sep 17 00:00:00 2001 From: Krishna Dhakal <7krishna7dhakal7@gmail.com> Date: Thu, 2 Nov 2023 17:08:59 +0545 Subject: [PATCH 050/869] Grafana-UI: Create fast path in Text component (#76167) Text component fast path Truncated text an isolated component --- .../grafana-ui/src/components/Text/Text.tsx | 90 ++++++------------- .../src/components/Text/TruncatedText.tsx | 64 +++++++++++++ 2 files changed, 91 insertions(+), 63 deletions(-) create mode 100644 packages/grafana-ui/src/components/Text/TruncatedText.tsx diff --git a/packages/grafana-ui/src/components/Text/Text.tsx b/packages/grafana-ui/src/components/Text/Text.tsx index eb036f186e761..29f3a46c1134a 100644 --- a/packages/grafana-ui/src/components/Text/Text.tsx +++ b/packages/grafana-ui/src/components/Text/Text.tsx @@ -1,12 +1,11 @@ import { css } from '@emotion/css'; -import React, { createElement, CSSProperties, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react'; -import ReactDomServer from 'react-dom/server'; +import React, { createElement, CSSProperties } from 'react'; import { GrafanaTheme2, ThemeTypographyVariantTypes } from '@grafana/data'; import { useStyles2 } from '../../themes'; -import { Tooltip } from '../Tooltip/Tooltip'; +import { TruncatedText } from './TruncatedText'; import { customWeight, customColor, customVariant } from './utils'; export interface TextProps extends Omit, 'className' | 'style'> { @@ -30,70 +29,35 @@ export interface TextProps extends Omit, 'clas export const Text = React.forwardRef( ({ element = 'span', variant, weight, color, truncate, italic, textAlignment, children, ...restProps }, ref) => { const styles = useStyles2(getTextStyles, element, variant, color, weight, truncate, italic, textAlignment); - const [isOverflowing, setIsOverflowing] = useState(false); - const internalRef = useRef(null); - // wire up the forwarded ref to the internal ref - useImperativeHandle(ref, () => internalRef.current); + const childElement = (ref: React.ForwardedRef | undefined) => { + return createElement( + element, + { + ...restProps, + style: undefined, // Remove the style prop to avoid overriding the styles + className: styles, + // When overflowing, the internalRef is passed to the tooltip, which forwards it to the child element + ref, + }, + children + ); + }; - const childElement = createElement( - element, - { - ...restProps, - style: undefined, // remove style prop to avoid overriding the styles - className: styles, - // when overflowing, the internalRef is passed to the tooltip which forwards it on to the child element - ref: isOverflowing ? undefined : internalRef, - }, - children - ); + // A 'span' is an inline element, so it can't be truncated + // and it should be wrapped in a parent element that will show the tooltip + if (!truncate || element === 'span') { + return childElement(undefined); + } - const resizeObserver = useMemo( - () => - new ResizeObserver((entries) => { - for (const entry of entries) { - if (entry.target.clientWidth && entry.target.scrollWidth) { - if (entry.target.scrollWidth > entry.target.clientWidth) { - setIsOverflowing(true); - } - if (entry.target.scrollWidth <= entry.target.clientWidth) { - setIsOverflowing(false); - } - } - } - }), - [] + return ( + ); - - useEffect(() => { - const { current } = internalRef; - if (current && truncate) { - resizeObserver.observe(current); - } - return () => { - resizeObserver.disconnect(); - }; - }, [isOverflowing, resizeObserver, truncate]); - - const getTooltipText = (children: NonNullable) => { - if (typeof children === 'string') { - return children; - } - const html = ReactDomServer.renderToStaticMarkup(<>{children}); - const getRidOfTags = html.replace(/(<([^>]+)>)/gi, ''); - return getRidOfTags; - }; - // A 'span' is an inline element therefore it can't be truncated - // and it should be wrapped in a parent element that is the one that will show the tooltip - if (truncate && isOverflowing && element !== 'span') { - return ( - - {childElement} - - ); - } else { - return childElement; - } } ); diff --git a/packages/grafana-ui/src/components/Text/TruncatedText.tsx b/packages/grafana-ui/src/components/Text/TruncatedText.tsx new file mode 100644 index 0000000000000..e74cbd36bb72e --- /dev/null +++ b/packages/grafana-ui/src/components/Text/TruncatedText.tsx @@ -0,0 +1,64 @@ +import React, { useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react'; +import ReactDOMServer from 'react-dom/server'; + +import { Tooltip } from '../Tooltip/Tooltip'; + +interface TruncatedTextProps { + childElement: (ref: React.ForwardedRef | undefined) => React.ReactElement; + children: NonNullable; +} + +export const TruncatedText = React.forwardRef(({ childElement, children }, ref) => { + const [isOverflowing, setIsOverflowing] = useState(false); + const internalRef = useRef(null); + + // Wire up the forwarded ref to the internal ref + useImperativeHandle(ref, () => internalRef.current); + + const resizeObserver = useMemo( + () => + new ResizeObserver((entries) => { + for (const entry of entries) { + if (entry.target.clientWidth && entry.target.scrollWidth) { + if (entry.target.scrollWidth > entry.target.clientWidth) { + setIsOverflowing(true); + } + if (entry.target.scrollWidth <= entry.target.clientWidth) { + setIsOverflowing(false); + } + } + } + }), + [] + ); + + useEffect(() => { + const { current } = internalRef; + if (current) { + resizeObserver.observe(current); + } + return () => { + resizeObserver.disconnect(); + }; + }, [setIsOverflowing, resizeObserver]); + + const getTooltipText = (children: NonNullable) => { + if (typeof children === 'string') { + return children; + } + const html = ReactDOMServer.renderToStaticMarkup(<>{children}); + return html.replace(/(<([^>]+)>)/gi, ''); + }; + + if (isOverflowing) { + return ( + + {childElement(undefined)} + + ); + } else { + return childElement(internalRef); + } +}); + +TruncatedText.displayName = 'TruncatedText'; From 34eb29c3bf6441dfc4baad6d9561530ef24eaafc Mon Sep 17 00:00:00 2001 From: brendamuir <100768211+brendamuir@users.noreply.github.com> Date: Thu, 2 Nov 2023 12:31:47 +0100 Subject: [PATCH 051/869] Adds alerts from panels feature to cloud whats new (#77547) --- docs/sources/whatsnew/whats-new-next/index.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/docs/sources/whatsnew/whats-new-next/index.md b/docs/sources/whatsnew/whats-new-next/index.md index 743f5457d0cae..61e4375d39d36 100644 --- a/docs/sources/whatsnew/whats-new-next/index.md +++ b/docs/sources/whatsnew/whats-new-next/index.md @@ -229,6 +229,17 @@ The Grafana Assume Role authentication provider lets Grafana Cloud users of the To learn more, refer to the [CloudWatch authentication documentation](/docs/grafana/next/datasources/aws-cloudwatch/aws-authentication). +## Create alerts from panels + + + + +October 23, 2023 + +_Generally available in Grafana Cloud_ + +Create alerts from dashboard panels. You can reuse the panel queries and create alerts based on them. + ## No basic role From 00a596b2e058c6f1b2a375e5df46fecda97e9c36 Mon Sep 17 00:00:00 2001 From: Andres Martinez Gotor Date: Thu, 2 Nov 2023 13:26:16 +0100 Subject: [PATCH 052/869] Chore: Add app URL to the plugin config (#77455) --- go.mod | 2 +- go.sum | 2 ++ pkg/plugins/envvars/envvars.go | 5 +++++ pkg/plugins/envvars/envvars_test.go | 11 +++++++++++ 4 files changed, 19 insertions(+), 1 deletion(-) diff --git a/go.mod b/go.mod index 14b37ffc1c780..cf38d15efaff4 100644 --- a/go.mod +++ b/go.mod @@ -65,7 +65,7 @@ require ( github.com/grafana/cuetsy v0.1.10 // @grafana/grafana-as-code github.com/grafana/grafana-aws-sdk v0.19.1 // @grafana/aws-datasources github.com/grafana/grafana-azure-sdk-go v1.9.0 // @grafana/backend-platform - github.com/grafana/grafana-plugin-sdk-go v0.187.0 // @grafana/plugins-platform-backend + github.com/grafana/grafana-plugin-sdk-go v0.189.0 // @grafana/plugins-platform-backend github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 // @grafana/backend-platform github.com/hashicorp/go-hclog v1.5.0 // @grafana/plugins-platform-backend github.com/hashicorp/go-plugin v1.4.9 // @grafana/plugins-platform-backend diff --git a/go.sum b/go.sum index 9de11b6e8705a..94188a7f9f96c 100644 --- a/go.sum +++ b/go.sum @@ -1841,6 +1841,8 @@ github.com/grafana/grafana-plugin-sdk-go v0.94.0/go.mod h1:3VXz4nCv6wH5SfgB3mlW3 github.com/grafana/grafana-plugin-sdk-go v0.114.0/go.mod h1:D7x3ah+1d4phNXpbnOaxa/osSaZlwh9/ZUnGGzegRbk= github.com/grafana/grafana-plugin-sdk-go v0.187.0 h1:lOwoFbbTs27KqR3F32GvOX9Et3Ek8p8qsFw+SUJtAAM= github.com/grafana/grafana-plugin-sdk-go v0.187.0/go.mod h1:PHK8eQOz3ES28RmImdTHNOTxBZaH6mb/ytJGxk7VVJc= +github.com/grafana/grafana-plugin-sdk-go v0.189.0 h1:30n0dtehLT0Z0DdRfatk1INwXOKfCb9LKaSFl8zBj9g= +github.com/grafana/grafana-plugin-sdk-go v0.189.0/go.mod h1:nctofuR6fyhx3Stbnh5ha6setroeqqBgO0Rj9s4t86o= github.com/grafana/kindsys v0.0.0-20230508162304-452481b63482 h1:1YNoeIhii4UIIQpCPU+EXidnqf449d0C3ZntAEt4KSo= github.com/grafana/kindsys v0.0.0-20230508162304-452481b63482/go.mod h1:GNcfpy5+SY6RVbNGQW264gC0r336Dm+0zgQ5vt6+M8Y= github.com/grafana/prometheus-alertmanager v0.25.1-0.20231027171310-70c52bf65758 h1:ATUhvJSJwzdzhnmzUI92fxVFqyqmcnzJ47wtHTK3LW4= diff --git a/pkg/plugins/envvars/envvars.go b/pkg/plugins/envvars/envvars.go index 57259ca28db56..8951b49e46a7c 100644 --- a/pkg/plugins/envvars/envvars.go +++ b/pkg/plugins/envvars/envvars.go @@ -10,6 +10,7 @@ import ( "github.com/grafana/grafana-aws-sdk/pkg/awsds" "github.com/grafana/grafana-azure-sdk-go/azsettings" + "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana-plugin-sdk-go/backend/proxy" "github.com/grafana/grafana-plugin-sdk-go/experimental/featuretoggles" @@ -79,6 +80,10 @@ func (s *Service) Get(ctx context.Context, p *plugins.Plugin) []string { func (s *Service) GetConfigMap(ctx context.Context, _ string, _ *auth.ExternalService) map[string]string { m := make(map[string]string) + if s.cfg.GrafanaAppURL != "" { + m[backend.AppURL] = s.cfg.GrafanaAppURL + } + // TODO add support via plugin SDK //if externalService != nil { // m[oauthtokenretriever.AppURL] = s.cfg.GrafanaAppURL diff --git a/pkg/plugins/envvars/envvars_test.go b/pkg/plugins/envvars/envvars_test.go index 46cc8b56e527d..cdee809c0b4c7 100644 --- a/pkg/plugins/envvars/envvars_test.go +++ b/pkg/plugins/envvars/envvars_test.go @@ -500,3 +500,14 @@ func TestService_GetConfigMap_featureToggles(t *testing.T) { } }) } + +func TestService_GetConfigMap_appURL(t *testing.T) { + t.Run("Uses the configured app URL", func(t *testing.T) { + s := &Service{ + cfg: &config.Cfg{ + GrafanaAppURL: "https://myorg.com/", + }, + } + require.Equal(t, map[string]string{"GF_APP_URL": "https://myorg.com/"}, s.GetConfigMap(context.Background(), "", nil)) + }) +} From c73a2bde9cda93c9bec1adf55c47fc8ec2fb1ef8 Mon Sep 17 00:00:00 2001 From: Stephen Yeargin Date: Thu, 2 Nov 2023 08:50:32 -0500 Subject: [PATCH 053/869] Documentation: Update Hubot Integration documentation (#76925) * Update Hubot Integration documentation The script package has improved over the years to allow for direct uploading to Slack, etc. This updates the documentation to reflect that. * Apply suggestions from code review Co-authored-by: lwandz13 <126723338+lwandz13@users.noreply.github.com> --------- Co-authored-by: lwandz13 <126723338+lwandz13@users.noreply.github.com> --- .../tutorials/integrate-hubot/index.md | 20 +++++++------------ 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/docs/sources/tutorials/integrate-hubot/index.md b/docs/sources/tutorials/integrate-hubot/index.md index d6b769987f1f1..35299df80b60d 100644 --- a/docs/sources/tutorials/integrate-hubot/index.md +++ b/docs/sources/tutorials/integrate-hubot/index.md @@ -25,10 +25,7 @@ Grafana 2.0 shipped with a great feature that enables it to render any graph or No matter what data source you are using, the PNG image of the Graph will look the same as it does in your browser. -This guide will show you how to install and configure the [Hubot-Grafana](https://github.com/stephenyeargin/hubot-grafana) plugin. This plugin allows you to tell hubot to render any dashboard or graph right from a channel in Slack, Hipchat or Basecamp. The bot will respond with an image of the graph and a link that will take you to the graph. - -> _Amazon S3 Required_: The hubot-grafana script will upload the rendered graphs to Amazon S3. This -> is so Hipchat and Slack can show them reliably (they require the image to be publicly available). +This guide shows you how to install and configure the [Hubot-Grafana](https://github.com/stephenyeargin/hubot-grafana) plugin. This plugin allows you to tell Hubot to render any dashboard or graph right from a channel in Slack, Basecamp, or any other supported Hubot adapter. The bot will respond with an image of the graph and a link that will take you to the graph. {{< figure src="/static/img/docs/tutorials/hubot_grafana.png" max-width="800px" >}} @@ -40,7 +37,7 @@ This guide will show you how to install and configure the [Hubot-Grafana](https: Hubot is very easy to install and host. If you do not already have a bot up and running please read the official [Getting Started With Hubot](https://hubot.github.com/docs/) guide. -## Install Hubot-Grafana script +## Install the Hubot-Grafana script In your Hubot project repo install the Grafana plugin using `npm`: @@ -56,18 +53,15 @@ Edit the file external-scripts.json, and add hubot-grafana to the list of plugin ## Configure -The `hubot-grafana` plugin requires a number of environment variables to be set in order to work properly. +The Hubot-Grafana plugin requires two environment variables to be set in order to work properly. ```bash export HUBOT_GRAFANA_HOST=https://play.grafana.org export HUBOT_GRAFANA_API_KEY=abcd01234deadbeef01234 -export HUBOT_GRAFANA_S3_BUCKET=mybucket -export HUBOT_GRAFANA_S3_ACCESS_KEY_ID=ABCDEF123456XYZ -export HUBOT_GRAFANA_S3_SECRET_ACCESS_KEY=aBcD01234dEaDbEef01234 -export HUBOT_GRAFANA_S3_PREFIX=graphs -export HUBOT_GRAFANA_S3_REGION=us-standard ``` +There are [additional environment variables](https://github.com/stephenyeargin/hubot-grafana?tab=readme-ov-file#general-settings) that you can set to control the appearance of the graphs. + ### Grafana server side rendering The hubot plugin will take advantage of the Grafana server side rendering feature that can render any panel on the server using phantomjs. Grafana ships with a phantomjs binary (Linux only). @@ -80,9 +74,9 @@ To verify that this feature works try the `Direct link to rendered image` link i You need to set the environment variable `HUBOT_GRAFANA_API_KEY` to a Grafana API Key. You can add these from the API Keys page which you find in the Organization dropdown. -### Amazon S3 +### Image uploading -The `S3` options are optional but for the images to work properly in services like Slack and Hipchat they need to publicly available. By specifying the `S3` options the hubot-grafana script will publish the rendered panel to `S3` and it will use that URL when it posts to Slack or Hipchat. +There are several approaches to uploading the rendered graphs. If you are using Slack, Rocket.Chat, or Telegram, the adapter's native uploader will take care of sending it through their respective API. If your Hubot is hosted on a platform that doesn't support uploads (such as IRC), you can use the [built-in S3 uploader](https://github.com/stephenyeargin/hubot-grafana/wiki/Amazon-S3-Image-Hosting). Note if you configure S3, it will not use the adapter's upload features. ## Hubot commands From d5f749482a92454c2e7b41d3eb1dc2871d0ead31 Mon Sep 17 00:00:00 2001 From: Andres Martinez Gotor Date: Thu, 2 Nov 2023 14:56:47 +0100 Subject: [PATCH 054/869] Ignore dist folder for core plugin (#77549) --- pkg/plugins/manager/loader/finder/local.go | 10 +++++-- .../manager/loader/finder/local_test.go | 30 +++++++++++++++---- pkg/util/filepath.go | 13 ++++---- 3 files changed, 38 insertions(+), 15 deletions(-) diff --git a/pkg/plugins/manager/loader/finder/local.go b/pkg/plugins/manager/loader/finder/local.go index a46ab30bc7023..9428a4bac2b74 100644 --- a/pkg/plugins/manager/loader/finder/local.go +++ b/pkg/plugins/manager/loader/finder/local.go @@ -57,7 +57,11 @@ func (l *Local) Find(ctx context.Context, src plugins.PluginSource) ([]*plugins. continue } - paths, err := l.getAbsPluginJSONPaths(path) + followDistFolder := true + if src.PluginClass(ctx) == plugins.ClassCore { + followDistFolder = false + } + paths, err := l.getAbsPluginJSONPaths(path, followDistFolder) if err != nil { return nil, err } @@ -154,7 +158,7 @@ func (l *Local) readPluginJSON(pluginJSONPath string) (plugins.JSONData, error) return plugin, nil } -func (l *Local) getAbsPluginJSONPaths(path string) ([]string, error) { +func (l *Local) getAbsPluginJSONPaths(path string, followDistFolder bool) ([]string, error) { var pluginJSONPaths []string var err error @@ -163,7 +167,7 @@ func (l *Local) getAbsPluginJSONPaths(path string) ([]string, error) { return []string{}, err } - if err = walk(path, true, true, + if err = walk(path, true, true, followDistFolder, func(currentPath string, fi os.FileInfo, err error) error { if err != nil { if errors.Is(err, os.ErrNotExist) { diff --git a/pkg/plugins/manager/loader/finder/local_test.go b/pkg/plugins/manager/loader/finder/local_test.go index 42cefb50a1aa3..8b2be26565ee1 100644 --- a/pkg/plugins/manager/loader/finder/local_test.go +++ b/pkg/plugins/manager/loader/finder/local_test.go @@ -274,7 +274,7 @@ func TestFinder_Find(t *testing.T) { func TestFinder_getAbsPluginJSONPaths(t *testing.T) { t.Run("When scanning a folder that doesn't exists shouldn't return an error", func(t *testing.T) { origWalk := walk - walk = func(path string, followSymlinks, detectSymlinkInfiniteLoop bool, walkFn util.WalkFunc) error { + walk = func(path string, followSymlinks, detectSymlinkInfiniteLoop, followDistFolder bool, walkFn util.WalkFunc) error { return walkFn(path, nil, os.ErrNotExist) } t.Cleanup(func() { @@ -282,14 +282,14 @@ func TestFinder_getAbsPluginJSONPaths(t *testing.T) { }) finder := NewLocalFinder(false) - paths, err := finder.getAbsPluginJSONPaths("test") + paths, err := finder.getAbsPluginJSONPaths("test", true) require.NoError(t, err) require.Empty(t, paths) }) t.Run("When scanning a folder that lacks permission shouldn't return an error", func(t *testing.T) { origWalk := walk - walk = func(path string, followSymlinks, detectSymlinkInfiniteLoop bool, walkFn util.WalkFunc) error { + walk = func(path string, followSymlinks, detectSymlinkInfiniteLoop, followDistFolder bool, walkFn util.WalkFunc) error { return walkFn(path, nil, os.ErrPermission) } t.Cleanup(func() { @@ -297,14 +297,14 @@ func TestFinder_getAbsPluginJSONPaths(t *testing.T) { }) finder := NewLocalFinder(false) - paths, err := finder.getAbsPluginJSONPaths("test") + paths, err := finder.getAbsPluginJSONPaths("test", true) require.NoError(t, err) require.Empty(t, paths) }) t.Run("When scanning a folder that returns a non-handled error should return that error", func(t *testing.T) { origWalk := walk - walk = func(path string, followSymlinks, detectSymlinkInfiniteLoop bool, walkFn util.WalkFunc) error { + walk = func(path string, followSymlinks, detectSymlinkInfiniteLoop, followDistFolder bool, walkFn util.WalkFunc) error { return walkFn(path, nil, errors.New("random error")) } t.Cleanup(func() { @@ -312,10 +312,28 @@ func TestFinder_getAbsPluginJSONPaths(t *testing.T) { }) finder := NewLocalFinder(false) - paths, err := finder.getAbsPluginJSONPaths("test") + paths, err := finder.getAbsPluginJSONPaths("test", true) require.Error(t, err) require.Empty(t, paths) }) + + t.Run("should forward if the dist folder should be evaluated", func(t *testing.T) { + origWalk := walk + walk = func(path string, followSymlinks, detectSymlinkInfiniteLoop, followDistFolder bool, walkFn util.WalkFunc) error { + if followDistFolder { + return walkFn(path, nil, errors.New("unexpected followDistFolder")) + } + return walkFn(path, nil, filepath.SkipDir) + } + t.Cleanup(func() { + walk = origWalk + }) + + finder := NewLocalFinder(false) + paths, err := finder.getAbsPluginJSONPaths("test", false) + require.ErrorIs(t, err, filepath.SkipDir) + require.Empty(t, paths) + }) } var fsComparer = cmp.Comparer(func(fs1 plugins.FS, fs2 plugins.FS) bool { diff --git a/pkg/util/filepath.go b/pkg/util/filepath.go index 1034245ac595e..527457aadbbbb 100644 --- a/pkg/util/filepath.go +++ b/pkg/util/filepath.go @@ -21,7 +21,7 @@ type WalkFunc func(resolvedPath string, info os.FileInfo, err error) error // can detect infinite loops while following sym links. // It solves the issue where your WalkFunc needs a path relative to the symbolic link // (resolving links within walkfunc loses the path to the symbolic link for each traversal). -func Walk(path string, followSymlinks bool, detectSymlinkInfiniteLoop bool, walkFn WalkFunc) error { +func Walk(path string, followSymlinks bool, detectSymlinkInfiniteLoop bool, followDistFolder bool, walkFn WalkFunc) error { info, err := os.Lstat(path) if err != nil { return err @@ -34,7 +34,7 @@ func Walk(path string, followSymlinks bool, detectSymlinkInfiniteLoop bool, walk symlinkPathsFollowed = make(map[string]bool, 8) } } - return walk(path, info, resolvedPath, symlinkPathsFollowed, walkFn) + return walk(path, info, resolvedPath, symlinkPathsFollowed, followDistFolder, walkFn) } // walk walks the path. It is a helper/sibling function to Walk. @@ -43,7 +43,7 @@ func Walk(path string, followSymlinks bool, detectSymlinkInfiniteLoop bool, walk // // If resolvedPath is "", then we are not following symbolic links. // If symlinkPathsFollowed is not nil, then we need to detect infinite loop. -func walk(path string, info os.FileInfo, resolvedPath string, symlinkPathsFollowed map[string]bool, walkFn WalkFunc) error { +func walk(path string, info os.FileInfo, resolvedPath string, symlinkPathsFollowed map[string]bool, followDistFolder bool, walkFn WalkFunc) error { if info == nil { return errors.New("walk: Nil FileInfo passed") } @@ -81,7 +81,7 @@ func walk(path string, info os.FileInfo, resolvedPath string, symlinkPathsFollow if err != nil { return err } - return walk(path, info2, path2, symlinkPathsFollowed, walkFn) + return walk(path, info2, path2, symlinkPathsFollowed, followDistFolder, walkFn) } else if info.IsDir() { list, err := os.ReadDir(path) if err != nil { @@ -102,12 +102,13 @@ func walk(path string, info os.FileInfo, resolvedPath string, symlinkPathsFollow subFiles = append(subFiles, subFile{path: path2, resolvedPath: resolvedPath2, fileInfo: fileInfo}) } - if containsDistFolder(subFiles) { + if containsDistFolder(subFiles) && followDistFolder { err := walk( filepath.Join(path, "dist"), info, filepath.Join(resolvedPath, "dist"), symlinkPathsFollowed, + followDistFolder, walkFn) if err != nil { @@ -115,7 +116,7 @@ func walk(path string, info os.FileInfo, resolvedPath string, symlinkPathsFollow } } else { for _, p := range subFiles { - err = walk(p.path, p.fileInfo, p.resolvedPath, symlinkPathsFollowed, walkFn) + err = walk(p.path, p.fileInfo, p.resolvedPath, symlinkPathsFollowed, followDistFolder, walkFn) if err != nil { return err From 9a905b6312b614700dd63183575f5d15935882d8 Mon Sep 17 00:00:00 2001 From: Ashley Harrison Date: Thu, 2 Nov 2023 14:23:19 +0000 Subject: [PATCH 055/869] Navigation: Updates to the docs for admin subsections (#77500) updates to the docs for admin subsections --- docs/sources/administration/api-keys/index.md | 6 +++--- .../enterprise-licensing/_index.md | 4 +++- .../activate-license-on-ecs/index.md | 2 +- .../activate-license-on-eks/index.md | 2 +- .../manage-license-in-aws-marketplace/index.md | 2 +- .../organization-management/index.md | 8 ++++---- .../organization-preferences/index.md | 16 +++++++++++----- .../administration/plugin-management/index.md | 10 +++++----- .../access-control/assign-rbac-roles/index.md | 4 ++-- .../administration/stats-and-license/index.md | 4 ++-- .../administration/team-management/index.md | 12 ++++++------ .../user-management/manage-org-users/index.md | 10 +++++----- .../server-user-management/_index.md | 10 +++++----- .../add-remove-user-to-org/index.md | 4 ++-- .../index.md | 2 +- .../change-user-org-permissions/index.md | 2 +- 16 files changed, 53 insertions(+), 45 deletions(-) diff --git a/docs/sources/administration/api-keys/index.md b/docs/sources/administration/api-keys/index.md index f2e1be18e751c..6ea118897244c 100644 --- a/docs/sources/administration/api-keys/index.md +++ b/docs/sources/administration/api-keys/index.md @@ -48,7 +48,7 @@ To follow these instructions, you need at least one of the following: To create an API, complete the following steps: 1. Sign in to Grafana. -1. Click **Administration** in the left-side menu and select **API Keys**. +1. Click **Administration** in the left-side menu, **Users and access**, and select **API Keys**. 1. Click **Add API key**. 1. Enter a unique name for the key. 1. In the **Role** field, select one of the following access levels you want to assign to the key. @@ -101,7 +101,7 @@ For more information about permissions, refer to [Roles and permissions]({{< rel To migrate all API keys to service accounts, complete the following steps: -1. Sign in to Grafana, point to **Configuration** (the gear icon), and click **API Keys**. +1. Sign in to Grafana, point to **Administration**, **Users and access**, and click **API Keys**. 1. In the top of the page, find the section which says **Switch from API keys to service accounts** 1. Click **Migrate to service accounts now**. 1. A confirmation window will appear, asking to confirm the migration. Click **Yes, migrate now** if you are willing to continue. @@ -110,7 +110,7 @@ To migrate all API keys to service accounts, complete the following steps: To migrate a single API key to a service account, complete the following steps: 1. Sign in to Grafana. -1. Click **Administration** in the left-side menu and select **API Keys**. +1. Click **Administration** in the left-side menu, **Users and access**, and select **API Keys**. 1. Find the API Key you want to migrate. 1. Click **Migrate to service account**. diff --git a/docs/sources/administration/enterprise-licensing/_index.md b/docs/sources/administration/enterprise-licensing/_index.md index 966e0889d7221..fc8e07e712319 100644 --- a/docs/sources/administration/enterprise-licensing/_index.md +++ b/docs/sources/administration/enterprise-licensing/_index.md @@ -52,7 +52,7 @@ There is more than one way to add the license to a Grafana instance: This is the preferred option for single instance installations of Grafana Enterprise. 1. Sign in as a Grafana server administrator. -1. Click **Administration > Stats and license** in the side navigation menu. +1. Click **Administration > General > Stats and license** in the side navigation menu. 1. Click **Upload a new token**. 1. Select your license file, and upload it. @@ -207,6 +207,8 @@ To determine the number of active users: 1. Click **Administration** in the side navigation menu. +1. Click **General**. + 1. Click **Stats and license**. 1. Review the utilization count on the **Utilization** panel. diff --git a/docs/sources/administration/enterprise-licensing/activate-aws-marketplace-license/activate-license-on-ecs/index.md b/docs/sources/administration/enterprise-licensing/activate-aws-marketplace-license/activate-license-on-ecs/index.md index 6f6a35e5eea38..fa06cd3aa0287 100644 --- a/docs/sources/administration/enterprise-licensing/activate-aws-marketplace-license/activate-license-on-ecs/index.md +++ b/docs/sources/administration/enterprise-licensing/activate-aws-marketplace-license/activate-license-on-ecs/index.md @@ -112,6 +112,6 @@ In this task you configure Grafana Enterprise to validate the license with AWS i ### Task 4: Start or restart Grafana 1. To restart Grafana and activate your license, update the service running Grafana to use the latest revision of the task definition that you created. -1. After you update the service, navigate to your Grafana instance, sign in with Grafana Admin credentials, and navigate to **Administration > Stats and license** to validate that your license is active. +1. After you update the service, navigate to your Grafana instance, sign in with Grafana Admin credentials, and navigate to **Administration > General > Stats and license** to validate that your license is active. For more information about validating that your license is active, refer to [Grafana Enterprise license restrictions]({{< relref "../../#grafana-enterprise-license-restrictions" >}}). diff --git a/docs/sources/administration/enterprise-licensing/activate-aws-marketplace-license/activate-license-on-eks/index.md b/docs/sources/administration/enterprise-licensing/activate-aws-marketplace-license/activate-license-on-eks/index.md index a63c1af461d80..49060c0989db5 100644 --- a/docs/sources/administration/enterprise-licensing/activate-aws-marketplace-license/activate-license-on-eks/index.md +++ b/docs/sources/administration/enterprise-licensing/activate-aws-marketplace-license/activate-license-on-eks/index.md @@ -123,7 +123,7 @@ To restart Grafana on a Kubernetes cluster, 1. Run the command `kubectl rollout restart deployment my-release`. -1. After you update the service, navigate to your Grafana instance, sign in with Grafana Admin credentials, and navigate to **Administration > Stats and license** to validate that your license is active. +1. After you update the service, navigate to your Grafana instance, sign in with Grafana Admin credentials, and navigate to **Administration > General > Stats and license** to validate that your license is active. For more information about restarting Grafana, refer to [Restart Grafana]({{< relref "../../../../setup-grafana/start-restart-grafana/" >}}). diff --git a/docs/sources/administration/enterprise-licensing/activate-aws-marketplace-license/manage-license-in-aws-marketplace/index.md b/docs/sources/administration/enterprise-licensing/activate-aws-marketplace-license/manage-license-in-aws-marketplace/index.md index 74a1152a1a312..56ba176742d14 100644 --- a/docs/sources/administration/enterprise-licensing/activate-aws-marketplace-license/manage-license-in-aws-marketplace/index.md +++ b/docs/sources/administration/enterprise-licensing/activate-aws-marketplace-license/manage-license-in-aws-marketplace/index.md @@ -36,7 +36,7 @@ You can use AWS Marketplace to make the following modifications to your Grafana 1. Sign in to Grafana as a Server Administrator. -1. Click **Administration** in the side navigation menu, and then **Stats and license**. +1. Click **Administration** in the side navigation menu, **General**, and then **Stats and license**. 1. In the **Token** section under **Enterprise License**, click **Renew token**. diff --git a/docs/sources/administration/organization-management/index.md b/docs/sources/administration/organization-management/index.md index 0d93b93e5d6c4..a332c8110dbab 100644 --- a/docs/sources/administration/organization-management/index.md +++ b/docs/sources/administration/organization-management/index.md @@ -59,7 +59,7 @@ Complete this task when you want to view a list of existing organizations. **To view a list of organizations:** 1. Sign in to Grafana as a server administrator. -1. Click **Administration** in the left-side menu, and then **Organizations**. +1. Click **Administration** in the left-side menu, **General**, and then **Organizations**. ## Create an organization @@ -72,7 +72,7 @@ Create an organization when you want to isolate dashboards and other resources f **To create an organization:** 1. Sign in to Grafana as a server administrator. -1. Click **Administration** in the left-side menu, and then **Organizations**. +1. Click **Administration** in the left-side menu, **General**, and then **Organizations**. 1. Click **+ New org**. 1. Enter the name of the new organization and click **Create**. @@ -100,7 +100,7 @@ Deleting the organization also deletes all teams and dashboards associated the o **To delete an organization:** 1. Sign in to Grafana as a server administrator. -1. Click **Administration** in the left-side menu, and then **Organizations**. +1. Click **Administration** in the left-side menu, **General**, and then **Organizations**. 1. Click the red **X** next to the organization that you want to delete. 1. Click **Delete**. @@ -115,6 +115,6 @@ Edit an organization when you want to change its name. **To edit an organization:** 1. Sign in to Grafana as a server administrator. -1. Click **Administration** in the left-side menu, and then **Organizations**. +1. Click **Administration** in the left-side menu, **General**, and then **Organizations**. 1. Click the organization you want to edit. 1. Update the organization name and click **Update**. diff --git a/docs/sources/administration/organization-preferences/index.md b/docs/sources/administration/organization-preferences/index.md index 211deadfa1760..913f6ca8345f4 100644 --- a/docs/sources/administration/organization-preferences/index.md +++ b/docs/sources/administration/organization-preferences/index.md @@ -44,6 +44,7 @@ Grafana server administrators and organization administrators can change organiz Follow these instructions if you are a Grafana Server Admin. 1. Click **Administration** in the left-side menu. +1. Click **General**. 1. Click **Organizations**. 1. In the organization list, click the name of the organization that you want to change. 1. In **Name**, enter the new organization name. @@ -54,6 +55,7 @@ Follow these instructions if you are a Grafana Server Admin. If you are an Organization Admin, follow these steps: 1. Click **Administration** in the left-side menu. +1. Click **General**. 1. Click **Default preferences**. 1. In **Organization name**, enter the new name. 1. Click **Update organization name**. @@ -63,7 +65,7 @@ If you are an Organization Admin, follow these steps: Organization administrators and team administrators can change team names and email addresses. To change the team name or email, follow these steps: -1. Click **Administration** in the left-side menu and select **Team**. +1. Click **Administration** in the left-side menu, **Users and access**, and select **Team**. 1. In the team list, click the name of the team that you want to change. 1. Click the **Settings** tab. 1. In the Team details section, you can edit the following: @@ -112,6 +114,7 @@ To see what the current settings are, refer to [View server settings]({{< relref Organization administrators can change the UI theme for all users in an organization. 1. Click **Administration** in the left-side menu. +1. Click **General**. 1. Click **Default preferences**. 1. In the Preferences section, select the UI theme. 1. Click **Save**. @@ -120,7 +123,7 @@ Organization administrators can change the UI theme for all users in an organiza Organization and team administrators can change the UI theme for all users on a team. -1. Click **Administration** in the left-side menu and select **Teams**. +1. Click **Administration** in the left-side menu, **Users and access**, and select **Teams**. 1. Click the team for which you want to change the UI theme. 1. Click the **Settings** tab. 1. In the Preferences section, select the UI theme. @@ -149,6 +152,7 @@ Grafana server administrators can choose a default timezone for all users on the Organization administrators can choose a default timezone for their organization. 1. Click **Administration** in the left-side menu. +1. Click **General**. 1. Click **Default preferences**. 1. Click to select an option in the **Timezone** list. **Default** is either the browser local timezone or the timezone selected at a higher level. 1. Click **Save**. @@ -157,7 +161,7 @@ Organization administrators can choose a default timezone for their organization Organization administrators and team administrators can choose a default timezone for all users on a team. -1. Click **Administration** in the left-side menu and select **Teams**. +1. Click **Administration** in the left-side menu, **Users and access**, and select **Teams**. 1. Click the team for which you want to change the timezone. 1. Click the **Settings** tab. 1. Click to select an option in the **Timezone** list. **Default** is either the browser local timezone or the timezone selected at a higher level. @@ -209,6 +213,7 @@ Organization administrators can choose a default home dashboard for their organi 1. Navigate to the dashboard you want to set as the home dashboard. 1. Click the star next to the dashboard title to mark the dashboard as a favorite if it is not already. 1. Click **Administration** in the left-side menu. +1. Click **General**. 1. Click **Default preferences**. 1. In the **Home Dashboard** field, select the dashboard that you want to use for your home dashboard. Options include all starred dashboards. 1. Click **Save**. @@ -219,7 +224,7 @@ Organization administrators and Team Admins can set a default home dashboard for 1. Navigate to the dashboard you want to set as the home dashboard. 1. Click the star next to the dashboard title to mark the dashboard as a favorite if it is not already. -1. Click **Administration** in the left-side menu and select **Teams**. +1. Click **Administration** in the left-side menu, **Users and access**, and select **Teams**. 1. Click the team for which you want to change the home dashboard. 1. Click the **Settings** tab. 1. In the **Home Dashboard** field, select the dashboard that you want to use for your home dashboard. Options include all starred dashboards. @@ -246,6 +251,7 @@ Grafana server administrators can change the default Grafana UI language for all Organization administrators can change the language for all users in an organization. 1. Click **Administration** in the left-side menu. +1. Click **General**. 1. Click **Default preferences**. 1. In the Preferences section, select an option in the **Language** dropdown. 1. Click **Save**. @@ -254,7 +260,7 @@ Organization administrators can change the language for all users in an organiza Organization and team administrators can set a default language for all users on a team. -1. Click **Administration** in the left-side menu and select **Teams**. +1. Click **Administration** in the left-side menu, **Users and access**, and select **Teams**. 1. Click the team for which you want to change the language. 1. Click the **Settings** tab. 1. In the Preferences section, select an option in the **Language** dropdown. diff --git a/docs/sources/administration/plugin-management/index.md b/docs/sources/administration/plugin-management/index.md index bf84e299280d1..2fd0da044320b 100644 --- a/docs/sources/administration/plugin-management/index.md +++ b/docs/sources/administration/plugin-management/index.md @@ -85,13 +85,13 @@ Before following the steps below, make sure you are logged in as a Grafana admin -Administrators can find the Plugin catalog at **Administration > Plugins**. +Administrators can find the Plugin catalog at **Administration > Plugins and data > Plugins**. ### Browse plugins To browse for available plugins: -1. In Grafana, click **Administration > Plugins** in the side navigation menu to view installed plugins. +1. In Grafana, click **Administration > Plugins and data > Plugins** in the side navigation menu to view installed plugins. 1. Click the **All** filter to browse all available plugins. 1. Click the **Data sources**, **Panels**, or **Applications** buttons to filter by plugin type. @@ -99,7 +99,7 @@ To browse for available plugins: To install a plugin: -1. In Grafana, click **Administration > Plugins** in the side navigation menu to view installed plugins. +1. In Grafana, click **Administration > Plugins and data > Plugins** in the side navigation menu to view installed plugins. 1. Click the **All** filter to browse all available plugins. 1. Browse and find a plugin. 1. Click on the plugin logo. @@ -111,7 +111,7 @@ When the update is complete, you see a confirmation message that the installatio To update a plugin: -1. In Grafana, click **Administration > Plugins** in the side navigation menu to view installed plugins. +1. In Grafana, click **Administration > Plugins and data > Plugins** in the side navigation menu to view installed plugins. 1. Click on the plugin logo. 1. Click **Update**. @@ -121,7 +121,7 @@ When the update is complete, you see a confirmation message that the update was To uninstall a plugin: -1. In Grafana, click **Administration > Plugins** in the side navigation menu to view installed plugins. +1. In Grafana, click **Administration > Plugins and data > Plugins** in the side navigation menu to view installed plugins. 1. Click on the plugin logo. 1. Click **Uninstall**. diff --git a/docs/sources/administration/roles-and-permissions/access-control/assign-rbac-roles/index.md b/docs/sources/administration/roles-and-permissions/access-control/assign-rbac-roles/index.md index a86cc311720be..5f442a00dfa60 100644 --- a/docs/sources/administration/roles-and-permissions/access-control/assign-rbac-roles/index.md +++ b/docs/sources/administration/roles-and-permissions/access-control/assign-rbac-roles/index.md @@ -54,14 +54,14 @@ In both cases, the assignment applies only to the user, team or service account For more information about switching organizations, refer to [Switch organizations]({{< relref "../../../user-management/user-preferences/_index.md#switch-organizations" >}}). -3. In the left-side menu, click **Administration** and then **Users**, **Teams**, or **Service accounts**. +3. In the left-side menu, click **Administration**, **Users and access**, and then **Users**, **Teams**, or **Service accounts**. 4. In the **Role** column, select the fixed role that you want to assign to the user, team, or service account. 5. Click **Update**. **To assign a fixed role as a server administrator:** 1. Sign in to Grafana as a server administrator. -1. Click **Administration** in the left-side menu, and then **Users**. +1. Click **Administration** in the left-side menu, **Users and access**, and then **Users**. 1. Click a user. 1. In the Organizations section, click **Change role**. 1. Select a role within an organization that you want to assign to the user. diff --git a/docs/sources/administration/stats-and-license/index.md b/docs/sources/administration/stats-and-license/index.md index 52255d2e0215a..1643fd505cf5e 100644 --- a/docs/sources/administration/stats-and-license/index.md +++ b/docs/sources/administration/stats-and-license/index.md @@ -34,7 +34,7 @@ If you are a Grafana server administrator, use the Settings tab to view the sett ### View server settings 1. Log in to your Grafana server with an account that has the Grafana Admin flag set. -1. Click **Administration** in the left-side menu, and then **Settings**. +1. Click **Administration** in the left-side menu, **General**, and then **Settings**. ### Available settings @@ -51,7 +51,7 @@ If you are a Grafana server admin, then you can view useful statistics about you ### View server stats 1. Log in to your Grafana server with an account that has the Grafana Admin flag set. -1. Click **Administration** in the left-side menu, and then **Stats and license**. +1. Click **Administration** in the left-side menu, **General**, and then **Stats and license**. ### Available stats diff --git a/docs/sources/administration/team-management/index.md b/docs/sources/administration/team-management/index.md index 9ade42e25554e..747f55e1971bf 100644 --- a/docs/sources/administration/team-management/index.md +++ b/docs/sources/administration/team-management/index.md @@ -45,7 +45,7 @@ A user can belong to multiple teams. To create a team: 1. Sign in to Grafana as an organization administrator or team administrator. -1. Click **Administration** in the left-side menu and select **Teams**. +1. Click **Administration** in the left-side menu, **Users and access**, and select **Teams**. 1. Click **New Team**. 1. Complete the fields and click **Create**. 1. Click **Add member**. @@ -59,7 +59,7 @@ Add a team member to an existing team whenever you want to provide access to tea To add a team member: 1. Sign in to Grafana as an organization administrator. -1. Click **Administration** in the left-side menu and select **Teams**. +1. Click **Administration** in the left-side menu, **Users and access**, and select **Teams**. 1. Click the name of the team to which you want to add members, and click **Add member**. 1. Locate and select a user. 1. Choose if you want to add the user as a team Member or an Admin. @@ -72,7 +72,7 @@ Complete this task when you want to add or modify team member permissions. To grant team member permissions: 1. Sign in to Grafana as an organization administrator or a team administrator. -1. Click **Administration** in the left-side menu and select **Teams**. +1. Click **Administration** in the left-side menu, **Users and access**, and select **Teams**. 1. Click the name of the team for which you want to add or modify team member permissions. 1. In the team member list, find and click the user that you want to change. You can use the search field to filter the list if necessary. 1. In the Permission column, select the new user permission level. @@ -84,7 +84,7 @@ You can remove a team member when you no longer want to apply team permissions t To remove a team member: 1. Sign in to Grafana as an organization administrator or team administrator. -1. Click **Administration** in the left-side menu and select **Teams**. +1. Click **Administration** in the left-side menu, **Users and access**, and select **Teams**. 1. Click a team from which you want to remove a user. 1. Click the **X** next to the name of the user. @@ -95,7 +95,7 @@ Delete a team when you no longer need it. This action permanently deletes the te To delete a team: 1. Sign in to Grafana as an organization administrator. -1. Click **Administration** in the left-side menu and select **Teams**. +1. Click **Administration** in the left-side menu, **Users and access**, and select **Teams**. 1. Click the **X** next to the name of the team. 1. Click **Delete**. @@ -106,7 +106,7 @@ See the complete list of teams in your Grafana organization. To view a list of teams: 1. Sign in to Grafana as an organization administrator or a team administrator. -1. Click **Administration** in the left-side menu and select **Teams**. +1. Click **Administration** in the left-side menu, **Users and access**, and select **Teams**. The role you use to sign in to Grafana determines how you see team lists. diff --git a/docs/sources/administration/user-management/manage-org-users/index.md b/docs/sources/administration/user-management/manage-org-users/index.md index c01c8546679a1..85c8a8bb88235 100644 --- a/docs/sources/administration/user-management/manage-org-users/index.md +++ b/docs/sources/administration/user-management/manage-org-users/index.md @@ -38,7 +38,7 @@ You can see a list of users with accounts in your Grafana organization. If neces **To view a list of organization users**: 1. Sign in to Grafana as an organization administrator. -1. Navigate to **Administration > Users**. +1. Navigate to **Administration > Users and access > Users**. {{% admonition type="note" %}} If you have [server administrator]({{< relref "../../roles-and-permissions/#grafana-server-administrators" >}}) permissions, you can also [view a global list of users]({{< relref "../server-user-management#view-a-list-of-users" >}}) in the Server Admin section of Grafana. @@ -59,7 +59,7 @@ Organization roles sync from the authentication provider on user sign-in. To pre **To change the organization role of a user**: 1. Sign in to Grafana as an organization administrator. -1. Navigate to **Administration > Users**. +1. Navigate to **Administration > Users and access > Users**. 1. Find the user account for which you want to change the role. If necessary, use the search field to filter the list. @@ -96,7 +96,7 @@ If you have [server administrator]({{< relref "../../roles-and-permissions/#graf > **Note**: It might be that you are currently in the proper organization and don't need to switch organizations. -1. Navigate to **Administration > Users**. +1. Navigate to **Administration > Users and access > Users**. 1. Click **Organization users**. 1. Click **Invite**. 1. Enter the following information: @@ -127,7 +127,7 @@ The **Pending Invites** button is only visible if there are unanswered invitatio **To manage a pending invitation**: 1. Sign in to Grafana as an organization administrator. -1. Navigate to **Administration > Users**. +1. Navigate to **Administration > Users and access > Users**. 1. Click **Pending Invites**. The **Pending Invites** button appears only when there are unaccepted invitations. @@ -149,7 +149,7 @@ This action does not remove the user account from the Grafana server. **To remove a user from an organization**: 1. Sign in to Grafana as an organization administrator. -1. Navigate to **Administration > Users**. +1. Navigate to **Administration > Users and access > Users**. 1. Find the user account that you want to remove from the organization. Use the search field to filter the list, if necessary. diff --git a/docs/sources/administration/user-management/server-user-management/_index.md b/docs/sources/administration/user-management/server-user-management/_index.md index 50a38bed56931..f5d14efcd25d4 100644 --- a/docs/sources/administration/user-management/server-user-management/_index.md +++ b/docs/sources/administration/user-management/server-user-management/_index.md @@ -39,7 +39,7 @@ You can see a list of users with accounts on your Grafana server. This action mi **To view a list of users**: 1. Sign in to Grafana as a server administrator. -1. Click **Administration** in the left-side menu, and then **Users**. +1. Click **Administration** in the left-side menu, **Users and access**, and then **Users**. {{% admonition type="note" %}} If you have [organization administrator]({{< relref "../../roles-and-permissions/#organization-roles" >}}) permissions and _not_ [server administrator]({{< relref "../../roles-and-permissions/#grafana-server-administrators" >}}) permissions, you can still [view of list of users in a given organization]({{< relref "../manage-org-users/#view-a-list-of-organization-users" >}}). @@ -56,7 +56,7 @@ View user details when you want to see login, and organizations and permissions **To view user details**: 1. Sign in to Grafana as a server administrator. -1. Click **Administration** in the left-side menu, and then **Users**. +1. Click **Administration** in the left-side menu, **Users and access**, and then **Users**. 1. Click a user. A user account contains the following sections. @@ -88,7 +88,7 @@ Edit a user account when you want to modify user login credentials, or delete, d **To edit a user account**: 1. Sign in to Grafana as a server administrator. -1. Click **Administration** in the left-side menu, and then **Users**. +1. Click **Administration** in the left-side menu, **Users and access**, and then **Users**. 1. Click a user. 1. Complete any of the following actions, as necessary. @@ -115,7 +115,7 @@ When you configure advanced authentication using Oauth, SAML, LDAP, or the Auth **To add a user**: 1. Sign in to Grafana as a server administrator. -1. Click **Administration** in the left-side menu, and then **Users**. +1. Click **Administration** in the left-side menu, **Users and access**, and then **Users**. 1. Click **New user**. 1. Complete the fields and click **Create user**. @@ -136,7 +136,7 @@ The force logout action can apply to one device that is logged in to Grafana, or - Ensure you have Grafana server administrator privileges 1. Sign in to Grafana as a server administrator. -1. Click **Administration** in the left-side menu, and then **Users**. +1. Click **Administration** in the left-side menu, **Users and access**, and then **Users**. 1. Click a user. 1. Scroll down to the Sessions section. 1. Perform one of the following actions: diff --git a/docs/sources/administration/user-management/server-user-management/add-remove-user-to-org/index.md b/docs/sources/administration/user-management/server-user-management/add-remove-user-to-org/index.md index 1d8f5a70ac3d9..04ddd0dc4909e 100644 --- a/docs/sources/administration/user-management/server-user-management/add-remove-user-to-org/index.md +++ b/docs/sources/administration/user-management/server-user-management/add-remove-user-to-org/index.md @@ -31,7 +31,7 @@ You are required to specify an Admin role for each organization. The first user **To add a user to an organization**: 1. Sign in to Grafana as a server administrator. -1. Click **Administration** in the left-side menu, and then **Users**. +1. Click **Administration** in the left-side menu, **Users and access**, and then **Users**. 1. Click a user. 1. In the Organizations section, click **Add user to organization**. 1. Select an organization and a role. @@ -57,7 +57,7 @@ Remove a user from an organization when they no longer require access to the das **To remove a user from an organization**: 1. Sign in to Grafana as a server administrator. -1. Click **Administration** in the left-side menu, and then **Users**. +1. Click **Administration** in the left-side menu, **Users and access**, and then **Users**. 1. Click a user. 1. In the Organization section, click **Remove from organization** next to the organization from which you want to remove the user. 1. Click **Confirm removal**. diff --git a/docs/sources/administration/user-management/server-user-management/assign-remove-server-admin-privileges/index.md b/docs/sources/administration/user-management/server-user-management/assign-remove-server-admin-privileges/index.md index d02cc9c19a30a..c017f104bdd66 100644 --- a/docs/sources/administration/user-management/server-user-management/assign-remove-server-admin-privileges/index.md +++ b/docs/sources/administration/user-management/server-user-management/assign-remove-server-admin-privileges/index.md @@ -27,7 +27,7 @@ Server administrators are "super-admins" with full permissions to create, read, **To assign or remove Grafana administrator privileges**: 1. Sign in to Grafana as a server administrator. -1. Click **Administration** in the left-side menu, and then **Users**. +1. Click **Administration** in the left-side menu, **Users and access**, and then **Users**. 1. Click a user. 1. In the Permissions section, next to Grafana Admin, click **Change**. 1. Click **Yes** or **No**, depending on whether or not you want this user to have the Grafana server administrator role. diff --git a/docs/sources/administration/user-management/server-user-management/change-user-org-permissions/index.md b/docs/sources/administration/user-management/server-user-management/change-user-org-permissions/index.md index 3a72c6b15b161..4569a5ac8f2c9 100644 --- a/docs/sources/administration/user-management/server-user-management/change-user-org-permissions/index.md +++ b/docs/sources/administration/user-management/server-user-management/change-user-org-permissions/index.md @@ -21,7 +21,7 @@ Update organization permissions when you want to enhance or restrict a user's ac **To change a user's organization permissions**: 1. Sign in to Grafana as a server administrator. -1. Click **Administration** in the left-side menu, and then **Users**. +1. Click **Administration** in the left-side menu, **Users and access**, and then **Users**. 1. Click a user. 1. In the Organizations section, click **Change role** for the role you want to change 1. Select another role. From 5892a64e9fe42fdd36be505400dfabcf83d5eedb Mon Sep 17 00:00:00 2001 From: Ashley Harrison Date: Thu, 2 Nov 2023 15:21:36 +0000 Subject: [PATCH 056/869] CustomScrollbar: Remove chevrons from scroll indicators (#77498) remove chevrons from scroll indicators --- .../CustomScrollbar/ScrollIndicators.tsx | 21 ++----------------- 1 file changed, 2 insertions(+), 19 deletions(-) diff --git a/packages/grafana-ui/src/components/CustomScrollbar/ScrollIndicators.tsx b/packages/grafana-ui/src/components/CustomScrollbar/ScrollIndicators.tsx index eed018c364821..b22711457ffca 100644 --- a/packages/grafana-ui/src/components/CustomScrollbar/ScrollIndicators.tsx +++ b/packages/grafana-ui/src/components/CustomScrollbar/ScrollIndicators.tsx @@ -1,11 +1,9 @@ import { css, cx } from '@emotion/css'; -import classNames from 'classnames'; import React, { useEffect, useRef, useState } from 'react'; import { GrafanaTheme2 } from '@grafana/data'; import { useStyles2 } from '../../themes'; -import { Icon } from '../Icon/Icon'; export const ScrollIndicators = ({ children }: React.PropsWithChildren<{}>) => { const [showScrollTopIndicator, setShowTopScrollIndicator] = useState(false); @@ -39,9 +37,7 @@ export const ScrollIndicators = ({ children }: React.PropsWithChildren<{}>) => { className={cx(styles.scrollIndicator, styles.scrollTopIndicator, { [styles.scrollIndicatorVisible]: showScrollTopIndicator, })} - > - -
    + />
    {children} @@ -51,9 +47,7 @@ export const ScrollIndicators = ({ children }: React.PropsWithChildren<{}>) => { className={cx(styles.scrollIndicator, styles.scrollBottomIndicator, { [styles.scrollIndicatorVisible]: showScrollBottomIndicator, })} - > - -
    + /> ); }; @@ -85,16 +79,5 @@ const getStyles = (theme: GrafanaTheme2) => { scrollIndicatorVisible: css({ opacity: 1, }), - scrollIcon: css({ - left: '50%', - position: 'absolute', - transform: 'translateX(-50%)', - }), - scrollTopIcon: css({ - top: 0, - }), - scrollBottomIcon: css({ - bottom: 0, - }), }; }; From e714c9303e73024dbf4689aa6f44d89654dad248 Mon Sep 17 00:00:00 2001 From: Kyle Cunningham Date: Thu, 2 Nov 2023 10:25:48 -0500 Subject: [PATCH 057/869] Timeseries to table transformation: Update Output Changes (#77415) * Break out labels into separate fields * More Updates * Minor test changes * Use 'A' for transformed refId * Make sure tests pass * Add additional test * Prettier * Remove dead comment * Update time field selection options * remove console.log --------- Co-authored-by: Victor Marin --- .../TimeSeriesTableTransformEditor.tsx | 68 ++++---- .../timeSeriesTableTransformer.test.ts | 85 ++++++---- .../timeSeriesTableTransformer.ts | 148 ++++++++++-------- 3 files changed, 171 insertions(+), 130 deletions(-) diff --git a/public/app/features/transformers/timeSeriesTable/TimeSeriesTableTransformEditor.tsx b/public/app/features/transformers/timeSeriesTable/TimeSeriesTableTransformEditor.tsx index 13eefd29ef43d..6e996eed2a95f 100644 --- a/public/app/features/transformers/timeSeriesTable/TimeSeriesTableTransformEditor.tsx +++ b/public/app/features/transformers/timeSeriesTable/TimeSeriesTableTransformEditor.tsx @@ -7,9 +7,10 @@ import { ReducerID, isReducerID, SelectableValue, - getFieldDisplayName, + Field, + FieldType, } from '@grafana/data'; -import { InlineFieldRow, InlineField, StatsPicker, InlineSwitch, Select } from '@grafana/ui'; +import { InlineFieldRow, InlineField, StatsPicker, Select, InlineLabel } from '@grafana/ui'; import { timeSeriesTableTransformer, @@ -22,19 +23,8 @@ export function TimeSeriesTableTransformEditor({ options, onChange, }: TransformerUIProps) { - const timeFields: Array> = []; const refIdMap = getRefData(input); - // Retrieve time fields - for (const frame of input) { - for (const field of frame.fields) { - if (field.type === 'time') { - const name = getFieldDisplayName(field, frame, input); - timeFields.push({ label: name, value: name }); - } - } - } - const onSelectTimefield = useCallback( (refId: string, value: SelectableValue) => { const val = value?.value !== undefined ? value.value : ''; @@ -65,32 +55,45 @@ export function TimeSeriesTableTransformEditor({ [onChange, options] ); - const onMergeSeriesToggle = useCallback( - (refId: string) => { - const mergeSeries = options[refId]?.mergeSeries !== undefined ? !options[refId].mergeSeries : false; - onChange({ - ...options, - [refId]: { - ...options[refId], - mergeSeries, - }, - }); - }, - [onChange, options] - ); - let configRows = []; for (const refId of Object.keys(refIdMap)) { + // Get time fields for the current refId + const timeFields: Record> = {}; + const timeValues: Array> = []; + + // Get a map of time fields, we map + // by field name and assume that time fields + // in the same query with the same name + // are the same + for (const frame of input) { + if (frame.refId === refId) { + for (const field of frame.fields) { + if (field.type === 'time') { + timeFields[field.name] = field; + } + } + } + } + + for (const timeField of Object.values(timeFields)) { + const { name } = timeField; + timeValues.push({ label: name, value: name }); + } + configRows.push( + + {`Trend #${refId}`} + { readOnly={readOnly} disabled={ !getSupportedTransTypeDetails(watch(`config.transformations.${index}.type`)) - .showExpression + .expressionDetails.show } id={`config.transformations.${fieldVal.id}.expression`} /> @@ -221,7 +223,8 @@ export const TransformationsEditor = (props: Props) => { defaultValue={fieldVal.mapValue} readOnly={readOnly} disabled={ - !getSupportedTransTypeDetails(watch(`config.transformations.${index}.type`)).showMapValue + !getSupportedTransTypeDetails(watch(`config.transformations.${index}.type`)) + .mapValueDetails.show } id={`config.transformations.${fieldVal.id}.mapValue`} /> @@ -266,48 +269,3 @@ export const TransformationsEditor = (props: Props) => { ); }; - -interface SupportedTransformationTypeDetails { - label: string; - value: string; - description?: string; - showExpression: boolean; - showMapValue: boolean; - requireExpression?: boolean; -} - -function getSupportedTransTypeDetails(transType: SupportedTransformationType): SupportedTransformationTypeDetails { - switch (transType) { - case SupportedTransformationType.Logfmt: - return { - label: 'Logfmt', - value: SupportedTransformationType.Logfmt, - description: 'Parse provided field with logfmt to get variables', - showExpression: false, - showMapValue: false, - }; - case SupportedTransformationType.Regex: - return { - label: 'Regular expression', - value: SupportedTransformationType.Regex, - description: - 'Field will be parsed with regex. Use named capture groups to return multiple variables, or a single unnamed capture group to add variable to named map value.', - showExpression: true, - showMapValue: true, - requireExpression: true, - }; - default: - return { label: transType, value: transType, showExpression: false, showMapValue: false }; - } -} - -const getTransformOptions = () => { - return Object.values(SupportedTransformationType).map((transformationType) => { - const transType = getSupportedTransTypeDetails(transformationType); - return { - label: transType.label, - value: transType.value, - description: transType.description, - }; - }); -}; diff --git a/public/app/features/correlations/Forms/types.ts b/public/app/features/correlations/Forms/types.ts index d3e10e581c456..747ef8f97ba9b 100644 --- a/public/app/features/correlations/Forms/types.ts +++ b/public/app/features/correlations/Forms/types.ts @@ -17,3 +17,67 @@ export type TransformationDTO = { expression?: string; mapValue?: string; }; + +export interface TransformationFieldDetails { + show: boolean; + required?: boolean; + helpText?: string; +} + +interface SupportedTransformationTypeDetails { + label: string; + value: SupportedTransformationType; + description?: string; + expressionDetails: TransformationFieldDetails; + mapValueDetails: TransformationFieldDetails; +} + +export function getSupportedTransTypeDetails( + transType: SupportedTransformationType +): SupportedTransformationTypeDetails { + switch (transType) { + case SupportedTransformationType.Logfmt: + return { + label: 'Logfmt', + value: SupportedTransformationType.Logfmt, + description: 'Parse provided field with logfmt to get variables', + expressionDetails: { show: false }, + mapValueDetails: { show: false }, + }; + case SupportedTransformationType.Regex: + return { + label: 'Regular expression', + value: SupportedTransformationType.Regex, + description: + 'Field will be parsed with regex. Use named capture groups to return multiple variables, or a single unnamed capture group to add variable to named map value. Regex is case insensitive.', + expressionDetails: { + show: true, + required: true, + helpText: 'Use capture groups to extract a portion of the field.', + }, + mapValueDetails: { + show: true, + required: false, + helpText: 'Defines the name of the variable if the capture group is not named.', + }, + }; + default: + return { + label: transType, + value: transType, + expressionDetails: { show: false }, + mapValueDetails: { show: false }, + }; + } +} + +export const getTransformOptions = () => { + return Object.values(SupportedTransformationType).map((transformationType) => { + const transType = getSupportedTransTypeDetails(transformationType); + return { + label: transType.label, + value: transType.value, + description: transType.description, + }; + }); +}; diff --git a/public/app/features/correlations/utils.ts b/public/app/features/correlations/utils.ts index cba91591e9263..14abefccc0ddc 100644 --- a/public/app/features/correlations/utils.ts +++ b/public/app/features/correlations/utils.ts @@ -1,7 +1,8 @@ import { lastValueFrom } from 'rxjs'; import { DataFrame, DataLinkConfigOrigin } from '@grafana/data'; -import { getBackendSrv } from '@grafana/runtime'; +import { getBackendSrv, getDataSourceSrv } from '@grafana/runtime'; +import { ExploreItemState } from 'app/types'; import { formatValueName } from '../explore/PrometheusListView/ItemLabels'; @@ -90,3 +91,19 @@ export const createCorrelation = async ( ): Promise => { return getBackendSrv().post(`/api/datasources/uid/${sourceUID}/correlations`, correlation); }; + +const getDSInstanceForPane = async (pane: ExploreItemState) => { + if (pane.datasourceInstance?.meta.mixed) { + return await getDataSourceSrv().get(pane.queries[0].datasource); + } else { + return pane.datasourceInstance; + } +}; + +export const generateDefaultLabel = async (sourcePane: ExploreItemState, targetPane: ExploreItemState) => { + return Promise.all([getDSInstanceForPane(sourcePane), getDSInstanceForPane(targetPane)]).then((dsInstances) => { + return dsInstances[0]?.name !== undefined && dsInstances[1]?.name !== undefined + ? `${dsInstances[0]?.name} to ${dsInstances[1]?.name}` + : ''; + }); +}; diff --git a/public/app/features/explore/CorrelationEditorModeBar.tsx b/public/app/features/explore/CorrelationEditorModeBar.tsx index e4f4a761b9e79..c714b951c07ff 100644 --- a/public/app/features/explore/CorrelationEditorModeBar.tsx +++ b/public/app/features/explore/CorrelationEditorModeBar.tsx @@ -9,36 +9,84 @@ import { Button, HorizontalGroup, Icon, Tooltip, useStyles2 } from '@grafana/ui' import { CORRELATION_EDITOR_POST_CONFIRM_ACTION, ExploreItemState, useDispatch, useSelector } from 'app/types'; import { CorrelationUnsavedChangesModal } from './CorrelationUnsavedChangesModal'; +import { showModalMessage } from './correlationEditLogic'; import { saveCurrentCorrelation } from './state/correlations'; import { changeDatasource } from './state/datasource'; import { changeCorrelationHelperData } from './state/explorePane'; import { changeCorrelationEditorDetails, splitClose } from './state/main'; import { runQueries } from './state/query'; -import { selectCorrelationDetails } from './state/selectors'; +import { selectCorrelationDetails, selectIsHelperShowing } from './state/selectors'; export const CorrelationEditorModeBar = ({ panes }: { panes: Array<[string, ExploreItemState]> }) => { const dispatch = useDispatch(); const styles = useStyles2(getStyles); const correlationDetails = useSelector(selectCorrelationDetails); - const [showSavePrompt, setShowSavePrompt] = useState(false); + const isHelperShowing = useSelector(selectIsHelperShowing); + const [saveMessage, setSaveMessage] = useState(undefined); // undefined means do not show // handle refreshing and closing the tab - useBeforeUnload(correlationDetails?.dirty || false, 'Save correlation?'); + useBeforeUnload(correlationDetails?.correlationDirty || false, 'Save correlation?'); + useBeforeUnload( + (!correlationDetails?.correlationDirty && correlationDetails?.queryEditorDirty) || false, + 'The query editor was changed. Save correlation before continuing?' + ); - // handle exiting (staying within explore) + // decide if we are displaying prompt, perform action if not useEffect(() => { - if (correlationDetails?.isExiting && correlationDetails?.dirty) { - setShowSavePrompt(true); - } else if (correlationDetails?.isExiting && !correlationDetails?.dirty) { - dispatch( - changeCorrelationEditorDetails({ - editorMode: false, - dirty: false, - isExiting: false, - }) - ); + if (correlationDetails?.isExiting) { + const { correlationDirty, queryEditorDirty } = correlationDetails; + let isActionLeft = undefined; + let action = undefined; + if (correlationDetails.postConfirmAction) { + isActionLeft = correlationDetails.postConfirmAction.isActionLeft; + action = correlationDetails.postConfirmAction.action; + } else { + // closing the editor only + action = CORRELATION_EDITOR_POST_CONFIRM_ACTION.CLOSE_EDITOR; + isActionLeft = false; + } + + const modalMessage = showModalMessage(action, isActionLeft, correlationDirty, queryEditorDirty); + if (modalMessage !== undefined) { + setSaveMessage(modalMessage); + } else { + // if no prompt, perform action + if ( + action === CORRELATION_EDITOR_POST_CONFIRM_ACTION.CHANGE_DATASOURCE && + correlationDetails.postConfirmAction + ) { + const { exploreId, changeDatasourceUid } = correlationDetails?.postConfirmAction; + if (exploreId && changeDatasourceUid) { + dispatch(changeDatasource(exploreId, changeDatasourceUid, { importQueries: true })); + dispatch( + changeCorrelationEditorDetails({ + isExiting: false, + }) + ); + } + } else if ( + action === CORRELATION_EDITOR_POST_CONFIRM_ACTION.CLOSE_PANE && + correlationDetails.postConfirmAction + ) { + const { exploreId } = correlationDetails?.postConfirmAction; + if (exploreId !== undefined) { + dispatch(splitClose(exploreId)); + dispatch( + changeCorrelationEditorDetails({ + isExiting: false, + }) + ); + } + } else if (action === CORRELATION_EDITOR_POST_CONFIRM_ACTION.CLOSE_EDITOR) { + dispatch( + changeCorrelationEditorDetails({ + editorMode: false, + }) + ); + } + } } - }, [correlationDetails?.dirty, correlationDetails?.isExiting, dispatch]); + }, [correlationDetails, dispatch, isHelperShowing]); // clear data when unmounted useUnmount(() => { @@ -46,7 +94,7 @@ export const CorrelationEditorModeBar = ({ panes }: { panes: Array<[string, Expl changeCorrelationEditorDetails({ editorMode: false, isExiting: false, - dirty: false, + correlationDirty: false, label: undefined, description: undefined, canSave: false, @@ -64,15 +112,12 @@ export const CorrelationEditorModeBar = ({ panes }: { panes: Array<[string, Expl }); }); - const closePaneAndReset = (exploreId: string) => { - setShowSavePrompt(false); - dispatch(splitClose(exploreId)); - reportInteraction('grafana_explore_split_view_closed'); + const resetEditor = () => { dispatch( changeCorrelationEditorDetails({ editorMode: true, isExiting: false, - dirty: false, + correlationDirty: false, label: undefined, description: undefined, canSave: false, @@ -90,43 +135,39 @@ export const CorrelationEditorModeBar = ({ panes }: { panes: Array<[string, Expl }); }; - const changeDatasourceAndReset = (exploreId: string, datasourceUid: string) => { - setShowSavePrompt(false); + const closePane = (exploreId: string) => { + setSaveMessage(undefined); + dispatch(splitClose(exploreId)); + reportInteraction('grafana_explore_split_view_closed'); + }; + + const changeDatasourcePostAction = (exploreId: string, datasourceUid: string) => { + setSaveMessage(undefined); dispatch(changeDatasource(exploreId, datasourceUid, { importQueries: true })); - dispatch( - changeCorrelationEditorDetails({ - editorMode: true, - isExiting: false, - dirty: false, - label: undefined, - description: undefined, - canSave: false, - }) - ); - panes.forEach((pane) => { - dispatch( - changeCorrelationHelperData({ - exploreId: pane[0], - correlationEditorHelperData: undefined, - }) - ); - }); }; - const saveCorrelation = (skipPostConfirmAction: boolean) => { - dispatch(saveCurrentCorrelation(correlationDetails?.label, correlationDetails?.description)); + const saveCorrelationPostAction = (skipPostConfirmAction: boolean) => { + dispatch( + saveCurrentCorrelation( + correlationDetails?.label, + correlationDetails?.description, + correlationDetails?.transformations + ) + ); if (!skipPostConfirmAction && correlationDetails?.postConfirmAction !== undefined) { const { exploreId, action, changeDatasourceUid } = correlationDetails?.postConfirmAction; if (action === CORRELATION_EDITOR_POST_CONFIRM_ACTION.CLOSE_PANE) { - closePaneAndReset(exploreId); + closePane(exploreId); + resetEditor(); } else if ( action === CORRELATION_EDITOR_POST_CONFIRM_ACTION.CHANGE_DATASOURCE && changeDatasourceUid !== undefined ) { - changeDatasourceAndReset(exploreId, changeDatasourceUid); + changeDatasource(exploreId, changeDatasourceUid); + resetEditor(); } } else { - dispatch(changeCorrelationEditorDetails({ editorMode: false, dirty: false, isExiting: false })); + dispatch(changeCorrelationEditorDetails({ editorMode: false, correlationDirty: false, isExiting: false })); } }; @@ -138,7 +179,7 @@ export const CorrelationEditorModeBar = ({ panes }: { panes: Array<[string, Expl if ( location.pathname !== '/explore' && (correlationDetails?.editorMode || false) && - (correlationDetails?.dirty || false) + (correlationDetails?.correlationDirty || false) ) { return 'You have unsaved correlation data. Continue?'; } else { @@ -147,19 +188,20 @@ export const CorrelationEditorModeBar = ({ panes }: { panes: Array<[string, Expl }} /> - {showSavePrompt && ( + {saveMessage !== undefined && ( { if (correlationDetails?.postConfirmAction !== undefined) { const { exploreId, action, changeDatasourceUid } = correlationDetails?.postConfirmAction; if (action === CORRELATION_EDITOR_POST_CONFIRM_ACTION.CLOSE_PANE) { - closePaneAndReset(exploreId); + closePane(exploreId); } else if ( action === CORRELATION_EDITOR_POST_CONFIRM_ACTION.CHANGE_DATASOURCE && changeDatasourceUid !== undefined ) { - changeDatasourceAndReset(exploreId, changeDatasourceUid); + changeDatasourcePostAction(exploreId, changeDatasourceUid); } + dispatch(changeCorrelationEditorDetails({ isExiting: false })); } else { // exit correlations mode // if we are discarding the in progress correlation, reset everything @@ -167,7 +209,7 @@ export const CorrelationEditorModeBar = ({ panes }: { panes: Array<[string, Expl dispatch( changeCorrelationEditorDetails({ editorMode: false, - dirty: false, + correlationDirty: false, isExiting: false, }) ); @@ -176,11 +218,12 @@ export const CorrelationEditorModeBar = ({ panes }: { panes: Array<[string, Expl onCancel={() => { // if we are cancelling the exit, set the editor mode back to true and hide the prompt dispatch(changeCorrelationEditorDetails({ isExiting: false })); - setShowSavePrompt(false); + setSaveMessage(undefined); }} onSave={() => { - saveCorrelation(false); + saveCorrelationPostAction(false); }} + message={saveMessage} /> )}
    @@ -194,7 +237,7 @@ export const CorrelationEditorModeBar = ({ panes }: { panes: Array<[string, Expl fill="outline" className={correlationDetails?.canSave ? styles.buttonColor : styles.disabledButtonColor} onClick={() => { - saveCorrelation(true); + saveCorrelationPostAction(true); }} > Save diff --git a/public/app/features/explore/CorrelationHelper.tsx b/public/app/features/explore/CorrelationHelper.tsx index fecf3d03af5ce..c0b704c350279 100644 --- a/public/app/features/explore/CorrelationHelper.tsx +++ b/public/app/features/explore/CorrelationHelper.tsx @@ -1,14 +1,35 @@ +import { css } from '@emotion/css'; import React, { useState, useEffect, useId } from 'react'; import { useForm } from 'react-hook-form'; +import { useAsync } from 'react-use'; -import { ExploreCorrelationHelperData } from '@grafana/data'; -import { Collapse, Alert, Field, Input } from '@grafana/ui'; +import { DataLinkTransformationConfig, ExploreCorrelationHelperData, GrafanaTheme2 } from '@grafana/data'; +import { + Collapse, + Alert, + Field, + Input, + Button, + Card, + IconButton, + useStyles2, + DeleteButton, + Tooltip, + Icon, + Stack, +} from '@grafana/ui'; import { useDispatch, useSelector } from 'app/types'; +import { getTransformationVars } from '../correlations/transformations'; +import { generateDefaultLabel } from '../correlations/utils'; + +import { CorrelationTransformationAddModal } from './CorrelationTransformationAddModal'; +import { changeCorrelationHelperData } from './state/explorePane'; import { changeCorrelationEditorDetails } from './state/main'; -import { selectCorrelationDetails } from './state/selectors'; +import { selectCorrelationDetails, selectPanes } from './state/selectors'; interface Props { + exploreId: string; correlations: ExploreCorrelationHelperData; } @@ -17,60 +38,242 @@ interface FormValues { description: string; } -export const CorrelationHelper = ({ correlations }: Props) => { +export const CorrelationHelper = ({ exploreId, correlations }: Props) => { const dispatch = useDispatch(); - const { register, watch } = useForm(); - const [isOpen, setIsOpen] = useState(false); + const styles = useStyles2(getStyles); + const panes = useSelector(selectPanes); + const panesVals = Object.values(panes); + const { value: defaultLabel, loading: loadingLabel } = useAsync( + async () => await generateDefaultLabel(panesVals[0]!, panesVals[1]!), + [ + panesVals[0]?.datasourceInstance, + panesVals[0]?.queries[0].datasource, + panesVals[1]?.datasourceInstance, + panesVals[1]?.queries[0].datasource, + ] + ); + + const { register, watch, getValues, setValue } = useForm(); + const [isLabelDescOpen, setIsLabelDescOpen] = useState(false); + const [isTransformOpen, setIsTransformOpen] = useState(false); + const [showTransformationAddModal, setShowTransformationAddModal] = useState(false); + const [transformations, setTransformations] = useState([]); + const [transformationIdxToEdit, setTransformationIdxToEdit] = useState(undefined); const correlationDetails = useSelector(selectCorrelationDetails); const id = useId(); + // only fire once on mount to allow save button to enable / disable when unmounted + useEffect(() => { + dispatch(changeCorrelationEditorDetails({ canSave: true })); + return () => { + dispatch(changeCorrelationEditorDetails({ canSave: false })); + }; + }, [dispatch]); + + useEffect(() => { + if ( + !loadingLabel && + defaultLabel !== undefined && + !correlationDetails?.correlationDirty && + getValues('label') !== '' + ) { + setValue('label', defaultLabel); + } + }, [correlationDetails?.correlationDirty, defaultLabel, getValues, loadingLabel, setValue]); + useEffect(() => { const subscription = watch((value) => { - let dirty = false; + let dirty = correlationDetails?.correlationDirty || false; - if (!correlationDetails?.dirty && (value.label !== '' || value.description !== '')) { + if (!dirty && (value.label !== defaultLabel || value.description !== '')) { dirty = true; - } else if (correlationDetails?.dirty && value.label.trim() === '' && value.description.trim() === '') { + } else if (dirty && value.label === defaultLabel && value.description.trim() === '') { dirty = false; } - dispatch(changeCorrelationEditorDetails({ label: value.label, description: value.description, dirty: dirty })); + dispatch( + changeCorrelationEditorDetails({ label: value.label, description: value.description, correlationDirty: dirty }) + ); }); return () => subscription.unsubscribe(); - }, [correlationDetails?.dirty, dispatch, watch]); + }, [correlationDetails?.correlationDirty, defaultLabel, dispatch, watch]); - // only fire once on mount to allow save button to enable / disable when unmounted useEffect(() => { - dispatch(changeCorrelationEditorDetails({ canSave: true })); + const dirty = + !correlationDetails?.correlationDirty && transformations.length > 0 ? true : correlationDetails?.correlationDirty; + dispatch(changeCorrelationEditorDetails({ transformations: transformations, correlationDirty: dirty })); + let transVarRecords: Record = {}; + transformations.forEach((transformation) => { + const transformationVars = getTransformationVars( + { + type: transformation.type, + expression: transformation.expression, + mapValue: transformation.mapValue, + }, + correlations.vars[transformation.field!], + transformation.field! + ); - return () => { - dispatch(changeCorrelationEditorDetails({ canSave: false })); - }; - }, [dispatch]); + Object.keys(transformationVars).forEach((key) => { + transVarRecords[key] = transformationVars[key]?.value; + }); + }); + + dispatch( + changeCorrelationHelperData({ + exploreId: exploreId, + correlationEditorHelperData: { + resultField: correlations.resultField, + origVars: correlations.origVars, + vars: { ...correlations.origVars, ...transVarRecords }, + }, + }) + ); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [dispatch, transformations]); return ( - - The correlation link will appear by the {correlations.resultField} field. You can use the following - variables to set up your correlations: -
    -        {Object.entries(correlations.vars).map((entry) => {
    -          return `\$\{${entry[0]}\} = ${entry[1]}\n`;
    -        })}
    -      
    - { - setIsOpen(!isOpen); - }} - label="Label/Description" - > - - - - - - - -
    + <> + {showTransformationAddModal && ( + { + setTransformationIdxToEdit(undefined); + setShowTransformationAddModal(false); + }} + onSave={(transformation: DataLinkTransformationConfig) => { + if (transformationIdxToEdit !== undefined) { + const editTransformations = [...transformations]; + editTransformations[transformationIdxToEdit] = transformation; + setTransformations(editTransformations); + setTransformationIdxToEdit(undefined); + } else { + setTransformations([...transformations, transformation]); + } + setShowTransformationAddModal(false); + }} + fieldList={correlations.vars} + transformationToEdit={ + transformationIdxToEdit !== undefined ? transformations[transformationIdxToEdit] : undefined + } + /> + )} + + The correlation link will appear by the {correlations.resultField} field. You can use the following + variables to set up your correlations: +
    +          {Object.entries(correlations.vars).map((entry) => {
    +            return `\$\{${entry[0]}\} = ${entry[1]}\n`;
    +          })}
    +        
    + { + setIsLabelDescOpen(!isLabelDescOpen); + }} + label={ + + Label / Description + {!isLabelDescOpen && !loadingLabel && ( + {`Label: ${getValues('label') || defaultLabel}`} + )} + + } + > + + { + if (getValues('label') === '' && defaultLabel !== undefined) { + setValue('label', defaultLabel); + } + }} + /> + + + + + + { + setIsTransformOpen(!isTransformOpen); + }} + label={ + + Transformations + + + + + } + > + + {transformations.map((transformation, i) => { + const { type, field, expression, mapValue } = transformation; + const detailsString = [ + (mapValue ?? '').length > 0 ? `Variable name: ${mapValue}` : undefined, + (expression ?? '').length > 0 ? ( + <> + Expression: {expression} + + ) : undefined, + ].filter((val) => val); + return ( + + + {field}: {type} + + {detailsString.length > 0 && ( + {detailsString} + )} + + { + setTransformationIdxToEdit(i); + setShowTransformationAddModal(true); + }} + /> + setTransformations(transformations.filter((_, idx) => i !== idx))} + closeOnConfirm + /> + + + ); + })} + +
    + ); }; + +const getStyles = (theme: GrafanaTheme2) => { + return { + labelCollapseDetails: css({ + marginLeft: theme.spacing(2), + ...theme.typography['bodySmall'], + fontStyle: 'italic', + }), + transformationAction: css({ + marginBottom: theme.spacing(2), + }), + transformationMeta: css({ + alignItems: 'baseline', + }), + }; +}; diff --git a/public/app/features/explore/CorrelationTransformationAddModal.tsx b/public/app/features/explore/CorrelationTransformationAddModal.tsx new file mode 100644 index 0000000000000..63388852e3e79 --- /dev/null +++ b/public/app/features/explore/CorrelationTransformationAddModal.tsx @@ -0,0 +1,240 @@ +import { css } from '@emotion/css'; +import React, { useId, useState, useMemo, useEffect } from 'react'; +import Highlighter from 'react-highlight-words'; +import { useForm } from 'react-hook-form'; + +import { DataLinkTransformationConfig, ScopedVars } from '@grafana/data'; +import { Button, Field, Icon, Input, InputControl, Label, Modal, Select, Tooltip, Stack } from '@grafana/ui'; + +import { + getSupportedTransTypeDetails, + getTransformOptions, + TransformationFieldDetails, +} from '../correlations/Forms/types'; +import { getTransformationVars } from '../correlations/transformations'; + +interface CorrelationTransformationAddModalProps { + onCancel: () => void; + onSave: (transformation: DataLinkTransformationConfig) => void; + fieldList: Record; + transformationToEdit?: DataLinkTransformationConfig; +} + +interface ShowFormFields { + expressionDetails: TransformationFieldDetails; + mapValueDetails: TransformationFieldDetails; +} + +const LabelWithTooltip = ({ label, tooltipText }: { label: string; tooltipText: string }) => ( + + + + + + +); + +export const CorrelationTransformationAddModal = ({ + onSave, + onCancel, + fieldList, + transformationToEdit, +}: CorrelationTransformationAddModalProps) => { + const [exampleValue, setExampleValue] = useState(undefined); + const [transformationVars, setTransformationVars] = useState({}); + const [formFieldsVis, setFormFieldsVis] = useState({ + mapValueDetails: { show: false }, + expressionDetails: { show: false }, + }); + const [isExpValid, setIsExpValid] = useState(false); // keep the highlighter from erroring on bad expressions + const [validToSave, setValidToSave] = useState(false); + const { getValues, control, register, watch } = useForm({ + defaultValues: useMemo(() => { + if (transformationToEdit) { + const exampleVal = fieldList[transformationToEdit?.field!]; + setExampleValue(exampleVal); + if (transformationToEdit?.expression) { + setIsExpValid(true); + } + const transformationTypeDetails = getSupportedTransTypeDetails(transformationToEdit?.type!); + setFormFieldsVis({ + mapValueDetails: transformationTypeDetails.mapValueDetails, + expressionDetails: transformationTypeDetails.expressionDetails, + }); + + const transformationVars = getTransformationVars( + { + type: transformationToEdit?.type!, + expression: transformationToEdit?.expression, + mapValue: transformationToEdit?.mapValue, + }, + exampleVal || '', + transformationToEdit?.field! + ); + setTransformationVars({ ...transformationVars }); + setValidToSave(true); + return { + type: transformationToEdit?.type, + field: transformationToEdit?.field, + mapValue: transformationToEdit?.mapValue, + expression: transformationToEdit?.expression, + }; + } else { + return undefined; + } + }, [fieldList, transformationToEdit]), + }); + const id = useId(); + + useEffect(() => { + const subscription = watch((formValues) => { + const expression = formValues.expression; + let isExpressionValid = false; + if (expression !== undefined) { + isExpressionValid = true; + try { + new RegExp(expression); + } catch (e) { + isExpressionValid = false; + } + } else { + isExpressionValid = !formFieldsVis.expressionDetails.show; + } + setIsExpValid(isExpressionValid); + const transformationVars = getTransformationVars( + { + type: formValues.type, + expression: isExpressionValid ? expression : '', + mapValue: formValues.mapValue, + }, + fieldList[formValues.field!] || '', + formValues.field! + ); + + const transKeys = Object.keys(transformationVars); + setTransformationVars(transKeys.length > 0 ? { ...transformationVars } : {}); + + if (transKeys.length === 0 || !isExpressionValid) { + setValidToSave(false); + } else { + setValidToSave(true); + } + }); + return () => subscription.unsubscribe(); + }, [fieldList, formFieldsVis.expressionDetails.show, watch]); + + return ( + +

    + A transformation extracts variables out of a single field. These variables will be available along with your + field variables. +

    + + ( + { + onChange(value.value); + const transformationTypeDetails = getSupportedTransTypeDetails(value.value!); + setFormFieldsVis({ + mapValueDetails: transformationTypeDetails.mapValueDetails, + expressionDetails: transformationTypeDetails.expressionDetails, + }); + }} + options={getTransformOptions()} + aria-label="type" + /> + )} + name={`type` as const} + /> + + {formFieldsVis.expressionDetails.show && ( + + ) : ( + 'Expression' + ) + } + htmlFor={`${id}-expression`} + required={formFieldsVis.expressionDetails.required} + > + + + )} + {formFieldsVis.mapValueDetails.show && ( + + ) : ( + 'Variable Name' + ) + } + htmlFor={`${id}-mapValue`} + > + + + )} + {Object.entries(transformationVars).length > 0 && ( + <> + This transformation will add the following variables: +
    +                {Object.entries(transformationVars).map((entry) => {
    +                  return `\$\{${entry[0]}\} = ${entry[1]?.value}\n`;
    +                })}
    +              
    + + )} + + )} + + + + +
    + ); +}; diff --git a/public/app/features/explore/CorrelationUnsavedChangesModal.tsx b/public/app/features/explore/CorrelationUnsavedChangesModal.tsx index 19100150fd99a..45883d7a87b73 100644 --- a/public/app/features/explore/CorrelationUnsavedChangesModal.tsx +++ b/public/app/features/explore/CorrelationUnsavedChangesModal.tsx @@ -4,27 +4,28 @@ import React from 'react'; import { Button, Modal } from '@grafana/ui'; interface UnsavedChangesModalProps { + message: string; onDiscard: () => void; onCancel: () => void; onSave: () => void; } -export const CorrelationUnsavedChangesModal = ({ onSave, onDiscard, onCancel }: UnsavedChangesModalProps) => { +export const CorrelationUnsavedChangesModal = ({ onSave, onDiscard, onCancel, message }: UnsavedChangesModalProps) => { return ( -
    Do you want to save changes to this Correlation?
    +
    {message}
    diff --git a/public/app/core/components/RolePicker/RolePickerMenu.tsx b/public/app/core/components/RolePicker/RolePickerMenu.tsx index 1da2a057b52ee..1bfe1d63ce7cc 100644 --- a/public/app/core/components/RolePicker/RolePickerMenu.tsx +++ b/public/app/core/components/RolePicker/RolePickerMenu.tsx @@ -63,6 +63,7 @@ interface RolePickerMenuProps { updateDisabled?: boolean; apply?: boolean; offset: { vertical: number; horizontal: number }; + menuLeft?: boolean; } export const RolePickerMenu = ({ @@ -78,6 +79,7 @@ export const RolePickerMenu = ({ onUpdate, updateDisabled, offset, + menuLeft, apply, }: RolePickerMenuProps): JSX.Element => { const [selectedOptions, setSelectedOptions] = useState(appliedRoles); @@ -206,11 +208,12 @@ export const RolePickerMenu = ({ className={cx( styles.menu, customStyles.menuWrapper, - { [customStyles.menuLeft]: offset.horizontal > 0 }, - css` - bottom: ${offset.vertical > 0 ? `${offset.vertical}px` : 'unset'}; - top: ${offset.vertical < 0 ? `${Math.abs(offset.vertical)}px` : 'unset'}; - ` + { [customStyles.menuLeft]: menuLeft }, + css({ + top: `${offset.vertical}px`, + left: !menuLeft ? `${offset.horizontal}px` : 'unset', + right: menuLeft ? `${offset.horizontal}px` : 'unset', + }) )} >
    @@ -248,7 +251,7 @@ export const RolePickerMenu = ({ selectedOptions={selectedOptions} onRoleChange={onChange} onClearSubMenu={onClearSubMenu} - showOnLeftSubMenu={offset.horizontal > 0} + showOnLeftSubMenu={menuLeft} /> ))} diff --git a/public/app/core/components/RolePicker/constants.ts b/public/app/core/components/RolePicker/constants.ts index 3b6c35447dadb..f2c0f8818fafb 100644 --- a/public/app/core/components/RolePicker/constants.ts +++ b/public/app/core/components/RolePicker/constants.ts @@ -1,3 +1,11 @@ -export const MENU_MAX_HEIGHT = 300; // max height for the picker's dropdown menu export const ROLE_PICKER_WIDTH = 360; -export const ROLE_PICKER_SUBMENU_MIN_WIDTH = 260; + +export const MENU_MAX_HEIGHT = 300; // max height for the picker's dropdown menu + +export const ROLE_PICKER_MENU_MIN_WIDTH = 320; +export const ROLE_PICKER_MENU_MAX_WIDTH = 360; + +export const ROLE_PICKER_SUBMENU_MIN_WIDTH = 320; +export const ROLE_PICKER_SUBMENU_MAX_WIDTH = 360; + +export const ROLE_PICKER_MAX_MENU_WIDTH = ROLE_PICKER_MENU_MAX_WIDTH + ROLE_PICKER_SUBMENU_MAX_WIDTH; diff --git a/public/app/core/components/RolePicker/styles.ts b/public/app/core/components/RolePicker/styles.ts index 74ce953af80c3..befe0602a92db 100644 --- a/public/app/core/components/RolePicker/styles.ts +++ b/public/app/core/components/RolePicker/styles.ts @@ -2,7 +2,12 @@ import { css } from '@emotion/css'; import { GrafanaTheme2 } from '@grafana/data'; -import { ROLE_PICKER_SUBMENU_MIN_WIDTH } from './constants'; +import { + ROLE_PICKER_MENU_MAX_WIDTH, + ROLE_PICKER_MENU_MIN_WIDTH, + ROLE_PICKER_SUBMENU_MAX_WIDTH, + ROLE_PICKER_SUBMENU_MIN_WIDTH, +} from './constants'; export const getStyles = (theme: GrafanaTheme2) => ({ hideScrollBar: css({ @@ -24,18 +29,19 @@ export const getStyles = (theme: GrafanaTheme2) => ({ minWidth: 'auto', }), menu: css({ - minWidth: `${ROLE_PICKER_SUBMENU_MIN_WIDTH}px`, + minWidth: `${ROLE_PICKER_MENU_MIN_WIDTH}px`, + maxWidth: `${ROLE_PICKER_MENU_MAX_WIDTH}px`, '& > div': { paddingTop: theme.spacing(1), }, }), menuLeft: css({ - right: 0, flexDirection: 'row-reverse', }), subMenu: css({ height: '100%', minWidth: `${ROLE_PICKER_SUBMENU_MIN_WIDTH}px`, + maxWidth: `${ROLE_PICKER_SUBMENU_MAX_WIDTH}px`, display: 'flex', flexDirection: 'column', borderLeft: `1px solid ${theme.components.input.borderColor}`, From c50ada3a1aadde739be538a4e1d431882253168b Mon Sep 17 00:00:00 2001 From: linoman <2051016+linoman@users.noreply.github.com> Date: Fri, 3 Nov 2023 10:27:43 +0100 Subject: [PATCH 076/869] auth: wire service account proxy (#77215) * Add interface verification compliance * rework service account api to a provider * wire the service accounts api * rewire the implementation of sa srv for the proxy --------- Co-authored-by: Misi --- pkg/server/wire.go | 4 +++- pkg/services/serviceaccounts/manager/service.go | 10 ++-------- pkg/services/serviceaccounts/proxy/service.go | 13 +++++++++++++ 3 files changed, 18 insertions(+), 9 deletions(-) diff --git a/pkg/server/wire.go b/pkg/server/wire.go index 24231f93fbe97..95db107db6d2e 100644 --- a/pkg/server/wire.go +++ b/pkg/server/wire.go @@ -123,6 +123,7 @@ import ( "github.com/grafana/grafana/pkg/services/serviceaccounts" "github.com/grafana/grafana/pkg/services/serviceaccounts/extsvcaccounts" serviceaccountsmanager "github.com/grafana/grafana/pkg/services/serviceaccounts/manager" + serviceaccountsproxy "github.com/grafana/grafana/pkg/services/serviceaccounts/proxy" serviceaccountsretriever "github.com/grafana/grafana/pkg/services/serviceaccounts/retriever" "github.com/grafana/grafana/pkg/services/shorturls" "github.com/grafana/grafana/pkg/services/shorturls/shorturlimpl" @@ -288,7 +289,8 @@ var wireBasicSet = wire.NewSet( ossaccesscontrol.ProvideServiceAccountPermissions, wire.Bind(new(accesscontrol.ServiceAccountPermissionsService), new(*ossaccesscontrol.ServiceAccountPermissionsService)), serviceaccountsmanager.ProvideServiceAccountsService, - wire.Bind(new(serviceaccounts.Service), new(*serviceaccountsmanager.ServiceAccountsService)), + serviceaccountsproxy.ProvideServiceAccountsProxy, + wire.Bind(new(serviceaccounts.Service), new(*serviceaccountsproxy.ServiceAccountsProxy)), expr.ProvideService, featuremgmt.ProvideManagerService, featuremgmt.ProvideToggles, diff --git a/pkg/services/serviceaccounts/manager/service.go b/pkg/services/serviceaccounts/manager/service.go index 8a9b557c145b8..141e79e1a9482 100644 --- a/pkg/services/serviceaccounts/manager/service.go +++ b/pkg/services/serviceaccounts/manager/service.go @@ -6,7 +6,6 @@ import ( "fmt" "time" - "github.com/grafana/grafana/pkg/api/routing" "github.com/grafana/grafana/pkg/infra/kvstore" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/usagestats" @@ -14,7 +13,6 @@ import ( "github.com/grafana/grafana/pkg/services/apikey" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/serviceaccounts" - "github.com/grafana/grafana/pkg/services/serviceaccounts/api" "github.com/grafana/grafana/pkg/services/serviceaccounts/database" "github.com/grafana/grafana/pkg/services/serviceaccounts/secretscan" "github.com/grafana/grafana/pkg/services/sqlstore" @@ -39,15 +37,12 @@ type ServiceAccountsService struct { func ProvideServiceAccountsService( cfg *setting.Cfg, - ac accesscontrol.AccessControl, - routeRegister routing.RouteRegister, usageStats usagestats.Service, store *sqlstore.SQLStore, apiKeyService apikey.Service, kvStore kvstore.KVStore, userService user.Service, orgService org.Service, - permissionService accesscontrol.ServiceAccountPermissionsService, accesscontrolService accesscontrol.Service, ) (*ServiceAccountsService, error) { serviceAccountsStore := database.ProvideServiceAccountsStore( @@ -70,9 +65,6 @@ func ProvideServiceAccountsService( usageStats.RegisterMetricsFunc(s.getUsageMetrics) - serviceaccountsAPI := api.NewServiceAccountsAPI(cfg, s, ac, accesscontrolService, routeRegister, permissionService) - serviceaccountsAPI.RegisterAPIEndpoints() - s.secretScanEnabled = cfg.SectionWithEnvOverrides("secretscan").Key("enabled").MustBool(false) s.secretScanInterval = cfg.SectionWithEnvOverrides("secretscan"). Key("interval").MustDuration(defaultSecretScanInterval) @@ -146,6 +138,8 @@ func (sa *ServiceAccountsService) Run(ctx context.Context) error { } } +var _ serviceaccounts.Service = (*ServiceAccountsService)(nil) + func (sa *ServiceAccountsService) CreateServiceAccount(ctx context.Context, orgID int64, saForm *serviceaccounts.CreateServiceAccountForm) (*serviceaccounts.ServiceAccountDTO, error) { if err := validOrgID(orgID); err != nil { return nil, err diff --git a/pkg/services/serviceaccounts/proxy/service.go b/pkg/services/serviceaccounts/proxy/service.go index 929a527189edf..62a3c9d05dcb6 100644 --- a/pkg/services/serviceaccounts/proxy/service.go +++ b/pkg/services/serviceaccounts/proxy/service.go @@ -4,12 +4,16 @@ import ( "context" "strings" + "github.com/grafana/grafana/pkg/api/routing" "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/apikey" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/serviceaccounts" + "github.com/grafana/grafana/pkg/services/serviceaccounts/api" "github.com/grafana/grafana/pkg/services/serviceaccounts/extsvcaccounts" "github.com/grafana/grafana/pkg/services/serviceaccounts/manager" + "github.com/grafana/grafana/pkg/setting" ) // ServiceAccountsProxy is a proxy for the serviceaccounts.Service interface @@ -23,14 +27,23 @@ type ServiceAccountsProxy struct { } func ProvideServiceAccountsProxy( + cfg *setting.Cfg, + ac accesscontrol.AccessControl, + accesscontrolService accesscontrol.Service, features *featuremgmt.FeatureManager, + permissionService accesscontrol.ServiceAccountPermissionsService, proxiedService *manager.ServiceAccountsService, + routeRegister routing.RouteRegister, ) (*ServiceAccountsProxy, error) { s := &ServiceAccountsProxy{ log: log.New("serviceaccounts.proxy"), proxiedService: proxiedService, isProxyEnabled: features.IsEnabled(featuremgmt.FlagExternalServiceAccounts) || features.IsEnabled(featuremgmt.FlagExternalServiceAuth), } + + serviceaccountsAPI := api.NewServiceAccountsAPI(cfg, s, ac, accesscontrolService, routeRegister, permissionService) + serviceaccountsAPI.RegisterAPIEndpoints() + return s, nil } From f32bc1742260efe6900c94b77fd3a985e94c8bbb Mon Sep 17 00:00:00 2001 From: Ashley Harrison Date: Fri, 3 Nov 2023 09:54:25 +0000 Subject: [PATCH 077/869] Navigation: Use `LoadingBar` in `CommandPalette` (#77506) try using LoadingBar in CommandPalette --- .../app/features/commandPalette/CommandPalette.tsx | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/public/app/features/commandPalette/CommandPalette.tsx b/public/app/features/commandPalette/CommandPalette.tsx index 93427ae8b08e4..0ccb405da39eb 100644 --- a/public/app/features/commandPalette/CommandPalette.tsx +++ b/public/app/features/commandPalette/CommandPalette.tsx @@ -16,7 +16,7 @@ import React, { useEffect, useMemo, useRef } from 'react'; import { GrafanaTheme2 } from '@grafana/data'; import { reportInteraction } from '@grafana/runtime'; -import { Icon, Spinner, useStyles2 } from '@grafana/ui'; +import { Icon, LoadingBar, useStyles2 } from '@grafana/ui'; import { t } from 'app/core/internationalization'; import { KBarResults } from './KBarResults'; @@ -58,11 +58,14 @@ export function CommandPalette() {
    - {isFetchingSearchResults ? : } + +
    + {isFetchingSearchResults && } +
    @@ -161,6 +164,12 @@ const getSearchStyles = (theme: GrafanaTheme2) => { overflow: 'hidden', boxShadow: theme.shadows.z3, }), + loadingBarContainer: css({ + position: 'absolute', + left: 0, + right: 0, + bottom: 0, + }), searchContainer: css({ alignItems: 'center', background: theme.components.input.background, @@ -168,6 +177,7 @@ const getSearchStyles = (theme: GrafanaTheme2) => { display: 'flex', gap: theme.spacing(1), padding: theme.spacing(1, 2), + position: 'relative', }), search: css({ fontSize: theme.typography.fontSize, From 577b3f2fb20f189c95d806d5ef5f016f35097933 Mon Sep 17 00:00:00 2001 From: littlelionking <124090330+littlelionking@users.noreply.github.com> Date: Fri, 3 Nov 2023 06:44:59 -0400 Subject: [PATCH 078/869] Docs: Reduce `location` indentation to match time_interval_spec (#77291) Reduce location indentation to match time_interval_spec https://prometheus.io/docs/alerting/latest/configuration/#time_interval_spec Co-authored-by: brendamuir <100768211+brendamuir@users.noreply.github.com> --- .../provision-alerting-resources/file-provisioning/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/sources/alerting/set-up/provision-alerting-resources/file-provisioning/index.md b/docs/sources/alerting/set-up/provision-alerting-resources/file-provisioning/index.md index b9ca09e094654..1ac14176febbb 100644 --- a/docs/sources/alerting/set-up/provision-alerting-resources/file-provisioning/index.md +++ b/docs/sources/alerting/set-up/provision-alerting-resources/file-provisioning/index.md @@ -652,7 +652,7 @@ muteTimes: - times: - start_time: '06:00' end_time: '23:59' - location: 'UTC' + location: 'UTC' weekdays: ['monday:wednesday', 'saturday', 'sunday'] months: ['1:3', 'may:august', 'december'] years: ['2020:2022', '2030'] From 6bf4d0cbc6756ec8e8fb64b3a9d7b76f4c4194f5 Mon Sep 17 00:00:00 2001 From: Jacob Zelek Date: Fri, 3 Nov 2023 05:15:54 -0700 Subject: [PATCH 079/869] DashboardGrid: Add support to filter panels using variable (#77112) * DashboardGrid panel filter * Missed segment and changes per PR discussion * Hide feature flag from docs --------- Co-authored-by: Dominik Prokop --- .betterer.results | 2 +- .../src/types/featureToggles.gen.ts | 1 + pkg/services/featuremgmt/registry.go | 8 + pkg/services/featuremgmt/toggles_gen.csv | 1 + pkg/services/featuremgmt/toggles_gen.go | 4 + .../dashboard/dashgrid/DashboardGrid.test.tsx | 149 ++++++++++++++++-- .../dashboard/dashgrid/DashboardGrid.tsx | 88 ++++++++++- .../app/features/variables/state/actions.ts | 1 + public/app/features/variables/types.ts | 2 + 9 files changed, 237 insertions(+), 19 deletions(-) diff --git a/.betterer.results b/.betterer.results index 4b6eb1946a2c6..c3508a49fe399 100644 --- a/.betterer.results +++ b/.betterer.results @@ -3292,7 +3292,7 @@ exports[`better eslint`] = { [0, 0, 0, "Use data-testid for E2E selectors instead of aria-label", "4"] ], "public/app/features/dashboard/dashgrid/DashboardGrid.tsx:5381": [ - [0, 0, 0, "Unexpected any. Specify a different type.", "0"] + [0, 0, 0, "Do not use any type assertions.", "0"] ], "public/app/features/dashboard/dashgrid/DashboardPanel.tsx:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"] diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index 9315579eb18df..e21dc9e57e5fc 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -157,4 +157,5 @@ export interface FeatureToggles { annotationPermissionUpdate?: boolean; extractFieldsNameDeduplication?: boolean; dashboardSceneForViewers?: boolean; + panelFilterVariable?: boolean; } diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index d28bca5998eee..8170e1766cde1 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -974,5 +974,13 @@ var ( FrontendOnly: true, Owner: grafanaDashboardsSquad, }, + { + Name: "panelFilterVariable", + Description: "Enables use of the `systemPanelFilterVar` variable to filter panels in a dashboard", + Stage: FeatureStageExperimental, + FrontendOnly: true, + Owner: grafanaDashboardsSquad, + HideFromDocs: true, + }, } ) diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv index 8276c937fa3c4..7fe4cb32ae732 100644 --- a/pkg/services/featuremgmt/toggles_gen.csv +++ b/pkg/services/featuremgmt/toggles_gen.csv @@ -138,3 +138,4 @@ alertmanagerRemoteOnly,experimental,@grafana/alerting-squad,false,false,false,fa annotationPermissionUpdate,experimental,@grafana/grafana-authnz-team,false,false,false,false extractFieldsNameDeduplication,experimental,@grafana/grafana-bi-squad,false,false,false,true dashboardSceneForViewers,experimental,@grafana/dashboards-squad,false,false,false,true +panelFilterVariable,experimental,@grafana/dashboards-squad,false,false,false,true diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go index bd3493e5147a5..4ea5dd8b5b0f8 100644 --- a/pkg/services/featuremgmt/toggles_gen.go +++ b/pkg/services/featuremgmt/toggles_gen.go @@ -562,4 +562,8 @@ const ( // FlagDashboardSceneForViewers // Enables dashboard rendering using Scenes for viewer roles FlagDashboardSceneForViewers = "dashboardSceneForViewers" + + // FlagPanelFilterVariable + // Enables use of the `systemPanelFilterVar` variable to filter panels in a dashboard + FlagPanelFilterVariable = "panelFilterVariable" ) diff --git a/public/app/features/dashboard/dashgrid/DashboardGrid.test.tsx b/public/app/features/dashboard/dashgrid/DashboardGrid.test.tsx index 853391201a012..c8c439eacb9a0 100644 --- a/public/app/features/dashboard/dashgrid/DashboardGrid.test.tsx +++ b/public/app/features/dashboard/dashgrid/DashboardGrid.test.tsx @@ -1,23 +1,73 @@ -import { render } from '@testing-library/react'; +import { act, render, screen } from '@testing-library/react'; import React from 'react'; +import { Provider } from 'react-redux'; +import { Router } from 'react-router-dom'; +import { useEffectOnce } from 'react-use'; +import { AutoSizerProps } from 'react-virtualized-auto-sizer'; +import { getGrafanaContextMock } from 'test/mocks/getGrafanaContextMock'; +import { TextBoxVariableModel } from '@grafana/data'; +import { locationService } from '@grafana/runtime'; import { Dashboard } from '@grafana/schema'; +import appEvents from 'app/core/app_events'; +import { GrafanaContext } from 'app/core/context/GrafanaContext'; +import { GetVariables } from 'app/features/variables/state/selectors'; +import { VariablesChanged } from 'app/features/variables/types'; +import { configureStore } from 'app/store/configureStore'; import { DashboardMeta } from 'app/types'; import { DashboardModel } from '../state'; import { createDashboardModelFixture } from '../state/__fixtures__/dashboardFixtures'; -import { DashboardGrid, Props } from './DashboardGrid'; +import { DashboardGrid, PANEL_FILTER_VARIABLE, Props } from './DashboardGrid'; import { Props as LazyLoaderProps } from './LazyLoader'; +jest.mock('@grafana/runtime', () => ({ + ...jest.requireActual('@grafana/runtime'), + config: { + ...jest.requireActual('@grafana/runtime').config, + featureToggles: { + panelFilterVariable: true, + }, + }, +})); + jest.mock('app/features/dashboard/dashgrid/LazyLoader', () => { - const LazyLoader = ({ children }: LazyLoaderProps) => { - return <>{children}; + const LazyLoader = ({ children, onLoad }: Pick) => { + useEffectOnce(() => { + onLoad?.(); + }); + return <>{typeof children === 'function' ? children({ isInView: true }) : children}; }; return { LazyLoader }; }); -function getTestDashboard(overrides?: Partial, metaOverrides?: Partial): DashboardModel { +jest.mock('react-virtualized-auto-sizer', () => { + // The size of the children need to be small enough to be outside the view. + // So it does not trigger the query to be run by the PanelQueryRunner. + return ({ children }: AutoSizerProps) => children({ height: 1, width: 1 }); +}); + +function setup(props: Props) { + const context = getGrafanaContextMock(); + const store = configureStore({}); + + return render( + + + + + + + + ); +} + +function getTestDashboard( + overrides?: Partial, + metaOverrides?: Partial, + getVariablesFromState?: GetVariables +): DashboardModel { const data = Object.assign( { title: 'My dashboard', @@ -30,20 +80,20 @@ function getTestDashboard(overrides?: Partial, metaOverrides?: Partia }, { id: 2, - type: 'graph2', - title: 'My graph2', + type: 'table', + title: 'My table', gridPos: { x: 0, y: 10, w: 25, h: 10 }, }, { id: 3, - type: 'graph3', - title: 'My graph3', + type: 'table', + title: 'My table 2', gridPos: { x: 0, y: 20, w: 25, h: 100 }, }, { id: 4, - type: 'graph4', - title: 'My graph4', + type: 'gauge', + title: 'My gauge', gridPos: { x: 0, y: 120, w: 25, h: 10 }, }, ], @@ -51,17 +101,88 @@ function getTestDashboard(overrides?: Partial, metaOverrides?: Partia overrides ); - return createDashboardModelFixture(data, metaOverrides); + return createDashboardModelFixture(data, metaOverrides, getVariablesFromState); } describe('DashboardGrid', () => { - it('should render without error', () => { + it('Should render panels', async () => { const props: Props = { editPanel: null, viewPanel: null, isEditable: true, dashboard: getTestDashboard(), }; - expect(() => render()).not.toThrow(); + + act(() => { + setup(props); + }); + + expect(await screen.findByText('My graph')).toBeInTheDocument(); + expect(await screen.findByText('My table')).toBeInTheDocument(); + expect(await screen.findByText('My table 2')).toBeInTheDocument(); + expect(await screen.findByText('My gauge')).toBeInTheDocument(); + }); + + it('Should allow filtering panels', async () => { + const props: Props = { + editPanel: null, + viewPanel: null, + isEditable: true, + dashboard: getTestDashboard(), + }; + act(() => { + setup(props); + }); + + act(() => { + appEvents.publish( + new VariablesChanged({ + panelIds: [], + refreshAll: false, + variable: { + type: 'textbox', + id: PANEL_FILTER_VARIABLE, + current: { + value: 'My graph', + }, + } as TextBoxVariableModel, + }) + ); + }); + const table = screen.queryByText('My table'); + const table2 = screen.queryByText('My table 2'); + const gauge = screen.queryByText('My gauge'); + + expect(await screen.findByText('My graph')).toBeInTheDocument(); + expect(table).toBeNull(); + expect(table2).toBeNull(); + expect(gauge).toBeNull(); + }); + + it('Should rendered filtered panels on init when filter variable is present', async () => { + const props: Props = { + editPanel: null, + viewPanel: null, + isEditable: true, + dashboard: getTestDashboard(undefined, undefined, () => [ + { + id: PANEL_FILTER_VARIABLE, + type: 'textbox', + query: 'My tab', + } as TextBoxVariableModel, + ]), + }; + + act(() => { + setup(props); + }); + + const graph = screen.queryByText('My graph'); + const gauge = screen.queryByText('My gauge'); + + expect(await screen.findByText('My table')).toBeInTheDocument(); + expect(await screen.findByText('My table 2')).toBeInTheDocument(); + expect(graph).toBeNull(); + expect(gauge).toBeNull(); }); }); diff --git a/public/app/features/dashboard/dashgrid/DashboardGrid.tsx b/public/app/features/dashboard/dashgrid/DashboardGrid.tsx index 924944700dcba..b4220cc25c791 100644 --- a/public/app/features/dashboard/dashgrid/DashboardGrid.tsx +++ b/public/app/features/dashboard/dashgrid/DashboardGrid.tsx @@ -5,8 +5,10 @@ import AutoSizer from 'react-virtualized-auto-sizer'; import { Subscription } from 'rxjs'; import { config } from '@grafana/runtime'; +import appEvents from 'app/core/app_events'; import { GRID_CELL_HEIGHT, GRID_CELL_VMARGIN, GRID_COLUMN_COUNT } from 'app/core/constants'; import { contextSrv } from 'app/core/services/context_srv'; +import { VariablesChanged } from 'app/features/variables/types'; import { DashboardPanelsChangedEvent } from 'app/types/events'; import { AddLibraryPanelWidget } from '../components/AddLibraryPanelWidget'; @@ -18,6 +20,8 @@ import { GridPos } from '../state/PanelModel'; import DashboardEmpty from './DashboardEmpty'; import { DashboardPanel } from './DashboardPanel'; +export const PANEL_FILTER_VARIABLE = 'systemPanelFilterVar'; + export interface Props { dashboard: DashboardModel; isEditable: boolean; @@ -25,7 +29,12 @@ export interface Props { viewPanel: PanelModel | null; hidePanelMenus?: boolean; } -export class DashboardGrid extends PureComponent { + +interface State { + panelFilter?: RegExp; +} + +export class DashboardGrid extends PureComponent { private panelMap: { [key: string]: PanelModel } = {}; private eventSubs = new Subscription(); private windowHeight = 1200; @@ -37,10 +46,43 @@ export class DashboardGrid extends PureComponent { constructor(props: Props) { super(props); + this.state = { + panelFilter: undefined, + }; } componentDidMount() { const { dashboard } = this.props; + + if (config.featureToggles.panelFilterVariable) { + // If panel filter variable is set on load then + // update state to filter panels + for (const variable of dashboard.getVariables()) { + if (variable.id === PANEL_FILTER_VARIABLE) { + if ('query' in variable) { + this.setPanelFilter(variable.query); + } + break; + } + } + + this.eventSubs.add( + appEvents.subscribe(VariablesChanged, (e) => { + if (e.payload.variable?.id === PANEL_FILTER_VARIABLE) { + if ('current' in e.payload.variable) { + let variable = e.payload.variable.current; + if ('value' in variable) { + let value = variable.value; + if (typeof value === 'string') { + this.setPanelFilter(value as string); + } + } + } + } + }) + ); + } + this.eventSubs.add(dashboard.events.subscribe(DashboardPanelsChangedEvent, this.triggerForceUpdate)); } @@ -48,10 +90,25 @@ export class DashboardGrid extends PureComponent { this.eventSubs.unsubscribe(); } + setPanelFilter(regex: string) { + // Only set the panels filter if the systemPanelFilterVar variable + // is a non-empty string + let panelFilter = undefined; + if (regex.length > 0) { + panelFilter = new RegExp(regex, 'i'); + } + + this.setState({ + panelFilter: panelFilter, + }); + } + buildLayout() { const layout: ReactGridLayout.Layout[] = []; this.panelMap = {}; + const { panelFilter } = this.state; + let count = 0; for (const panel of this.props.dashboard.panels) { if (!panel.key) { panel.key = `panel-${panel.id}-${Date.now()}`; @@ -78,13 +135,27 @@ export class DashboardGrid extends PureComponent { panelPos.isDraggable = panel.collapsed; } - layout.push(panelPos); + if (!panelFilter) { + layout.push(panelPos); + } else { + if (panelFilter.test(panel.title)) { + panelPos.isResizable = false; + panelPos.isDraggable = false; + panelPos.x = (count % 2) * GRID_COLUMN_COUNT; + panelPos.y = Math.floor(count / 2); + layout.push(panelPos); + count++; + } + } } return layout; } onLayoutChange = (newLayout: ReactGridLayout.Layout[]) => { + if (this.state.panelFilter) { + return; + } for (const newPos of newLayout) { this.panelMap[newPos.i!].updateGridPos(newPos, this.isLayoutInitialized); } @@ -136,6 +207,7 @@ export class DashboardGrid extends PureComponent { } renderPanels(gridWidth: number, isDashboardDraggable: boolean) { + const { panelFilter } = this.state; const panelElements = []; // Reset last panel bottom @@ -156,7 +228,7 @@ export class DashboardGrid extends PureComponent { // requires parent create stacking context to prevent overlap with parent elements const descIndex = this.props.dashboard.panels.length - panelElements.length; - panelElements.push( + const p = ( { }} ); + + if (!panelFilter) { + panelElements.push(p); + } else { + if (panelFilter.test(panel.title)) { + panelElements.push(p); + } + } } return panelElements; @@ -295,7 +375,7 @@ interface GrafanaGridItemProps extends React.HTMLAttributes { isViewing: boolean; windowHeight: number; windowWidth: number; - children: any; + children: any; // eslint-disable-line @typescript-eslint/no-explicit-any } /** diff --git a/public/app/features/variables/state/actions.ts b/public/app/features/variables/state/actions.ts index 9e4f3d4b812e4..53f0b96d5a13e 100644 --- a/public/app/features/variables/state/actions.ts +++ b/public/app/features/variables/state/actions.ts @@ -623,6 +623,7 @@ export const variableUpdated = ( : { refreshAll: false, panelIds: Array.from(getAllAffectedPanelIdsForVariableChange([variableInState.id], g, panelVars)), + variable: getVariable(identifier, state), }; const node = g.getNode(variableInState.name); diff --git a/public/app/features/variables/types.ts b/public/app/features/variables/types.ts index 11c66dcfafd4e..f943bf1efd477 100644 --- a/public/app/features/variables/types.ts +++ b/public/app/features/variables/types.ts @@ -8,6 +8,7 @@ import { QueryEditorProps, BaseVariableModel, VariableHide, + TypedVariableModel, } from '@grafana/data'; export { /** @deprecated Import from @grafana/data instead */ @@ -95,6 +96,7 @@ export type VariableQueryEditorType< export interface VariablesChangedEvent { refreshAll: boolean; panelIds: number[]; + variable?: TypedVariableModel; } export class VariablesChanged extends BusEventWithPayload { From d624a5d490ce845eeba9c8bfb98aa0f7f8be3e56 Mon Sep 17 00:00:00 2001 From: Vardan Torosyan Date: Fri, 3 Nov 2023 13:20:39 +0100 Subject: [PATCH 080/869] Chore: Replace grafana-authnz-team with identity-access-team as code owners (#77609) * Chore: Replace grafana-authnz-team with identity-access-team as code owner * Chore: Replace grafana-authnz-team with identity-access-team as code owner * Fix the failing test --- .github/CODEOWNERS | 66 ++++++++++++------------ pkg/api/org_users.go | 2 +- pkg/services/featuremgmt/codeowners.go | 2 +- pkg/services/featuremgmt/registry.go | 20 +++---- pkg/services/featuremgmt/toggles_gen.csv | 20 +++---- scripts/modowners/README.md | 4 +- scripts/modowners/modowners.go | 2 +- 7 files changed, 58 insertions(+), 58 deletions(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 125af1b385378..353b0f9873d18 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -69,8 +69,8 @@ /pkg/apis/ @grafana/grafana-app-platform-squad /pkg/bus/ @grafana/backend-platform /pkg/cmd/ @grafana/backend-platform -/pkg/components/apikeygen/ @grafana/grafana-authnz-team -/pkg/components/satokengen/ @grafana/grafana-authnz-team +/pkg/components/apikeygen/ @grafana/identity-access-team +/pkg/components/satokengen/ @grafana/identity-access-team /pkg/components/dashdiffs/ @grafana/backend-platform /pkg/components/imguploader/ @grafana/backend-platform /pkg/components/loki/ @grafana/backend-platform @@ -97,7 +97,7 @@ /pkg/models/ @grafana/backend-platform /pkg/server/ @grafana/backend-platform /pkg/services/annotations/ @grafana/backend-platform -/pkg/services/apikey/ @grafana/grafana-authnz-team +/pkg/services/apikey/ @grafana/identity-access-team /pkg/services/cleanup/ @grafana/backend-platform /pkg/services/contexthandler/ @grafana/backend-platform /pkg/services/correlations/ @grafana/explore-squad @@ -130,10 +130,10 @@ /pkg/services/star/ @grafana/backend-platform /pkg/services/stats/ @grafana/backend-platform /pkg/services/tag/ @grafana/backend-platform -/pkg/services/team/ @grafana/grafana-authnz-team +/pkg/services/team/ @grafana/identity-access-team /pkg/services/temp_user/ @grafana/backend-platform /pkg/services/updatechecker/ @grafana/backend-platform -/pkg/services/user/ @grafana/grafana-authnz-team +/pkg/services/user/ @grafana/identity-access-team /pkg/services/validations/ @grafana/backend-platform /pkg/setting/ @grafana/backend-platform /pkg/tests/ @grafana/backend-platform @@ -155,7 +155,7 @@ # devenv # Backend code, developers environment -/devenv/docker/blocks/auth/ @grafana/grafana-authnz-team +/devenv/docker/blocks/auth/ @grafana/identity-access-team # Logs code, developers environment /devenv/docker/blocks/loki* @grafana/observability-logs @@ -369,10 +369,10 @@ cypress.config.js @grafana/grafana-frontend-platform /public/app/core/components/GraphNG/ @grafana/dataviz-squad /public/app/core/components/TimeSeries/ @grafana/dataviz-squad /public/app/features/all.ts @grafana/grafana-frontend-platform -/public/app/features/admin/ @grafana/grafana-authnz-team -/public/app/features/auth-config/ @grafana/grafana-authnz-team +/public/app/features/admin/ @grafana/identity-access-team +/public/app/features/auth-config/ @grafana/identity-access-team /public/app/features/annotations/ @grafana/grafana-frontend-platform -/public/app/features/api-keys/ @grafana/grafana-authnz-team +/public/app/features/api-keys/ @grafana/identity-access-team /public/app/features/canvas/ @grafana/dataviz-squad /public/app/features/geo/ @grafana/dataviz-squad /public/app/features/visualization/data-hover/ @grafana/dataviz-squad @@ -407,12 +407,12 @@ cypress.config.js @grafana/grafana-frontend-platform /public/app/features/scenes/ @grafana/dashboards-squad /public/app/features/browse-dashboards/ @grafana/grafana-frontend-platform /public/app/features/search/ @grafana/grafana-frontend-platform -/public/app/features/serviceaccounts/ @grafana/grafana-authnz-team +/public/app/features/serviceaccounts/ @grafana/identity-access-team /public/app/features/storage/ @grafana/grafana-app-platform-squad -/public/app/features/teams/ @grafana/grafana-authnz-team +/public/app/features/teams/ @grafana/identity-access-team /public/app/features/templating/ @grafana/dashboards-squad /public/app/features/transformers/ @grafana/grafana-bi-squad -/public/app/features/users/ @grafana/grafana-authnz-team +/public/app/features/users/ @grafana/identity-access-team /public/app/features/variables/ @grafana/dashboards-squad /public/app/plugins/panel/alertGroups/ @grafana/alerting-frontend /public/app/plugins/panel/alertlist/ @grafana/alerting-frontend @@ -488,7 +488,7 @@ cypress.config.js @grafana/grafana-frontend-platform -/scripts/benchmark-access-control.sh @grafana/grafana-authnz-team +/scripts/benchmark-access-control.sh @grafana/identity-access-team /scripts/check-breaking-changes.sh @grafana/plugins-platform-frontend /scripts/ci-* @grafana/grafana-delivery /scripts/circle-* @grafana/grafana-delivery @@ -566,25 +566,25 @@ scripts/generate-icon-bundle.js @grafana/plugins-platform-frontend @grafana/graf /grafana-mixin/ @grafana/hosted-grafana-team # Grafana authentication and authorization -/pkg/login/ @grafana/grafana-authnz-team -/pkg/services/accesscontrol/ @grafana/grafana-authnz-team -/pkg/services/anonymous/ @grafana/grafana-authnz-team -/pkg/services/auth/ @grafana/grafana-authnz-team -/pkg/services/authn/ @grafana/grafana-authnz-team -/pkg/services/signingkeys/ @grafana/grafana-authnz-team -/pkg/services/dashboards/accesscontrol.go @grafana/grafana-authnz-team -/pkg/services/datasources/guardian/ @grafana/grafana-authnz-team -/pkg/services/guardian/ @grafana/grafana-authnz-team -/pkg/services/ldap/ @grafana/grafana-authnz-team -/pkg/services/login/ @grafana/grafana-authnz-team -/pkg/services/loginattempt/ @grafana/grafana-authnz-team -/pkg/services/extsvcauth/ @grafana/grafana-authnz-team -/pkg/services/oauthtoken/ @grafana/grafana-authnz-team -/pkg/services/serviceaccounts/ @grafana/grafana-authnz-team +/pkg/login/ @grafana/identity-access-team +/pkg/services/accesscontrol/ @grafana/identity-access-team +/pkg/services/anonymous/ @grafana/identity-access-team +/pkg/services/auth/ @grafana/identity-access-team +/pkg/services/authn/ @grafana/identity-access-team +/pkg/services/signingkeys/ @grafana/identity-access-team +/pkg/services/dashboards/accesscontrol.go @grafana/identity-access-team +/pkg/services/datasources/guardian/ @grafana/identity-access-team +/pkg/services/guardian/ @grafana/identity-access-team +/pkg/services/ldap/ @grafana/identity-access-team +/pkg/services/login/ @grafana/identity-access-team +/pkg/services/loginattempt/ @grafana/identity-access-team +/pkg/services/extsvcauth/ @grafana/identity-access-team +/pkg/services/oauthtoken/ @grafana/identity-access-team +/pkg/services/serviceaccounts/ @grafana/identity-access-team # Support bundles -/public/app/features/support-bundles/ @grafana/grafana-authnz-team -/pkg/services/supportbundles/ @grafana/grafana-authnz-team +/public/app/features/support-bundles/ @grafana/identity-access-team +/pkg/services/supportbundles/ @grafana/identity-access-team # Grafana Operator Experience Team /pkg/infra/httpclient/httpclientprovider/sigv4_middleware.go @grafana/grafana-operator-experience-squad @@ -674,9 +674,9 @@ embed.go @grafana/grafana-as-code # Conf /conf/defaults.ini @torkelo /conf/sample.ini @torkelo -/conf/ldap.toml @grafana/grafana-authnz-team -/conf/ldap_multiple.toml @grafana/grafana-authnz-team -/conf/provisioning/access-control/ @grafana/grafana-authnz-team +/conf/ldap.toml @grafana/identity-access-team +/conf/ldap_multiple.toml @grafana/identity-access-team +/conf/provisioning/access-control/ @grafana/identity-access-team /conf/provisioning/alerting/ @grafana/alerting-backend-product /conf/provisioning/dashboards/ @grafana/dashboards-squad /conf/provisioning/datasources/ @grafana/plugins-platform-backend diff --git a/pkg/api/org_users.go b/pkg/api/org_users.go index f55641edf40f6..27960b020310c 100644 --- a/pkg/api/org_users.go +++ b/pkg/api/org_users.go @@ -325,7 +325,7 @@ func (hs *HTTPServer) searchOrgUsersHelper(c *contextmodel.ReqContext, query *or // Get accesscontrol metadata and IPD labels for users in the target org accessControlMetadata := map[string]accesscontrol.Metadata{} if c.QueryBool("accesscontrol") && c.SignedInUser.Permissions != nil { - // TODO https://github.com/grafana/grafana-authnz-team/issues/268 - user access control service for fetching permissions from another organization + // TODO https://github.com/grafana/identity-access-team/issues/268 - user access control service for fetching permissions from another organization permissions, ok := c.SignedInUser.Permissions[query.OrgID] if ok { accessControlMetadata = accesscontrol.GetResourcesMetadata(c.Req.Context(), permissions, "users:id:", userIDs) diff --git a/pkg/services/featuremgmt/codeowners.go b/pkg/services/featuremgmt/codeowners.go index 2a555a2906ec9..42e0c13962345 100644 --- a/pkg/services/featuremgmt/codeowners.go +++ b/pkg/services/featuremgmt/codeowners.go @@ -14,7 +14,7 @@ const ( grafanaBackendPlatformSquad codeowner = "@grafana/backend-platform" grafanaPluginsPlatformSquad codeowner = "@grafana/plugins-platform-backend" grafanaAsCodeSquad codeowner = "@grafana/grafana-as-code" - grafanaAuthnzSquad codeowner = "@grafana/grafana-authnz-team" + identityAccessTeam codeowner = "@grafana/identity-access-team" grafanaObservabilityLogsSquad codeowner = "@grafana/observability-logs" grafanaObservabilityTracesAndProfilingSquad codeowner = "@grafana/observability-traces-and-profiling" grafanaObservabilityMetricsSquad codeowner = "@grafana/observability-metrics" diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index 8170e1766cde1..291b84d79aa47 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -228,7 +228,7 @@ var ( Name: "accessControlOnCall", Description: "Access control primitives for OnCall", Stage: FeatureStagePublicPreview, - Owner: grafanaAuthnzSquad, + Owner: identityAccessTeam, }, { Name: "nestedFolders", @@ -248,7 +248,7 @@ var ( Name: "accessTokenExpirationCheck", Description: "Enable OAuth access_token expiration check and token refresh using the refresh_token", Stage: FeatureStageGeneralAvailability, - Owner: grafanaAuthnzSquad, + Owner: identityAccessTeam, }, { Name: "emptyDashboardPage", @@ -317,7 +317,7 @@ var ( Name: "gcomOnlyExternalOrgRoleSync", Description: "Prohibits a user from changing organization roles synced with Grafana Cloud auth provider", Stage: FeatureStageGeneralAvailability, - Owner: grafanaAuthnzSquad, + Owner: identityAccessTeam, }, { Name: "prometheusMetricEncyclopedia", @@ -339,7 +339,7 @@ var ( Name: "clientTokenRotation", Description: "Replaces the current in-request token rotation so that the client initiates the rotation", Stage: FeatureStageExperimental, - Owner: grafanaAuthnzSquad, + Owner: identityAccessTeam, }, { Name: "prometheusDataplane", @@ -418,7 +418,7 @@ var ( Description: "Starts an OAuth2 authentication provider for external services", Stage: FeatureStageExperimental, RequiresDevMode: true, - Owner: grafanaAuthnzSquad, + Owner: identityAccessTeam, }, { Name: "refactorVariablesTimeRange", @@ -637,7 +637,7 @@ var ( Description: "Support faster dashboard and folder search by splitting permission scopes into parts", Stage: FeatureStagePublicPreview, FrontendOnly: false, - Owner: grafanaAuthnzSquad, + Owner: identityAccessTeam, RequiresRestart: true, }, { @@ -799,7 +799,7 @@ var ( Name: "idForwarding", Description: "Generate signed id token for identity that can be forwarded to plugins and external services", Stage: FeatureStageExperimental, - Owner: grafanaAuthnzSquad, + Owner: identityAccessTeam, RequiresDevMode: true, }, { @@ -814,7 +814,7 @@ var ( Description: "Automatic service account and token setup for plugins", Stage: FeatureStageExperimental, RequiresDevMode: true, - Owner: grafanaAuthnzSquad, + Owner: identityAccessTeam, }, { Name: "panelMonitoring", @@ -884,7 +884,7 @@ var ( Description: "Enables datasources to apply team headers to the client requests", Stage: FeatureStageExperimental, FrontendOnly: false, - Owner: grafanaAuthnzSquad, + Owner: identityAccessTeam, }, { Name: "awsDatasourcesNewFormStyling", @@ -958,7 +958,7 @@ var ( Description: "Separate annotation permissions from dashboard permissions to allow for more granular control.", Stage: FeatureStageExperimental, RequiresDevMode: false, - Owner: grafanaAuthnzSquad, + Owner: identityAccessTeam, }, { Name: "extractFieldsNameDeduplication", diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv index 7fe4cb32ae732..f4d39ea7d2feb 100644 --- a/pkg/services/featuremgmt/toggles_gen.csv +++ b/pkg/services/featuremgmt/toggles_gen.csv @@ -31,10 +31,10 @@ athenaAsyncQueryDataSupport,GA,@grafana/aws-datasources,false,false,false,true cloudwatchNewRegionsHandler,experimental,@grafana/aws-datasources,false,false,false,false showDashboardValidationWarnings,experimental,@grafana/dashboards-squad,false,false,false,false mysqlAnsiQuotes,experimental,@grafana/backend-platform,false,false,false,false -accessControlOnCall,preview,@grafana/grafana-authnz-team,false,false,false,false +accessControlOnCall,preview,@grafana/identity-access-team,false,false,false,false nestedFolders,preview,@grafana/backend-platform,false,false,false,false nestedFolderPicker,GA,@grafana/grafana-frontend-platform,false,false,false,true -accessTokenExpirationCheck,GA,@grafana/grafana-authnz-team,false,false,false,false +accessTokenExpirationCheck,GA,@grafana/identity-access-team,false,false,false,false emptyDashboardPage,GA,@grafana/dashboards-squad,false,false,false,true disablePrometheusExemplarSampling,GA,@grafana/observability-metrics,false,false,false,false alertingBacktesting,experimental,@grafana/alerting-squad,false,false,false,false @@ -44,10 +44,10 @@ logsContextDatasourceUi,GA,@grafana/observability-logs,false,false,false,true lokiQuerySplitting,GA,@grafana/observability-logs,false,false,false,true lokiQuerySplittingConfig,experimental,@grafana/observability-logs,false,false,false,true individualCookiePreferences,experimental,@grafana/backend-platform,false,false,false,false -gcomOnlyExternalOrgRoleSync,GA,@grafana/grafana-authnz-team,false,false,false,false +gcomOnlyExternalOrgRoleSync,GA,@grafana/identity-access-team,false,false,false,false prometheusMetricEncyclopedia,GA,@grafana/observability-metrics,false,false,false,true influxdbBackendMigration,GA,@grafana/observability-metrics,false,false,false,true -clientTokenRotation,experimental,@grafana/grafana-authnz-team,false,false,false,false +clientTokenRotation,experimental,@grafana/identity-access-team,false,false,false,false prometheusDataplane,GA,@grafana/observability-metrics,false,false,false,false lokiMetricDataplane,GA,@grafana/observability-logs,false,false,false,false lokiLogsDataplane,experimental,@grafana/observability-logs,false,false,false,false @@ -59,7 +59,7 @@ alertStateHistoryLokiPrimary,experimental,@grafana/alerting-squad,false,false,fa alertStateHistoryLokiOnly,experimental,@grafana/alerting-squad,false,false,false,false unifiedRequestLog,experimental,@grafana/backend-platform,false,false,false,false renderAuthJWT,preview,@grafana/grafana-as-code,false,false,false,false -externalServiceAuth,experimental,@grafana/grafana-authnz-team,true,false,false,false +externalServiceAuth,experimental,@grafana/identity-access-team,true,false,false,false refactorVariablesTimeRange,preview,@grafana/dashboards-squad,false,false,false,false useCachingService,GA,@grafana/grafana-operator-experience-squad,false,false,true,false enableElasticsearchBackendQuerying,GA,@grafana/observability-logs,false,false,false,false @@ -90,7 +90,7 @@ grafanaAPIServer,experimental,@grafana/grafana-app-platform-squad,false,false,fa grafanaAPIServerWithExperimentalAPIs,experimental,@grafana/grafana-app-platform-squad,false,false,false,false featureToggleAdminPage,experimental,@grafana/grafana-operator-experience-squad,false,false,true,false awsAsyncQueryCaching,preview,@grafana/aws-datasources,false,false,false,false -splitScopes,preview,@grafana/grafana-authnz-team,false,false,true,false +splitScopes,preview,@grafana/identity-access-team,false,false,true,false azureMonitorDataplane,GA,@grafana/partner-datasources,false,false,false,false traceToProfiles,experimental,@grafana/observability-traces-and-profiling,false,false,false,true permissionsFilterRemoveSubquery,experimental,@grafana/backend-platform,false,false,false,false @@ -112,9 +112,9 @@ alertingContactPointsV2,preview,@grafana/alerting-squad,false,false,false,true externalCorePlugins,experimental,@grafana/plugins-platform-backend,false,false,false,false pluginsAPIMetrics,experimental,@grafana/plugins-platform-backend,false,false,false,true httpSLOLevels,experimental,@grafana/hosted-grafana-team,false,false,true,false -idForwarding,experimental,@grafana/grafana-authnz-team,true,false,false,false +idForwarding,experimental,@grafana/identity-access-team,true,false,false,false cloudWatchWildCardDimensionValues,GA,@grafana/aws-datasources,false,false,false,false -externalServiceAccounts,experimental,@grafana/grafana-authnz-team,true,false,false,false +externalServiceAccounts,experimental,@grafana/identity-access-team,true,false,false,false panelMonitoring,experimental,@grafana/dataviz-squad,false,false,false,true enableNativeHTTPHistogram,experimental,@grafana/hosted-grafana-team,false,false,false,false formatString,experimental,@grafana/grafana-bi-squad,false,false,false,true @@ -124,7 +124,7 @@ kubernetesPlaylistsAPI,experimental,@grafana/grafana-app-platform-squad,false,fa cloudWatchBatchQueries,preview,@grafana/aws-datasources,false,false,false,false navAdminSubsections,experimental,@grafana/grafana-frontend-platform,false,false,false,false recoveryThreshold,experimental,@grafana/alerting-squad,false,false,true,false -teamHttpHeaders,experimental,@grafana/grafana-authnz-team,false,false,false,false +teamHttpHeaders,experimental,@grafana/identity-access-team,false,false,false,false awsDatasourcesNewFormStyling,experimental,@grafana/aws-datasources,false,false,false,true cachingOptimizeSerializationMemoryUsage,experimental,@grafana/grafana-operator-experience-squad,false,false,false,false panelTitleSearchInV1,experimental,@grafana/backend-platform,true,false,false,false @@ -135,7 +135,7 @@ prometheusPromQAIL,experimental,@grafana/observability-metrics,false,false,false alertmanagerRemoteSecondary,experimental,@grafana/alerting-squad,false,false,false,false alertmanagerRemotePrimary,experimental,@grafana/alerting-squad,false,false,false,false alertmanagerRemoteOnly,experimental,@grafana/alerting-squad,false,false,false,false -annotationPermissionUpdate,experimental,@grafana/grafana-authnz-team,false,false,false,false +annotationPermissionUpdate,experimental,@grafana/identity-access-team,false,false,false,false extractFieldsNameDeduplication,experimental,@grafana/grafana-bi-squad,false,false,false,true dashboardSceneForViewers,experimental,@grafana/dashboards-squad,false,false,false,true panelFilterVariable,experimental,@grafana/dashboards-squad,false,false,false,true diff --git a/scripts/modowners/README.md b/scripts/modowners/README.md index f93e841ac8294..5b70fe37f4844 100644 --- a/scripts/modowners/README.md +++ b/scripts/modowners/README.md @@ -47,7 +47,7 @@ Example output: @grafana/dataviz-squad 1 @grafana/backend-platform 75 @grafana/grafana-as-code 11 -@grafana/grafana-authnz-team 6 +@grafana/identity-access-team 6 @grafana/partner-datasources 4 ``` @@ -67,7 +67,7 @@ List all dependencies of given owner(s). Example CLI command to list all direct dependencies owned by Delivery and Authnz: -`go run scripts/modowners/modowners.go modules -o @grafana/grafana-delivery,@grafana/grafana-authnz-team go.mod` +`go run scripts/modowners/modowners.go modules -o @grafana/grafana-delivery,@grafana/identity-access-team go.mod` Example output: diff --git a/scripts/modowners/modowners.go b/scripts/modowners/modowners.go index 9650e53243269..f34c42a1a8565 100755 --- a/scripts/modowners/modowners.go +++ b/scripts/modowners/modowners.go @@ -129,7 +129,7 @@ func owners(fileSystem fs.FS, logger *log.Logger, args []string) error { } // Print dependencies for a given owner. Can specify one or more owners. -// An example CLI command to list all direct dependencies owned by Delivery and Authnz `go run scripts/modowners/modowners.go modules -o @grafana/grafana-delivery,@grafana/grafana-authnz-team go.mod` +// An example CLI command to list all direct dependencies owned by Delivery and Authnz `go run scripts/modowners/modowners.go modules -o @grafana/grafana-delivery,@grafana/identity-access-team go.mod` func modules(fileSystem fs.FS, logger *log.Logger, args []string) error { fs := flag.NewFlagSet("modules", flag.ExitOnError) indirect := fs.Bool("i", false, "print indirect dependencies") From 19cd7dbae1b994130cabb3bd42e34effc369458e Mon Sep 17 00:00:00 2001 From: Will Browne Date: Fri, 3 Nov 2023 14:01:08 +0100 Subject: [PATCH 081/869] Plugins: Add API for creating pluginv2 proto client (#77492) * first pass * remove client from plugin * fix wire * update * undo import change * add opts * add check * tidy * re-use logic * rollback changes --- .../backendplugin/grpcplugin/client.go | 58 ++++----- .../backendplugin/grpcplugin/client_proto.go | 123 ++++++++++++++++++ .../backendplugin/grpcplugin/client_v2.go | 6 +- .../backendplugin/grpcplugin/grpc_plugin.go | 20 +-- 4 files changed, 164 insertions(+), 43 deletions(-) create mode 100644 pkg/plugins/backendplugin/grpcplugin/client_proto.go diff --git a/pkg/plugins/backendplugin/grpcplugin/client.go b/pkg/plugins/backendplugin/grpcplugin/client.go index 013dd2d041920..2905a1462cf0a 100644 --- a/pkg/plugins/backendplugin/grpcplugin/client.go +++ b/pkg/plugins/backendplugin/grpcplugin/client.go @@ -27,6 +27,18 @@ var handshake = goplugin.HandshakeConfig{ MagicCookieValue: grpcplugin.MagicCookieValue, } +// pluginSet is list of plugins supported on v2. +var pluginSet = map[int]goplugin.PluginSet{ + grpcplugin.ProtocolVersion: { + "diagnostics": &grpcplugin.DiagnosticsGRPCPlugin{}, + "resource": &grpcplugin.ResourceGRPCPlugin{}, + "data": &grpcplugin.DataGRPCPlugin{}, + "stream": &grpcplugin.StreamGRPCPlugin{}, + "renderer": &pluginextensionv2.RendererGRPCPlugin{}, + "secretsmanager": &secretsmanagerplugin.SecretsManagerGRPCPlugin{}, + }, +} + func newClientConfig(executablePath string, args []string, env []string, logger log.Logger, versionedPlugins map[int]goplugin.PluginSet) *goplugin.ClientConfig { // We can ignore gosec G201 here, since the dynamic part of executablePath comes from the plugin definition @@ -70,18 +82,6 @@ type PluginDescriptor struct { startSecretsManagerFn StartSecretsManagerFunc } -// getV2PluginSet returns list of plugins supported on v2. -func getV2PluginSet() goplugin.PluginSet { - return goplugin.PluginSet{ - "diagnostics": &grpcplugin.DiagnosticsGRPCPlugin{}, - "resource": &grpcplugin.ResourceGRPCPlugin{}, - "data": &grpcplugin.DataGRPCPlugin{}, - "stream": &grpcplugin.StreamGRPCPlugin{}, - "renderer": &pluginextensionv2.RendererGRPCPlugin{}, - "secretsmanager": &secretsmanagerplugin.SecretsManagerGRPCPlugin{}, - } -} - // NewBackendPlugin creates a new backend plugin factory used for registering a backend plugin. func NewBackendPlugin(pluginID, executablePath string, executableArgs ...string) backendplugin.PluginFactoryFunc { return newBackendPlugin(pluginID, executablePath, true, executableArgs...) @@ -95,38 +95,32 @@ func NewUnmanagedBackendPlugin(pluginID, executablePath string, executableArgs . // NewBackendPlugin creates a new backend plugin factory used for registering a backend plugin. func newBackendPlugin(pluginID, executablePath string, managed bool, executableArgs ...string) backendplugin.PluginFactoryFunc { return newPlugin(PluginDescriptor{ - pluginID: pluginID, - executablePath: executablePath, - executableArgs: executableArgs, - managed: managed, - versionedPlugins: map[int]goplugin.PluginSet{ - grpcplugin.ProtocolVersion: getV2PluginSet(), - }, + pluginID: pluginID, + executablePath: executablePath, + executableArgs: executableArgs, + managed: managed, + versionedPlugins: pluginSet, }) } // NewRendererPlugin creates a new renderer plugin factory used for registering a backend renderer plugin. func NewRendererPlugin(pluginID, executablePath string, startFn StartRendererFunc) backendplugin.PluginFactoryFunc { return newPlugin(PluginDescriptor{ - pluginID: pluginID, - executablePath: executablePath, - managed: false, - versionedPlugins: map[int]goplugin.PluginSet{ - grpcplugin.ProtocolVersion: getV2PluginSet(), - }, - startRendererFn: startFn, + pluginID: pluginID, + executablePath: executablePath, + managed: false, + versionedPlugins: pluginSet, + startRendererFn: startFn, }) } // NewSecretsManagerPlugin creates a new secrets manager plugin factory used for registering a backend secrets manager plugin. func NewSecretsManagerPlugin(pluginID, executablePath string, startFn StartSecretsManagerFunc) backendplugin.PluginFactoryFunc { return newPlugin(PluginDescriptor{ - pluginID: pluginID, - executablePath: executablePath, - managed: false, - versionedPlugins: map[int]goplugin.PluginSet{ - grpcplugin.ProtocolVersion: getV2PluginSet(), - }, + pluginID: pluginID, + executablePath: executablePath, + managed: false, + versionedPlugins: pluginSet, startSecretsManagerFn: startFn, }) } diff --git a/pkg/plugins/backendplugin/grpcplugin/client_proto.go b/pkg/plugins/backendplugin/grpcplugin/client_proto.go new file mode 100644 index 0000000000000..cd392b4c693c1 --- /dev/null +++ b/pkg/plugins/backendplugin/grpcplugin/client_proto.go @@ -0,0 +1,123 @@ +package grpcplugin + +import ( + "context" + "errors" + + "google.golang.org/grpc" + + "github.com/grafana/grafana-plugin-sdk-go/genproto/pluginv2" + + "github.com/grafana/grafana/pkg/plugins/log" +) + +var ( + errClientNotStarted = errors.New("plugin client has not been started") +) + +var _ ProtoClient = (*protoClient)(nil) + +type ProtoClient interface { + pluginv2.DataClient + pluginv2.ResourceClient + pluginv2.DiagnosticsClient + pluginv2.StreamClient + + PluginID() string + Logger() log.Logger + Start(context.Context) error + Stop(context.Context) error +} + +type protoClient struct { + plugin *grpcPlugin +} + +type ProtoClientOpts struct { + PluginID string + ExecutablePath string + ExecutableArgs []string + Env []string + Logger log.Logger +} + +func NewProtoClient(opts ProtoClientOpts) (ProtoClient, error) { + p := newGrpcPlugin( + PluginDescriptor{ + pluginID: opts.PluginID, + managed: true, + executablePath: opts.ExecutablePath, + executableArgs: opts.ExecutableArgs, + versionedPlugins: pluginSet, + }, + opts.Logger, + func() []string { return opts.Env }, + ) + + return &protoClient{plugin: p}, nil +} + +func (r *protoClient) PluginID() string { + return r.plugin.descriptor.pluginID +} + +func (r *protoClient) Logger() log.Logger { + return r.plugin.logger +} + +func (r *protoClient) Start(ctx context.Context) error { + return r.plugin.Start(ctx) +} + +func (r *protoClient) Stop(ctx context.Context) error { + return r.plugin.Stop(ctx) +} + +func (r *protoClient) QueryData(ctx context.Context, in *pluginv2.QueryDataRequest, opts ...grpc.CallOption) (*pluginv2.QueryDataResponse, error) { + if r.plugin.pluginClient == nil { + return nil, errClientNotStarted + } + return r.plugin.pluginClient.DataClient.QueryData(ctx, in, opts...) +} + +func (r *protoClient) CallResource(ctx context.Context, in *pluginv2.CallResourceRequest, opts ...grpc.CallOption) (pluginv2.Resource_CallResourceClient, error) { + if r.plugin.pluginClient == nil { + return nil, errClientNotStarted + } + return r.plugin.pluginClient.ResourceClient.CallResource(ctx, in, opts...) +} + +func (r *protoClient) CheckHealth(ctx context.Context, in *pluginv2.CheckHealthRequest, opts ...grpc.CallOption) (*pluginv2.CheckHealthResponse, error) { + if r.plugin.pluginClient == nil { + return nil, errClientNotStarted + } + return r.plugin.pluginClient.DiagnosticsClient.CheckHealth(ctx, in, opts...) +} + +func (r *protoClient) CollectMetrics(ctx context.Context, in *pluginv2.CollectMetricsRequest, opts ...grpc.CallOption) (*pluginv2.CollectMetricsResponse, error) { + if r.plugin.pluginClient == nil { + return nil, errClientNotStarted + } + return r.plugin.pluginClient.DiagnosticsClient.CollectMetrics(ctx, in, opts...) +} + +func (r *protoClient) SubscribeStream(ctx context.Context, in *pluginv2.SubscribeStreamRequest, opts ...grpc.CallOption) (*pluginv2.SubscribeStreamResponse, error) { + if r.plugin.pluginClient == nil { + return nil, errClientNotStarted + } + return r.plugin.pluginClient.StreamClient.SubscribeStream(ctx, in, opts...) +} + +func (r *protoClient) RunStream(ctx context.Context, in *pluginv2.RunStreamRequest, opts ...grpc.CallOption) (pluginv2.Stream_RunStreamClient, error) { + if r.plugin.pluginClient == nil { + return nil, errClientNotStarted + } + return r.plugin.pluginClient.StreamClient.RunStream(ctx, in, opts...) +} + +func (r *protoClient) PublishStream(ctx context.Context, in *pluginv2.PublishStreamRequest, opts ...grpc.CallOption) (*pluginv2.PublishStreamResponse, error) { + if r.plugin.pluginClient == nil { + return nil, errClientNotStarted + } + return r.plugin.pluginClient.StreamClient.PublishStream(ctx, in, opts...) +} diff --git a/pkg/plugins/backendplugin/grpcplugin/client_v2.go b/pkg/plugins/backendplugin/grpcplugin/client_v2.go index b6334e9d904ed..804fc78f25ea4 100644 --- a/pkg/plugins/backendplugin/grpcplugin/client_v2.go +++ b/pkg/plugins/backendplugin/grpcplugin/client_v2.go @@ -28,7 +28,7 @@ type ClientV2 struct { secretsmanagerplugin.SecretsManagerPlugin } -func newClientV2(descriptor PluginDescriptor, logger log.Logger, rpcClient plugin.ClientProtocol) (pluginClient, error) { +func newClientV2(descriptor PluginDescriptor, logger log.Logger, rpcClient plugin.ClientProtocol) (*ClientV2, error) { rawDiagnostics, err := rpcClient.Dispense("diagnostics") if err != nil { return nil, err @@ -59,7 +59,7 @@ func newClientV2(descriptor PluginDescriptor, logger log.Logger, rpcClient plugi return nil, err } - c := ClientV2{} + c := &ClientV2{} if rawDiagnostics != nil { if diagnosticsClient, ok := rawDiagnostics.(grpcplugin.DiagnosticsClient); ok { c.DiagnosticsClient = diagnosticsClient @@ -108,7 +108,7 @@ func newClientV2(descriptor PluginDescriptor, logger log.Logger, rpcClient plugi } } - return &c, nil + return c, nil } func (c *ClientV2) CollectMetrics(ctx context.Context, req *backend.CollectMetricsRequest) (*backend.CollectMetricsResult, error) { diff --git a/pkg/plugins/backendplugin/grpcplugin/grpc_plugin.go b/pkg/plugins/backendplugin/grpcplugin/grpc_plugin.go index 4a6c452bf5265..ffd723690f59b 100644 --- a/pkg/plugins/backendplugin/grpcplugin/grpc_plugin.go +++ b/pkg/plugins/backendplugin/grpcplugin/grpc_plugin.go @@ -26,7 +26,7 @@ type grpcPlugin struct { descriptor PluginDescriptor clientFactory func() *plugin.Client client *plugin.Client - pluginClient pluginClient + pluginClient *ClientV2 logger log.Logger mutex sync.RWMutex decommissioned bool @@ -35,13 +35,17 @@ type grpcPlugin struct { // newPlugin allocates and returns a new gRPC (external) backendplugin.Plugin. func newPlugin(descriptor PluginDescriptor) backendplugin.PluginFactoryFunc { return func(pluginID string, logger log.Logger, env func() []string) (backendplugin.Plugin, error) { - return &grpcPlugin{ - descriptor: descriptor, - logger: logger, - clientFactory: func() *plugin.Client { - return plugin.NewClient(newClientConfig(descriptor.executablePath, descriptor.executableArgs, env(), logger, descriptor.versionedPlugins)) - }, - }, nil + return newGrpcPlugin(descriptor, logger, env), nil + } +} + +func newGrpcPlugin(descriptor PluginDescriptor, logger log.Logger, env func() []string) *grpcPlugin { + return &grpcPlugin{ + descriptor: descriptor, + logger: logger, + clientFactory: func() *plugin.Client { + return plugin.NewClient(newClientConfig(descriptor.executablePath, descriptor.executableArgs, env(), logger, descriptor.versionedPlugins)) + }, } } From d06abedca22714d10f11a24668984a9fbc59cdca Mon Sep 17 00:00:00 2001 From: Santiago Date: Fri, 3 Nov 2023 14:33:12 +0100 Subject: [PATCH 082/869] Docs: Improve our mocks section (backend style guide) (#77615) --- contribute/backend/style-guide.md | 31 ++++++++++++++----------------- 1 file changed, 14 insertions(+), 17 deletions(-) diff --git a/contribute/backend/style-guide.md b/contribute/backend/style-guide.md index 08d1eb8ff99d3..17a063d3dc521 100644 --- a/contribute/backend/style-guide.md +++ b/contribute/backend/style-guide.md @@ -66,25 +66,23 @@ Use [`t.Cleanup`](https://golang.org/pkg/testing/#T.Cleanup) to clean up resourc ### Mock -Optionally, we use [`mock.Mock`](https://github.com/stretchr/testify#mock-package) package to generate mocks. This is +Optionally, we use [`mock.Mock`](https://github.com/stretchr/testify#mock-package) package to write mocks. This is useful when you expect different behaviours of the same function. #### Tips -- Use `Once()` or `Times(n)` to make this mock only works `n` times. -- Use `mockedClass.AssertExpectations(t)` to guarantee that the mock is called the times asked. - - If any mock set is not called or its expects more calls, the test fails. +- Use `Once()` or `Times(n)` to make a method call work `n` times. +- Use `mockedClass.AssertExpectations(t)` to guarantee that methods are called the times asked. + - If any method is not called the expected amount of times, the test fails. - You can pass `mock.Anything` as argument if you don't care about the argument passed. -- Use `mockedClass.AssertNotCalled(t, "FunctionName")` to assert that this test is not called. +- Use `mockedClass.AssertNotCalled(t, "MethodName")` to assert that a method was not called. #### Example -This is an example to easily create a mock of an interface. - Given this interface: ```go -func MyInterface interface { +type MyInterface interface { Get(ctx context.Context, id string) (Object, error) } ``` @@ -92,39 +90,38 @@ func MyInterface interface { Mock implementation should be like this: ```go -import +import "github.com/stretchr/testify/mock" -func MockImplementation struct { +type MockImplementation struct { mock.Mock } -func (m *MockImplementation) Get(ctx context.Context, id string) error { +func (m *MockImplementation) Get(ctx context.Context, id string) (Object, error) { args := m.Called(ctx, id) // Pass all arguments in order here return args.Get(0).(Object), args.Error(1) } ``` -And use it as the following way: +And use it in the following way: ```go - objectToReturn := Object{Message: "abc"} errToReturn := errors.New("my error") myMock := &MockImplementation{} defer myMock.AssertExpectations(t) -myMock.On("Get", mock.Anything, "id1").Return(objectToReturn, errToReturn).Once() -myMock.On("Get", mock.Anything, "id2").Return(Object{}, nil).Once() +myMock.On("Get", mock.Anything, "id1").Return(Object{}, errToReturn).Once() +myMock.On("Get", mock.Anything, "id2").Return(objectToReturn, nil).Once() anyService := NewService(myMock) -resp, err := anyService.Call("id1") -assert.Equal(t, resp.Message, objectToReturn.Message) +resp, err := anyService.Call("id1") assert.Error(t, err, errToReturn) resp, err = anyService.Call("id2") assert.Nil(t, err) +assert.Equal(t, resp.Message, objectToReturn.Message) ``` #### Mockery From 225a69ba029b1fd4e7b960b90b86f9466983b1e3 Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Fri, 3 Nov 2023 15:02:57 +0100 Subject: [PATCH 083/869] Team LBAC: Fix backend validation (#77612) * Team LBAC: Fix backend validation * more tests * use slices.ContainsFunc() --- pkg/api/datasources.go | 55 ++++++++++++++++++------------------- pkg/api/datasources_test.go | 12 +++++--- 2 files changed, 35 insertions(+), 32 deletions(-) diff --git a/pkg/api/datasources.go b/pkg/api/datasources.go index d4fb40a7394c9..028ef7cccbeb4 100644 --- a/pkg/api/datasources.go +++ b/pkg/api/datasources.go @@ -11,8 +11,10 @@ import ( "strconv" "strings" - "github.com/grafana/grafana-plugin-sdk-go/backend" + "github.com/prometheus/prometheus/promql/parser" + "golang.org/x/exp/slices" + "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana/pkg/api/datasource" "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/api/response" @@ -336,16 +338,18 @@ func validateURL(cmdType string, url string) response.Response { // This is done to prevent data source proxy from being used to circumvent auth proxy. // For more context take a look at CVE-2022-35957 func validateJSONData(ctx context.Context, jsonData *simplejson.Json, cfg *setting.Cfg, features *featuremgmt.FeatureManager) error { - if jsonData == nil || !cfg.AuthProxyEnabled { + if jsonData == nil { return nil } - for key, value := range jsonData.MustMap() { - if strings.HasPrefix(key, datasources.CustomHeaderName) { - header := fmt.Sprint(value) - if http.CanonicalHeaderKey(header) == http.CanonicalHeaderKey(cfg.AuthProxyHeaderName) { - datasourcesLogger.Error("Forbidden to add a data source header with a name equal to auth proxy header name", "headerName", key) - return errors.New("validation error, invalid header name specified") + if cfg.AuthProxyEnabled { + for key, value := range jsonData.MustMap() { + if strings.HasPrefix(key, datasources.CustomHeaderName) { + header := fmt.Sprint(value) + if http.CanonicalHeaderKey(header) == http.CanonicalHeaderKey(cfg.AuthProxyHeaderName) { + datasourcesLogger.Error("Forbidden to add a data source header with a name equal to auth proxy header name", "headerName", key) + return errors.New("validation error, invalid header name specified") + } } } } @@ -374,11 +378,13 @@ func validateTeamHTTPHeaderJSON(jsonData *simplejson.Json) error { // each teams headers for _, teamheaders := range teamHTTPHeadersJSON { for _, header := range teamheaders { - if !contains(validHeaders, header.Header) { + if !slices.ContainsFunc(validHeaders, func(v string) bool { + return http.CanonicalHeaderKey(v) == http.CanonicalHeaderKey(header.Header) + }) { datasourcesLogger.Error("Cannot add a data source team header that is different than", "headerName", header.Header) return errors.New("validation error, invalid header name specified") } - if !teamHTTPHeaderValueRegexMatch(header.Value) { + if !validateLBACHeader(header.Value) { datasourcesLogger.Error("Cannot add a data source team header value with invalid value", "headerValue", header.Value) return errors.New("validation error, invalid header value syntax") } @@ -387,27 +393,20 @@ func validateTeamHTTPHeaderJSON(jsonData *simplejson.Json) error { return nil } -func contains(slice []string, value string) bool { - for _, v := range slice { - if http.CanonicalHeaderKey(v) == http.CanonicalHeaderKey(value) { - return true - } - } - return false -} - -// teamHTTPHeaderValueRegexMatch returns true if the header value matches the regex -// words separated by special characters -// namespace!="auth", env="prod", env!~"dev" -func teamHTTPHeaderValueRegexMatch(headervalue string) bool { - // link to regex: https://regex101.com/r/I8KhZz/1 - // 1234:{ name!="value",foo!~"bar" } - exp := `^\d+:{(?:\s*\w+\s*(?:=|!=|=~|!~)\s*\"\w+\"\s*,*)+}$` - reg, err := regexp.Compile(exp) +// validateLBACHeader returns true if the header value matches the syntax +// 1234:{ name!="value",foo!~"bar" } +func validateLBACHeader(headervalue string) bool { + exp := `^\d+:(.+)` + pattern, err := regexp.Compile(exp) if err != nil { return false } - return reg.Match([]byte(strings.TrimSpace(headervalue))) + match := pattern.FindSubmatch([]byte(strings.TrimSpace(headervalue))) + if match == nil || len(match) < 2 { + return false + } + _, err = parser.ParseMetricSelector(string(match[1])) + return err == nil } // swagger:route POST /datasources datasources addDataSource diff --git a/pkg/api/datasources_test.go b/pkg/api/datasources_test.go index f3b9ee53b3b9f..9599e33505c13 100644 --- a/pkg/api/datasources_test.go +++ b/pkg/api/datasources_test.go @@ -481,18 +481,22 @@ func TestAPI_datasources_AccessControl(t *testing.T) { } } -// TeamHTTPHeaderValueRegexMatch returns a regex that can be used to check -func TestTeamHTTPHeaderValueRegexMatch(t *testing.T) { +func TestValidateLBACHeader(t *testing.T) { testcases := []struct { desc string teamHeaderValue string want bool }{ { - desc: "Should be valid regex match for team headervalue", + desc: "Should allow valid header", teamHeaderValue: `1234:{ name!="value",foo!~"bar" }`, want: true, }, + { + desc: "Should allow valid selector", + teamHeaderValue: `1234:{ name!="value",foo!~"bar/baz.foo" }`, + want: true, + }, { desc: "Should return false for incorrect header value", teamHeaderValue: `1234:!="value",foo!~"bar" }`, @@ -501,7 +505,7 @@ func TestTeamHTTPHeaderValueRegexMatch(t *testing.T) { } for _, tc := range testcases { t.Run(tc.desc, func(t *testing.T) { - assert.Equal(t, tc.want, teamHTTPHeaderValueRegexMatch(tc.teamHeaderValue)) + assert.Equal(t, tc.want, validateLBACHeader(tc.teamHeaderValue)) }) } } From f40de8b61361bdcf060376b240d2492050aa2e02 Mon Sep 17 00:00:00 2001 From: Kat Yang <69819079+yangkb09@users.noreply.github.com> Date: Fri, 3 Nov 2023 10:12:40 -0400 Subject: [PATCH 084/869] Chore: Deprecate folderIDs in FolderFilter (#77590) Chore: Deprecate FolderID in FolderFilter --- pkg/services/libraryelements/writers.go | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/pkg/services/libraryelements/writers.go b/pkg/services/libraryelements/writers.go index 9d29b1a4b8412..73b8744f63421 100644 --- a/pkg/services/libraryelements/writers.go +++ b/pkg/services/libraryelements/writers.go @@ -72,9 +72,10 @@ func writeExcludeSQL(query model.SearchLibraryElementsQuery, builder *db.SQLBuil type FolderFilter struct { includeGeneralFolder bool - folderIDs []string - folderUIDs []string - parseError error + // Deprecated: use FolderUID instead + folderIDs []string + folderUIDs []string + parseError error } func parseFolderFilter(query model.SearchLibraryElementsQuery) FolderFilter { @@ -85,7 +86,7 @@ func parseFolderFilter(query model.SearchLibraryElementsQuery) FolderFilter { result := FolderFilter{ includeGeneralFolder: true, - folderIDs: folderIDs, + folderIDs: folderIDs, // nolint:staticcheck folderUIDs: folderUIDs, parseError: nil, } @@ -98,6 +99,7 @@ func parseFolderFilter(query model.SearchLibraryElementsQuery) FolderFilter { if hasFolderFilter { result.includeGeneralFolder = false folderIDs = strings.Split(query.FolderFilter, ",") + // nolint:staticcheck result.folderIDs = folderIDs for _, filter := range folderIDs { folderID, err := strconv.ParseInt(filter, 10, 64) @@ -130,6 +132,7 @@ func parseFolderFilter(query model.SearchLibraryElementsQuery) FolderFilter { func (f *FolderFilter) writeFolderFilterSQL(includeGeneral bool, builder *db.SQLBuilder) error { var sql bytes.Buffer params := make([]any, 0) + // nolint:staticcheck for _, filter := range f.folderIDs { folderID, err := strconv.ParseInt(filter, 10, 64) if err != nil { From ef7b5831695f51442673235863307e81d8175d26 Mon Sep 17 00:00:00 2001 From: Brendan O'Handley Date: Fri, 3 Nov 2023 10:15:31 -0400 Subject: [PATCH 085/869] OpenTSDB: Use refid to support alerting on multiple queries (#77575) * add refid to responses * add test for refId --- pkg/tsdb/opentsdb/opentsdb.go | 10 +++++--- pkg/tsdb/opentsdb/opentsdb_test.go | 39 ++++++++++++++++++++++++++++-- 2 files changed, 43 insertions(+), 6 deletions(-) diff --git a/pkg/tsdb/opentsdb/opentsdb.go b/pkg/tsdb/opentsdb/opentsdb.go index 8dbf3c8a71224..5d1902c7ac947 100644 --- a/pkg/tsdb/opentsdb/opentsdb.go +++ b/pkg/tsdb/opentsdb/opentsdb.go @@ -70,6 +70,8 @@ func (s *Service) QueryData(ctx context.Context, req *backend.QueryDataRequest) q := req.Queries[0] + myRefID := q.RefID + tsdbQuery.Start = q.TimeRange.From.UnixNano() / int64(time.Millisecond) tsdbQuery.End = q.TimeRange.To.UnixNano() / int64(time.Millisecond) @@ -105,7 +107,7 @@ func (s *Service) QueryData(ctx context.Context, req *backend.QueryDataRequest) } }() - result, err := s.parseResponse(logger, res) + result, err := s.parseResponse(logger, res, myRefID) if err != nil { return &backend.QueryDataResponse{}, err } @@ -136,7 +138,7 @@ func (s *Service) createRequest(ctx context.Context, logger log.Logger, dsInfo * return req, nil } -func (s *Service) parseResponse(logger log.Logger, res *http.Response) (*backend.QueryDataResponse, error) { +func (s *Service) parseResponse(logger log.Logger, res *http.Response, myRefID string) (*backend.QueryDataResponse, error) { resp := backend.NewQueryDataResponse() body, err := io.ReadAll(res.Body) @@ -181,9 +183,9 @@ func (s *Service) parseResponse(logger log.Logger, res *http.Response) (*backend data.NewField("time", nil, timeVector), data.NewField("value", tags, values))) } - result := resp.Responses["A"] + result := resp.Responses[myRefID] result.Frames = frames - resp.Responses["A"] = result + resp.Responses[myRefID] = result return resp, nil } diff --git a/pkg/tsdb/opentsdb/opentsdb_test.go b/pkg/tsdb/opentsdb/opentsdb_test.go index 22faa22e289b0..a48c14188f4ab 100644 --- a/pkg/tsdb/opentsdb/opentsdb_test.go +++ b/pkg/tsdb/opentsdb/opentsdb_test.go @@ -33,7 +33,7 @@ func TestOpenTsdbExecutor(t *testing.T) { t.Run("Parse response should handle invalid JSON", func(t *testing.T) { response := `{ invalid }` - result, err := service.parseResponse(logger, &http.Response{Body: io.NopCloser(strings.NewReader(response))}) + result, err := service.parseResponse(logger, &http.Response{Body: io.NopCloser(strings.NewReader(response))}, "A") require.Nil(t, result) require.Error(t, err) }) @@ -63,7 +63,7 @@ func TestOpenTsdbExecutor(t *testing.T) { resp := http.Response{Body: io.NopCloser(strings.NewReader(response))} resp.StatusCode = 200 - result, err := service.parseResponse(logger, &resp) + result, err := service.parseResponse(logger, &resp, "A") require.NoError(t, err) frame := result.Responses["A"] @@ -73,6 +73,41 @@ func TestOpenTsdbExecutor(t *testing.T) { } }) + t.Run("ref id is not hard coded", func(t *testing.T) { + myRefid := "reference id" + + response := ` + [ + { + "metric": "test", + "dps": { + "1405544146": 50.0 + }, + "tags" : { + "env": "prod", + "app": "grafana" + } + } + ]` + + testFrame := data.NewFrame("test", + data.NewField("time", nil, []time.Time{ + time.Date(2014, 7, 16, 20, 55, 46, 0, time.UTC), + }), + data.NewField("value", map[string]string{"env": "prod", "app": "grafana"}, []float64{ + 50}), + ) + + resp := http.Response{Body: io.NopCloser(strings.NewReader(response))} + resp.StatusCode = 200 + result, err := service.parseResponse(logger, &resp, myRefid) + require.NoError(t, err) + + if diff := cmp.Diff(testFrame, result.Responses[myRefid].Frames[0], data.FrameTestCompareOptions()...); diff != "" { + t.Errorf("Result mismatch (-want +got):\n%s", diff) + } + }) + t.Run("Build metric with downsampling enabled", func(t *testing.T) { query := backend.DataQuery{ JSON: []byte(` From 6b729389b513ad9faeb4cec6f2ad249c0bed3504 Mon Sep 17 00:00:00 2001 From: Brendan O'Handley Date: Fri, 3 Nov 2023 10:16:02 -0400 Subject: [PATCH 086/869] Prometheus: Set httpMethod as POST for new query client when not defined (#77503) set httpMethod as POST for new query client when not defined --- pkg/tsdb/prometheus/querydata/request.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pkg/tsdb/prometheus/querydata/request.go b/pkg/tsdb/prometheus/querydata/request.go index 5992e5ded631e..0307a984b4673 100644 --- a/pkg/tsdb/prometheus/querydata/request.go +++ b/pkg/tsdb/prometheus/querydata/request.go @@ -64,6 +64,10 @@ func New( return nil, err } + if httpMethod == "" { + httpMethod = http.MethodPost + } + promClient := client.NewClient(httpClient, httpMethod, settings.URL) // standard deviation sampler is the default for backwards compatibility @@ -97,6 +101,7 @@ func (s *QueryData) Execute(ctx context.Context, req *backend.QueryDataRequest) if err != nil { return &result, err } + r := s.fetch(ctx, s.client, query, req.Headers) if r == nil { s.log.FromContext(ctx).Debug("Received nil response from runQuery", "query", query.Expr) From 67b29720524e02b38ed2edd252bd0d465536acc2 Mon Sep 17 00:00:00 2001 From: Dan Cech Date: Fri, 3 Nov 2023 10:30:52 -0400 Subject: [PATCH 087/869] Chore: add/update sqlstore-related helper functions (#77408) * add/update sqlstore-related helper functions * add documentation & tests for InsertQuery and UpdateQuery, make generated SQL deterministic by sorting columns * remove old log line --- pkg/infra/db/db.go | 3 + pkg/infra/db/dbtest/dbtest.go | 5 + pkg/services/sqlstore/logger.go | 1 - pkg/services/sqlstore/migrator/dialect.go | 77 ++++++++++++ .../sqlstore/migrator/dialect_test.go | 117 ++++++++++++++++++ pkg/services/sqlstore/migrator/migrator.go | 2 +- .../sqlstore/migrator/mysql_dialect.go | 3 + pkg/services/sqlstore/session/session.go | 11 +- .../store/entity/sqlstash/querybuilder.go | 29 +++-- 9 files changed, 237 insertions(+), 11 deletions(-) create mode 100644 pkg/services/sqlstore/migrator/dialect_test.go diff --git a/pkg/infra/db/db.go b/pkg/infra/db/db.go index c921a6499c1a2..137a0dd9e141d 100644 --- a/pkg/infra/db/db.go +++ b/pkg/infra/db/db.go @@ -5,6 +5,7 @@ import ( "os" "xorm.io/core" + "xorm.io/xorm" "github.com/grafana/grafana/pkg/services/sqlstore" "github.com/grafana/grafana/pkg/services/sqlstore/migrator" @@ -29,6 +30,8 @@ type DB interface { GetDialect() migrator.Dialect // GetDBType returns the name of the database type available to the runtime. GetDBType() core.DbType + // GetEngine returns the underlying xorm engine. + GetEngine() *xorm.Engine // GetSqlxSession is an experimental extension to use sqlx instead of xorm to // communicate with the database. GetSqlxSession() *session.SessionDB diff --git a/pkg/infra/db/dbtest/dbtest.go b/pkg/infra/db/dbtest/dbtest.go index d3c7afeb68490..7aa4c098b865c 100644 --- a/pkg/infra/db/dbtest/dbtest.go +++ b/pkg/infra/db/dbtest/dbtest.go @@ -4,6 +4,7 @@ import ( "context" "xorm.io/core" + "xorm.io/xorm" "github.com/grafana/grafana/pkg/services/sqlstore" "github.com/grafana/grafana/pkg/services/sqlstore/migrator" @@ -43,6 +44,10 @@ func (f *FakeDB) GetDialect() migrator.Dialect { return nil } +func (f *FakeDB) GetEngine() *xorm.Engine { + return nil +} + func (f *FakeDB) GetSqlxSession() *session.SessionDB { return nil } diff --git a/pkg/services/sqlstore/logger.go b/pkg/services/sqlstore/logger.go index e8132a97c43c5..818652f44aa87 100644 --- a/pkg/services/sqlstore/logger.go +++ b/pkg/services/sqlstore/logger.go @@ -100,7 +100,6 @@ func (s *XormLogger) SetLevel(l core.LogLevel) { // ShowSQL implement core.ILogger func (s *XormLogger) ShowSQL(show ...bool) { - s.grafanaLog.Error("ShowSQL", "show", "show") if len(show) == 0 { s.showSQL = true return diff --git a/pkg/services/sqlstore/migrator/dialect.go b/pkg/services/sqlstore/migrator/dialect.go index a284c47b13444..183b619de8339 100644 --- a/pkg/services/sqlstore/migrator/dialect.go +++ b/pkg/services/sqlstore/migrator/dialect.go @@ -5,6 +5,7 @@ import ( "strconv" "strings" + "golang.org/x/exp/slices" "xorm.io/xorm" ) @@ -73,6 +74,14 @@ type Dialect interface { Unlock(LockCfg) error GetDBName(string) (string, error) + + // InsertQuery accepts a table name and a map of column names to values to insert. + // It returns a query string and a slice of parameters that can be executed against the database. + InsertQuery(tableName string, row map[string]any) (string, []any, error) + // UpdateQuery accepts a table name, a map of column names to values to update, and a map of + // column names to values to use in the where clause. + // It returns a query string and a slice of parameters that can be executed against the database. + UpdateQuery(tableName string, row map[string]any, where map[string]any) (string, []any, error) } type LockCfg struct { @@ -344,3 +353,71 @@ func (b *BaseDialect) OrderBy(order string) string { func (b *BaseDialect) GetDBName(_ string) (string, error) { return "", nil } + +func (b *BaseDialect) InsertQuery(tableName string, row map[string]any) (string, []any, error) { + if len(row) < 1 { + return "", nil, fmt.Errorf("no columns provided") + } + + // allocate slices + cols := make([]string, 0, len(row)) + vals := make([]any, 0, len(row)) + keys := make([]string, 0, len(row)) + + // create sorted list of columns + for col := range row { + keys = append(keys, col) + } + slices.Sort[string](keys) + + // build query and values + for _, col := range keys { + cols = append(cols, b.dialect.Quote(col)) + vals = append(vals, row[col]) + } + + return fmt.Sprintf("INSERT INTO %s (%s) VALUES (%s)", b.dialect.Quote(tableName), strings.Join(cols, ", "), strings.Repeat("?, ", len(row)-1)+"?"), vals, nil +} + +func (b *BaseDialect) UpdateQuery(tableName string, row map[string]any, where map[string]any) (string, []any, error) { + if len(row) < 1 { + return "", nil, fmt.Errorf("no columns provided") + } + + if len(where) < 1 { + return "", nil, fmt.Errorf("no where clause provided") + } + + // allocate slices + cols := make([]string, 0, len(row)) + whereCols := make([]string, 0, len(where)) + vals := make([]any, 0, len(row)+len(where)) + keys := make([]string, 0, len(row)) + + // create sorted list of columns to update + for col := range row { + keys = append(keys, col) + } + slices.Sort[string](keys) + + // build update query and values + for _, col := range keys { + cols = append(cols, b.dialect.Quote(col)+"=?") + vals = append(vals, row[col]) + } + + // create sorted list of columns for where clause + keys = make([]string, 0, len(where)) + for col := range where { + keys = append(keys, col) + } + slices.Sort[string](keys) + + // build where clause and values + for _, col := range keys { + whereCols = append(whereCols, b.dialect.Quote(col)+"=?") + vals = append(vals, where[col]) + } + + return fmt.Sprintf("UPDATE %s SET %s WHERE %s", b.dialect.Quote(tableName), strings.Join(cols, ", "), strings.Join(whereCols, " AND ")), vals, nil +} diff --git a/pkg/services/sqlstore/migrator/dialect_test.go b/pkg/services/sqlstore/migrator/dialect_test.go new file mode 100644 index 0000000000000..7558e5f41a937 --- /dev/null +++ b/pkg/services/sqlstore/migrator/dialect_test.go @@ -0,0 +1,117 @@ +package migrator + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestInsertQuery(t *testing.T) { + tests := []struct { + name string + tableName string + values map[string]any + expectedErr bool + expectedPostgresQuery string + expectedPostgresArgs []any + expectedMySQLQuery string + expectedMySQLArgs []any + expectedSQLiteQuery string + expectedSQLiteArgs []any + }{ + { + "insert one", + "some_table", + map[string]any{"col1": "val1", "col2": "val2", "col3": "val3"}, + false, + "INSERT INTO \"some_table\" (\"col1\", \"col2\", \"col3\") VALUES (?, ?, ?)", + []any{"val1", "val2", "val3"}, + "INSERT INTO `some_table` (`col1`, `col2`, `col3`) VALUES (?, ?, ?)", + []any{"val1", "val2", "val3"}, + "INSERT INTO `some_table` (`col1`, `col2`, `col3`) VALUES (?, ?, ?)", + []any{"val1", "val2", "val3"}, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + var db Dialect + db = NewPostgresDialect() + q, args, err := db.InsertQuery(tc.tableName, tc.values) + + require.True(t, (err != nil) == tc.expectedErr) + require.Equal(t, tc.expectedPostgresQuery, q, "Postgres query incorrect") + require.Equal(t, tc.expectedPostgresArgs, args, "Postgres args incorrect") + + db = NewMysqlDialect() + q, args, err = db.InsertQuery(tc.tableName, tc.values) + + require.True(t, (err != nil) == tc.expectedErr) + require.Equal(t, tc.expectedMySQLQuery, q, "MySQL query incorrect") + require.Equal(t, tc.expectedMySQLArgs, args, "MySQL args incorrect") + + db = NewSQLite3Dialect() + q, args, err = db.InsertQuery(tc.tableName, tc.values) + + require.True(t, (err != nil) == tc.expectedErr) + require.Equal(t, tc.expectedSQLiteQuery, q, "SQLite query incorrect") + require.Equal(t, tc.expectedSQLiteArgs, args, "SQLite args incorrect") + }) + } +} + +func TestUpdateQuery(t *testing.T) { + tests := []struct { + name string + tableName string + values map[string]any + where map[string]any + expectedErr bool + expectedPostgresQuery string + expectedPostgresArgs []any + expectedMySQLQuery string + expectedMySQLArgs []any + expectedSQLiteQuery string + expectedSQLiteArgs []any + }{ + { + "insert one", + "some_table", + map[string]any{"col1": "val1", "col2": "val2", "col3": "val3"}, + map[string]any{"key1": 10}, + false, + "UPDATE \"some_table\" SET \"col1\"=?, \"col2\"=?, \"col3\"=? WHERE \"key1\"=?", + []any{"val1", "val2", "val3", 10}, + "UPDATE `some_table` SET `col1`=?, `col2`=?, `col3`=? WHERE `key1`=?", + []any{"val1", "val2", "val3", 10}, + "UPDATE `some_table` SET `col1`=?, `col2`=?, `col3`=? WHERE `key1`=?", + []any{"val1", "val2", "val3", 10}, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + var db Dialect + db = NewPostgresDialect() + q, args, err := db.UpdateQuery(tc.tableName, tc.values, tc.where) + + require.True(t, (err != nil) == tc.expectedErr) + require.Equal(t, tc.expectedPostgresQuery, q, "Postgres query incorrect") + require.Equal(t, tc.expectedPostgresArgs, args, "Postgres args incorrect") + + db = NewMysqlDialect() + q, args, err = db.UpdateQuery(tc.tableName, tc.values, tc.where) + + require.True(t, (err != nil) == tc.expectedErr) + require.Equal(t, tc.expectedMySQLQuery, q, "MySQL query incorrect") + require.Equal(t, tc.expectedMySQLArgs, args, "MySQL args incorrect") + + db = NewSQLite3Dialect() + q, args, err = db.UpdateQuery(tc.tableName, tc.values, tc.where) + + require.True(t, (err != nil) == tc.expectedErr) + require.Equal(t, tc.expectedSQLiteQuery, q, "SQLite query incorrect") + require.Equal(t, tc.expectedSQLiteArgs, args, "SQLite args incorrect") + }) + } +} diff --git a/pkg/services/sqlstore/migrator/migrator.go b/pkg/services/sqlstore/migrator/migrator.go index e32a768364462..30e2fc74142da 100644 --- a/pkg/services/sqlstore/migrator/migrator.go +++ b/pkg/services/sqlstore/migrator/migrator.go @@ -59,7 +59,7 @@ func NewScopedMigrator(engine *xorm.Engine, cfg *setting.Cfg, scope string) *Mig mg.Logger = log.New("migrator") } else { mg.tableName = scope + "_migration_log" - mg.Logger = log.New(scope + " migrator") + mg.Logger = log.New(scope + "-migrator") } return mg } diff --git a/pkg/services/sqlstore/migrator/mysql_dialect.go b/pkg/services/sqlstore/migrator/mysql_dialect.go index 76433c142e518..d9a2f79e0cf3d 100644 --- a/pkg/services/sqlstore/migrator/mysql_dialect.go +++ b/pkg/services/sqlstore/migrator/mysql_dialect.go @@ -69,6 +69,9 @@ func (db *MySQLDialect) SQLType(c *Column) string { c.Length = 64 case DB_NVarchar: res = DB_Varchar + case DB_Uuid: + res = DB_Char + c.Length = 36 default: res = c.Type } diff --git a/pkg/services/sqlstore/session/session.go b/pkg/services/sqlstore/session/session.go index efde384b77b1f..b585925c75382 100644 --- a/pkg/services/sqlstore/session/session.go +++ b/pkg/services/sqlstore/session/session.go @@ -42,7 +42,7 @@ func (gs *SessionDB) NamedExec(ctx context.Context, query string, arg any) (sql. return gs.sqlxdb.NamedExecContext(ctx, gs.sqlxdb.Rebind(query), arg) } -func (gs *SessionDB) driverName() string { +func (gs *SessionDB) DriverName() string { return gs.sqlxdb.DriverName() } @@ -69,7 +69,7 @@ func (gs *SessionDB) WithTransaction(ctx context.Context, callback func(*Session } func (gs *SessionDB) ExecWithReturningId(ctx context.Context, query string, args ...any) (int64, error) { - return execWithReturningId(ctx, gs.driverName(), query, gs, args...) + return execWithReturningId(ctx, gs.DriverName(), query, gs, args...) } type SessionTx struct { @@ -125,3 +125,10 @@ func execWithReturningId(ctx context.Context, driverName string, query string, s } return id, nil } + +type SessionQuerier interface { + Query(ctx context.Context, query string, args ...any) (*sql.Rows, error) +} + +var _ SessionQuerier = &SessionDB{} +var _ SessionQuerier = &SessionTx{} diff --git a/pkg/services/store/entity/sqlstash/querybuilder.go b/pkg/services/store/entity/sqlstash/querybuilder.go index 23b1bb2bef70b..fe5e2d5b23735 100644 --- a/pkg/services/store/entity/sqlstash/querybuilder.go +++ b/pkg/services/store/entity/sqlstash/querybuilder.go @@ -1,8 +1,13 @@ package sqlstash -import "strings" +import ( + "strings" + + "github.com/grafana/grafana/pkg/services/sqlstore/migrator" +) type selectQuery struct { + dialect migrator.Dialect fields []string // SELECT xyz from string // FROM object limit int64 @@ -12,21 +17,27 @@ type selectQuery struct { args []any } -func (q *selectQuery) addWhere(f string, val any) { - q.args = append(q.args, val) - q.where = append(q.where, f+"=?") +func (q *selectQuery) addWhere(f string, val ...any) { + q.args = append(q.args, val...) + // if the field contains a question mark, we assume it's a raw where clause + if strings.Contains(f, "?") { + q.where = append(q.where, f) + // otherwise we assume it's a field name + } else { + q.where = append(q.where, q.dialect.Quote(f)+"=?") + } } func (q *selectQuery) addWhereInSubquery(f string, subquery string, subqueryArgs []any) { q.args = append(q.args, subqueryArgs...) - q.where = append(q.where, f+" IN ("+subquery+")") + q.where = append(q.where, q.dialect.Quote(f)+" IN ("+subquery+")") } func (q *selectQuery) addWhereIn(f string, vals []string) { count := len(vals) if count > 1 { sb := strings.Builder{} - sb.WriteString(f) + sb.WriteString(q.dialect.Quote(f)) sb.WriteString(" IN (") for i := 0; i < count; i++ { if i > 0 { @@ -46,7 +57,11 @@ func (q *selectQuery) toQuery() (string, []any) { args := q.args sb := strings.Builder{} sb.WriteString("SELECT ") - sb.WriteString(strings.Join(q.fields, ",")) + quotedFields := make([]string, len(q.fields)) + for i, f := range q.fields { + quotedFields[i] = q.dialect.Quote(f) + } + sb.WriteString(strings.Join(quotedFields, ",")) sb.WriteString(" FROM ") sb.WriteString(q.from) From 61d63d3034d7eed4837c63e1f5cb2f6b7114758d Mon Sep 17 00:00:00 2001 From: Victor Marin <36818606+mdvictor@users.noreply.github.com> Date: Fri, 3 Nov 2023 16:39:58 +0200 Subject: [PATCH 088/869] Transformations: Cumulative and window modes for `Add field from calculation` (#77029) * cumulative sum * refactor and create new mode * refactor - use reduceOptions for new mode also * revert naming * Add window function, rename statistical to cumulative (#77066) * Add window function, rename statistical to cumulative * Fix merge errors * fix more merge errors * refactor + add window funcs Co-authored-by: Oscar Kilhed * add ff + tests + centered moving avg Co-authored-by: Oscar Kilhed * make sum and mean cumulative more efficient (#77173) * make sum and mean cumulative more efficient * remove cumulative variance, add window stddev * refactor window to not use reducer for mean. wip variance stdDev * fix tests after optimization --------- Co-authored-by: Victor Marin * optimize window func (#77266) * make sum and mean cumulative more efficient * remove cumulative variance, add window stddev * refactor window to not use reducer for mean. wip variance stdDev * fix tests after optimization * fix test lint * optimize window * tests are passing * fix nulls * fix all nulls --------- Co-authored-by: Victor Marin * change window size to be percentage * fix tests to use percentage * fixed/percentage window size (#77369) * Add docs for cumulative and window functions of the add field from calculation transform. (#77352) add docs * splling * change WindowType -> WindowAlignment * update betterer * refactor getWindowCreator * add docs to content.ts * add feature toggle message --------- Co-authored-by: Oscar Kilhed --- .betterer.results | 7 +- .../transform-data/index.md | 26 + .../feature-toggles/index.md | 1 + .../transformers/calculateField.test.ts | 496 +++++++++++++++++- .../transformers/calculateField.ts | 318 ++++++++++- .../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 + .../app/features/transformers/docs/content.ts | 20 + .../CalculateFieldTransformerEditor.tsx | 223 +++++++- 11 files changed, 1071 insertions(+), 33 deletions(-) diff --git a/.betterer.results b/.betterer.results index c3508a49fe399..0e1dfd0c5a05b 100644 --- a/.betterer.results +++ b/.betterer.results @@ -5089,7 +5089,12 @@ exports[`better eslint`] = { [0, 0, 0, "Styles should be written using objects.", "0"] ], "public/app/features/transformers/editors/CalculateFieldTransformerEditor.tsx:5381": [ - [0, 0, 0, "Do not use any type assertions.", "0"] + [0, 0, 0, "Do not use any type assertions.", "0"], + [0, 0, 0, "Do not use any type assertions.", "1"], + [0, 0, 0, "Do not use any type assertions.", "2"], + [0, 0, 0, "Do not use any type assertions.", "3"], + [0, 0, 0, "Do not use any type assertions.", "4"], + [0, 0, 0, "Do not use any type assertions.", "5"] ], "public/app/features/transformers/editors/ConvertFieldTypeTransformerEditor.tsx:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"] diff --git a/docs/sources/panels-visualizations/query-transform-data/transform-data/index.md b/docs/sources/panels-visualizations/query-transform-data/transform-data/index.md index 5bbec1eba119d..3d81888d5d233 100644 --- a/docs/sources/panels-visualizations/query-transform-data/transform-data/index.md +++ b/docs/sources/panels-visualizations/query-transform-data/transform-data/index.md @@ -127,6 +127,7 @@ You can perform the following transformations on your data. Use this transformation to add a new field calculated from two other fields. Each transformation allows you to add one new field. - **Mode -** Select a mode: + - **Reduce row -** Apply selected calculation on each row of selected fields independently. - **Binary operation -** Apply basic binary operations (for example, sum or multiply) on values in a single row from two selected fields. - **Unary operation -** Apply basic unary operations on values in a single row from a selected field. The available operations are: @@ -135,7 +136,32 @@ Use this transformation to add a new field calculated from two other fields. Eac - **Natural logarithm (ln)** - Returns the natural logarithm of a given expression. - **Floor (floor)** - Returns the largest integer less than or equal to a given expression. - **Ceiling (ceil)** - Returns the smallest integer greater than or equal to a given expression. + - **Cumulative functions** - Apply functions on the current row and all preceding rows. + + **Note:** This mode is an experimental feature. Engineering and on-call support is not available. + Documentation is either limited or not provided outside of code comments. No SLA is provided. + Enable the 'addFieldFromCalculationStatFunctions' in Grafana to use this feature. + Contact Grafana Support to enable this feature in Grafana Cloud. + + - **Total** - Calculates the cumulative total up to and including the current row. + - **Mean** - Calculates the mean up to and including the current row. + + - **Window functions** - Apply window functions. The window can either be **trailing** or **centered**. + With a trailing window the current row will be the last row in the window. + With a centered window the window will be centered on the current row. + For even window sizes, the window will be centered between the current row, and the previous row. + + **Note:** This mode is an experimental feature. Engineering and on-call support is not available. + Documentation is either limited or not provided outside of code comments. No SLA is provided. + Enable the 'addFieldFromCalculationStatFunctions' in Grafana to use this feature. + Contact Grafana Support to enable this feature in Grafana Cloud. + + - **Mean** - Calculates the moving mean or running average. + - **Stddev** - Calculates the moving standard deviation. + - **Variance** - Calculates the moving variance. + - **Row index -** Insert a field with the row index. + - **Field name -** Select the names of fields you want to use in the calculation for the new field. - **Calculation -** If you select **Reduce row** mode, then the **Calculation** field appears. Click in the field to see a list of calculation choices you can use to create the new field. For information about available calculations, refer to [Calculation types][]. - **Operation -** If you select **Binary operation** or **Unary operation** mode, then the **Operation** fields appear. These fields allow you to apply basic math operations on values in a single row from selected fields. You can also use numerical values for binary operations. 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 9aec15cb7fe9b..79fe5fa3dfa03 100644 --- a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md +++ b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md @@ -157,6 +157,7 @@ Experimental features might be changed or removed without prior notice. | `costManagementUi` | Toggles the display of the cost management ui plugin | | `managedPluginsInstall` | Install managed plugins directly from plugins catalog | | `prometheusPromQAIL` | Prometheus and AI/ML to assist users in creating a query | +| `addFieldFromCalculationStatFunctions` | Add cumulative and window functions to the add field from calculation transformation | | `alertmanagerRemoteSecondary` | Enable Grafana to sync configuration and state with a remote Alertmanager. | | `alertmanagerRemotePrimary` | Enable Grafana to have a remote Alertmanager instance as the primary Alertmanager. | | `alertmanagerRemoteOnly` | Disable the internal Alertmanager and only use the external one defined. | diff --git a/packages/grafana-data/src/transformations/transformers/calculateField.test.ts b/packages/grafana-data/src/transformations/transformers/calculateField.test.ts index 2ebb18c4aa547..31518b76b54dc 100644 --- a/packages/grafana-data/src/transformations/transformers/calculateField.test.ts +++ b/packages/grafana-data/src/transformations/transformers/calculateField.test.ts @@ -7,7 +7,13 @@ import { mockTransformationsRegistry } from '../../utils/tests/mockTransformatio import { ReducerID } from '../fieldReducer'; import { transformDataFrame } from '../transformDataFrame'; -import { CalculateFieldMode, calculateFieldTransformer, ReduceOptions } from './calculateField'; +import { + CalculateFieldMode, + calculateFieldTransformer, + ReduceOptions, + WindowSizeMode, + WindowAlignment, +} from './calculateField'; import { DataTransformerID } from './ids'; const seriesA = toDataFrame({ @@ -398,4 +404,492 @@ describe('calculateField transformer w/ timeseries', () => { `); }); }); + + it('calculates centered moving average on odd window size', async () => { + const cfg = { + id: DataTransformerID.calculateField, + options: { + mode: CalculateFieldMode.WindowFunctions, + window: { + windowAlignment: WindowAlignment.Centered, + field: 'x', + windowSize: 1, + windowSizeMode: WindowSizeMode.Percentage, + reducer: ReducerID.mean, + }, + }, + }; + + const series = toDataFrame({ + fields: [{ name: 'x', type: FieldType.number, values: [1, 2, 3] }], + }); + + await expect(transformDataFrame([cfg], [series])).toEmitValuesWith((received) => { + const data = received[0][0]; + + expect(data.fields.length).toEqual(2); + expect(data.fields[1].values[0]).toEqual(1.5); + expect(data.fields[1].values[1]).toEqual(2); + expect(data.fields[1].values[2]).toEqual(2.5); + }); + }); + + it('calculates centered moving average on even window size', async () => { + const cfg = { + id: DataTransformerID.calculateField, + options: { + mode: CalculateFieldMode.WindowFunctions, + window: { + windowAlignment: WindowAlignment.Centered, + field: 'x', + windowSize: 0.5, + windowSizeMode: WindowSizeMode.Percentage, + reducer: ReducerID.mean, + }, + }, + }; + + const series = toDataFrame({ + fields: [{ name: 'x', type: FieldType.number, values: [1, 2, 3] }], + }); + + await expect(transformDataFrame([cfg], [series])).toEmitValuesWith((received) => { + const data = received[0][0]; + + expect(data.fields.length).toEqual(2); + expect(data.fields[1].values[0]).toEqual(1); + expect(data.fields[1].values[1]).toEqual(1.5); + expect(data.fields[1].values[2]).toEqual(2.5); + }); + }); + + it('calculates centered moving average when window size larger than dataset', async () => { + const cfg = { + id: DataTransformerID.calculateField, + options: { + mode: CalculateFieldMode.WindowFunctions, + window: { + windowAlignment: WindowAlignment.Centered, + field: 'x', + windowSize: 5, + windowSizeMode: WindowSizeMode.Percentage, + reducer: ReducerID.mean, + }, + }, + }; + + const series = toDataFrame({ + fields: [{ name: 'x', type: FieldType.number, values: [1, 2, 3] }], + }); + + await expect(transformDataFrame([cfg], [series])).toEmitValuesWith((received) => { + const data = received[0][0]; + + expect(data.fields.length).toEqual(2); + expect(data.fields[1].values[0]).toEqual(2); + expect(data.fields[1].values[1]).toEqual(2); + expect(data.fields[1].values[2]).toEqual(2); + }); + }); + + it('calculates trailing moving average', async () => { + const cfg = { + id: DataTransformerID.calculateField, + options: { + mode: CalculateFieldMode.WindowFunctions, + window: { + windowAlignment: WindowAlignment.Trailing, + field: 'x', + windowSize: 1, + windowSizeMode: WindowSizeMode.Percentage, + reducer: ReducerID.mean, + }, + }, + }; + + const series = toDataFrame({ + fields: [{ name: 'x', type: FieldType.number, values: [1, 2, 3] }], + }); + + await expect(transformDataFrame([cfg], [series])).toEmitValuesWith((received) => { + const data = received[0][0]; + + expect(data.fields.length).toEqual(2); + expect(data.fields[1].values[0]).toEqual(1); + expect(data.fields[1].values[1]).toEqual(1.5); + expect(data.fields[1].values[2]).toEqual(2); + }); + }); + + it('throws error when calculating moving average if window size < 1', async () => { + const cfg = { + id: DataTransformerID.calculateField, + options: { + mode: CalculateFieldMode.WindowFunctions, + window: { + windowAlignment: WindowAlignment.Trailing, + field: 'x', + windowSize: 0, + windowSizeMode: WindowSizeMode.Percentage, + reducer: ReducerID.mean, + }, + }, + }; + + const series = toDataFrame({ + fields: [{ name: 'x', type: FieldType.number, values: [1, 2, 3] }], + }); + + await expect(transformDataFrame([cfg], [series])).toEmitValuesWith((received) => { + const err = new Error('Add field from calculation transformation - Window size must be larger than 0'); + expect(received[0]).toEqual(err); + }); + }); + + it('calculates cumulative total', async () => { + const cfg = { + id: DataTransformerID.calculateField, + options: { + mode: CalculateFieldMode.CumulativeFunctions, + cumulative: { + field: 'x', + reducer: ReducerID.sum, + }, + }, + }; + + const series = toDataFrame({ + fields: [{ name: 'x', type: FieldType.number, values: [1, 2, 3] }], + }); + + await expect(transformDataFrame([cfg], [series])).toEmitValuesWith((received) => { + const data = received[0][0]; + + expect(data.fields.length).toEqual(2); + expect(data.fields[1].values[0]).toEqual(1); + expect(data.fields[1].values[1]).toEqual(3); + expect(data.fields[1].values[2]).toEqual(6); + }); + }); + + it('calculates cumulative mean', async () => { + const cfg = { + id: DataTransformerID.calculateField, + options: { + mode: CalculateFieldMode.CumulativeFunctions, + cumulative: { + field: 'x', + reducer: ReducerID.mean, + }, + }, + }; + + const series = toDataFrame({ + fields: [{ name: 'x', type: FieldType.number, values: [1, 2, 3] }], + }); + + await expect(transformDataFrame([cfg], [series])).toEmitValuesWith((received) => { + const data = received[0][0]; + + expect(data.fields.length).toEqual(2); + expect(data.fields[1].values[0]).toEqual(1); + expect(data.fields[1].values[1]).toEqual(1.5); + expect(data.fields[1].values[2]).toEqual(2); + }); + }); + + it('calculates cumulative total with nulls', async () => { + const cfg = { + id: DataTransformerID.calculateField, + options: { + mode: CalculateFieldMode.CumulativeFunctions, + cumulative: { + field: 'x', + reducer: ReducerID.sum, + }, + }, + }; + + const series = toDataFrame({ + fields: [{ name: 'x', type: FieldType.number, values: [1, null, 2, 3] }], + }); + + await expect(transformDataFrame([cfg], [series])).toEmitValuesWith((received) => { + const data = received[0][0]; + + expect(data.fields.length).toEqual(2); + expect(data.fields[1].values[0]).toEqual(1); + expect(data.fields[1].values[1]).toEqual(1); + expect(data.fields[1].values[2]).toEqual(3); + expect(data.fields[1].values[3]).toEqual(6); + }); + }); + + it('calculates trailing moving average with nulls', async () => { + const cfg = { + id: DataTransformerID.calculateField, + options: { + mode: CalculateFieldMode.WindowFunctions, + window: { + windowAlignment: WindowAlignment.Trailing, + field: 'x', + windowSize: 0.75, + windowSizeMode: WindowSizeMode.Percentage, + reducer: ReducerID.mean, + }, + }, + }; + + const series = toDataFrame({ + fields: [{ name: 'x', type: FieldType.number, values: [1, null, 2, 7] }], + }); + + await expect(transformDataFrame([cfg], [series])).toEmitValuesWith((received) => { + const data = received[0][0]; + + expect(data.fields.length).toEqual(2); + expect(data.fields[1].values[0]).toEqual(1); + expect(data.fields[1].values[1]).toEqual(1); + expect(data.fields[1].values[2]).toEqual(1.5); + expect(data.fields[1].values[3]).toEqual(4.5); + }); + }); + + it('calculates trailing moving variance', async () => { + const cfg = { + id: DataTransformerID.calculateField, + options: { + mode: CalculateFieldMode.WindowFunctions, + window: { + windowAlignment: WindowAlignment.Trailing, + field: 'x', + windowSize: 1, + windowSizeMode: WindowSizeMode.Percentage, + reducer: ReducerID.variance, + }, + }, + }; + + const series = toDataFrame({ + fields: [{ name: 'x', type: FieldType.number, values: [1, 2, 3] }], + }); + + await expect(transformDataFrame([cfg], [series])).toEmitValuesWith((received) => { + const data = received[0][0]; + + expect(data.fields.length).toEqual(2); + expect(data.fields[1].values[0]).toEqual(0); + expect(data.fields[1].values[1]).toEqual(0.25); + expect(data.fields[1].values[2]).toBeCloseTo(0.6666666, 4); + }); + }); + + it('calculates centered moving stddev', async () => { + const cfg = { + id: DataTransformerID.calculateField, + options: { + mode: CalculateFieldMode.WindowFunctions, + window: { + windowAlignment: WindowAlignment.Centered, + field: 'x', + windowSize: 1, + windowSizeMode: WindowSizeMode.Percentage, + reducer: ReducerID.stdDev, + }, + }, + }; + + const series = toDataFrame({ + fields: [{ name: 'x', type: FieldType.number, values: [1, 2, 3] }], + }); + + await expect(transformDataFrame([cfg], [series])).toEmitValuesWith((received) => { + const data = received[0][0]; + + expect(data.fields.length).toEqual(2); + expect(data.fields[1].values[0]).toEqual(0.5); + expect(data.fields[1].values[1]).toBeCloseTo(0.8164, 2); + expect(data.fields[1].values[2]).toEqual(0.5); + }); + }); + + it('calculates centered moving stddev with null', async () => { + const cfg = { + id: DataTransformerID.calculateField, + options: { + mode: CalculateFieldMode.WindowFunctions, + window: { + windowAlignment: WindowAlignment.Centered, + field: 'x', + windowSize: 0.75, + windowSizeMode: WindowSizeMode.Percentage, + reducer: ReducerID.stdDev, + }, + }, + }; + + const series = toDataFrame({ + fields: [{ name: 'x', type: FieldType.number, values: [1, null, 2, 3] }], + }); + + await expect(transformDataFrame([cfg], [series])).toEmitValuesWith((received) => { + const data = received[0][0]; + + expect(data.fields.length).toEqual(2); + expect(data.fields[1].values[0]).toEqual(0); + expect(data.fields[1].values[1]).toEqual(0.5); + expect(data.fields[1].values[2]).toEqual(0.5); + expect(data.fields[1].values[3]).toEqual(0.5); + }); + }); + + it('calculates centered moving average with nulls', async () => { + const cfg = { + id: DataTransformerID.calculateField, + options: { + mode: CalculateFieldMode.WindowFunctions, + window: { + windowAlignment: WindowAlignment.Centered, + field: 'x', + windowSize: 0.75, + windowSizeMode: WindowSizeMode.Percentage, + reducer: ReducerID.mean, + }, + }, + }; + + const series = toDataFrame({ + fields: [{ name: 'x', type: FieldType.number, values: [1, null, 2, 7] }], + }); + + await expect(transformDataFrame([cfg], [series])).toEmitValuesWith((received) => { + const data = received[0][0]; + + expect(data.fields.length).toEqual(2); + expect(data.fields[1].values[0]).toEqual(1); + expect(data.fields[1].values[1]).toEqual(1.5); + expect(data.fields[1].values[2]).toEqual(4.5); + expect(data.fields[1].values[3]).toEqual(4.5); + }); + }); + + it('calculates centered moving average with only nulls', async () => { + const cfg = { + id: DataTransformerID.calculateField, + options: { + mode: CalculateFieldMode.WindowFunctions, + window: { + windowAlignment: WindowAlignment.Centered, + field: 'x', + windowSize: 0.75, + windowSizeMode: WindowSizeMode.Percentage, + reducer: ReducerID.mean, + }, + }, + }; + + const series = toDataFrame({ + fields: [{ name: 'x', type: FieldType.number, values: [null, null, null, null] }], + }); + + await expect(transformDataFrame([cfg], [series])).toEmitValuesWith((received) => { + const data = received[0][0]; + + expect(data.fields.length).toEqual(2); + expect(data.fields[1].values[0]).toEqual(0); + expect(data.fields[1].values[1]).toEqual(0); + expect(data.fields[1].values[2]).toEqual(0); + expect(data.fields[1].values[3]).toEqual(0); + }); + }); + + it('calculates centered moving average with 4 values', async () => { + const cfg = { + id: DataTransformerID.calculateField, + options: { + mode: CalculateFieldMode.WindowFunctions, + window: { + windowAlignment: WindowAlignment.Centered, + field: 'x', + windowSize: 0.75, + windowSizeMode: WindowSizeMode.Percentage, + reducer: ReducerID.mean, + }, + }, + }; + + const series = toDataFrame({ + fields: [{ name: 'x', type: FieldType.number, values: [1, 2, 3, 4] }], + }); + + await expect(transformDataFrame([cfg], [series])).toEmitValuesWith((received) => { + const data = received[0][0]; + + expect(data.fields.length).toEqual(2); + expect(data.fields[1].values[0]).toEqual(1.5); + expect(data.fields[1].values[1]).toEqual(2); + expect(data.fields[1].values[2]).toEqual(3); + expect(data.fields[1].values[3]).toEqual(3.5); + }); + }); + + it('calculates trailing moving variance with null in the middle', async () => { + const cfg = { + id: DataTransformerID.calculateField, + options: { + mode: CalculateFieldMode.WindowFunctions, + window: { + windowAlignment: WindowAlignment.Trailing, + field: 'x', + windowSize: 0.75, + windowSizeMode: WindowSizeMode.Percentage, + reducer: ReducerID.variance, + }, + }, + }; + + const series = toDataFrame({ + fields: [{ name: 'x', type: FieldType.number, values: [1, null, 2, 3] }], + }); + + await expect(transformDataFrame([cfg], [series])).toEmitValuesWith((received) => { + const data = received[0][0]; + + expect(data.fields.length).toEqual(2); + expect(data.fields[1].values[0]).toEqual(0); + expect(data.fields[1].values[1]).toEqual(0); + expect(data.fields[1].values[2]).toEqual(0.25); + expect(data.fields[1].values[3]).toEqual(0.25); + }); + }); + + it('calculates trailing moving variance with null in position 0', async () => { + const cfg = { + id: DataTransformerID.calculateField, + options: { + mode: CalculateFieldMode.WindowFunctions, + window: { + windowAlignment: WindowAlignment.Trailing, + field: 'x', + windowSize: 0.75, + windowSizeMode: WindowSizeMode.Percentage, + reducer: ReducerID.variance, + }, + }, + }; + + const series = toDataFrame({ + fields: [{ name: 'x', type: FieldType.number, values: [null, 1, 2, 3] }], + }); + + await expect(transformDataFrame([cfg], [series])).toEmitValuesWith((received) => { + const data = received[0][0]; + + expect(data.fields.length).toEqual(2); + expect(data.fields[1].values[0]).toEqual(0); + expect(data.fields[1].values[1]).toEqual(0); + expect(data.fields[1].values[2]).toEqual(0.25); + expect(data.fields[1].values[3]).toBeCloseTo(0.6666666, 4); + }); + }); }); diff --git a/packages/grafana-data/src/transformations/transformers/calculateField.ts b/packages/grafana-data/src/transformations/transformers/calculateField.ts index e749dfecf726c..d8a9ca0dff0e5 100644 --- a/packages/grafana-data/src/transformations/transformers/calculateField.ts +++ b/packages/grafana-data/src/transformations/transformers/calculateField.ts @@ -16,17 +16,40 @@ import { noopTransformer } from './noop'; export enum CalculateFieldMode { ReduceRow = 'reduceRow', + CumulativeFunctions = 'cumulativeFunctions', + WindowFunctions = 'windowFunctions', BinaryOperation = 'binary', UnaryOperation = 'unary', Index = 'index', } +export enum WindowSizeMode { + Percentage = 'percentage', + Fixed = 'fixed', +} + +export enum WindowAlignment { + Trailing = 'trailing', + Centered = 'centered', +} + export interface ReduceOptions { include?: string[]; // Assume all fields reducer: ReducerID; nullValueMode?: NullValueMode; } +export interface CumulativeOptions { + field?: string; + reducer: ReducerID; +} + +export interface WindowOptions extends CumulativeOptions { + windowSize?: number; + windowSizeMode?: WindowSizeMode; + windowAlignment?: WindowAlignment; +} + export interface UnaryOptions { operator: UnaryOperationID; fieldName: string; @@ -46,6 +69,13 @@ const defaultReduceOptions: ReduceOptions = { reducer: ReducerID.sum, }; +export const defaultWindowOptions: WindowOptions = { + reducer: ReducerID.mean, + windowAlignment: WindowAlignment.Trailing, + windowSizeMode: WindowSizeMode.Percentage, + windowSize: 0.1, +}; + const defaultBinaryOptions: BinaryOptions = { left: '', operator: BinaryOperationID.Add, @@ -64,6 +94,8 @@ export interface CalculateFieldTransformerOptions { // Only one should be filled reduce?: ReduceOptions; + window?: WindowOptions; + cumulative?: CumulativeOptions; binary?: BinaryOptions; unary?: UnaryOptions; index?: IndexOptions; @@ -104,39 +136,49 @@ export const calculateFieldTransformer: DataTransformerInfo { - const indexArr = [...Array(frame.length).keys()]; + creator = getBinaryCreator(defaults(binaryOptions, defaultBinaryOptions), data); + break; + case CalculateFieldMode.Index: + return data.map((frame) => { + const indexArr = [...Array(frame.length).keys()]; - if (options.index?.asPercentile) { - for (let i = 0; i < indexArr.length; i++) { - indexArr[i] = indexArr[i] / indexArr.length; + if (options.index?.asPercentile) { + for (let i = 0; i < indexArr.length; i++) { + indexArr[i] = indexArr[i] / indexArr.length; + } } - } - const f = { - name: options.alias ?? 'Row', - type: FieldType.number, - values: indexArr, - config: options.index?.asPercentile ? { unit: 'percentunit' } : {}, - }; - return { - ...frame, - fields: options.replaceFields ? [f] : [...frame.fields, f], - }; - }); + const f = { + name: options.alias ?? 'Row', + type: FieldType.number, + values: indexArr, + config: options.index?.asPercentile ? { unit: 'percentunit' } : {}, + }; + return { + ...frame, + fields: options.replaceFields ? [f] : [...frame.fields, f], + }; + }); } // Nothing configured @@ -180,6 +222,213 @@ export const calculateFieldTransformer: DataTransformerInfo { + const window = Math.ceil( + options.windowSize! * (options.windowSizeMode === WindowSizeMode.Percentage ? frame.length : 1) + ); + + // Find the columns that should be examined + let selectedField: Field | null = null; + for (const field of frame.fields) { + if (matcher(field, frame, allFrames)) { + selectedField = field; + break; + } + } + + if (!selectedField) { + return; + } + + if (![ReducerID.mean, ReducerID.stdDev, ReducerID.variance].includes(options.reducer)) { + throw new Error(`Add field from calculation transformation - Unsupported reducer: ${options.reducer}`); + } + + if (options.windowAlignment === WindowAlignment.Centered) { + return getCenteredWindowValues(frame, options.reducer, selectedField, window); + } else { + return getTrailingWindowValues(frame, options.reducer, selectedField, window); + } + }; +} + +function getTrailingWindowValues(frame: DataFrame, reducer: ReducerID, selectedField: Field, window: number) { + const vals: number[] = []; + let sum = 0; + let count = 0; + for (let i = 0; i < frame.length; i++) { + if (reducer === ReducerID.mean) { + const currentValue = selectedField.values[i]; + if (currentValue !== null) { + count++; + sum += currentValue; + + if (i > window - 1) { + sum -= selectedField.values[i - window]; + count--; + } + } + vals.push(count === 0 ? 0 : sum / count); + } else if (reducer === ReducerID.variance) { + const start = Math.max(0, i - window + 1); + const end = i + 1; + vals.push(calculateVariance(selectedField.values.slice(start, end))); + } else if (reducer === ReducerID.stdDev) { + const start = Math.max(0, i - window + 1); + const end = i + 1; + vals.push(calculateStdDev(selectedField.values.slice(start, end))); + } + } + return vals; +} + +function getCenteredWindowValues(frame: DataFrame, reducer: ReducerID, selectedField: Field, window: number) { + const vals: number[] = []; + let sum = 0; + let count = 0; + // Current value (i) is included in the leading part of the window. Which means if the window size is odd, + // the leading part of the window will be larger than the trailing part. + const leadingPartOfWindow = Math.ceil(window / 2) - 1; + const trailingPartOfWindow = Math.floor(window / 2); + for (let i = 0; i < frame.length; i++) { + const first = i - trailingPartOfWindow; + const last = i + leadingPartOfWindow; + if (reducer === ReducerID.mean) { + if (i === 0) { + // We're at the start and need to prime the leading part of the window + for (let x = 0; x < leadingPartOfWindow + 1 && x < selectedField.values.length; x++) { + if (selectedField.values[x] !== null) { + sum += selectedField.values[x]; + count++; + } + } + } else { + if (last < selectedField.values.length) { + // Last is inside the data and should be added. + if (selectedField.values[last] !== null) { + sum += selectedField.values[last]; + count++; + } + } + if (first > 0) { + // Remove values that have fallen outside of the window, if the start of the window isn't outside of the data. + if (selectedField.values[first - 1] !== null) { + sum -= selectedField.values[first - 1]; + count--; + } + } + } + vals.push(count === 0 ? 0 : sum / count); + } else if (reducer === ReducerID.variance) { + const windowVals = selectedField.values.slice( + Math.max(0, first), + Math.min(last + 1, selectedField.values.length) + ); + vals.push(calculateVariance(windowVals)); + } else if (reducer === ReducerID.stdDev) { + const windowVals = selectedField.values.slice( + Math.max(0, first), + Math.min(last + 1, selectedField.values.length) + ); + vals.push(calculateStdDev(windowVals)); + } + } + return vals; +} + +function calculateVariance(vals: number[]): number { + if (vals.length < 1) { + return 0; + } + let squareSum = 0; + let runningMean = 0; + let nonNullCount = 0; + for (let i = 0; i < vals.length; i++) { + const currentValue = vals[i]; + if (currentValue !== null) { + nonNullCount++; + let _oldMean = runningMean; + runningMean += (currentValue - _oldMean) / nonNullCount; + squareSum += (currentValue - _oldMean) * (currentValue - runningMean); + } + } + if (nonNullCount === 0) { + return 0; + } + const variance = squareSum / nonNullCount; + return variance; +} + +function calculateStdDev(vals: number[]): number { + return Math.sqrt(calculateVariance(vals)); +} + +function getCumulativeCreator(options: CumulativeOptions, allFrames: DataFrame[]): ValuesCreator { + let matcher = getFieldMatcher({ + id: FieldMatcherID.numeric, + }); + + if (options.field) { + matcher = getFieldMatcher({ + id: FieldMatcherID.byNames, + options: { + names: [options.field], + }, + }); + } + + if (![ReducerID.mean, ReducerID.sum].includes(options.reducer)) { + throw new Error(`Add field from calculation transformation - Unsupported reducer: ${options.reducer}`); + } + + return (frame: DataFrame) => { + // Find the columns that should be examined + let selectedField: Field | null = null; + for (const field of frame.fields) { + if (matcher(field, frame, allFrames)) { + selectedField = field; + break; + } + } + + if (!selectedField) { + return; + } + + const vals: number[] = []; + + let total = 0; + for (let i = 0; i < frame.length; i++) { + total += selectedField.values[i]; + if (options.reducer === ReducerID.sum) { + vals.push(total); + } else if (options.reducer === ReducerID.mean) { + vals.push(total / (i + 1)); + } + } + + return vals; + }; +} + function getReduceRowCreator(options: ReduceOptions, allFrames: DataFrame[]): ValuesCreator { let matcher = getFieldMatcher({ id: FieldMatcherID.numeric, @@ -227,6 +476,7 @@ function getReduceRowCreator(options: ReduceOptions, allFrames: DataFrame[]): Va for (let j = 0; j < size; j++) { row.values[j] = columns[j][i]; } + vals.push(reducer(row, ignoreNulls, nullAsZero)[options.reducer]); } @@ -309,6 +559,16 @@ export function getNameFromOptions(options: CalculateFieldTransformerOptions) { } switch (options.mode) { + case CalculateFieldMode.CumulativeFunctions: { + const { cumulative } = options; + return `cumulative ${cumulative?.reducer ?? ''}${cumulative?.field ? `(${cumulative.field})` : ''}`; + } + case CalculateFieldMode.WindowFunctions: { + const { window } = options; + return `${window?.windowAlignment ?? ''} moving ${window?.reducer ?? ''}${ + window?.field ? `(${window.field})` : '' + }`; + } case CalculateFieldMode.UnaryOperation: { const { unary } = options; return `${unary?.operator ?? ''}${unary?.fieldName ? `(${unary.fieldName})` : ''}`; diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index e21dc9e57e5fc..ed59e657fa2d2 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -151,6 +151,7 @@ export interface FeatureToggles { costManagementUi?: boolean; managedPluginsInstall?: boolean; prometheusPromQAIL?: boolean; + addFieldFromCalculationStatFunctions?: boolean; alertmanagerRemoteSecondary?: boolean; alertmanagerRemotePrimary?: boolean; alertmanagerRemoteOnly?: boolean; diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index 291b84d79aa47..4b8ab1b622120 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -935,6 +935,13 @@ var ( FrontendOnly: true, Owner: grafanaObservabilityMetricsSquad, }, + { + Name: "addFieldFromCalculationStatFunctions", + Description: "Add cumulative and window functions to the add field from calculation transformation", + Stage: FeatureStageExperimental, + FrontendOnly: true, + Owner: grafanaBiSquad, + }, { Name: "alertmanagerRemoteSecondary", Description: "Enable Grafana to sync configuration and state with a remote Alertmanager.", diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv index f4d39ea7d2feb..4fd2c1e0b811e 100644 --- a/pkg/services/featuremgmt/toggles_gen.csv +++ b/pkg/services/featuremgmt/toggles_gen.csv @@ -132,6 +132,7 @@ pluginsInstrumentationStatusSource,experimental,@grafana/plugins-platform-backen costManagementUi,experimental,@grafana/databases-frontend,false,false,false,false managedPluginsInstall,experimental,@grafana/plugins-platform-backend,false,false,false,false prometheusPromQAIL,experimental,@grafana/observability-metrics,false,false,false,true +addFieldFromCalculationStatFunctions,experimental,@grafana/grafana-bi-squad,false,false,false,true alertmanagerRemoteSecondary,experimental,@grafana/alerting-squad,false,false,false,false alertmanagerRemotePrimary,experimental,@grafana/alerting-squad,false,false,false,false alertmanagerRemoteOnly,experimental,@grafana/alerting-squad,false,false,false,false diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go index 4ea5dd8b5b0f8..15be0d1570f40 100644 --- a/pkg/services/featuremgmt/toggles_gen.go +++ b/pkg/services/featuremgmt/toggles_gen.go @@ -539,6 +539,10 @@ const ( // Prometheus and AI/ML to assist users in creating a query FlagPrometheusPromQAIL = "prometheusPromQAIL" + // FlagAddFieldFromCalculationStatFunctions + // Add cumulative and window functions to the add field from calculation transformation + FlagAddFieldFromCalculationStatFunctions = "addFieldFromCalculationStatFunctions" + // FlagAlertmanagerRemoteSecondary // Enable Grafana to sync configuration and state with a remote Alertmanager. FlagAlertmanagerRemoteSecondary = "alertmanagerRemoteSecondary" diff --git a/public/app/features/transformers/docs/content.ts b/public/app/features/transformers/docs/content.ts index 19ffdb3b1ae70..2ce18627e02df 100644 --- a/public/app/features/transformers/docs/content.ts +++ b/public/app/features/transformers/docs/content.ts @@ -50,6 +50,26 @@ export const transformationDocsContent: TransformationDocsContentType = { - **Natural logarithm (ln)** - Returns the natural logarithm of a given expression. - **Floor (floor)** - Returns the largest integer less than or equal to a given expression. - **Ceiling (ceil)** - Returns the smallest integer greater than or equal to a given expression. + - **Cumulative functions** - Apply functions on the current row and all preceding rows. + + **Note:** This mode is an experimental feature. Engineering and on-call support is not available. + Documentation is either limited or not provided outside of code comments. No SLA is provided. + Enable the 'addFieldFromCalculationStatFunctions' in Grafana to use this feature. + Contact Grafana Support to enable this feature in Grafana Cloud. + - **Total** - Calculates the cumulative total up to and including the current row. + - **Mean** - Calculates the mean up to and including the current row. + - **Window functions** - Apply window functions. The window can either be **trailing** or **centered**. + With a trailing window the current row will be the last row in the window. + With a centered window the window will be centered on the current row. + For even window sizes, the window will be centered between the current row, and the previous row. + + **Note:** This mode is an experimental feature. Engineering and on-call support is not available. + Documentation is either limited or not provided outside of code comments. No SLA is provided. + Enable the 'addFieldFromCalculationStatFunctions' in Grafana to use this feature. + Contact Grafana Support to enable this feature in Grafana Cloud. + - **Mean** - Calculates the moving mean or running average. + - **Stddev** - Calculates the moving standard deviation. + - **Variance** - Calculates the moving variance. - **Row index -** Insert a field with the row index. - **Field name -** Select the names of fields you want to use in the calculation for the new field. - **Calculation -** If you select **Reduce row** mode, then the **Calculation** field appears. Click in the field to see a list of calculation choices you can use to create the new field. For information about available calculations, refer to [Calculation types][]. diff --git a/public/app/features/transformers/editors/CalculateFieldTransformerEditor.tsx b/public/app/features/transformers/editors/CalculateFieldTransformerEditor.tsx index 92efe73a8d29b..83769c26603b9 100644 --- a/public/app/features/transformers/editors/CalculateFieldTransformerEditor.tsx +++ b/public/app/features/transformers/editors/CalculateFieldTransformerEditor.tsx @@ -24,10 +24,15 @@ import { BinaryOptions, UnaryOptions, CalculateFieldMode, + WindowAlignment, CalculateFieldTransformerOptions, getNameFromOptions, IndexOptions, ReduceOptions, + CumulativeOptions, + WindowOptions, + WindowSizeMode, + defaultWindowOptions, } from '@grafana/data/src/transformations/transformers/calculateField'; import { getTemplateSrv, config as cfg } from '@grafana/runtime'; import { @@ -38,9 +43,11 @@ import { InlineLabel, InlineSwitch, Input, + RadioButtonGroup, Select, StatsPicker, } from '@grafana/ui'; +import { NumberInput } from 'app/core/components/OptionsUI/NumberInput'; interface CalculateFieldTransformerEditorProps extends TransformerUIProps {} @@ -57,6 +64,13 @@ const calculationModes = [ { value: CalculateFieldMode.Index, label: 'Row index' }, ]; +if (cfg.featureToggles.addFieldFromCalculationStatFunctions) { + calculationModes.push( + { value: CalculateFieldMode.CumulativeFunctions, label: 'Cumulative functions' }, + { value: CalculateFieldMode.WindowFunctions, label: 'Window functions' } + ); +} + const okTypes = new Set([FieldType.time, FieldType.number, FieldType.string]); const labelWidth = 16; @@ -188,6 +202,9 @@ export class CalculateFieldTransformerEditor extends React.PureComponent< onModeChanged = (value: SelectableValue) => { const { options, onChange } = this.props; const mode = value.value ?? CalculateFieldMode.BinaryOperation; + if (mode === CalculateFieldMode.WindowFunctions) { + options.window = options.window ?? defaultWindowOptions; + } onChange({ ...options, mode, @@ -203,14 +220,13 @@ export class CalculateFieldTransformerEditor extends React.PureComponent< }; //--------------------------------------------------------- - // Reduce by Row + // Cumulative functions //--------------------------------------------------------- updateReduceOptions = (v: ReduceOptions) => { const { options, onChange } = this.props; onChange({ ...options, - mode: CalculateFieldMode.ReduceRow, reduce: v, }); }; @@ -250,6 +266,149 @@ export class CalculateFieldTransformerEditor extends React.PureComponent< ); } + //--------------------------------------------------------- + // Window functions + //--------------------------------------------------------- + + updateWindowOptions = (v: WindowOptions) => { + const { options, onChange } = this.props; + onChange({ + ...options, + mode: CalculateFieldMode.WindowFunctions, + window: v, + }); + }; + + onWindowFieldChange = (v: SelectableValue) => { + const { window } = this.props.options; + this.updateWindowOptions({ + ...window!, + field: v.value!, + }); + }; + + onWindowSizeChange = (v?: number) => { + const { window } = this.props.options; + this.updateWindowOptions({ + ...window!, + windowSize: v && window?.windowSizeMode === WindowSizeMode.Percentage ? v / 100 : v, + }); + }; + + onWindowSizeModeChange = (val: string) => { + const { window } = this.props.options; + const mode = val as WindowSizeMode; + this.updateWindowOptions({ + ...window!, + windowSize: window?.windowSize + ? mode === WindowSizeMode.Percentage + ? window!.windowSize! / 100 + : window!.windowSize! * 100 + : undefined, + windowSizeMode: mode, + }); + }; + + onWindowStatsChange = (stats: string[]) => { + const reducer = stats.length ? (stats[0] as ReducerID) : ReducerID.sum; + + const { window } = this.props.options; + this.updateWindowOptions({ ...window, reducer }); + }; + + onTypeChange = (val: string) => { + const { window } = this.props.options; + this.updateWindowOptions({ + ...window!, + windowAlignment: val as WindowAlignment, + }); + }; + + renderWindowFunctions(options?: WindowOptions) { + const { names } = this.state; + options = defaults(options, { reducer: ReducerID.sum }); + const selectOptions = names.map((v) => ({ label: v, value: v })); + const typeOptions = [ + { label: 'Trailing', value: WindowAlignment.Trailing }, + { label: 'Centered', value: WindowAlignment.Centered }, + ]; + const windowSizeModeOptions = [ + { label: 'Percentage', value: WindowSizeMode.Percentage }, + { label: 'Fixed', value: WindowSizeMode.Fixed }, + ]; + + return ( + <> + + + + + ext.id === ReducerID.sum || ext.id === ReducerID.mean} + /> + + + ); + } + //--------------------------------------------------------- // Binary Operator //--------------------------------------------------------- @@ -468,6 +685,8 @@ export class CalculateFieldTransformerEditor extends React.PureComponent< {mode === CalculateFieldMode.BinaryOperation && this.renderBinaryOperation(options.binary)} {mode === CalculateFieldMode.UnaryOperation && this.renderUnaryOperation(options.unary)} {mode === CalculateFieldMode.ReduceRow && this.renderReduceRow(options.reduce)} + {mode === CalculateFieldMode.CumulativeFunctions && this.renderCumulativeFunctions(options.cumulative)} + {mode === CalculateFieldMode.WindowFunctions && this.renderWindowFunctions(options.window)} {mode === CalculateFieldMode.Index && this.renderRowIndex(options.index)} Date: Fri, 3 Nov 2023 07:49:01 -0700 Subject: [PATCH 089/869] EntityStore: Use protobuf for summary objects (#77600) use protobuf for summary --- pkg/services/store/entity/entity.pb.go | 457 ++++++++++++++---- pkg/services/store/entity/entity.proto | 45 ++ pkg/services/store/entity/entity_grpc.pb.go | 2 +- pkg/services/store/entity/models.go | 53 -- pkg/services/store/kind/dashboard/summary.go | 9 +- .../gdev-walk-graph-gradient-area-fills.json | 20 +- .../gdev-walk-graph-shared-tooltips.json | 50 +- .../gdev-walk-graph-time-regions.json | 36 +- .../testdata/gdev-walk-graph_tests.json | 122 ++--- .../testdata/gdev-walk-graph_y_axis.json | 40 +- .../testdata/with-library-panels-info.json | 14 +- pkg/services/store/kind/dataframe/summary.go | 7 +- .../store/kind/dataframe/summary_test.go | 6 +- pkg/services/store/kind/dummy/summary.go | 6 +- pkg/services/store/kind/geojson/summary.go | 4 +- .../store/kind/geojson/summary_test.go | 8 +- .../store/kind/jsonobj/summary_test.go | 2 +- pkg/services/store/kind/png/summary.go | 7 +- pkg/services/store/kind/png/summary_test.go | 6 +- pkg/services/store/kind/snapshot/summary.go | 4 +- 20 files changed, 586 insertions(+), 312 deletions(-) diff --git a/pkg/services/store/entity/entity.pb.go b/pkg/services/store/entity/entity.pb.go index 1201b7f71fc24..1012b87a26264 100644 --- a/pkg/services/store/entity/entity.pb.go +++ b/pkg/services/store/entity/entity.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.30.0 -// protoc v4.23.4 +// protoc-gen-go v1.31.0 +// protoc v4.24.4 // source: entity.proto package entity @@ -1752,6 +1752,211 @@ func (x *EntityWatchResponse) GetAction() EntityWatchResponse_Action { return EntityWatchResponse_UNKNOWN } +type EntitySummary struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + UID string `protobuf:"bytes,1,opt,name=UID,proto3" json:"UID,omitempty"` + Kind string `protobuf:"bytes,2,opt,name=kind,proto3" json:"kind,omitempty"` + Name string `protobuf:"bytes,3,opt,name=name,proto3" json:"name,omitempty"` + Description string `protobuf:"bytes,4,opt,name=description,proto3" json:"description,omitempty"` + // Key value pairs. Tags are are represented as keys with empty values + Labels map[string]string `protobuf:"bytes,5,rep,name=labels,proto3" json:"labels,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` + // Parent folder UID + Folder string `protobuf:"bytes,6,opt,name=folder,proto3" json:"folder,omitempty"` + // URL safe version of the name. It will be unique within the folder + Slug string `protobuf:"bytes,7,opt,name=slug,proto3" json:"slug,omitempty"` + // When errors exist + Error *EntityErrorInfo `protobuf:"bytes,8,opt,name=error,proto3" json:"error,omitempty"` + // Optional field values. The schema will define and document possible values for a given kind + Fields map[string]string `protobuf:"bytes,9,rep,name=fields,proto3" json:"fields,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` + // eg: panels within dashboard + Nested []*EntitySummary `protobuf:"bytes,10,rep,name=nested,proto3" json:"nested,omitempty"` + // Optional references to external things + References []*EntityExternalReference `protobuf:"bytes,11,rep,name=references,proto3" json:"references,omitempty"` +} + +func (x *EntitySummary) Reset() { + *x = EntitySummary{} + if protoimpl.UnsafeEnabled { + mi := &file_entity_proto_msgTypes[19] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *EntitySummary) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*EntitySummary) ProtoMessage() {} + +func (x *EntitySummary) ProtoReflect() protoreflect.Message { + mi := &file_entity_proto_msgTypes[19] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use EntitySummary.ProtoReflect.Descriptor instead. +func (*EntitySummary) Descriptor() ([]byte, []int) { + return file_entity_proto_rawDescGZIP(), []int{19} +} + +func (x *EntitySummary) GetUID() string { + if x != nil { + return x.UID + } + return "" +} + +func (x *EntitySummary) GetKind() string { + if x != nil { + return x.Kind + } + return "" +} + +func (x *EntitySummary) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *EntitySummary) GetDescription() string { + if x != nil { + return x.Description + } + return "" +} + +func (x *EntitySummary) GetLabels() map[string]string { + if x != nil { + return x.Labels + } + return nil +} + +func (x *EntitySummary) GetFolder() string { + if x != nil { + return x.Folder + } + return "" +} + +func (x *EntitySummary) GetSlug() string { + if x != nil { + return x.Slug + } + return "" +} + +func (x *EntitySummary) GetError() *EntityErrorInfo { + if x != nil { + return x.Error + } + return nil +} + +func (x *EntitySummary) GetFields() map[string]string { + if x != nil { + return x.Fields + } + return nil +} + +func (x *EntitySummary) GetNested() []*EntitySummary { + if x != nil { + return x.Nested + } + return nil +} + +func (x *EntitySummary) GetReferences() []*EntityExternalReference { + if x != nil { + return x.References + } + return nil +} + +type EntityExternalReference struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // Category of dependency + // eg: datasource, plugin, runtime + Family string `protobuf:"bytes,1,opt,name=family,proto3" json:"family,omitempty"` + // datasource > prometheus|influx|... + // plugin > panel | datasource + // runtime > transformer + Type string `protobuf:"bytes,2,opt,name=type,proto3" json:"type,omitempty"` + // datasource > UID + // plugin > plugin identifier + // runtime > name lookup + Identifier string `protobuf:"bytes,3,opt,name=identifier,proto3" json:"identifier,omitempty"` +} + +func (x *EntityExternalReference) Reset() { + *x = EntityExternalReference{} + if protoimpl.UnsafeEnabled { + mi := &file_entity_proto_msgTypes[20] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *EntityExternalReference) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*EntityExternalReference) ProtoMessage() {} + +func (x *EntityExternalReference) ProtoReflect() protoreflect.Message { + mi := &file_entity_proto_msgTypes[20] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use EntityExternalReference.ProtoReflect.Descriptor instead. +func (*EntityExternalReference) Descriptor() ([]byte, []int) { + return file_entity_proto_rawDescGZIP(), []int{20} +} + +func (x *EntityExternalReference) GetFamily() string { + if x != nil { + return x.Family + } + return "" +} + +func (x *EntityExternalReference) GetType() string { + if x != nil { + return x.Type + } + return "" +} + +func (x *EntityExternalReference) GetIdentifier() string { + if x != nil { + return x.Identifier + } + return "" +} + var File_entity_proto protoreflect.FileDescriptor var file_entity_proto_rawDesc = []byte{ @@ -1994,52 +2199,93 @@ var file_entity_proto_rawDesc = []byte{ 0x06, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x2f, 0x0a, 0x06, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x0b, 0x0a, 0x07, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x0b, 0x0a, 0x07, 0x55, 0x50, 0x44, 0x41, 0x54, 0x45, 0x44, 0x10, 0x01, 0x12, 0x0b, 0x0a, 0x07, 0x44, - 0x45, 0x4c, 0x45, 0x54, 0x45, 0x44, 0x10, 0x02, 0x32, 0xb2, 0x04, 0x0a, 0x0b, 0x45, 0x6e, 0x74, - 0x69, 0x74, 0x79, 0x53, 0x74, 0x6f, 0x72, 0x65, 0x12, 0x31, 0x0a, 0x04, 0x52, 0x65, 0x61, 0x64, - 0x12, 0x19, 0x2e, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x2e, 0x52, 0x65, 0x61, 0x64, 0x45, 0x6e, - 0x74, 0x69, 0x74, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x0e, 0x2e, 0x65, 0x6e, - 0x74, 0x69, 0x74, 0x79, 0x2e, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x12, 0x4c, 0x0a, 0x09, 0x42, - 0x61, 0x74, 0x63, 0x68, 0x52, 0x65, 0x61, 0x64, 0x12, 0x1e, 0x2e, 0x65, 0x6e, 0x74, 0x69, 0x74, - 0x79, 0x2e, 0x42, 0x61, 0x74, 0x63, 0x68, 0x52, 0x65, 0x61, 0x64, 0x45, 0x6e, 0x74, 0x69, 0x74, - 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1f, 0x2e, 0x65, 0x6e, 0x74, 0x69, 0x74, - 0x79, 0x2e, 0x42, 0x61, 0x74, 0x63, 0x68, 0x52, 0x65, 0x61, 0x64, 0x45, 0x6e, 0x74, 0x69, 0x74, - 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x40, 0x0a, 0x05, 0x57, 0x72, 0x69, - 0x74, 0x65, 0x12, 0x1a, 0x2e, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x2e, 0x57, 0x72, 0x69, 0x74, - 0x65, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, + 0x45, 0x4c, 0x45, 0x54, 0x45, 0x44, 0x10, 0x02, 0x22, 0xa2, 0x04, 0x0a, 0x0d, 0x45, 0x6e, 0x74, + 0x69, 0x74, 0x79, 0x53, 0x75, 0x6d, 0x6d, 0x61, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x55, 0x49, + 0x44, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x55, 0x49, 0x44, 0x12, 0x12, 0x0a, 0x04, + 0x6b, 0x69, 0x6e, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6b, 0x69, 0x6e, 0x64, + 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, + 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x20, 0x0a, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, + 0x69, 0x6f, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, + 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x39, 0x0a, 0x06, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x73, + 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x21, 0x2e, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x2e, + 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x53, 0x75, 0x6d, 0x6d, 0x61, 0x72, 0x79, 0x2e, 0x4c, 0x61, + 0x62, 0x65, 0x6c, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x06, 0x6c, 0x61, 0x62, 0x65, 0x6c, + 0x73, 0x12, 0x16, 0x0a, 0x06, 0x66, 0x6f, 0x6c, 0x64, 0x65, 0x72, 0x18, 0x06, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x06, 0x66, 0x6f, 0x6c, 0x64, 0x65, 0x72, 0x12, 0x12, 0x0a, 0x04, 0x73, 0x6c, 0x75, + 0x67, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x73, 0x6c, 0x75, 0x67, 0x12, 0x2d, 0x0a, + 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x65, + 0x6e, 0x74, 0x69, 0x74, 0x79, 0x2e, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x45, 0x72, 0x72, 0x6f, + 0x72, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x12, 0x39, 0x0a, 0x06, + 0x66, 0x69, 0x65, 0x6c, 0x64, 0x73, 0x18, 0x09, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x21, 0x2e, 0x65, + 0x6e, 0x74, 0x69, 0x74, 0x79, 0x2e, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x53, 0x75, 0x6d, 0x6d, + 0x61, 0x72, 0x79, 0x2e, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, + 0x06, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x73, 0x12, 0x2d, 0x0a, 0x06, 0x6e, 0x65, 0x73, 0x74, 0x65, + 0x64, 0x18, 0x0a, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, + 0x2e, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x53, 0x75, 0x6d, 0x6d, 0x61, 0x72, 0x79, 0x52, 0x06, + 0x6e, 0x65, 0x73, 0x74, 0x65, 0x64, 0x12, 0x3f, 0x0a, 0x0a, 0x72, 0x65, 0x66, 0x65, 0x72, 0x65, + 0x6e, 0x63, 0x65, 0x73, 0x18, 0x0b, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x65, 0x6e, 0x74, + 0x69, 0x74, 0x79, 0x2e, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x45, 0x78, 0x74, 0x65, 0x72, 0x6e, + 0x61, 0x6c, 0x52, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x52, 0x0a, 0x72, 0x65, 0x66, + 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x73, 0x1a, 0x39, 0x0a, 0x0b, 0x4c, 0x61, 0x62, 0x65, 0x6c, + 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, + 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, + 0x38, 0x01, 0x1a, 0x39, 0x0a, 0x0b, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x73, 0x45, 0x6e, 0x74, 0x72, + 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, + 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x65, 0x0a, + 0x17, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x45, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x52, + 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x66, 0x61, 0x6d, 0x69, + 0x6c, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x66, 0x61, 0x6d, 0x69, 0x6c, 0x79, + 0x12, 0x12, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, + 0x74, 0x79, 0x70, 0x65, 0x12, 0x1e, 0x0a, 0x0a, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, + 0x65, 0x72, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, + 0x66, 0x69, 0x65, 0x72, 0x32, 0xb2, 0x04, 0x0a, 0x0b, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x53, + 0x74, 0x6f, 0x72, 0x65, 0x12, 0x31, 0x0a, 0x04, 0x52, 0x65, 0x61, 0x64, 0x12, 0x19, 0x2e, 0x65, + 0x6e, 0x74, 0x69, 0x74, 0x79, 0x2e, 0x52, 0x65, 0x61, 0x64, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x0e, 0x2e, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, + 0x2e, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x12, 0x4c, 0x0a, 0x09, 0x42, 0x61, 0x74, 0x63, 0x68, + 0x52, 0x65, 0x61, 0x64, 0x12, 0x1e, 0x2e, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x2e, 0x42, 0x61, + 0x74, 0x63, 0x68, 0x52, 0x65, 0x61, 0x64, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1f, 0x2e, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x2e, 0x42, 0x61, + 0x74, 0x63, 0x68, 0x52, 0x65, 0x61, 0x64, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x40, 0x0a, 0x05, 0x57, 0x72, 0x69, 0x74, 0x65, 0x12, 0x1a, 0x2e, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x2e, 0x57, 0x72, 0x69, 0x74, 0x65, 0x45, 0x6e, 0x74, - 0x69, 0x74, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x43, 0x0a, 0x06, 0x44, - 0x65, 0x6c, 0x65, 0x74, 0x65, 0x12, 0x1b, 0x2e, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x2e, 0x44, - 0x65, 0x6c, 0x65, 0x74, 0x65, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x1a, 0x1c, 0x2e, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x2e, 0x44, 0x65, 0x6c, 0x65, - 0x74, 0x65, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, - 0x12, 0x46, 0x0a, 0x07, 0x48, 0x69, 0x73, 0x74, 0x6f, 0x72, 0x79, 0x12, 0x1c, 0x2e, 0x65, 0x6e, - 0x74, 0x69, 0x74, 0x79, 0x2e, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x48, 0x69, 0x73, 0x74, 0x6f, - 0x72, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x65, 0x6e, 0x74, 0x69, - 0x74, 0x79, 0x2e, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x48, 0x69, 0x73, 0x74, 0x6f, 0x72, 0x79, - 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x43, 0x0a, 0x06, 0x53, 0x65, 0x61, 0x72, - 0x63, 0x68, 0x12, 0x1b, 0x2e, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x2e, 0x45, 0x6e, 0x74, 0x69, - 0x74, 0x79, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, - 0x1c, 0x2e, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x2e, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x53, - 0x65, 0x61, 0x72, 0x63, 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x42, 0x0a, - 0x05, 0x57, 0x61, 0x74, 0x63, 0x68, 0x12, 0x1a, 0x2e, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x2e, - 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x57, 0x61, 0x74, 0x63, 0x68, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x2e, 0x45, 0x6e, 0x74, 0x69, - 0x74, 0x79, 0x57, 0x61, 0x74, 0x63, 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x30, - 0x01, 0x12, 0x4a, 0x0a, 0x0a, 0x41, 0x64, 0x6d, 0x69, 0x6e, 0x57, 0x72, 0x69, 0x74, 0x65, 0x12, - 0x1f, 0x2e, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x2e, 0x41, 0x64, 0x6d, 0x69, 0x6e, 0x57, 0x72, - 0x69, 0x74, 0x65, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x1a, 0x1b, 0x2e, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x2e, 0x57, 0x72, 0x69, 0x74, 0x65, 0x45, - 0x6e, 0x74, 0x69, 0x74, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x32, 0x5e, 0x0a, - 0x10, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x53, 0x74, 0x6f, 0x72, 0x65, 0x41, 0x64, 0x6d, 0x69, - 0x6e, 0x12, 0x4a, 0x0a, 0x0a, 0x41, 0x64, 0x6d, 0x69, 0x6e, 0x57, 0x72, 0x69, 0x74, 0x65, 0x12, - 0x1f, 0x2e, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x2e, 0x41, 0x64, 0x6d, 0x69, 0x6e, 0x57, 0x72, - 0x69, 0x74, 0x65, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x1a, 0x1b, 0x2e, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x2e, 0x57, 0x72, 0x69, 0x74, 0x65, 0x45, - 0x6e, 0x74, 0x69, 0x74, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x36, 0x5a, - 0x34, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x72, 0x61, 0x66, - 0x61, 0x6e, 0x61, 0x2f, 0x67, 0x72, 0x61, 0x66, 0x61, 0x6e, 0x61, 0x2f, 0x70, 0x6b, 0x67, 0x2f, - 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2f, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x2f, 0x65, - 0x6e, 0x74, 0x69, 0x74, 0x79, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x69, 0x74, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x65, 0x6e, 0x74, + 0x69, 0x74, 0x79, 0x2e, 0x57, 0x72, 0x69, 0x74, 0x65, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x43, 0x0a, 0x06, 0x44, 0x65, 0x6c, 0x65, 0x74, + 0x65, 0x12, 0x1b, 0x2e, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, + 0x65, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c, + 0x2e, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x45, 0x6e, + 0x74, 0x69, 0x74, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x46, 0x0a, 0x07, + 0x48, 0x69, 0x73, 0x74, 0x6f, 0x72, 0x79, 0x12, 0x1c, 0x2e, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, + 0x2e, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x48, 0x69, 0x73, 0x74, 0x6f, 0x72, 0x79, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x2e, 0x45, + 0x6e, 0x74, 0x69, 0x74, 0x79, 0x48, 0x69, 0x73, 0x74, 0x6f, 0x72, 0x79, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x43, 0x0a, 0x06, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x12, 0x1b, + 0x2e, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x2e, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x53, 0x65, + 0x61, 0x72, 0x63, 0x68, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c, 0x2e, 0x65, 0x6e, + 0x74, 0x69, 0x74, 0x79, 0x2e, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x53, 0x65, 0x61, 0x72, 0x63, + 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x42, 0x0a, 0x05, 0x57, 0x61, 0x74, + 0x63, 0x68, 0x12, 0x1a, 0x2e, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x2e, 0x45, 0x6e, 0x74, 0x69, + 0x74, 0x79, 0x57, 0x61, 0x74, 0x63, 0x68, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, + 0x2e, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x2e, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x57, 0x61, + 0x74, 0x63, 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x30, 0x01, 0x12, 0x4a, 0x0a, + 0x0a, 0x41, 0x64, 0x6d, 0x69, 0x6e, 0x57, 0x72, 0x69, 0x74, 0x65, 0x12, 0x1f, 0x2e, 0x65, 0x6e, + 0x74, 0x69, 0x74, 0x79, 0x2e, 0x41, 0x64, 0x6d, 0x69, 0x6e, 0x57, 0x72, 0x69, 0x74, 0x65, 0x45, + 0x6e, 0x74, 0x69, 0x74, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x65, + 0x6e, 0x74, 0x69, 0x74, 0x79, 0x2e, 0x57, 0x72, 0x69, 0x74, 0x65, 0x45, 0x6e, 0x74, 0x69, 0x74, + 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x32, 0x5e, 0x0a, 0x10, 0x45, 0x6e, 0x74, + 0x69, 0x74, 0x79, 0x53, 0x74, 0x6f, 0x72, 0x65, 0x41, 0x64, 0x6d, 0x69, 0x6e, 0x12, 0x4a, 0x0a, + 0x0a, 0x41, 0x64, 0x6d, 0x69, 0x6e, 0x57, 0x72, 0x69, 0x74, 0x65, 0x12, 0x1f, 0x2e, 0x65, 0x6e, + 0x74, 0x69, 0x74, 0x79, 0x2e, 0x41, 0x64, 0x6d, 0x69, 0x6e, 0x57, 0x72, 0x69, 0x74, 0x65, 0x45, + 0x6e, 0x74, 0x69, 0x74, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x65, + 0x6e, 0x74, 0x69, 0x74, 0x79, 0x2e, 0x57, 0x72, 0x69, 0x74, 0x65, 0x45, 0x6e, 0x74, 0x69, 0x74, + 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x36, 0x5a, 0x34, 0x67, 0x69, 0x74, + 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x72, 0x61, 0x66, 0x61, 0x6e, 0x61, 0x2f, + 0x67, 0x72, 0x61, 0x66, 0x61, 0x6e, 0x61, 0x2f, 0x70, 0x6b, 0x67, 0x2f, 0x73, 0x65, 0x72, 0x76, + 0x69, 0x63, 0x65, 0x73, 0x2f, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x2f, 0x65, 0x6e, 0x74, 0x69, 0x74, + 0x79, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -2055,7 +2301,7 @@ func file_entity_proto_rawDescGZIP() []byte { } var file_entity_proto_enumTypes = make([]protoimpl.EnumInfo, 2) -var file_entity_proto_msgTypes = make([]protoimpl.MessageInfo, 22) +var file_entity_proto_msgTypes = make([]protoimpl.MessageInfo, 26) var file_entity_proto_goTypes = []interface{}{ (WriteEntityResponse_Status)(0), // 0: entity.WriteEntityResponse.Status (EntityWatchResponse_Action)(0), // 1: entity.EntityWatchResponse.Action @@ -2078,59 +2324,68 @@ var file_entity_proto_goTypes = []interface{}{ (*EntitySearchResponse)(nil), // 18: entity.EntitySearchResponse (*EntityWatchRequest)(nil), // 19: entity.EntityWatchRequest (*EntityWatchResponse)(nil), // 20: entity.EntityWatchResponse - nil, // 21: entity.EntitySearchRequest.LabelsEntry - nil, // 22: entity.EntitySearchResult.LabelsEntry - nil, // 23: entity.EntityWatchRequest.LabelsEntry - (*grn.GRN)(nil), // 24: grn.GRN + (*EntitySummary)(nil), // 21: entity.EntitySummary + (*EntityExternalReference)(nil), // 22: entity.EntityExternalReference + nil, // 23: entity.EntitySearchRequest.LabelsEntry + nil, // 24: entity.EntitySearchResult.LabelsEntry + nil, // 25: entity.EntityWatchRequest.LabelsEntry + nil, // 26: entity.EntitySummary.LabelsEntry + nil, // 27: entity.EntitySummary.FieldsEntry + (*grn.GRN)(nil), // 28: grn.GRN } var file_entity_proto_depIdxs = []int32{ - 24, // 0: entity.Entity.GRN:type_name -> grn.GRN + 28, // 0: entity.Entity.GRN:type_name -> grn.GRN 3, // 1: entity.Entity.origin:type_name -> entity.EntityOriginInfo - 24, // 2: entity.ReadEntityRequest.GRN:type_name -> grn.GRN + 28, // 2: entity.ReadEntityRequest.GRN:type_name -> grn.GRN 6, // 3: entity.BatchReadEntityRequest.batch:type_name -> entity.ReadEntityRequest 2, // 4: entity.BatchReadEntityResponse.results:type_name -> entity.Entity - 24, // 5: entity.WriteEntityRequest.GRN:type_name -> grn.GRN - 24, // 6: entity.AdminWriteEntityRequest.GRN:type_name -> grn.GRN + 28, // 5: entity.WriteEntityRequest.GRN:type_name -> grn.GRN + 28, // 6: entity.AdminWriteEntityRequest.GRN:type_name -> grn.GRN 3, // 7: entity.AdminWriteEntityRequest.origin:type_name -> entity.EntityOriginInfo 4, // 8: entity.WriteEntityResponse.error:type_name -> entity.EntityErrorInfo - 24, // 9: entity.WriteEntityResponse.GRN:type_name -> grn.GRN + 28, // 9: entity.WriteEntityResponse.GRN:type_name -> grn.GRN 5, // 10: entity.WriteEntityResponse.entity:type_name -> entity.EntityVersionInfo 0, // 11: entity.WriteEntityResponse.status:type_name -> entity.WriteEntityResponse.Status - 24, // 12: entity.DeleteEntityRequest.GRN:type_name -> grn.GRN - 24, // 13: entity.EntityHistoryRequest.GRN:type_name -> grn.GRN - 24, // 14: entity.EntityHistoryResponse.GRN:type_name -> grn.GRN + 28, // 12: entity.DeleteEntityRequest.GRN:type_name -> grn.GRN + 28, // 13: entity.EntityHistoryRequest.GRN:type_name -> grn.GRN + 28, // 14: entity.EntityHistoryResponse.GRN:type_name -> grn.GRN 5, // 15: entity.EntityHistoryResponse.versions:type_name -> entity.EntityVersionInfo - 21, // 16: entity.EntitySearchRequest.labels:type_name -> entity.EntitySearchRequest.LabelsEntry - 24, // 17: entity.EntitySearchResult.GRN:type_name -> grn.GRN - 22, // 18: entity.EntitySearchResult.labels:type_name -> entity.EntitySearchResult.LabelsEntry + 23, // 16: entity.EntitySearchRequest.labels:type_name -> entity.EntitySearchRequest.LabelsEntry + 28, // 17: entity.EntitySearchResult.GRN:type_name -> grn.GRN + 24, // 18: entity.EntitySearchResult.labels:type_name -> entity.EntitySearchResult.LabelsEntry 17, // 19: entity.EntitySearchResponse.results:type_name -> entity.EntitySearchResult - 24, // 20: entity.EntityWatchRequest.GRN:type_name -> grn.GRN - 23, // 21: entity.EntityWatchRequest.labels:type_name -> entity.EntityWatchRequest.LabelsEntry + 28, // 20: entity.EntityWatchRequest.GRN:type_name -> grn.GRN + 25, // 21: entity.EntityWatchRequest.labels:type_name -> entity.EntityWatchRequest.LabelsEntry 2, // 22: entity.EntityWatchResponse.entity:type_name -> entity.Entity 1, // 23: entity.EntityWatchResponse.action:type_name -> entity.EntityWatchResponse.Action - 6, // 24: entity.EntityStore.Read:input_type -> entity.ReadEntityRequest - 7, // 25: entity.EntityStore.BatchRead:input_type -> entity.BatchReadEntityRequest - 9, // 26: entity.EntityStore.Write:input_type -> entity.WriteEntityRequest - 12, // 27: entity.EntityStore.Delete:input_type -> entity.DeleteEntityRequest - 14, // 28: entity.EntityStore.History:input_type -> entity.EntityHistoryRequest - 16, // 29: entity.EntityStore.Search:input_type -> entity.EntitySearchRequest - 19, // 30: entity.EntityStore.Watch:input_type -> entity.EntityWatchRequest - 10, // 31: entity.EntityStore.AdminWrite:input_type -> entity.AdminWriteEntityRequest - 10, // 32: entity.EntityStoreAdmin.AdminWrite:input_type -> entity.AdminWriteEntityRequest - 2, // 33: entity.EntityStore.Read:output_type -> entity.Entity - 8, // 34: entity.EntityStore.BatchRead:output_type -> entity.BatchReadEntityResponse - 11, // 35: entity.EntityStore.Write:output_type -> entity.WriteEntityResponse - 13, // 36: entity.EntityStore.Delete:output_type -> entity.DeleteEntityResponse - 15, // 37: entity.EntityStore.History:output_type -> entity.EntityHistoryResponse - 18, // 38: entity.EntityStore.Search:output_type -> entity.EntitySearchResponse - 20, // 39: entity.EntityStore.Watch:output_type -> entity.EntityWatchResponse - 11, // 40: entity.EntityStore.AdminWrite:output_type -> entity.WriteEntityResponse - 11, // 41: entity.EntityStoreAdmin.AdminWrite:output_type -> entity.WriteEntityResponse - 33, // [33:42] is the sub-list for method output_type - 24, // [24:33] is the sub-list for method input_type - 24, // [24:24] is the sub-list for extension type_name - 24, // [24:24] is the sub-list for extension extendee - 0, // [0:24] is the sub-list for field type_name + 26, // 24: entity.EntitySummary.labels:type_name -> entity.EntitySummary.LabelsEntry + 4, // 25: entity.EntitySummary.error:type_name -> entity.EntityErrorInfo + 27, // 26: entity.EntitySummary.fields:type_name -> entity.EntitySummary.FieldsEntry + 21, // 27: entity.EntitySummary.nested:type_name -> entity.EntitySummary + 22, // 28: entity.EntitySummary.references:type_name -> entity.EntityExternalReference + 6, // 29: entity.EntityStore.Read:input_type -> entity.ReadEntityRequest + 7, // 30: entity.EntityStore.BatchRead:input_type -> entity.BatchReadEntityRequest + 9, // 31: entity.EntityStore.Write:input_type -> entity.WriteEntityRequest + 12, // 32: entity.EntityStore.Delete:input_type -> entity.DeleteEntityRequest + 14, // 33: entity.EntityStore.History:input_type -> entity.EntityHistoryRequest + 16, // 34: entity.EntityStore.Search:input_type -> entity.EntitySearchRequest + 19, // 35: entity.EntityStore.Watch:input_type -> entity.EntityWatchRequest + 10, // 36: entity.EntityStore.AdminWrite:input_type -> entity.AdminWriteEntityRequest + 10, // 37: entity.EntityStoreAdmin.AdminWrite:input_type -> entity.AdminWriteEntityRequest + 2, // 38: entity.EntityStore.Read:output_type -> entity.Entity + 8, // 39: entity.EntityStore.BatchRead:output_type -> entity.BatchReadEntityResponse + 11, // 40: entity.EntityStore.Write:output_type -> entity.WriteEntityResponse + 13, // 41: entity.EntityStore.Delete:output_type -> entity.DeleteEntityResponse + 15, // 42: entity.EntityStore.History:output_type -> entity.EntityHistoryResponse + 18, // 43: entity.EntityStore.Search:output_type -> entity.EntitySearchResponse + 20, // 44: entity.EntityStore.Watch:output_type -> entity.EntityWatchResponse + 11, // 45: entity.EntityStore.AdminWrite:output_type -> entity.WriteEntityResponse + 11, // 46: entity.EntityStoreAdmin.AdminWrite:output_type -> entity.WriteEntityResponse + 38, // [38:47] is the sub-list for method output_type + 29, // [29:38] is the sub-list for method input_type + 29, // [29:29] is the sub-list for extension type_name + 29, // [29:29] is the sub-list for extension extendee + 0, // [0:29] is the sub-list for field type_name } func init() { file_entity_proto_init() } @@ -2367,6 +2622,30 @@ func file_entity_proto_init() { return nil } } + file_entity_proto_msgTypes[19].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*EntitySummary); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_entity_proto_msgTypes[20].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*EntityExternalReference); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } } type x struct{} out := protoimpl.TypeBuilder{ @@ -2374,7 +2653,7 @@ func file_entity_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_entity_proto_rawDesc, NumEnums: 2, - NumMessages: 22, + NumMessages: 26, NumExtensions: 0, NumServices: 2, }, diff --git a/pkg/services/store/entity/entity.proto b/pkg/services/store/entity/entity.proto index 52682a88806e9..8c9e1d20e0119 100644 --- a/pkg/services/store/entity/entity.proto +++ b/pkg/services/store/entity/entity.proto @@ -396,6 +396,51 @@ message EntityWatchResponse { } } +message EntitySummary { + string UID = 1; + string kind = 2; + + string name = 3; + string description = 4; + + // Key value pairs. Tags are are represented as keys with empty values + map labels = 5; + + // Parent folder UID + string folder = 6; + + // URL safe version of the name. It will be unique within the folder + string slug = 7; + + // When errors exist + EntityErrorInfo error = 8; + + // Optional field values. The schema will define and document possible values for a given kind + map fields = 9; + + // eg: panels within dashboard + repeated EntitySummary nested = 10; + + // Optional references to external things + repeated EntityExternalReference references = 11; +} + +message EntityExternalReference { + // Category of dependency + // eg: datasource, plugin, runtime + string family = 1; + + // datasource > prometheus|influx|... + // plugin > panel | datasource + // runtime > transformer + string type = 2; + + // datasource > UID + // plugin > plugin identifier + // runtime > name lookup + string identifier = 3; +} + //----------------------------------------------- // Storage interface diff --git a/pkg/services/store/entity/entity_grpc.pb.go b/pkg/services/store/entity/entity_grpc.pb.go index b7647be1c4bd9..50c30030a26f2 100644 --- a/pkg/services/store/entity/entity_grpc.pb.go +++ b/pkg/services/store/entity/entity_grpc.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go-grpc. DO NOT EDIT. // versions: // - protoc-gen-go-grpc v1.3.0 -// - protoc v4.23.4 +// - protoc v4.24.4 // source: entity.proto package entity diff --git a/pkg/services/store/entity/models.go b/pkg/services/store/entity/models.go index 1e8786d39b571..25524afc3ac25 100644 --- a/pkg/services/store/entity/models.go +++ b/pkg/services/store/entity/models.go @@ -86,59 +86,6 @@ type EntityKindInfo struct { MimeType string `json:"mimeType,omitempty"` } -// EntitySummary represents common data derived from a raw object bytes. -// The values should not depend on system state, and are derived from the raw object. -// This summary is used for a unified search and object listing -type EntitySummary struct { - UID string `json:"uid,omitempty"` - Kind string `json:"kind,omitempty"` - Name string `json:"name,omitempty"` - Description string `json:"description,omitempty"` - - // Key value pairs. Tags are are represented as keys with empty values - Labels map[string]string `json:"labels,omitempty"` - - // Parent folder UID - Folder string `json:"folder,omitempty"` - - // URL safe version of the name. It will be unique within the folder - Slug string `json:"slug,omitempty"` - - // When errors exist - Error *EntityErrorInfo `json:"error,omitempty"` - - // Optional field values. The schema will define and document possible values for a given kind - Fields map[string]any `json:"fields,omitempty"` - - // eg: panels within dashboard - Nested []*EntitySummary `json:"nested,omitempty"` - - // Optional references to external things - References []*EntityExternalReference `json:"references,omitempty"` - - // The summary can not be extended - _ any -} - -// Reference to another object outside itself -// This message is derived from the object body and can be used to search for references. -// This does not represent a method to declare a reference to another object. -type EntityExternalReference struct { - // Category of dependency - // eg: datasource, plugin, runtime - Family string `json:"family,omitempty"` - - // datasource > prometheus|influx|... - // plugin > panel | datasource - // runtime > transformer - Type string `json:"type,omitempty"` // flavor - - // datasource > UID - // plugin > plugin identifier - // runtime > name lookup - Identifier string `json:"ID,omitempty"` -} - // EntitySummaryBuilder will read an object, validate it, and return a summary, sanitized payload, or an error // This should not include values that depend on system state, only the raw object type EntitySummaryBuilder = func(ctx context.Context, uid string, body []byte) (*EntitySummary, []byte, error) diff --git a/pkg/services/store/kind/dashboard/summary.go b/pkg/services/store/kind/dashboard/summary.go index 6e7da053503be..7b845b5faa693 100644 --- a/pkg/services/store/kind/dashboard/summary.go +++ b/pkg/services/store/kind/dashboard/summary.go @@ -4,6 +4,7 @@ import ( "bytes" "context" "encoding/json" + "fmt" "strconv" "github.com/grafana/grafana/pkg/plugins" @@ -44,7 +45,7 @@ func NewStaticDashboardSummaryBuilder(lookup DatasourceLookup, sanitize bool) en summary := &entity.EntitySummary{ Labels: make(map[string]string), - Fields: make(map[string]any), + Fields: make(map[string]string), } stream := bytes.NewBuffer(body) dash, err := readDashboard(stream, lookup) @@ -62,9 +63,9 @@ func NewStaticDashboardSummaryBuilder(lookup DatasourceLookup, sanitize bool) en summary.Labels[v] = "" } if len(dash.TemplateVars) > 0 { - summary.Fields["hasTemplateVars"] = true + summary.Fields["hasTemplateVars"] = "true" } - summary.Fields["schemaVersion"] = dash.SchemaVersion + summary.Fields["schemaVersion"] = fmt.Sprint(dash.SchemaVersion) for _, panel := range dash.Panels { panelRefs := NewReferenceAccumulator() @@ -74,7 +75,7 @@ func NewStaticDashboardSummaryBuilder(lookup DatasourceLookup, sanitize bool) en } p.Name = panel.Title p.Description = panel.Description - p.Fields = make(map[string]any, 0) + p.Fields = make(map[string]string, 0) p.Fields["type"] = panel.Type if panel.Type != "row" { diff --git a/pkg/services/store/kind/dashboard/testdata/gdev-walk-graph-gradient-area-fills.json b/pkg/services/store/kind/dashboard/testdata/gdev-walk-graph-gradient-area-fills.json index 9eb1a26b1a547..3a448b1251dca 100644 --- a/pkg/services/store/kind/dashboard/testdata/gdev-walk-graph-gradient-area-fills.json +++ b/pkg/services/store/kind/dashboard/testdata/gdev-walk-graph-gradient-area-fills.json @@ -6,11 +6,11 @@ "panel-tests": "" }, "fields": { - "schemaVersion": 18 + "schemaVersion": "18" }, "nested": [ { - "uid": "graph-gradient-area-fills.json#2", + "UID": "graph-gradient-area-fills.json#2", "kind": "panel", "name": "Req/s", "fields": { @@ -23,12 +23,12 @@ { "family": "plugin", "type": "panel", - "ID": "graph" + "identifier": "graph" } ] }, { - "uid": "graph-gradient-area-fills.json#11", + "UID": "graph-gradient-area-fills.json#11", "kind": "panel", "name": "Req/s", "fields": { @@ -41,12 +41,12 @@ { "family": "plugin", "type": "panel", - "ID": "graph" + "identifier": "graph" } ] }, { - "uid": "graph-gradient-area-fills.json#7", + "UID": "graph-gradient-area-fills.json#7", "kind": "panel", "name": "Memory", "fields": { @@ -59,12 +59,12 @@ { "family": "plugin", "type": "panel", - "ID": "graph" + "identifier": "graph" } ] }, { - "uid": "graph-gradient-area-fills.json#10", + "UID": "graph-gradient-area-fills.json#10", "kind": "panel", "name": "Req/s", "fields": { @@ -77,7 +77,7 @@ { "family": "plugin", "type": "panel", - "ID": "graph" + "identifier": "graph" } ] } @@ -89,7 +89,7 @@ { "family": "plugin", "type": "panel", - "ID": "graph" + "identifier": "graph" } ] } \ No newline at end of file diff --git a/pkg/services/store/kind/dashboard/testdata/gdev-walk-graph-shared-tooltips.json b/pkg/services/store/kind/dashboard/testdata/gdev-walk-graph-shared-tooltips.json index 9406933bac239..2acdb5dfbb576 100644 --- a/pkg/services/store/kind/dashboard/testdata/gdev-walk-graph-shared-tooltips.json +++ b/pkg/services/store/kind/dashboard/testdata/gdev-walk-graph-shared-tooltips.json @@ -6,11 +6,11 @@ "panel-tests": "" }, "fields": { - "schemaVersion": 28 + "schemaVersion": "28" }, "nested": [ { - "uid": "graph-shared-tooltips.json#4", + "UID": "graph-shared-tooltips.json#4", "kind": "panel", "name": "two units", "fields": { @@ -23,12 +23,12 @@ { "family": "plugin", "type": "panel", - "ID": "timeseries" + "identifier": "timeseries" } ] }, { - "uid": "graph-shared-tooltips.json#13", + "UID": "graph-shared-tooltips.json#13", "kind": "panel", "name": "Speed vs Temperature (XY)", "fields": { @@ -41,22 +41,22 @@ { "family": "plugin", "type": "panel", - "ID": "xychart" + "identifier": "xychart" }, { "family": "runtime", "type": "transformer", - "ID": "organize" + "identifier": "organize" }, { "family": "runtime", "type": "transformer", - "ID": "seriesToColumns" + "identifier": "seriesToColumns" } ] }, { - "uid": "graph-shared-tooltips.json#2", + "UID": "graph-shared-tooltips.json#2", "kind": "panel", "name": "Cursor info", "fields": { @@ -69,12 +69,12 @@ { "family": "plugin", "type": "panel", - "ID": "debug" + "identifier": "debug" } ] }, { - "uid": "graph-shared-tooltips.json#5", + "UID": "graph-shared-tooltips.json#5", "kind": "panel", "name": "Only temperature", "fields": { @@ -87,12 +87,12 @@ { "family": "plugin", "type": "panel", - "ID": "timeseries" + "identifier": "timeseries" } ] }, { - "uid": "graph-shared-tooltips.json#9", + "UID": "graph-shared-tooltips.json#9", "kind": "panel", "name": "Only Speed", "fields": { @@ -105,12 +105,12 @@ { "family": "plugin", "type": "panel", - "ID": "timeseries" + "identifier": "timeseries" } ] }, { - "uid": "graph-shared-tooltips.json#11", + "UID": "graph-shared-tooltips.json#11", "kind": "panel", "name": "Panel Title", "fields": { @@ -123,12 +123,12 @@ { "family": "plugin", "type": "panel", - "ID": "timeseries" + "identifier": "timeseries" } ] }, { - "uid": "graph-shared-tooltips.json#8", + "UID": "graph-shared-tooltips.json#8", "kind": "panel", "name": "flot panel (temperature)", "fields": { @@ -141,12 +141,12 @@ { "family": "plugin", "type": "panel", - "ID": "graph" + "identifier": "graph" } ] }, { - "uid": "graph-shared-tooltips.json#10", + "UID": "graph-shared-tooltips.json#10", "kind": "panel", "name": "flot panel (no units)", "fields": { @@ -159,7 +159,7 @@ { "family": "plugin", "type": "panel", - "ID": "graph" + "identifier": "graph" } ] } @@ -171,32 +171,32 @@ { "family": "plugin", "type": "panel", - "ID": "debug" + "identifier": "debug" }, { "family": "plugin", "type": "panel", - "ID": "graph" + "identifier": "graph" }, { "family": "plugin", "type": "panel", - "ID": "timeseries" + "identifier": "timeseries" }, { "family": "plugin", "type": "panel", - "ID": "xychart" + "identifier": "xychart" }, { "family": "runtime", "type": "transformer", - "ID": "organize" + "identifier": "organize" }, { "family": "runtime", "type": "transformer", - "ID": "seriesToColumns" + "identifier": "seriesToColumns" } ] } \ No newline at end of file diff --git a/pkg/services/store/kind/dashboard/testdata/gdev-walk-graph-time-regions.json b/pkg/services/store/kind/dashboard/testdata/gdev-walk-graph-time-regions.json index d91dfe46b88fa..40e15af1cf195 100644 --- a/pkg/services/store/kind/dashboard/testdata/gdev-walk-graph-time-regions.json +++ b/pkg/services/store/kind/dashboard/testdata/gdev-walk-graph-time-regions.json @@ -6,11 +6,11 @@ "panel-tests": "" }, "fields": { - "schemaVersion": 18 + "schemaVersion": "18" }, "nested": [ { - "uid": "graph-time-regions.json#2", + "UID": "graph-time-regions.json#2", "kind": "panel", "name": "Business Hours", "fields": { @@ -19,17 +19,17 @@ "references": [ { "family": "ds", - "ID": "gdev-testdata" + "identifier": "gdev-testdata" }, { "family": "plugin", "type": "panel", - "ID": "graph" + "identifier": "graph" } ] }, { - "uid": "graph-time-regions.json#4", + "UID": "graph-time-regions.json#4", "kind": "panel", "name": "Sunday's 20-23", "fields": { @@ -38,17 +38,17 @@ "references": [ { "family": "ds", - "ID": "gdev-testdata" + "identifier": "gdev-testdata" }, { "family": "plugin", "type": "panel", - "ID": "graph" + "identifier": "graph" } ] }, { - "uid": "graph-time-regions.json#3", + "UID": "graph-time-regions.json#3", "kind": "panel", "name": "Each day of week", "fields": { @@ -57,17 +57,17 @@ "references": [ { "family": "ds", - "ID": "gdev-testdata" + "identifier": "gdev-testdata" }, { "family": "plugin", "type": "panel", - "ID": "graph" + "identifier": "graph" } ] }, { - "uid": "graph-time-regions.json#5", + "UID": "graph-time-regions.json#5", "kind": "panel", "name": "05:00", "fields": { @@ -76,17 +76,17 @@ "references": [ { "family": "ds", - "ID": "gdev-testdata" + "identifier": "gdev-testdata" }, { "family": "plugin", "type": "panel", - "ID": "graph" + "identifier": "graph" } ] }, { - "uid": "graph-time-regions.json#7", + "UID": "graph-time-regions.json#7", "kind": "panel", "name": "From 22:00 to 00:30 (crossing midnight)", "fields": { @@ -95,12 +95,12 @@ "references": [ { "family": "ds", - "ID": "gdev-testdata" + "identifier": "gdev-testdata" }, { "family": "plugin", "type": "panel", - "ID": "graph" + "identifier": "graph" } ] } @@ -108,12 +108,12 @@ "references": [ { "family": "ds", - "ID": "gdev-testdata" + "identifier": "gdev-testdata" }, { "family": "plugin", "type": "panel", - "ID": "graph" + "identifier": "graph" } ] } \ No newline at end of file diff --git a/pkg/services/store/kind/dashboard/testdata/gdev-walk-graph_tests.json b/pkg/services/store/kind/dashboard/testdata/gdev-walk-graph_tests.json index 61630217a55b5..44cece55fe986 100644 --- a/pkg/services/store/kind/dashboard/testdata/gdev-walk-graph_tests.json +++ b/pkg/services/store/kind/dashboard/testdata/gdev-walk-graph_tests.json @@ -6,11 +6,11 @@ "panel-tests": "" }, "fields": { - "schemaVersion": 16 + "schemaVersion": "16" }, "nested": [ { - "uid": "graph_tests.json#1", + "UID": "graph_tests.json#1", "kind": "panel", "name": "No Data Points Warning", "fields": { @@ -19,17 +19,17 @@ "references": [ { "family": "ds", - "ID": "gdev-testdata" + "identifier": "gdev-testdata" }, { "family": "plugin", "type": "panel", - "ID": "graph" + "identifier": "graph" } ] }, { - "uid": "graph_tests.json#2", + "UID": "graph_tests.json#2", "kind": "panel", "name": "Datapoints Outside Range Warning", "fields": { @@ -38,17 +38,17 @@ "references": [ { "family": "ds", - "ID": "gdev-testdata" + "identifier": "gdev-testdata" }, { "family": "plugin", "type": "panel", - "ID": "graph" + "identifier": "graph" } ] }, { - "uid": "graph_tests.json#3", + "UID": "graph_tests.json#3", "kind": "panel", "name": "Random walk series", "fields": { @@ -57,17 +57,17 @@ "references": [ { "family": "ds", - "ID": "gdev-testdata" + "identifier": "gdev-testdata" }, { "family": "plugin", "type": "panel", - "ID": "graph" + "identifier": "graph" } ] }, { - "uid": "graph_tests.json#4", + "UID": "graph_tests.json#4", "kind": "panel", "name": "Millisecond res x-axis and tooltip", "fields": { @@ -76,17 +76,17 @@ "references": [ { "family": "ds", - "ID": "gdev-testdata" + "identifier": "gdev-testdata" }, { "family": "plugin", "type": "panel", - "ID": "graph" + "identifier": "graph" } ] }, { - "uid": "graph_tests.json#6", + "UID": "graph_tests.json#6", "kind": "panel", "fields": { "type": "text" @@ -98,12 +98,12 @@ { "family": "plugin", "type": "panel", - "ID": "text" + "identifier": "text" } ] }, { - "uid": "graph_tests.json#5", + "UID": "graph_tests.json#5", "kind": "panel", "name": "2 yaxis and axis labels", "fields": { @@ -112,17 +112,17 @@ "references": [ { "family": "ds", - "ID": "gdev-testdata" + "identifier": "gdev-testdata" }, { "family": "plugin", "type": "panel", - "ID": "graph" + "identifier": "graph" } ] }, { - "uid": "graph_tests.json#7", + "UID": "graph_tests.json#7", "kind": "panel", "fields": { "type": "text" @@ -134,12 +134,12 @@ { "family": "plugin", "type": "panel", - "ID": "text" + "identifier": "text" } ] }, { - "uid": "graph_tests.json#8", + "UID": "graph_tests.json#8", "kind": "panel", "name": "null value connected", "fields": { @@ -148,17 +148,17 @@ "references": [ { "family": "ds", - "ID": "gdev-testdata" + "identifier": "gdev-testdata" }, { "family": "plugin", "type": "panel", - "ID": "graph" + "identifier": "graph" } ] }, { - "uid": "graph_tests.json#10", + "UID": "graph_tests.json#10", "kind": "panel", "name": "null value null as zero", "fields": { @@ -167,17 +167,17 @@ "references": [ { "family": "ds", - "ID": "gdev-testdata" + "identifier": "gdev-testdata" }, { "family": "plugin", "type": "panel", - "ID": "graph" + "identifier": "graph" } ] }, { - "uid": "graph_tests.json#13", + "UID": "graph_tests.json#13", "kind": "panel", "fields": { "type": "text" @@ -189,12 +189,12 @@ { "family": "plugin", "type": "panel", - "ID": "text" + "identifier": "text" } ] }, { - "uid": "graph_tests.json#9", + "UID": "graph_tests.json#9", "kind": "panel", "name": "Stacking value ontop of nulls", "fields": { @@ -203,17 +203,17 @@ "references": [ { "family": "ds", - "ID": "gdev-testdata" + "identifier": "gdev-testdata" }, { "family": "plugin", "type": "panel", - "ID": "graph" + "identifier": "graph" } ] }, { - "uid": "graph_tests.json#14", + "UID": "graph_tests.json#14", "kind": "panel", "fields": { "type": "text" @@ -225,12 +225,12 @@ { "family": "plugin", "type": "panel", - "ID": "text" + "identifier": "text" } ] }, { - "uid": "graph_tests.json#12", + "UID": "graph_tests.json#12", "kind": "panel", "name": "Stacking all series null segment", "fields": { @@ -239,17 +239,17 @@ "references": [ { "family": "ds", - "ID": "gdev-testdata" + "identifier": "gdev-testdata" }, { "family": "plugin", "type": "panel", - "ID": "graph" + "identifier": "graph" } ] }, { - "uid": "graph_tests.json#15", + "UID": "graph_tests.json#15", "kind": "panel", "fields": { "type": "text" @@ -261,12 +261,12 @@ { "family": "plugin", "type": "panel", - "ID": "text" + "identifier": "text" } ] }, { - "uid": "graph_tests.json#21", + "UID": "graph_tests.json#21", "kind": "panel", "name": "Null between points", "fields": { @@ -275,17 +275,17 @@ "references": [ { "family": "ds", - "ID": "gdev-testdata" + "identifier": "gdev-testdata" }, { "family": "plugin", "type": "panel", - "ID": "graph" + "identifier": "graph" } ] }, { - "uid": "graph_tests.json#22", + "UID": "graph_tests.json#22", "kind": "panel", "fields": { "type": "text" @@ -297,12 +297,12 @@ { "family": "plugin", "type": "panel", - "ID": "text" + "identifier": "text" } ] }, { - "uid": "graph_tests.json#20", + "UID": "graph_tests.json#20", "kind": "panel", "name": "Legend Table Single Series Should Take Minimum Height", "fields": { @@ -311,17 +311,17 @@ "references": [ { "family": "ds", - "ID": "gdev-testdata" + "identifier": "gdev-testdata" }, { "family": "plugin", "type": "panel", - "ID": "graph" + "identifier": "graph" } ] }, { - "uid": "graph_tests.json#16", + "UID": "graph_tests.json#16", "kind": "panel", "name": "Legend Table No Scroll Visible", "fields": { @@ -330,17 +330,17 @@ "references": [ { "family": "ds", - "ID": "gdev-testdata" + "identifier": "gdev-testdata" }, { "family": "plugin", "type": "panel", - "ID": "graph" + "identifier": "graph" } ] }, { - "uid": "graph_tests.json#17", + "UID": "graph_tests.json#17", "kind": "panel", "name": "Legend Table Should Scroll", "fields": { @@ -349,17 +349,17 @@ "references": [ { "family": "ds", - "ID": "gdev-testdata" + "identifier": "gdev-testdata" }, { "family": "plugin", "type": "panel", - "ID": "graph" + "identifier": "graph" } ] }, { - "uid": "graph_tests.json#18", + "UID": "graph_tests.json#18", "kind": "panel", "name": "Legend Table No Scroll Visible", "fields": { @@ -368,17 +368,17 @@ "references": [ { "family": "ds", - "ID": "gdev-testdata" + "identifier": "gdev-testdata" }, { "family": "plugin", "type": "panel", - "ID": "graph" + "identifier": "graph" } ] }, { - "uid": "graph_tests.json#19", + "UID": "graph_tests.json#19", "kind": "panel", "name": "Legend Table No Scroll Visible", "fields": { @@ -387,12 +387,12 @@ "references": [ { "family": "ds", - "ID": "gdev-testdata" + "identifier": "gdev-testdata" }, { "family": "plugin", "type": "panel", - "ID": "graph" + "identifier": "graph" } ] } @@ -403,17 +403,17 @@ }, { "family": "ds", - "ID": "gdev-testdata" + "identifier": "gdev-testdata" }, { "family": "plugin", "type": "panel", - "ID": "graph" + "identifier": "graph" }, { "family": "plugin", "type": "panel", - "ID": "text" + "identifier": "text" } ] } \ No newline at end of file diff --git a/pkg/services/store/kind/dashboard/testdata/gdev-walk-graph_y_axis.json b/pkg/services/store/kind/dashboard/testdata/gdev-walk-graph_y_axis.json index 8ee9d10095331..223bc667a4626 100644 --- a/pkg/services/store/kind/dashboard/testdata/gdev-walk-graph_y_axis.json +++ b/pkg/services/store/kind/dashboard/testdata/gdev-walk-graph_y_axis.json @@ -5,11 +5,11 @@ "panel-tests": "" }, "fields": { - "schemaVersion": 19 + "schemaVersion": "19" }, "nested": [ { - "uid": "graph_y_axis.json#7", + "UID": "graph_y_axis.json#7", "kind": "panel", "name": "Data from 0 - 10K (unit short)", "fields": { @@ -22,12 +22,12 @@ { "family": "plugin", "type": "panel", - "ID": "graph" + "identifier": "graph" } ] }, { - "uid": "graph_y_axis.json#5", + "UID": "graph_y_axis.json#5", "kind": "panel", "name": "Data from 0 - 10K (unit bytes metric)", "fields": { @@ -40,12 +40,12 @@ { "family": "plugin", "type": "panel", - "ID": "graph" + "identifier": "graph" } ] }, { - "uid": "graph_y_axis.json#4", + "UID": "graph_y_axis.json#4", "kind": "panel", "name": "Data from 0 - 10K (unit bytes IEC)", "fields": { @@ -58,12 +58,12 @@ { "family": "plugin", "type": "panel", - "ID": "graph" + "identifier": "graph" } ] }, { - "uid": "graph_y_axis.json#2", + "UID": "graph_y_axis.json#2", "kind": "panel", "name": "Data from 0 - 10K (unit short)", "fields": { @@ -76,12 +76,12 @@ { "family": "plugin", "type": "panel", - "ID": "graph" + "identifier": "graph" } ] }, { - "uid": "graph_y_axis.json#3", + "UID": "graph_y_axis.json#3", "kind": "panel", "name": "Data from 0.0002 - 0.001 (unit short)", "fields": { @@ -94,12 +94,12 @@ { "family": "plugin", "type": "panel", - "ID": "graph" + "identifier": "graph" } ] }, { - "uid": "graph_y_axis.json#6", + "UID": "graph_y_axis.json#6", "kind": "panel", "name": "Data from 12000 - 30000 (unit ms)", "fields": { @@ -112,12 +112,12 @@ { "family": "plugin", "type": "panel", - "ID": "graph" + "identifier": "graph" } ] }, { - "uid": "graph_y_axis.json#9", + "UID": "graph_y_axis.json#9", "kind": "panel", "name": "Data from 0 - 1B (unit short)", "fields": { @@ -130,12 +130,12 @@ { "family": "plugin", "type": "panel", - "ID": "graph" + "identifier": "graph" } ] }, { - "uid": "graph_y_axis.json#10", + "UID": "graph_y_axis.json#10", "kind": "panel", "name": "Data from 0 - 1B (unit bytes)", "fields": { @@ -148,12 +148,12 @@ { "family": "plugin", "type": "panel", - "ID": "graph" + "identifier": "graph" } ] }, { - "uid": "graph_y_axis.json#8", + "UID": "graph_y_axis.json#8", "kind": "panel", "name": "Data from 12000 - 30000 (unit ms)", "fields": { @@ -166,7 +166,7 @@ { "family": "plugin", "type": "panel", - "ID": "graph" + "identifier": "graph" } ] } @@ -178,7 +178,7 @@ { "family": "plugin", "type": "panel", - "ID": "graph" + "identifier": "graph" } ] } \ No newline at end of file diff --git a/pkg/services/store/kind/dashboard/testdata/with-library-panels-info.json b/pkg/services/store/kind/dashboard/testdata/with-library-panels-info.json index 6e32e91d39ca2..e360bc820e03a 100644 --- a/pkg/services/store/kind/dashboard/testdata/with-library-panels-info.json +++ b/pkg/services/store/kind/dashboard/testdata/with-library-panels-info.json @@ -1,11 +1,11 @@ { "name": "pppp", "fields": { - "schemaVersion": 38 + "schemaVersion": "38" }, "nested": [ { - "uid": "with-library-panels#1", + "UID": "with-library-panels#1", "kind": "panel", "name": "green pie", "fields": { @@ -17,7 +17,7 @@ }, { "family": "librarypanel", - "ID": "a7975b7a-fb53-4ab7-951d-15810953b54f" + "identifier": "a7975b7a-fb53-4ab7-951d-15810953b54f" }, { "family": "plugin", @@ -26,7 +26,7 @@ ] }, { - "uid": "with-library-panels#2", + "UID": "with-library-panels#2", "kind": "panel", "name": "green pie", "fields": { @@ -38,7 +38,7 @@ }, { "family": "librarypanel", - "ID": "e1d5f519-dabd-47c6-9ad7-83d181ce1cee" + "identifier": "e1d5f519-dabd-47c6-9ad7-83d181ce1cee" }, { "family": "plugin", @@ -53,11 +53,11 @@ }, { "family": "librarypanel", - "ID": "a7975b7a-fb53-4ab7-951d-15810953b54f" + "identifier": "a7975b7a-fb53-4ab7-951d-15810953b54f" }, { "family": "librarypanel", - "ID": "e1d5f519-dabd-47c6-9ad7-83d181ce1cee" + "identifier": "e1d5f519-dabd-47c6-9ad7-83d181ce1cee" }, { "family": "plugin", diff --git a/pkg/services/store/kind/dataframe/summary.go b/pkg/services/store/kind/dataframe/summary.go index 71334361f0636..e4e055c67e7d1 100644 --- a/pkg/services/store/kind/dataframe/summary.go +++ b/pkg/services/store/kind/dataframe/summary.go @@ -3,6 +3,7 @@ package dataframe import ( "context" "encoding/json" + "fmt" "github.com/grafana/grafana-plugin-sdk-go/data" @@ -38,9 +39,9 @@ func GetEntitySummaryBuilder() entity.EntitySummaryBuilder { Kind: entity.StandardKindDataFrame, Name: df.Name, UID: uid, - Fields: map[string]any{ - "rows": rows, - "cols": len(df.Fields), + Fields: map[string]string{ + "rows": fmt.Sprint(rows), + "cols": fmt.Sprint(len(df.Fields)), }, } if summary.Name == "" { diff --git a/pkg/services/store/kind/dataframe/summary_test.go b/pkg/services/store/kind/dataframe/summary_test.go index 9efe2c94c96cc..5fa2b51054aff 100644 --- a/pkg/services/store/kind/dataframe/summary_test.go +++ b/pkg/services/store/kind/dataframe/summary_test.go @@ -31,12 +31,12 @@ func TestDataFrameSummary(t *testing.T) { // fmt.Printf(string(asjson)) require.NoError(t, err) require.JSONEq(t, `{ - "uid": "somthing", + "UID": "somthing", "kind": "frame", "name": "http_requests_total", "fields": { - "cols": 4, - "rows": 3 + "cols": "4", + "rows": "3" } }`, string(asjson)) } diff --git a/pkg/services/store/kind/dummy/summary.go b/pkg/services/store/kind/dummy/summary.go index acc36713ab374..16846eeaaddc0 100644 --- a/pkg/services/store/kind/dummy/summary.go +++ b/pkg/services/store/kind/dummy/summary.go @@ -28,10 +28,10 @@ func GetEntitySummaryBuilder(kind string) entity.EntitySummaryBuilder { "tag1": "", "tag2": "", }, - Fields: map[string]any{ + Fields: map[string]string{ "field1": "a string", - "field2": 1.224, - "field4": true, + "field2": "1.224", + "field4": "true", }, Error: nil, // ignore for now Nested: nil, // ignore for now diff --git a/pkg/services/store/kind/geojson/summary.go b/pkg/services/store/kind/geojson/summary.go index dc253ffaf30a7..4adb5f8cca7e6 100644 --- a/pkg/services/store/kind/geojson/summary.go +++ b/pkg/services/store/kind/geojson/summary.go @@ -42,7 +42,7 @@ func GetEntitySummaryBuilder() entity.EntitySummaryBuilder { Kind: entity.StandardKindGeoJSON, Name: store.GuessNameFromUID(uid), UID: uid, - Fields: map[string]any{ + Fields: map[string]string{ "type": ftype, }, } @@ -50,7 +50,7 @@ func GetEntitySummaryBuilder() entity.EntitySummaryBuilder { if ftype == "FeatureCollection" { features, ok := geojson["features"].([]any) if ok { - summary.Fields["count"] = len(features) + summary.Fields["count"] = fmt.Sprint(len(features)) } } diff --git a/pkg/services/store/kind/geojson/summary_test.go b/pkg/services/store/kind/geojson/summary_test.go index b3dc846b67b45..57bf3eaa3d92a 100644 --- a/pkg/services/store/kind/geojson/summary_test.go +++ b/pkg/services/store/kind/geojson/summary_test.go @@ -24,12 +24,12 @@ func TestGeoJSONSummary(t *testing.T) { //fmt.Printf(string(asjson)) require.NoError(t, err) require.JSONEq(t, `{ - "uid": "hello", + "UID": "hello", "kind": "geojson", "name": "hello", "fields": { "type": "FeatureCollection", - "count": 0 + "count": "0" } }`, string(asjson)) @@ -43,12 +43,12 @@ func TestGeoJSONSummary(t *testing.T) { //fmt.Printf(string(asjson)) require.NoError(t, err) require.JSONEq(t, `{ - "uid": "gaz/airports.geojson", + "UID": "gaz/airports.geojson", "kind": "geojson", "name": "airports", "fields": { "type": "FeatureCollection", - "count": 888 + "count": "888" } }`, string(asjson)) } diff --git a/pkg/services/store/kind/jsonobj/summary_test.go b/pkg/services/store/kind/jsonobj/summary_test.go index 9866352b6fe83..661d8079d7f4d 100644 --- a/pkg/services/store/kind/jsonobj/summary_test.go +++ b/pkg/services/store/kind/jsonobj/summary_test.go @@ -32,7 +32,7 @@ func TestDataFrameSummary(t *testing.T) { require.NoError(t, err) require.JSONEq(t, `{ "name": "item", - "uid": "path/to/item", + "UID": "path/to/item", "kind": "jsonobj" }`, string(asjson)) } diff --git a/pkg/services/store/kind/png/summary.go b/pkg/services/store/kind/png/summary.go index 57a615ab1527a..4f5d09c09f608 100644 --- a/pkg/services/store/kind/png/summary.go +++ b/pkg/services/store/kind/png/summary.go @@ -3,6 +3,7 @@ package png import ( "bytes" "context" + "fmt" "image/png" "github.com/grafana/grafana/pkg/services/store" @@ -33,9 +34,9 @@ func GetEntitySummaryBuilder() entity.EntitySummaryBuilder { Kind: entity.StandardKindSVG, Name: store.GuessNameFromUID(uid), UID: uid, - Fields: map[string]any{ - "width": int64(size.X), - "height": int64(size.Y), + Fields: map[string]string{ + "width": fmt.Sprint(size.X), + "height": fmt.Sprint(size.Y), }, } return summary, body, nil diff --git a/pkg/services/store/kind/png/summary_test.go b/pkg/services/store/kind/png/summary_test.go index 00bf46271366d..301c1126ef809 100644 --- a/pkg/services/store/kind/png/summary_test.go +++ b/pkg/services/store/kind/png/summary_test.go @@ -22,12 +22,12 @@ func TestPNGSummary(t *testing.T) { //fmt.Printf(string(asjson)) require.NoError(t, err) require.JSONEq(t, `{ - "uid": "hello.png", + "UID": "hello.png", "kind": "svg", "name": "hello", "fields": { - "height": 60, - "width": 75 + "height": "60", + "width": "75" } }`, string(asjson)) } diff --git a/pkg/services/store/kind/snapshot/summary.go b/pkg/services/store/kind/snapshot/summary.go index d19ed188a1630..8cde29d3268a9 100644 --- a/pkg/services/store/kind/snapshot/summary.go +++ b/pkg/services/store/kind/snapshot/summary.go @@ -46,10 +46,10 @@ func GetEntitySummaryBuilder() entity.EntitySummaryBuilder { Name: obj.Name, Description: obj.Description, UID: uid, - Fields: map[string]interface{}{ + Fields: map[string]string{ "deleteKey": obj.DeleteKey, "externalURL": obj.ExternalURL, - "expires": obj.Expires, + "expires": fmt.Sprint(obj.Expires), }, References: []*entity.EntityExternalReference{ {Family: entity.StandardKindDashboard, Identifier: obj.DashboardUID}, From dd654fdc87768011d9f40144e51ebe69de7c112f Mon Sep 17 00:00:00 2001 From: Ryan McKinley Date: Fri, 3 Nov 2023 08:07:55 -0700 Subject: [PATCH 090/869] K8s/Playlist: Refactor apis packages so the types and registry are in different packages (#77586) --- pkg/api/playlist.go | 2 +- .../{openapi.go => zz_generated.openapi..go} | 2 +- pkg/apis/playlist/doc.go | 4 - pkg/apis/playlist/types.go | 65 ------- pkg/apis/playlist/v0alpha1/register.go | 74 -------- .../v0alpha1/zz_generated.conversion.go | 170 ------------------ .../playlist/v0alpha1/zz_generated.openapi.go | 2 +- pkg/apis/playlist/zz_generated.deepcopy.go | 123 ------------- pkg/apis/wireset.go | 17 -- pkg/registry/apis/apis.go | 12 +- .../apis/example}/register.go | 12 +- .../apis/example}/storage.go | 11 +- .../apis/playlist/conversions.go | 29 +-- .../apis/playlist/conversions_test.go | 0 .../apis/playlist/legacy_storage.go | 29 +-- pkg/{ => registry}/apis/playlist/register.go | 38 ++-- pkg/{ => registry}/apis/playlist/storage.go | 5 +- pkg/registry/apis/wireset.go | 11 +- pkg/services/grafana-apiserver/service.go | 2 +- 19 files changed, 91 insertions(+), 517 deletions(-) rename pkg/apis/example/v0alpha1/{openapi.go => zz_generated.openapi..go} (97%) delete mode 100644 pkg/apis/playlist/doc.go delete mode 100644 pkg/apis/playlist/types.go delete mode 100644 pkg/apis/playlist/v0alpha1/register.go delete mode 100644 pkg/apis/playlist/v0alpha1/zz_generated.conversion.go delete mode 100644 pkg/apis/playlist/zz_generated.deepcopy.go delete mode 100644 pkg/apis/wireset.go rename pkg/{apis/example/v0alpha1 => registry/apis/example}/register.go (97%) rename pkg/{apis/example/v0alpha1 => registry/apis/example}/storage.go (89%) rename pkg/{ => registry}/apis/playlist/conversions.go (75%) rename pkg/{ => registry}/apis/playlist/conversions_test.go (100%) rename pkg/{ => registry}/apis/playlist/legacy_storage.go (86%) rename pkg/{ => registry}/apis/playlist/register.go (76%) rename pkg/{ => registry}/apis/playlist/storage.go (83%) diff --git a/pkg/api/playlist.go b/pkg/api/playlist.go index 30be0e3b55007..3f1b95dc29fc6 100644 --- a/pkg/api/playlist.go +++ b/pkg/api/playlist.go @@ -12,8 +12,8 @@ import ( "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/api/routing" - internalplaylist "github.com/grafana/grafana/pkg/apis/playlist" "github.com/grafana/grafana/pkg/middleware" + internalplaylist "github.com/grafana/grafana/pkg/registry/apis/playlist" contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/grafana-apiserver/endpoints/request" diff --git a/pkg/apis/example/v0alpha1/openapi.go b/pkg/apis/example/v0alpha1/zz_generated.openapi..go similarity index 97% rename from pkg/apis/example/v0alpha1/openapi.go rename to pkg/apis/example/v0alpha1/zz_generated.openapi..go index 7d10f829ae03a..dc68decf0ef10 100644 --- a/pkg/apis/example/v0alpha1/openapi.go +++ b/pkg/apis/example/v0alpha1/zz_generated.openapi..go @@ -8,7 +8,7 @@ import ( // NOTE: this must match the golang fully qualified name! const kindKey = "github.com/grafana/grafana/pkg/apis/example/v0alpha1.RuntimeInfo" -func getOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenAPIDefinition { +func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenAPIDefinition { return map[string]common.OpenAPIDefinition{ kindKey: schema_pkg_apis_example_v0alpha1_RuntimeInfo(ref), } diff --git a/pkg/apis/playlist/doc.go b/pkg/apis/playlist/doc.go deleted file mode 100644 index f4eff4cd63aa2..0000000000000 --- a/pkg/apis/playlist/doc.go +++ /dev/null @@ -1,4 +0,0 @@ -// +k8s:deepcopy-gen=package -// +groupName=playlist.grafana.app - -package playlist // import "github.com/grafana/grafana/pkg/apis/playlist" diff --git a/pkg/apis/playlist/types.go b/pkg/apis/playlist/types.go deleted file mode 100644 index c7c8647086ad0..0000000000000 --- a/pkg/apis/playlist/types.go +++ /dev/null @@ -1,65 +0,0 @@ -package playlist - -import ( - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" -) - -// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object -type Playlist struct { - metav1.TypeMeta `json:",inline"` - // Standard object's metadata - // More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata - // +optional - metav1.ObjectMeta `json:"metadata,omitempty"` - - Spec Spec `json:"spec,omitempty"` -} - -// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object -type PlaylistList struct { - metav1.TypeMeta `json:",inline"` - // +optional - metav1.ListMeta `json:"metadata,omitempty"` - - Items []Playlist `json:"items,omitempty"` -} - -// Spec defines model for Spec. -type Spec struct { - // Name of the playlist. - Title string `json:"title"` - - // Interval sets the time between switching views in a playlist. - Interval string `json:"interval"` - - // The ordered list of items that the playlist will iterate over. - Items []Item `json:"items,omitempty"` -} - -// Defines values for ItemType. -const ( - ItemTypeDashboardByTag ItemType = "dashboard_by_tag" - ItemTypeDashboardByUid ItemType = "dashboard_by_uid" - - // deprecated -- should use UID - ItemTypeDashboardById ItemType = "dashboard_by_id" -) - -// Item defines model for Item. -type Item struct { - // Type of the item. - Type ItemType `json:"type"` - - // Value depends on type and describes the playlist item. - // - // - dashboard_by_id: The value is an internal numerical identifier set by Grafana. This - // is not portable as the numerical identifier is non-deterministic between different instances. - // Will be replaced by dashboard_by_uid in the future. (deprecated) - // - dashboard_by_tag: The value is a tag which is set on any number of dashboards. All - // dashboards behind the tag will be added to the playlist. - // - dashboard_by_uid: The value is the dashboard UID - Value string `json:"value"` -} - -// Type of the item. -type ItemType string diff --git a/pkg/apis/playlist/v0alpha1/register.go b/pkg/apis/playlist/v0alpha1/register.go deleted file mode 100644 index cc803ce8fbece..0000000000000 --- a/pkg/apis/playlist/v0alpha1/register.go +++ /dev/null @@ -1,74 +0,0 @@ -package v0alpha1 - -import ( - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/runtime/schema" - "k8s.io/apimachinery/pkg/runtime/serializer" - "k8s.io/apiserver/pkg/registry/generic" - genericapiserver "k8s.io/apiserver/pkg/server" - common "k8s.io/kube-openapi/pkg/common" - - grafanaapiserver "github.com/grafana/grafana/pkg/services/grafana-apiserver" - "github.com/grafana/grafana/pkg/services/grafana-apiserver/endpoints/request" - "github.com/grafana/grafana/pkg/services/playlist" - "github.com/grafana/grafana/pkg/setting" -) - -// GroupName is the group name for this API. -const GroupName = "playlist.grafana.app" -const VersionID = "v0alpha1" - -var _ grafanaapiserver.APIGroupBuilder = (*PlaylistAPIBuilder)(nil) - -// This is used just so wire has something unique to return -type PlaylistAPIBuilder struct { - service playlist.Service - namespacer request.NamespaceMapper - gv schema.GroupVersion -} - -func RegisterAPIService(p playlist.Service, - apiregistration grafanaapiserver.APIRegistrar, - cfg *setting.Cfg, -) *PlaylistAPIBuilder { - builder := &PlaylistAPIBuilder{ - service: p, - namespacer: request.GetNamespaceMapper(cfg), - gv: schema.GroupVersion{Group: GroupName, Version: VersionID}, - } - apiregistration.RegisterAPI(builder) - return builder -} - -func (b *PlaylistAPIBuilder) GetGroupVersion() schema.GroupVersion { - return b.gv -} - -func (b *PlaylistAPIBuilder) InstallSchema(scheme *runtime.Scheme) error { - scheme.AddKnownTypes(b.gv, - &Playlist{}, - &PlaylistList{}, - ) - if err := RegisterConversions(scheme); err != nil { - return err - } - metav1.AddToGroupVersion(scheme, b.gv) - return scheme.SetVersionPriority(b.gv) -} - -func (b *PlaylistAPIBuilder) GetAPIGroupInfo( - scheme *runtime.Scheme, - codecs serializer.CodecFactory, // pointer? - optsGetter generic.RESTOptionsGetter, -) (*genericapiserver.APIGroupInfo, error) { - return nil, nil -} - -func (b *PlaylistAPIBuilder) GetOpenAPIDefinitions() common.GetOpenAPIDefinitions { - return getOpenAPIDefinitions -} - -func (b *PlaylistAPIBuilder) GetAPIRoutes() *grafanaapiserver.APIRoutes { - return nil // no custom API routes -} diff --git a/pkg/apis/playlist/v0alpha1/zz_generated.conversion.go b/pkg/apis/playlist/v0alpha1/zz_generated.conversion.go deleted file mode 100644 index d89fef079e011..0000000000000 --- a/pkg/apis/playlist/v0alpha1/zz_generated.conversion.go +++ /dev/null @@ -1,170 +0,0 @@ -//go:build !ignore_autogenerated -// +build !ignore_autogenerated - -/* -Copyright The Kubernetes Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -// Code generated by conversion-gen. DO NOT EDIT. - -package v0alpha1 - -import ( - unsafe "unsafe" - - playlist "github.com/grafana/grafana/pkg/apis/playlist" - conversion "k8s.io/apimachinery/pkg/conversion" - runtime "k8s.io/apimachinery/pkg/runtime" -) - -// RegisterConversions adds conversion functions to the given scheme. -// Public to allow building arbitrary schemes. -func RegisterConversions(s *runtime.Scheme) error { - if err := s.AddGeneratedConversionFunc((*Item)(nil), (*playlist.Item)(nil), func(a, b interface{}, scope conversion.Scope) error { - return Convert_v0alpha1_Item_To_playlist_Item(a.(*Item), b.(*playlist.Item), scope) - }); err != nil { - return err - } - if err := s.AddGeneratedConversionFunc((*playlist.Item)(nil), (*Item)(nil), func(a, b interface{}, scope conversion.Scope) error { - return Convert_playlist_Item_To_v0alpha1_Item(a.(*playlist.Item), b.(*Item), scope) - }); err != nil { - return err - } - if err := s.AddGeneratedConversionFunc((*Playlist)(nil), (*playlist.Playlist)(nil), func(a, b interface{}, scope conversion.Scope) error { - return Convert_v0alpha1_Playlist_To_playlist_Playlist(a.(*Playlist), b.(*playlist.Playlist), scope) - }); err != nil { - return err - } - if err := s.AddGeneratedConversionFunc((*playlist.Playlist)(nil), (*Playlist)(nil), func(a, b interface{}, scope conversion.Scope) error { - return Convert_playlist_Playlist_To_v0alpha1_Playlist(a.(*playlist.Playlist), b.(*Playlist), scope) - }); err != nil { - return err - } - if err := s.AddGeneratedConversionFunc((*PlaylistList)(nil), (*playlist.PlaylistList)(nil), func(a, b interface{}, scope conversion.Scope) error { - return Convert_v0alpha1_PlaylistList_To_playlist_PlaylistList(a.(*PlaylistList), b.(*playlist.PlaylistList), scope) - }); err != nil { - return err - } - if err := s.AddGeneratedConversionFunc((*playlist.PlaylistList)(nil), (*PlaylistList)(nil), func(a, b interface{}, scope conversion.Scope) error { - return Convert_playlist_PlaylistList_To_v0alpha1_PlaylistList(a.(*playlist.PlaylistList), b.(*PlaylistList), scope) - }); err != nil { - return err - } - if err := s.AddGeneratedConversionFunc((*Spec)(nil), (*playlist.Spec)(nil), func(a, b interface{}, scope conversion.Scope) error { - return Convert_v0alpha1_Spec_To_playlist_Spec(a.(*Spec), b.(*playlist.Spec), scope) - }); err != nil { - return err - } - if err := s.AddGeneratedConversionFunc((*playlist.Spec)(nil), (*Spec)(nil), func(a, b interface{}, scope conversion.Scope) error { - return Convert_playlist_Spec_To_v0alpha1_Spec(a.(*playlist.Spec), b.(*Spec), scope) - }); err != nil { - return err - } - return nil -} - -func autoConvert_v0alpha1_Item_To_playlist_Item(in *Item, out *playlist.Item, s conversion.Scope) error { - out.Type = playlist.ItemType(in.Type) - out.Value = in.Value - return nil -} - -// Convert_v0alpha1_Item_To_playlist_Item is an autogenerated conversion function. -func Convert_v0alpha1_Item_To_playlist_Item(in *Item, out *playlist.Item, s conversion.Scope) error { - return autoConvert_v0alpha1_Item_To_playlist_Item(in, out, s) -} - -func autoConvert_playlist_Item_To_v0alpha1_Item(in *playlist.Item, out *Item, s conversion.Scope) error { - out.Type = ItemType(in.Type) - out.Value = in.Value - return nil -} - -// Convert_playlist_Item_To_v0alpha1_Item is an autogenerated conversion function. -func Convert_playlist_Item_To_v0alpha1_Item(in *playlist.Item, out *Item, s conversion.Scope) error { - return autoConvert_playlist_Item_To_v0alpha1_Item(in, out, s) -} - -func autoConvert_v0alpha1_Playlist_To_playlist_Playlist(in *Playlist, out *playlist.Playlist, s conversion.Scope) error { - out.ObjectMeta = in.ObjectMeta - if err := Convert_v0alpha1_Spec_To_playlist_Spec(&in.Spec, &out.Spec, s); err != nil { - return err - } - return nil -} - -// Convert_v0alpha1_Playlist_To_playlist_Playlist is an autogenerated conversion function. -func Convert_v0alpha1_Playlist_To_playlist_Playlist(in *Playlist, out *playlist.Playlist, s conversion.Scope) error { - return autoConvert_v0alpha1_Playlist_To_playlist_Playlist(in, out, s) -} - -func autoConvert_playlist_Playlist_To_v0alpha1_Playlist(in *playlist.Playlist, out *Playlist, s conversion.Scope) error { - out.ObjectMeta = in.ObjectMeta - if err := Convert_playlist_Spec_To_v0alpha1_Spec(&in.Spec, &out.Spec, s); err != nil { - return err - } - return nil -} - -// Convert_playlist_Playlist_To_v0alpha1_Playlist is an autogenerated conversion function. -func Convert_playlist_Playlist_To_v0alpha1_Playlist(in *playlist.Playlist, out *Playlist, s conversion.Scope) error { - return autoConvert_playlist_Playlist_To_v0alpha1_Playlist(in, out, s) -} - -func autoConvert_v0alpha1_PlaylistList_To_playlist_PlaylistList(in *PlaylistList, out *playlist.PlaylistList, s conversion.Scope) error { - out.ListMeta = in.ListMeta - out.Items = *(*[]playlist.Playlist)(unsafe.Pointer(&in.Items)) - return nil -} - -// Convert_v0alpha1_PlaylistList_To_playlist_PlaylistList is an autogenerated conversion function. -func Convert_v0alpha1_PlaylistList_To_playlist_PlaylistList(in *PlaylistList, out *playlist.PlaylistList, s conversion.Scope) error { - return autoConvert_v0alpha1_PlaylistList_To_playlist_PlaylistList(in, out, s) -} - -func autoConvert_playlist_PlaylistList_To_v0alpha1_PlaylistList(in *playlist.PlaylistList, out *PlaylistList, s conversion.Scope) error { - out.ListMeta = in.ListMeta - out.Items = *(*[]Playlist)(unsafe.Pointer(&in.Items)) - return nil -} - -// Convert_playlist_PlaylistList_To_v0alpha1_PlaylistList is an autogenerated conversion function. -func Convert_playlist_PlaylistList_To_v0alpha1_PlaylistList(in *playlist.PlaylistList, out *PlaylistList, s conversion.Scope) error { - return autoConvert_playlist_PlaylistList_To_v0alpha1_PlaylistList(in, out, s) -} - -func autoConvert_v0alpha1_Spec_To_playlist_Spec(in *Spec, out *playlist.Spec, s conversion.Scope) error { - out.Title = in.Title - out.Interval = in.Interval - out.Items = *(*[]playlist.Item)(unsafe.Pointer(&in.Items)) - return nil -} - -// Convert_v0alpha1_Spec_To_playlist_Spec is an autogenerated conversion function. -func Convert_v0alpha1_Spec_To_playlist_Spec(in *Spec, out *playlist.Spec, s conversion.Scope) error { - return autoConvert_v0alpha1_Spec_To_playlist_Spec(in, out, s) -} - -func autoConvert_playlist_Spec_To_v0alpha1_Spec(in *playlist.Spec, out *Spec, s conversion.Scope) error { - out.Title = in.Title - out.Interval = in.Interval - out.Items = *(*[]Item)(unsafe.Pointer(&in.Items)) - return nil -} - -// Convert_playlist_Spec_To_v0alpha1_Spec is an autogenerated conversion function. -func Convert_playlist_Spec_To_v0alpha1_Spec(in *playlist.Spec, out *Spec, s conversion.Scope) error { - return autoConvert_playlist_Spec_To_v0alpha1_Spec(in, out, s) -} diff --git a/pkg/apis/playlist/v0alpha1/zz_generated.openapi.go b/pkg/apis/playlist/v0alpha1/zz_generated.openapi.go index 4be50c3a8f743..0e9be61c3bfa4 100644 --- a/pkg/apis/playlist/v0alpha1/zz_generated.openapi.go +++ b/pkg/apis/playlist/v0alpha1/zz_generated.openapi.go @@ -14,7 +14,7 @@ import ( spec "k8s.io/kube-openapi/pkg/validation/spec" ) -func getOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenAPIDefinition { +func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenAPIDefinition { return map[string]common.OpenAPIDefinition{ "github.com/grafana/grafana/pkg/apis/playlist/v0alpha1.Item": schema_pkg_apis_playlist_v0alpha1_Item(ref), "github.com/grafana/grafana/pkg/apis/playlist/v0alpha1.Playlist": schema_pkg_apis_playlist_v0alpha1_Playlist(ref), diff --git a/pkg/apis/playlist/zz_generated.deepcopy.go b/pkg/apis/playlist/zz_generated.deepcopy.go deleted file mode 100644 index 87997bf707926..0000000000000 --- a/pkg/apis/playlist/zz_generated.deepcopy.go +++ /dev/null @@ -1,123 +0,0 @@ -//go:build !ignore_autogenerated -// +build !ignore_autogenerated - -/* -Copyright The Kubernetes Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -// Code generated by deepcopy-gen. DO NOT EDIT. - -package playlist - -import ( - runtime "k8s.io/apimachinery/pkg/runtime" -) - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *Item) DeepCopyInto(out *Item) { - *out = *in - return -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Item. -func (in *Item) DeepCopy() *Item { - if in == nil { - return nil - } - out := new(Item) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *Playlist) DeepCopyInto(out *Playlist) { - *out = *in - out.TypeMeta = in.TypeMeta - in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) - in.Spec.DeepCopyInto(&out.Spec) - return -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Playlist. -func (in *Playlist) DeepCopy() *Playlist { - if in == nil { - return nil - } - out := new(Playlist) - in.DeepCopyInto(out) - return out -} - -// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *Playlist) DeepCopyObject() runtime.Object { - if c := in.DeepCopy(); c != nil { - return c - } - return nil -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *PlaylistList) DeepCopyInto(out *PlaylistList) { - *out = *in - out.TypeMeta = in.TypeMeta - in.ListMeta.DeepCopyInto(&out.ListMeta) - if in.Items != nil { - in, out := &in.Items, &out.Items - *out = make([]Playlist, len(*in)) - for i := range *in { - (*in)[i].DeepCopyInto(&(*out)[i]) - } - } - return -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PlaylistList. -func (in *PlaylistList) DeepCopy() *PlaylistList { - if in == nil { - return nil - } - out := new(PlaylistList) - in.DeepCopyInto(out) - return out -} - -// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *PlaylistList) DeepCopyObject() runtime.Object { - if c := in.DeepCopy(); c != nil { - return c - } - return nil -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *Spec) DeepCopyInto(out *Spec) { - *out = *in - if in.Items != nil { - in, out := &in.Items, &out.Items - *out = make([]Item, len(*in)) - copy(*out, *in) - } - return -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Spec. -func (in *Spec) DeepCopy() *Spec { - if in == nil { - return nil - } - out := new(Spec) - in.DeepCopyInto(out) - return out -} diff --git a/pkg/apis/wireset.go b/pkg/apis/wireset.go deleted file mode 100644 index dd7465bf7e5ec..0000000000000 --- a/pkg/apis/wireset.go +++ /dev/null @@ -1,17 +0,0 @@ -package apis - -import ( - "github.com/google/wire" - - examplev0alpha1 "github.com/grafana/grafana/pkg/apis/example/v0alpha1" - "github.com/grafana/grafana/pkg/apis/playlist" - playlistsv0alpha1 "github.com/grafana/grafana/pkg/apis/playlist/v0alpha1" -) - -// WireSet is the list of all services -// NOTE: you must also register the service in: pkg/registry/apis/apis.go -var WireSet = wire.NewSet( - playlist.RegisterAPIService, - playlistsv0alpha1.RegisterAPIService, - examplev0alpha1.RegisterAPIService, -) diff --git a/pkg/registry/apis/apis.go b/pkg/registry/apis/apis.go index 300d6778f9cb2..302e4b8908e53 100644 --- a/pkg/registry/apis/apis.go +++ b/pkg/registry/apis/apis.go @@ -3,10 +3,9 @@ package apiregistry import ( "context" - examplev0alpha1 "github.com/grafana/grafana/pkg/apis/example/v0alpha1" - "github.com/grafana/grafana/pkg/apis/playlist" - playlistsv0alpha1 "github.com/grafana/grafana/pkg/apis/playlist/v0alpha1" "github.com/grafana/grafana/pkg/registry" + "github.com/grafana/grafana/pkg/registry/apis/example" + "github.com/grafana/grafana/pkg/registry/apis/playlist" ) var ( @@ -15,12 +14,11 @@ var ( type Service struct{} -// ProvideService is an entry point for each service that will force initialization +// ProvideRegistryServiceSink is an entry point for each service that will force initialization // and give each builder the chance to register itself with the main server -func ProvideService( +func ProvideRegistryServiceSink( _ *playlist.PlaylistAPIBuilder, - _ *playlistsv0alpha1.PlaylistAPIBuilder, - _ *examplev0alpha1.TestingAPIBuilder, + _ *example.TestingAPIBuilder, ) *Service { return &Service{} } diff --git a/pkg/apis/example/v0alpha1/register.go b/pkg/registry/apis/example/register.go similarity index 97% rename from pkg/apis/example/v0alpha1/register.go rename to pkg/registry/apis/example/register.go index 7dfca278ae6c8..7222d993d34c0 100644 --- a/pkg/apis/example/v0alpha1/register.go +++ b/pkg/registry/apis/example/register.go @@ -1,11 +1,9 @@ -package v0alpha1 +package example import ( "fmt" "net/http" - "github.com/grafana/grafana/pkg/services/featuremgmt" - grafanaapiserver "github.com/grafana/grafana/pkg/services/grafana-apiserver" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" @@ -19,6 +17,10 @@ import ( common "k8s.io/kube-openapi/pkg/common" "k8s.io/kube-openapi/pkg/spec3" "k8s.io/kube-openapi/pkg/validation/spec" + + example "github.com/grafana/grafana/pkg/apis/example/v0alpha1" + "github.com/grafana/grafana/pkg/services/featuremgmt" + grafanaapiserver "github.com/grafana/grafana/pkg/services/grafana-apiserver" ) // GroupName is the group name for this API. @@ -68,7 +70,7 @@ func (b *TestingAPIBuilder) GetAPIGroupInfo( } func (b *TestingAPIBuilder) GetOpenAPIDefinitions() common.GetOpenAPIDefinitions { - return getOpenAPIDefinitions + return example.GetOpenAPIDefinitions } // Register additional routes with the server @@ -188,7 +190,7 @@ var ( // Adds the list of known types to the given scheme. func addKnownTypes(scheme *runtime.Scheme) error { scheme.AddKnownTypes(SchemeGroupVersion, - &RuntimeInfo{}, + &example.RuntimeInfo{}, ) metav1.AddToGroupVersion(scheme, SchemeGroupVersion) return nil diff --git a/pkg/apis/example/v0alpha1/storage.go b/pkg/registry/apis/example/storage.go similarity index 89% rename from pkg/apis/example/v0alpha1/storage.go rename to pkg/registry/apis/example/storage.go index 33322f9581b9f..b1abd27ab18e1 100644 --- a/pkg/apis/example/v0alpha1/storage.go +++ b/pkg/registry/apis/example/storage.go @@ -1,4 +1,4 @@ -package v0alpha1 +package example import ( "context" @@ -9,6 +9,7 @@ import ( "k8s.io/apimachinery/pkg/runtime" "k8s.io/apiserver/pkg/registry/rest" + example "github.com/grafana/grafana/pkg/apis/example/v0alpha1" "github.com/grafana/grafana/pkg/setting" ) @@ -20,12 +21,12 @@ var ( ) type staticStorage struct { - info RuntimeInfo + info example.RuntimeInfo } func newDeploymentInfoStorage() *staticStorage { return &staticStorage{ - info: RuntimeInfo{ + info: example.RuntimeInfo{ TypeMeta: metav1.TypeMeta{ APIVersion: APIVersion, Kind: "DeploymentInfo", @@ -43,7 +44,7 @@ func newDeploymentInfoStorage() *staticStorage { } func (s *staticStorage) New() runtime.Object { - return &RuntimeInfo{} + return &example.RuntimeInfo{} } func (s *staticStorage) Destroy() {} @@ -57,7 +58,7 @@ func (s *staticStorage) GetSingularName() string { } func (s *staticStorage) NewList() runtime.Object { - return &RuntimeInfo{} + return &example.RuntimeInfo{} } func (s *staticStorage) ConvertToTable(ctx context.Context, object runtime.Object, tableOptions runtime.Object) (*metav1.Table, error) { diff --git a/pkg/apis/playlist/conversions.go b/pkg/registry/apis/playlist/conversions.go similarity index 75% rename from pkg/apis/playlist/conversions.go rename to pkg/registry/apis/playlist/conversions.go index adffda00343a7..60f785a8a605b 100644 --- a/pkg/apis/playlist/conversions.go +++ b/pkg/registry/apis/playlist/conversions.go @@ -10,14 +10,15 @@ import ( "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/types" + playlist "github.com/grafana/grafana/pkg/apis/playlist/v0alpha1" "github.com/grafana/grafana/pkg/kinds" "github.com/grafana/grafana/pkg/services/grafana-apiserver/endpoints/request" - "github.com/grafana/grafana/pkg/services/playlist" + playlistsvc "github.com/grafana/grafana/pkg/services/playlist" ) -func UnstructuredToLegacyPlaylist(item unstructured.Unstructured) *playlist.Playlist { +func UnstructuredToLegacyPlaylist(item unstructured.Unstructured) *playlistsvc.Playlist { spec := item.Object["spec"].(map[string]any) - return &playlist.Playlist{ + return &playlistsvc.Playlist{ UID: item.GetName(), Name: spec["title"].(string), Interval: spec["interval"].(string), @@ -25,9 +26,9 @@ func UnstructuredToLegacyPlaylist(item unstructured.Unstructured) *playlist.Play } } -func UnstructuredToLegacyPlaylistDTO(item unstructured.Unstructured) *playlist.PlaylistDTO { +func UnstructuredToLegacyPlaylistDTO(item unstructured.Unstructured) *playlistsvc.PlaylistDTO { spec := item.Object["spec"].(map[string]any) - dto := &playlist.PlaylistDTO{ + dto := &playlistsvc.PlaylistDTO{ Uid: item.GetName(), Name: spec["title"].(string), Interval: spec["interval"].(string), @@ -43,14 +44,14 @@ func UnstructuredToLegacyPlaylistDTO(item unstructured.Unstructured) *playlist.P return dto } -func convertToK8sResource(v *playlist.PlaylistDTO, namespacer request.NamespaceMapper) *Playlist { - spec := Spec{ +func convertToK8sResource(v *playlistsvc.PlaylistDTO, namespacer request.NamespaceMapper) *playlist.Playlist { + spec := playlist.Spec{ Title: v.Name, Interval: v.Interval, } for _, item := range v.Items { - spec.Items = append(spec.Items, Item{ - Type: ItemType(item.Type), + spec.Items = append(spec.Items, playlist.Item{ + Type: playlist.ItemType(item.Type), Value: item.Value, }) } @@ -63,7 +64,7 @@ func convertToK8sResource(v *playlist.PlaylistDTO, namespacer request.NamespaceM Key: fmt.Sprintf("%d", v.Id), }) } - return &Playlist{ + return &playlist.Playlist{ ObjectMeta: metav1.ObjectMeta{ Name: v.Uid, UID: types.UID(v.Uid), @@ -76,19 +77,19 @@ func convertToK8sResource(v *playlist.PlaylistDTO, namespacer request.NamespaceM } } -func convertToLegacyUpdateCommand(p *Playlist, orgId int64) (*playlist.UpdatePlaylistCommand, error) { +func convertToLegacyUpdateCommand(p *playlist.Playlist, orgId int64) (*playlistsvc.UpdatePlaylistCommand, error) { spec := p.Spec - cmd := &playlist.UpdatePlaylistCommand{ + cmd := &playlistsvc.UpdatePlaylistCommand{ UID: p.Name, Name: spec.Title, Interval: spec.Interval, OrgId: orgId, } for _, item := range spec.Items { - if item.Type == ItemTypeDashboardById { + if item.Type == playlist.ItemTypeDashboardById { return nil, fmt.Errorf("unsupported item type: %s", item.Type) } - cmd.Items = append(cmd.Items, playlist.PlaylistItem{ + cmd.Items = append(cmd.Items, playlistsvc.PlaylistItem{ Type: string(item.Type), Value: item.Value, }) diff --git a/pkg/apis/playlist/conversions_test.go b/pkg/registry/apis/playlist/conversions_test.go similarity index 100% rename from pkg/apis/playlist/conversions_test.go rename to pkg/registry/apis/playlist/conversions_test.go diff --git a/pkg/apis/playlist/legacy_storage.go b/pkg/registry/apis/playlist/legacy_storage.go similarity index 86% rename from pkg/apis/playlist/legacy_storage.go rename to pkg/registry/apis/playlist/legacy_storage.go index 7bad9013a473e..d5c962343ecb0 100644 --- a/pkg/apis/playlist/legacy_storage.go +++ b/pkg/registry/apis/playlist/legacy_storage.go @@ -12,8 +12,9 @@ import ( "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apiserver/pkg/registry/rest" + playlist "github.com/grafana/grafana/pkg/apis/playlist/v0alpha1" "github.com/grafana/grafana/pkg/services/grafana-apiserver/endpoints/request" - "github.com/grafana/grafana/pkg/services/playlist" + playlistsvc "github.com/grafana/grafana/pkg/services/playlist" ) var ( @@ -28,7 +29,7 @@ var ( ) type legacyStorage struct { - service playlist.Service + service playlistsvc.Service namespacer request.NamespaceMapper tableConverter rest.TableConvertor @@ -37,7 +38,7 @@ type legacyStorage struct { } func (s *legacyStorage) New() runtime.Object { - return &Playlist{} + return &playlist.Playlist{} } func (s *legacyStorage) Destroy() {} @@ -51,7 +52,7 @@ func (s *legacyStorage) GetSingularName() string { } func (s *legacyStorage) NewList() runtime.Object { - return &PlaylistList{} + return &playlist.PlaylistList{} } func (s *legacyStorage) ConvertToTable(ctx context.Context, object runtime.Object, tableOptions runtime.Object) (*metav1.Table, error) { @@ -70,7 +71,7 @@ func (s *legacyStorage) List(ctx context.Context, options *internalversion.ListO if options.Limit > 0 { limit = int(options.Limit) } - res, err := s.service.Search(ctx, &playlist.GetPlaylistsQuery{ + res, err := s.service.Search(ctx, &playlistsvc.GetPlaylistsQuery{ OrgId: info.OrgID, Limit: limit, }) @@ -78,9 +79,9 @@ func (s *legacyStorage) List(ctx context.Context, options *internalversion.ListO return nil, err } - list := &PlaylistList{} + list := &playlist.PlaylistList{} for _, v := range res { - p, err := s.service.Get(ctx, &playlist.GetPlaylistByUidQuery{ + p, err := s.service.Get(ctx, &playlistsvc.GetPlaylistByUidQuery{ UID: v.UID, OrgId: info.OrgID, }) @@ -101,12 +102,12 @@ func (s *legacyStorage) Get(ctx context.Context, name string, options *metav1.Ge return nil, err } - dto, err := s.service.Get(ctx, &playlist.GetPlaylistByUidQuery{ + dto, err := s.service.Get(ctx, &playlistsvc.GetPlaylistByUidQuery{ UID: name, OrgId: info.OrgID, }) if err != nil || dto == nil { - if errors.Is(err, playlist.ErrPlaylistNotFound) || err == nil { + if errors.Is(err, playlistsvc.ErrPlaylistNotFound) || err == nil { err = k8serrors.NewNotFound(s.SingularQualifiedResource, name) } return nil, err @@ -125,7 +126,7 @@ func (s *legacyStorage) Create(ctx context.Context, return nil, err } - p, ok := obj.(*Playlist) + p, ok := obj.(*playlist.Playlist) if !ok { return nil, fmt.Errorf("expected playlist?") } @@ -133,7 +134,7 @@ func (s *legacyStorage) Create(ctx context.Context, if err != nil { return nil, err } - out, err := s.service.Create(ctx, &playlist.CreatePlaylistCommand{ + out, err := s.service.Create(ctx, &playlistsvc.CreatePlaylistCommand{ UID: p.Name, Name: cmd.Name, Interval: cmd.Interval, @@ -169,7 +170,7 @@ func (s *legacyStorage) Update(ctx context.Context, if err != nil { return old, created, err } - p, ok := obj.(*Playlist) + p, ok := obj.(*playlist.Playlist) if !ok { return nil, created, fmt.Errorf("expected playlist after update") } @@ -197,11 +198,11 @@ func (s *legacyStorage) Delete(ctx context.Context, name string, deleteValidatio if err != nil { return nil, false, err } - p, ok := v.(*Playlist) + p, ok := v.(*playlist.Playlist) if !ok { return v, false, fmt.Errorf("expected a playlist response from Get") } - err = s.service.Delete(ctx, &playlist.DeletePlaylistCommand{ + err = s.service.Delete(ctx, &playlistsvc.DeletePlaylistCommand{ UID: name, OrgId: info.OrgID, }) diff --git a/pkg/apis/playlist/register.go b/pkg/registry/apis/playlist/register.go similarity index 76% rename from pkg/apis/playlist/register.go rename to pkg/registry/apis/playlist/register.go index 1658447dec229..0934c79623710 100644 --- a/pkg/apis/playlist/register.go +++ b/pkg/registry/apis/playlist/register.go @@ -13,28 +13,29 @@ import ( genericapiserver "k8s.io/apiserver/pkg/server" common "k8s.io/kube-openapi/pkg/common" + playlist "github.com/grafana/grafana/pkg/apis/playlist/v0alpha1" grafanaapiserver "github.com/grafana/grafana/pkg/services/grafana-apiserver" "github.com/grafana/grafana/pkg/services/grafana-apiserver/endpoints/request" grafanarest "github.com/grafana/grafana/pkg/services/grafana-apiserver/rest" "github.com/grafana/grafana/pkg/services/grafana-apiserver/utils" - "github.com/grafana/grafana/pkg/services/playlist" + playlistsvc "github.com/grafana/grafana/pkg/services/playlist" "github.com/grafana/grafana/pkg/setting" ) // GroupName is the group name for this API. const GroupName = "playlist.grafana.app" -const VersionID = runtime.APIVersionInternal +const VersionID = "v0alpha1" var _ grafanaapiserver.APIGroupBuilder = (*PlaylistAPIBuilder)(nil) // This is used just so wire has something unique to return type PlaylistAPIBuilder struct { - service playlist.Service + service playlistsvc.Service namespacer request.NamespaceMapper gv schema.GroupVersion } -func RegisterAPIService(p playlist.Service, +func RegisterAPIService(p playlistsvc.Service, apiregistration grafanaapiserver.APIRegistrar, cfg *setting.Cfg, ) *PlaylistAPIBuilder { @@ -53,10 +54,27 @@ func (b *PlaylistAPIBuilder) GetGroupVersion() schema.GroupVersion { func (b *PlaylistAPIBuilder) InstallSchema(scheme *runtime.Scheme) error { scheme.AddKnownTypes(b.gv, - &Playlist{}, - &PlaylistList{}, + &playlist.Playlist{}, + &playlist.PlaylistList{}, ) - return nil + + // Link this version to the internal representation. + // This is used for server-side-apply (PATCH), and avoids the error: + // "no kind is registered for the type" + scheme.AddKnownTypes(schema.GroupVersion{ + Group: b.gv.Group, + Version: runtime.APIVersionInternal, + }, + &playlist.Playlist{}, + &playlist.PlaylistList{}, + ) + + // If multiple versions exist, then register conversions from zz_generated.conversion.go + // if err := playlist.RegisterConversions(scheme); err != nil { + // return err + // } + metav1.AddToGroupVersion(scheme, b.gv) + return scheme.SetVersionPriority(b.gv) } func (b *PlaylistAPIBuilder) GetAPIGroupInfo( @@ -82,7 +100,7 @@ func (b *PlaylistAPIBuilder) GetAPIGroupInfo( {Name: "Created At", Type: "date"}, }, func(obj runtime.Object) ([]interface{}, error) { - m, ok := obj.(*Playlist) + m, ok := obj.(*playlist.Playlist) if !ok { return nil, fmt.Errorf("expected playlist") } @@ -105,12 +123,12 @@ func (b *PlaylistAPIBuilder) GetAPIGroupInfo( storage["playlists"] = grafanarest.NewDualWriter(legacyStore, store) } - apiGroupInfo.VersionedResourcesStorageMap["v0alpha1"] = storage + apiGroupInfo.VersionedResourcesStorageMap[VersionID] = storage return &apiGroupInfo, nil } func (b *PlaylistAPIBuilder) GetOpenAPIDefinitions() common.GetOpenAPIDefinitions { - return nil // no custom OpenAPI definitions + return playlist.GetOpenAPIDefinitions } func (b *PlaylistAPIBuilder) GetAPIRoutes() *grafanaapiserver.APIRoutes { diff --git a/pkg/apis/playlist/storage.go b/pkg/registry/apis/playlist/storage.go similarity index 83% rename from pkg/apis/playlist/storage.go rename to pkg/registry/apis/playlist/storage.go index d5d80e18b553d..bd723d34b2f28 100644 --- a/pkg/apis/playlist/storage.go +++ b/pkg/registry/apis/playlist/storage.go @@ -5,6 +5,7 @@ import ( "k8s.io/apiserver/pkg/registry/generic" genericregistry "k8s.io/apiserver/pkg/registry/generic/registry" + playlist "github.com/grafana/grafana/pkg/apis/playlist/v0alpha1" grafanaregistry "github.com/grafana/grafana/pkg/services/grafana-apiserver/registry/generic" grafanarest "github.com/grafana/grafana/pkg/services/grafana-apiserver/rest" ) @@ -19,8 +20,8 @@ func newStorage(scheme *runtime.Scheme, optsGetter generic.RESTOptionsGetter, le strategy := grafanaregistry.NewStrategy(scheme) store := &genericregistry.Store{ - NewFunc: func() runtime.Object { return &Playlist{} }, - NewListFunc: func() runtime.Object { return &PlaylistList{} }, + NewFunc: func() runtime.Object { return &playlist.Playlist{} }, + NewListFunc: func() runtime.Object { return &playlist.PlaylistList{} }, PredicateFunc: grafanaregistry.Matcher, DefaultQualifiedResource: legacy.DefaultQualifiedResource, SingularQualifiedResource: legacy.SingularQualifiedResource, diff --git a/pkg/registry/apis/wireset.go b/pkg/registry/apis/wireset.go index d96125ebc429d..7338136a182b4 100644 --- a/pkg/registry/apis/wireset.go +++ b/pkg/registry/apis/wireset.go @@ -3,10 +3,15 @@ package apiregistry import ( "github.com/google/wire" - "github.com/grafana/grafana/pkg/apis" + "github.com/grafana/grafana/pkg/registry/apis/example" + "github.com/grafana/grafana/pkg/registry/apis/playlist" ) var WireSet = wire.NewSet( - ProvideService, - apis.WireSet, + ProvideRegistryServiceSink, // dummy background service that forces registration + + // Each must be added here *and* in the ServiceSink above + // playlistV0.RegisterAPIService, + playlist.RegisterAPIService, + example.RegisterAPIService, ) diff --git a/pkg/services/grafana-apiserver/service.go b/pkg/services/grafana-apiserver/service.go index 21eec399f8c0a..aa7e6ed241c75 100644 --- a/pkg/services/grafana-apiserver/service.go +++ b/pkg/services/grafana-apiserver/service.go @@ -307,7 +307,7 @@ func (s *service) start(ctx context.Context) error { if err != nil { return err } - if g == nil { + if g == nil || len(g.PrioritizedVersions) < 1 { continue } err = server.InstallAPIGroup(g) From 35c1ee968673e68aa1a8b520f8a2792ced582d4c Mon Sep 17 00:00:00 2001 From: Ryan McKinley Date: Fri, 3 Nov 2023 08:14:51 -0700 Subject: [PATCH 091/869] EntityStore: Remove http access (can use apiserver now) (#77602) --- pkg/api/api.go | 5 - pkg/api/http_server.go | 5 +- pkg/server/wire.go | 2 - .../store/entity/httpentitystore/service.go | 357 ------------------ 4 files changed, 1 insertion(+), 368 deletions(-) delete mode 100644 pkg/services/store/entity/httpentitystore/service.go diff --git a/pkg/api/api.go b/pkg/api/api.go index ee3c5a828a665..cd639f935b69a 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -282,11 +282,6 @@ func (hs *HTTPServer) registerRoutes() { apiRoute.Group("/storage", hs.StorageService.RegisterHTTPRoutes) } - // Allow HTTP access to the entity storage feature (dev only for now) - if hs.Features.IsEnabled(featuremgmt.FlagEntityStore) { - apiRoute.Group("/entity", hs.httpEntityStore.RegisterHTTPRoutes) - } - if hs.Features.IsEnabled(featuremgmt.FlagPanelTitleSearch) { apiRoute.Group("/search-v2", hs.SearchV2HTTPService.RegisterHTTPRoutes) } diff --git a/pkg/api/http_server.go b/pkg/api/http_server.go index 412facb7cfb8b..1a9d45d149ad8 100644 --- a/pkg/api/http_server.go +++ b/pkg/api/http_server.go @@ -93,7 +93,6 @@ import ( starApi "github.com/grafana/grafana/pkg/services/star/api" "github.com/grafana/grafana/pkg/services/stats" "github.com/grafana/grafana/pkg/services/store" - "github.com/grafana/grafana/pkg/services/store/entity/httpentitystore" "github.com/grafana/grafana/pkg/services/tag" "github.com/grafana/grafana/pkg/services/team" tempUser "github.com/grafana/grafana/pkg/services/temp_user" @@ -146,7 +145,6 @@ type HTTPServer struct { Live *live.GrafanaLive LivePushGateway *pushhttp.Gateway StorageService store.StorageService - httpEntityStore httpentitystore.HTTPEntityStore SearchV2HTTPService searchV2.SearchHTTPService ContextHandler *contexthandler.ContextHandler LoggerMiddleware loggermw.Logger @@ -232,7 +230,7 @@ func ProvideHTTPServer(opts ServerOptions, cfg *setting.Cfg, routeRegister routi pluginsUpdateChecker *updatechecker.PluginsService, searchUsersService searchusers.Service, dataSourcesService datasources.DataSourceService, queryDataService query.Service, pluginFileStore plugins.FileStore, serviceaccountsService serviceaccounts.Service, - authInfoService login.AuthInfoService, storageService store.StorageService, httpEntityStore httpentitystore.HTTPEntityStore, + authInfoService login.AuthInfoService, storageService store.StorageService, notificationService *notifications.NotificationService, dashboardService dashboards.DashboardService, dashboardProvisioningService dashboards.DashboardProvisioningService, folderService folder.Service, dsGuardian guardian.DatasourceGuardianProvider, alertNotificationService *alerting.AlertNotificationService, @@ -309,7 +307,6 @@ func ProvideHTTPServer(opts ServerOptions, cfg *setting.Cfg, routeRegister routi secretsMigrator: secretsMigrator, secretsPluginMigrator: secretsPluginMigrator, secretsStore: secretsStore, - httpEntityStore: httpEntityStore, DataSourcesService: dataSourcesService, searchUsersService: searchUsersService, queryDataService: queryDataService, diff --git a/pkg/server/wire.go b/pkg/server/wire.go index 95db107db6d2e..61bebfdf71fc4 100644 --- a/pkg/server/wire.go +++ b/pkg/server/wire.go @@ -134,7 +134,6 @@ import ( "github.com/grafana/grafana/pkg/services/star/starimpl" "github.com/grafana/grafana/pkg/services/stats/statsimpl" "github.com/grafana/grafana/pkg/services/store" - "github.com/grafana/grafana/pkg/services/store/entity/httpentitystore" "github.com/grafana/grafana/pkg/services/store/entity/sqlstash" "github.com/grafana/grafana/pkg/services/store/kind" "github.com/grafana/grafana/pkg/services/store/resolver" @@ -347,7 +346,6 @@ var wireBasicSet = wire.NewSet( kind.ProvideService, // The registry of known kinds sqlstash.ProvideSQLEntityServer, resolver.ProvideEntityReferenceResolver, - httpentitystore.ProvideHTTPEntityStore, teamimpl.ProvideService, teamapi.ProvideTeamAPI, tempuserimpl.ProvideService, diff --git a/pkg/services/store/entity/httpentitystore/service.go b/pkg/services/store/entity/httpentitystore/service.go deleted file mode 100644 index 6b992f50f2eb6..0000000000000 --- a/pkg/services/store/entity/httpentitystore/service.go +++ /dev/null @@ -1,357 +0,0 @@ -package httpentitystore - -import ( - "fmt" - "io" - "net/http" - "net/url" - "strconv" - "strings" - - "github.com/grafana/grafana/pkg/api/response" - "github.com/grafana/grafana/pkg/api/routing" - "github.com/grafana/grafana/pkg/infra/grn" - "github.com/grafana/grafana/pkg/infra/log" - "github.com/grafana/grafana/pkg/middleware" - contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" - "github.com/grafana/grafana/pkg/services/store/entity" - "github.com/grafana/grafana/pkg/services/store/kind" - "github.com/grafana/grafana/pkg/util" - "github.com/grafana/grafana/pkg/web" -) - -type HTTPEntityStore interface { - // Register HTTP Access to the store - RegisterHTTPRoutes(routing.RouteRegister) -} - -type httpEntityStore struct { - store entity.EntityStoreServer - log log.Logger - kinds kind.KindRegistry -} - -func ProvideHTTPEntityStore(store entity.EntityStoreServer, kinds kind.KindRegistry) HTTPEntityStore { - return &httpEntityStore{ - store: store, - log: log.New("http-entity-store"), - kinds: kinds, - } -} - -// All registered under "api/entity" -func (s *httpEntityStore) RegisterHTTPRoutes(route routing.RouteRegister) { - // For now, require admin for everything - reqGrafanaAdmin := middleware.ReqSignedIn //.ReqGrafanaAdmin - - // Every * must parse to a GRN (uid+kind) - route.Get("/store/:kind/:uid", reqGrafanaAdmin, routing.Wrap(s.doGetEntity)) - route.Post("/store/:kind/:uid", reqGrafanaAdmin, routing.Wrap(s.doWriteEntity)) - route.Delete("/store/:kind/:uid", reqGrafanaAdmin, routing.Wrap(s.doDeleteEntity)) - route.Get("/raw/:kind/:uid", reqGrafanaAdmin, routing.Wrap(s.doGetRawEntity)) - route.Get("/history/:kind/:uid", reqGrafanaAdmin, routing.Wrap(s.doGetHistory)) - route.Get("/list/:uid", reqGrafanaAdmin, routing.Wrap(s.doListFolder)) // Simplified version of search -- path is prefix - route.Get("/search", reqGrafanaAdmin, routing.Wrap(s.doSearch)) - - // File upload - route.Post("/upload", reqGrafanaAdmin, routing.Wrap(s.doUpload)) -} - -// This function will extract UID+Kind from the requested path "*" in our router -// This is far from ideal! but is at least consistent for these endpoints. -// This will quickly be revisited as we explore how to encode UID+Kind in a "GRN" format -func (s *httpEntityStore) getGRNFromRequest(c *contextmodel.ReqContext) (*grn.GRN, map[string]string, error) { - params := web.Params(c.Req) - // Read parameters that are encoded in the URL - vals := c.Req.URL.Query() - for k, v := range vals { - if len(v) > 0 { - params[k] = v[0] - } - } - return &grn.GRN{ - TenantID: c.SignedInUser.GetOrgID(), - ResourceKind: params[":kind"], - ResourceIdentifier: params[":uid"], - }, params, nil -} - -func (s *httpEntityStore) doGetEntity(c *contextmodel.ReqContext) response.Response { - grn, params, err := s.getGRNFromRequest(c) - if err != nil { - return response.Error(400, err.Error(), err) - } - rsp, err := s.store.Read(c.Req.Context(), &entity.ReadEntityRequest{ - GRN: grn, - Version: params["version"], // ?version = XYZ - WithBody: params["body"] != "false", // default to true - WithSummary: params["summary"] == "true", // default to false - }) - if err != nil { - return response.Error(500, "error fetching entity", err) - } - if rsp == nil { - return response.Error(404, "not found", nil) - } - - // Configure etag support - currentEtag := rsp.ETag - previousEtag := c.Req.Header.Get("If-None-Match") - if previousEtag == currentEtag { - return response.CreateNormalResponse( - http.Header{ - "ETag": []string{rsp.ETag}, - }, - []byte{}, // nothing - http.StatusNotModified, // 304 - ) - } - - c.Resp.Header().Set("ETag", currentEtag) - return response.JSON(200, rsp) -} - -func (s *httpEntityStore) doGetRawEntity(c *contextmodel.ReqContext) response.Response { - grn, params, err := s.getGRNFromRequest(c) - if err != nil { - return response.Error(400, err.Error(), err) - } - rsp, err := s.store.Read(c.Req.Context(), &entity.ReadEntityRequest{ - GRN: grn, - Version: params["version"], // ?version = XYZ - WithBody: true, - WithSummary: false, - }) - if err != nil { - return response.Error(500, "?", err) - } - info, err := s.kinds.GetInfo(grn.ResourceKind) - if err != nil { - return response.Error(400, "Unsupported kind", err) - } - - if rsp != nil && rsp.Body != nil { - // Configure etag support - currentEtag := rsp.ETag - previousEtag := c.Req.Header.Get("If-None-Match") - if previousEtag == currentEtag { - return response.CreateNormalResponse( - http.Header{ - "ETag": []string{rsp.ETag}, - }, - []byte{}, // nothing - http.StatusNotModified, // 304 - ) - } - mime := info.MimeType - if mime == "" { - mime = "application/json" - } - return response.CreateNormalResponse( - http.Header{ - "Content-Type": []string{mime}, - "ETag": []string{currentEtag}, - }, - rsp.Body, - 200, - ) - } - return response.JSON(400, rsp) // ??? -} - -const MAX_UPLOAD_SIZE = 5 * 1024 * 1024 // 5MB - -func (s *httpEntityStore) doWriteEntity(c *contextmodel.ReqContext) response.Response { - grn, params, err := s.getGRNFromRequest(c) - if err != nil { - return response.Error(400, err.Error(), err) - } - - // Cap the max size - c.Req.Body = http.MaxBytesReader(c.Resp, c.Req.Body, MAX_UPLOAD_SIZE) - b, err := io.ReadAll(c.Req.Body) - if err != nil { - return response.Error(400, "error reading body", err) - } - - rsp, err := s.store.Write(c.Req.Context(), &entity.WriteEntityRequest{ - GRN: grn, - Body: b, - Folder: params["folder"], - Comment: params["comment"], - PreviousVersion: params["previousVersion"], - }) - if err != nil { - return response.Error(500, "?", err) - } - return response.JSON(200, rsp) -} - -func (s *httpEntityStore) doDeleteEntity(c *contextmodel.ReqContext) response.Response { - grn, params, err := s.getGRNFromRequest(c) - if err != nil { - return response.Error(400, err.Error(), err) - } - rsp, err := s.store.Delete(c.Req.Context(), &entity.DeleteEntityRequest{ - GRN: grn, - PreviousVersion: params["previousVersion"], - }) - if err != nil { - return response.Error(500, "?", err) - } - return response.JSON(200, rsp) -} - -func (s *httpEntityStore) doGetHistory(c *contextmodel.ReqContext) response.Response { - grn, params, err := s.getGRNFromRequest(c) - if err != nil { - return response.Error(400, err.Error(), err) - } - limit := int64(20) // params - rsp, err := s.store.History(c.Req.Context(), &entity.EntityHistoryRequest{ - GRN: grn, - Limit: limit, - NextPageToken: params["nextPageToken"], - }) - if err != nil { - return response.Error(500, "?", err) - } - return response.JSON(200, rsp) -} - -func (s *httpEntityStore) doUpload(c *contextmodel.ReqContext) response.Response { - c.Req.Body = http.MaxBytesReader(c.Resp, c.Req.Body, MAX_UPLOAD_SIZE) - if err := c.Req.ParseMultipartForm(MAX_UPLOAD_SIZE); err != nil { - msg := fmt.Sprintf("Please limit file uploaded under %s", util.ByteCountSI(MAX_UPLOAD_SIZE)) - return response.Error(400, msg, nil) - } - fileinfo := c.Req.MultipartForm.File - if len(fileinfo) < 1 { - return response.Error(400, "missing files", nil) - } - - var rsp []*entity.WriteEntityResponse - - message := getMultipartFormValue(c.Req, "message") - overwriteExistingFile := getMultipartFormValue(c.Req, "overwriteExistingFile") != "false" // must explicitly overwrite - folder := getMultipartFormValue(c.Req, "folder") - ctx := c.Req.Context() - - for _, fileHeaders := range fileinfo { - for _, fileHeader := range fileHeaders { - idx := strings.LastIndex(fileHeader.Filename, ".") - if idx <= 0 { - return response.Error(400, "Expecting file extension: "+fileHeader.Filename, nil) - } - - ext := strings.ToLower(fileHeader.Filename[idx+1:]) - kind, err := s.kinds.GetFromExtension(ext) - if err != nil || kind.ID == "" { - return response.Error(400, "Unsupported kind: "+fileHeader.Filename, err) - } - uid := fileHeader.Filename[:idx] - - file, err := fileHeader.Open() - if err != nil { - return response.Error(500, "Internal Server Error", err) - } - data, err := io.ReadAll(file) - if err != nil { - return response.Error(500, "Internal Server Error", err) - } - err = file.Close() - if err != nil { - return response.Error(500, "Internal Server Error", err) - } - - grn := &grn.GRN{ - ResourceIdentifier: uid, - ResourceKind: kind.ID, - TenantID: c.SignedInUser.GetOrgID(), - } - - if !overwriteExistingFile { - result, err := s.store.Read(ctx, &entity.ReadEntityRequest{ - GRN: grn, - WithBody: false, - WithSummary: false, - }) - if err != nil { - return response.Error(500, "Internal Server Error", err) - } - if result.GRN != nil { - return response.Error(400, "File name already in use", err) - } - } - - result, err := s.store.Write(ctx, &entity.WriteEntityRequest{ - GRN: grn, - Body: data, - Comment: message, - Folder: folder, - // PreviousVersion: params["previousVersion"], - }) - - if err != nil { - return response.Error(500, err.Error(), err) // TODO, better errors - } - rsp = append(rsp, result) - } - } - - return response.JSON(200, rsp) -} - -func (s *httpEntityStore) doListFolder(c *contextmodel.ReqContext) response.Response { - return response.JSON(501, "Not implemented yet") -} - -func (s *httpEntityStore) doSearch(c *contextmodel.ReqContext) response.Response { - vals := c.Req.URL.Query() - - req := &entity.EntitySearchRequest{ - WithBody: asBoolean("body", vals, false), - WithLabels: asBoolean("labels", vals, true), - WithFields: asBoolean("fields", vals, true), - Kind: vals["kind"], - Query: vals.Get("query"), - Folder: vals.Get("folder"), - Sort: vals["sort"], - } - if vals.Has("limit") { - limit, err := strconv.ParseInt(vals.Get("limit"), 10, 64) - if err != nil { - return response.Error(400, "bad limit", err) - } - req.Limit = limit - } - - rsp, err := s.store.Search(c.Req.Context(), req) - if err != nil { - return response.Error(500, "?", err) - } - return response.JSON(200, rsp) -} - -func asBoolean(key string, vals url.Values, defaultValue bool) bool { - v, ok := vals[key] - if !ok { - return defaultValue - } - if len(v) == 0 { - return true // single boolean parameter - } - b, err := strconv.ParseBool(v[0]) - if err != nil { - return defaultValue - } - return b -} - -func getMultipartFormValue(req *http.Request, key string) string { - v, ok := req.MultipartForm.Value[key] - if !ok || len(v) != 1 { - return "" - } - return v[0] -} From dc1b4ceb06c0d307fbb32954384c590e34794a27 Mon Sep 17 00:00:00 2001 From: Andrew Hackmann <5140848+bossinc@users.noreply.github.com> Date: Fri, 3 Nov 2023 10:18:02 -0500 Subject: [PATCH 092/869] Azure monitor/remove reference to core config (#77601) remove app config --- .../datasource/azuremonitor/components/ConfigEditor.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/public/app/plugins/datasource/azuremonitor/components/ConfigEditor.tsx b/public/app/plugins/datasource/azuremonitor/components/ConfigEditor.tsx index ee9bec6cb03e4..48c3448c7d7b9 100644 --- a/public/app/plugins/datasource/azuremonitor/components/ConfigEditor.tsx +++ b/public/app/plugins/datasource/azuremonitor/components/ConfigEditor.tsx @@ -2,9 +2,8 @@ import React, { PureComponent } from 'react'; import { DataSourcePluginOptionsEditorProps, SelectableValue, updateDatasourcePluginOption } from '@grafana/data'; import { ConfigSection, DataSourceDescription } from '@grafana/experimental'; -import { getBackendSrv, getTemplateSrv, isFetchError, TemplateSrv } from '@grafana/runtime'; +import { getBackendSrv, getTemplateSrv, isFetchError, TemplateSrv, config } from '@grafana/runtime'; import { Alert, Divider, SecureSocksProxySettings } from '@grafana/ui'; -import { config } from 'app/core/config'; import ResponseParser from '../azure_monitor/response_parser'; import { From 3dac37380d887c40b6d6fd419ad7bf5779c7a239 Mon Sep 17 00:00:00 2001 From: Gilles De Mey Date: Fri, 3 Nov 2023 16:36:53 +0100 Subject: [PATCH 093/869] Alerting: Fix export with modifications URL when mounted on subpath (#77622) --- .../rules/RuleDetailsActionButtons.tsx | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/public/app/features/alerting/unified/components/rules/RuleDetailsActionButtons.tsx b/public/app/features/alerting/unified/components/rules/RuleDetailsActionButtons.tsx index 44cd3979713d7..c3225e8bb6ea0 100644 --- a/public/app/features/alerting/unified/components/rules/RuleDetailsActionButtons.tsx +++ b/public/app/features/alerting/unified/components/rules/RuleDetailsActionButtons.tsx @@ -4,7 +4,7 @@ import React, { Fragment, useState } from 'react'; import { useLocation } from 'react-router-dom'; import { GrafanaTheme2, textUtil, urlUtil } from '@grafana/data'; -import { config, locationService } from '@grafana/runtime'; +import { config } from '@grafana/runtime'; import { Button, ClipboardButton, @@ -238,17 +238,11 @@ export const RuleDetailsActionButtons = ({ rule, rulesSource, isViewMode }: Prop } if (isGrafanaRulerRule(rulerRule)) { - moreActionsButtons.push( - - locationService.push( - createUrl(`/alerting/${encodeURIComponent(ruleId.stringifyIdentifier(identifier))}/modify-export`) - ) - } - /> + const modifyUrl = createUrl( + `/alerting/${encodeURIComponent(ruleId.stringifyIdentifier(identifier))}/modify-export` ); + + moreActionsButtons.push(); } if (hasCreateRulePermission && !isFederated) { From ade140c16141ea7d5dbdef753cfdd9365c172257 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Calisto?= Date: Fri, 3 Nov 2023 15:59:07 +0000 Subject: [PATCH 094/869] Feature Management: Define HideFromAdminPage and AllowSelfServe configs (#77580) * Feature Management: Define HideFromAdminPage and AllowSelfServe configs * update tests * add constraint for self-serve * Update pkg/services/featuremgmt/models.go Co-authored-by: Michael Mandrus <41969079+mmandrus@users.noreply.github.com> --------- Co-authored-by: Michael Mandrus <41969079+mmandrus@users.noreply.github.com> --- pkg/api/featuremgmt.go | 5 +- pkg/api/featuremgmt_test.go | 50 ++- pkg/services/featuremgmt/models.go | 10 +- pkg/services/featuremgmt/registry.go | 381 ++++++++++--------- pkg/services/featuremgmt/toggles_gen_test.go | 6 + 5 files changed, 259 insertions(+), 193 deletions(-) diff --git a/pkg/api/featuremgmt.go b/pkg/api/featuremgmt.go index da34568366f72..dbed785ae0ac8 100644 --- a/pkg/api/featuremgmt.go +++ b/pkg/api/featuremgmt.go @@ -98,7 +98,7 @@ func isFeatureHidden(flag featuremgmt.FeatureFlag, hideCfg map[string]struct{}) if _, ok := hideCfg[flag.Name]; ok { return true } - return flag.Stage == featuremgmt.FeatureStageUnknown || flag.Stage == featuremgmt.FeatureStageExperimental || flag.Stage == featuremgmt.FeatureStagePrivatePreview + return flag.Stage == featuremgmt.FeatureStageUnknown || flag.Stage == featuremgmt.FeatureStageExperimental || flag.Stage == featuremgmt.FeatureStagePrivatePreview || flag.HideFromAdminPage } // isFeatureWriteable returns whether a toggle on the admin page can be updated by the user. @@ -110,7 +110,8 @@ func isFeatureWriteable(flag featuremgmt.FeatureFlag, readOnlyCfg map[string]str if flag.Name == featuremgmt.FlagFeatureToggleAdminPage { return false } - return flag.Stage == featuremgmt.FeatureStageGeneralAvailability || flag.Stage == featuremgmt.FeatureStageDeprecated + allowSelfServe := flag.AllowSelfServe != nil && *flag.AllowSelfServe + return flag.Stage == featuremgmt.FeatureStageGeneralAvailability && allowSelfServe || flag.Stage == featuremgmt.FeatureStageDeprecated } // isFeatureEditingAllowed checks if the backend is properly configured to allow feature toggle changes from the UI diff --git a/pkg/api/featuremgmt_test.go b/pkg/api/featuremgmt_test.go index 120680dc78b7a..459f7dafa1b73 100644 --- a/pkg/api/featuremgmt_test.go +++ b/pkg/api/featuremgmt_test.go @@ -20,6 +20,15 @@ import ( "github.com/stretchr/testify/require" ) +func boolPtr(b bool) *bool { + return &b +} + +var ( + truePtr = boolPtr(true) + falsePtr = boolPtr(false) +) + func TestGetFeatureToggles(t *testing.T) { readPermissions := []accesscontrol.Permission{{Action: accesscontrol.ActionFeatureManagementRead}} @@ -107,20 +116,27 @@ func TestGetFeatureToggles(t *testing.T) { Name: "toggle3", Stage: featuremgmt.FeatureStagePrivatePreview, }, { - Name: "toggle4", - Stage: featuremgmt.FeatureStagePublicPreview, + Name: "toggle4", + Stage: featuremgmt.FeatureStagePublicPreview, + AllowSelfServe: truePtr, + }, { + Name: "toggle5", + Stage: featuremgmt.FeatureStageGeneralAvailability, + AllowSelfServe: truePtr, }, { - Name: "toggle5", - Stage: featuremgmt.FeatureStageGeneralAvailability, + Name: "toggle6", + Stage: featuremgmt.FeatureStageDeprecated, + AllowSelfServe: truePtr, }, { - Name: "toggle6", - Stage: featuremgmt.FeatureStageDeprecated, + Name: "toggle7", + Stage: featuremgmt.FeatureStageGeneralAvailability, + AllowSelfServe: falsePtr, }, } t.Run("unknown, experimental, and private preview toggles are hidden by default", func(t *testing.T) { result := runGetScenario(t, features, setting.FeatureMgmtSettings{}, readPermissions, http.StatusOK) - assert.Len(t, result, 3) + assert.Len(t, result, 4) _, ok := findResult(t, result, "toggle1") assert.False(t, ok) @@ -130,13 +146,13 @@ func TestGetFeatureToggles(t *testing.T) { assert.False(t, ok) }) - t.Run("only public preview and GA are writeable by default", func(t *testing.T) { + t.Run("only public preview and GA with AllowSelfServe are writeable", func(t *testing.T) { settings := setting.FeatureMgmtSettings{ AllowEditing: true, UpdateWebhook: "bogus", } result := runGetScenario(t, features, settings, readPermissions, http.StatusOK) - assert.Len(t, result, 3) + assert.Len(t, result, 4) t4, ok := findResult(t, result, "toggle4") assert.True(t, ok) @@ -155,7 +171,7 @@ func TestGetFeatureToggles(t *testing.T) { UpdateWebhook: "", } result := runGetScenario(t, features, settings, readPermissions, http.StatusOK) - assert.Len(t, result, 3) + assert.Len(t, result, 4) t4, ok := findResult(t, result, "toggle4") assert.True(t, ok) @@ -305,13 +321,15 @@ func TestSetFeatureToggles(t *testing.T) { Enabled: false, Stage: featuremgmt.FeatureStageGeneralAvailability, }, { - Name: "toggle4", - Enabled: false, - Stage: featuremgmt.FeatureStageGeneralAvailability, + Name: "toggle4", + Enabled: false, + Stage: featuremgmt.FeatureStageGeneralAvailability, + AllowSelfServe: truePtr, }, { - Name: "toggle5", - Enabled: false, - Stage: featuremgmt.FeatureStageDeprecated, + Name: "toggle5", + Enabled: false, + Stage: featuremgmt.FeatureStageDeprecated, + AllowSelfServe: truePtr, }, } diff --git a/pkg/services/featuremgmt/models.go b/pkg/services/featuremgmt/models.go index f2aaa7b1c196e..54bc36766a568 100644 --- a/pkg/services/featuremgmt/models.go +++ b/pkg/services/featuremgmt/models.go @@ -110,10 +110,12 @@ type FeatureFlag struct { // Special behavior flags RequiresDevMode bool `json:"requiresDevMode,omitempty"` // can not be enabled in production // This flag is currently unused. - RequiresRestart bool `json:"requiresRestart,omitempty"` // The server must be initialized with the value - RequiresLicense bool `json:"requiresLicense,omitempty"` // Must be enabled in the license - FrontendOnly bool `json:"frontend,omitempty"` // change is only seen in the frontend - HideFromDocs bool `json:"hideFromDocs,omitempty"` // don't add the values to docs + RequiresRestart bool `json:"requiresRestart,omitempty"` // The server must be initialized with the value + RequiresLicense bool `json:"requiresLicense,omitempty"` // Must be enabled in the license + FrontendOnly bool `json:"frontend,omitempty"` // change is only seen in the frontend + HideFromDocs bool `json:"hideFromDocs,omitempty"` // don't add the values to docs + HideFromAdminPage bool `json:"hideFromAdminPage,omitempty"` // don't display the feature in the admin page - add a comment with the reasoning + AllowSelfServe *bool `json:"allowSelfServe,omitempty"` // allow admin users to toggle the feature state from the admin page; this is required for GA toggles only // This field is only for the feature management API. To enable your feature toggle by default, use `Expression`. Enabled bool `json:"enabled,omitempty"` diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index 4b8ab1b622120..02e4f0b7c0ea1 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -7,13 +7,16 @@ package featuremgmt var ( + falsePtr = boolPtr(false) + truePtr = boolPtr(true) // Register each toggle here standardFeatureFlags = []FeatureFlag{ { - Name: "disableEnvelopeEncryption", - Description: "Disable envelope encryption (emergency only)", - Stage: FeatureStageGeneralAvailability, - Owner: grafanaAsCodeSquad, + Name: "disableEnvelopeEncryption", + Description: "Disable envelope encryption (emergency only)", + Stage: FeatureStageGeneralAvailability, + Owner: grafanaAsCodeSquad, + AllowSelfServe: falsePtr, }, { Name: "live-service-web-worker", @@ -36,11 +39,12 @@ var ( Owner: grafanaAppPlatformSquad, }, { - Name: "publicDashboards", - Description: "Enables public access to dashboards", - Stage: FeatureStageGeneralAvailability, - Owner: grafanaSharingSquad, - Expression: "true", // enabled by default + Name: "publicDashboards", + Description: "Enables public access to dashboards", + Stage: FeatureStageGeneralAvailability, + Owner: grafanaSharingSquad, + Expression: "true", // enabled by default + AllowSelfServe: truePtr, }, { Name: "publicDashboardsEmailSharing", @@ -57,10 +61,11 @@ var ( Owner: grafanaObservabilityLogsSquad, }, { - Name: "featureHighlights", - Description: "Highlight Grafana Enterprise features", - Stage: FeatureStageGeneralAvailability, - Owner: grafanaAsCodeSquad, + Name: "featureHighlights", + Description: "Highlight Grafana Enterprise features", + Stage: FeatureStageGeneralAvailability, + Owner: grafanaAsCodeSquad, + AllowSelfServe: falsePtr, }, { Name: "migrationLocking", @@ -81,12 +86,13 @@ var ( Owner: grafanaExploreSquad, }, { - Name: "exploreContentOutline", - Description: "Content outline sidebar", - Stage: FeatureStageGeneralAvailability, - Owner: grafanaExploreSquad, - Expression: "true", // enabled by default - FrontendOnly: true, + Name: "exploreContentOutline", + Description: "Content outline sidebar", + Stage: FeatureStageGeneralAvailability, + Owner: grafanaExploreSquad, + Expression: "true", // enabled by default + FrontendOnly: true, + AllowSelfServe: falsePtr, }, { Name: "datasourceQueryMultiStatus", @@ -149,11 +155,12 @@ var ( Owner: hostedGrafanaTeam, }, { - Name: "dataConnectionsConsole", - Description: "Enables a new top-level page called Connections. This page is an experiment that provides a better experience when you install and configure data sources and other plugins.", - Stage: FeatureStageGeneralAvailability, - Expression: "true", // turned on by default - Owner: grafanaPluginsPlatformSquad, + Name: "dataConnectionsConsole", + Description: "Enables a new top-level page called Connections. This page is an experiment that provides a better experience when you install and configure data sources and other plugins.", + Stage: FeatureStageGeneralAvailability, + Expression: "true", // turned on by default + Owner: grafanaPluginsPlatformSquad, + AllowSelfServe: falsePtr, }, { // Some plugins rely on topnav feature flag being enabled, so we cannot remove this until we @@ -185,26 +192,29 @@ var ( Owner: grafanaAppPlatformSquad, }, { - Name: "cloudWatchCrossAccountQuerying", - Description: "Enables cross-account querying in CloudWatch datasources", - Stage: FeatureStageGeneralAvailability, - Expression: "true", // enabled by default - Owner: awsDatasourcesSquad, + Name: "cloudWatchCrossAccountQuerying", + Description: "Enables cross-account querying in CloudWatch datasources", + Stage: FeatureStageGeneralAvailability, + Expression: "true", // enabled by default + Owner: awsDatasourcesSquad, + AllowSelfServe: falsePtr, }, { - Name: "redshiftAsyncQueryDataSupport", - Description: "Enable async query data support for Redshift", - Stage: FeatureStageGeneralAvailability, - Expression: "true", // enabled by default - Owner: awsDatasourcesSquad, + Name: "redshiftAsyncQueryDataSupport", + Description: "Enable async query data support for Redshift", + Stage: FeatureStageGeneralAvailability, + Expression: "true", // enabled by default + Owner: awsDatasourcesSquad, + AllowSelfServe: falsePtr, }, { - Name: "athenaAsyncQueryDataSupport", - Description: "Enable async query data support for Athena", - Stage: FeatureStageGeneralAvailability, - Expression: "true", // enabled by default - FrontendOnly: true, - Owner: awsDatasourcesSquad, + Name: "athenaAsyncQueryDataSupport", + Description: "Enable async query data support for Athena", + Stage: FeatureStageGeneralAvailability, + Expression: "true", // enabled by default + FrontendOnly: true, + Owner: awsDatasourcesSquad, + AllowSelfServe: falsePtr, }, { Name: "cloudwatchNewRegionsHandler", @@ -237,32 +247,36 @@ var ( Owner: grafanaBackendPlatformSquad, }, { - Name: "nestedFolderPicker", - Description: "Enables the new folder picker to work with nested folders. Requires the nestedFolders feature toggle", - Stage: FeatureStageGeneralAvailability, - Owner: grafanaFrontendPlatformSquad, - FrontendOnly: true, - Expression: "true", // enabled by default + Name: "nestedFolderPicker", + Description: "Enables the new folder picker to work with nested folders. Requires the nestedFolders feature toggle", + Stage: FeatureStageGeneralAvailability, + Owner: grafanaFrontendPlatformSquad, + FrontendOnly: true, + Expression: "true", // enabled by default + AllowSelfServe: falsePtr, }, { - Name: "accessTokenExpirationCheck", - Description: "Enable OAuth access_token expiration check and token refresh using the refresh_token", - Stage: FeatureStageGeneralAvailability, - Owner: identityAccessTeam, + Name: "accessTokenExpirationCheck", + Description: "Enable OAuth access_token expiration check and token refresh using the refresh_token", + Stage: FeatureStageGeneralAvailability, + Owner: identityAccessTeam, + AllowSelfServe: falsePtr, }, { - Name: "emptyDashboardPage", - Description: "Enable the redesigned user interface of a dashboard page that includes no panels", - Stage: FeatureStageGeneralAvailability, - FrontendOnly: true, - Expression: "true", // enabled by default - Owner: grafanaDashboardsSquad, + Name: "emptyDashboardPage", + Description: "Enable the redesigned user interface of a dashboard page that includes no panels", + Stage: FeatureStageGeneralAvailability, + FrontendOnly: true, + Expression: "true", // enabled by default + Owner: grafanaDashboardsSquad, + AllowSelfServe: falsePtr, }, { - Name: "disablePrometheusExemplarSampling", - Description: "Disable Prometheus exemplar sampling", - Stage: FeatureStageGeneralAvailability, - Owner: grafanaObservabilityMetricsSquad, + Name: "disablePrometheusExemplarSampling", + Description: "Disable Prometheus exemplar sampling", + Stage: FeatureStageGeneralAvailability, + Owner: grafanaObservabilityMetricsSquad, + AllowSelfServe: falsePtr, }, { Name: "alertingBacktesting", @@ -285,20 +299,22 @@ var ( Owner: grafanaAlertingSquad, }, { - Name: "logsContextDatasourceUi", - Description: "Allow datasource to provide custom UI for context view", - Stage: FeatureStageGeneralAvailability, - FrontendOnly: true, - Owner: grafanaObservabilityLogsSquad, - Expression: "true", // turned on by default + Name: "logsContextDatasourceUi", + Description: "Allow datasource to provide custom UI for context view", + Stage: FeatureStageGeneralAvailability, + FrontendOnly: true, + Owner: grafanaObservabilityLogsSquad, + Expression: "true", // turned on by default + AllowSelfServe: falsePtr, }, { - Name: "lokiQuerySplitting", - Description: "Split large interval queries into subqueries with smaller time intervals", - Stage: FeatureStageGeneralAvailability, - FrontendOnly: true, - Owner: grafanaObservabilityLogsSquad, - Expression: "true", // turned on by default + Name: "lokiQuerySplitting", + Description: "Split large interval queries into subqueries with smaller time intervals", + Stage: FeatureStageGeneralAvailability, + FrontendOnly: true, + Owner: grafanaObservabilityLogsSquad, + Expression: "true", // turned on by default + AllowSelfServe: falsePtr, }, { Name: "lokiQuerySplittingConfig", @@ -314,26 +330,29 @@ var ( Owner: grafanaBackendPlatformSquad, }, { - Name: "gcomOnlyExternalOrgRoleSync", - Description: "Prohibits a user from changing organization roles synced with Grafana Cloud auth provider", - Stage: FeatureStageGeneralAvailability, - Owner: identityAccessTeam, + Name: "gcomOnlyExternalOrgRoleSync", + Description: "Prohibits a user from changing organization roles synced with Grafana Cloud auth provider", + Stage: FeatureStageGeneralAvailability, + Owner: identityAccessTeam, + AllowSelfServe: falsePtr, }, { - Name: "prometheusMetricEncyclopedia", - Description: "Adds the metrics explorer component to the Prometheus query builder as an option in metric select", - Expression: "true", - Stage: FeatureStageGeneralAvailability, - FrontendOnly: true, - Owner: grafanaObservabilityMetricsSquad, + Name: "prometheusMetricEncyclopedia", + Description: "Adds the metrics explorer component to the Prometheus query builder as an option in metric select", + Expression: "true", + Stage: FeatureStageGeneralAvailability, + FrontendOnly: true, + Owner: grafanaObservabilityMetricsSquad, + AllowSelfServe: falsePtr, }, { - Name: "influxdbBackendMigration", - Description: "Query InfluxDB InfluxQL without the proxy", - Stage: FeatureStageGeneralAvailability, - FrontendOnly: true, - Owner: grafanaObservabilityMetricsSquad, - Expression: "true", // enabled by default + Name: "influxdbBackendMigration", + Description: "Query InfluxDB InfluxQL without the proxy", + Stage: FeatureStageGeneralAvailability, + FrontendOnly: true, + Owner: grafanaObservabilityMetricsSquad, + Expression: "true", // enabled by default + AllowSelfServe: falsePtr, }, { Name: "clientTokenRotation", @@ -342,18 +361,20 @@ var ( Owner: identityAccessTeam, }, { - Name: "prometheusDataplane", - Description: "Changes responses to from Prometheus to be compliant with the dataplane specification. In particular, when this feature toggle is active, the numeric `Field.Name` is set from 'Value' to the value of the `__name__` label.", - Expression: "true", - Stage: FeatureStageGeneralAvailability, - Owner: grafanaObservabilityMetricsSquad, + Name: "prometheusDataplane", + Description: "Changes responses to from Prometheus to be compliant with the dataplane specification. In particular, when this feature toggle is active, the numeric `Field.Name` is set from 'Value' to the value of the `__name__` label.", + Expression: "true", + Stage: FeatureStageGeneralAvailability, + Owner: grafanaObservabilityMetricsSquad, + AllowSelfServe: falsePtr, }, { - Name: "lokiMetricDataplane", - Description: "Changes metric responses from Loki to be compliant with the dataplane specification.", - Stage: FeatureStageGeneralAvailability, - Expression: "true", - Owner: grafanaObservabilityLogsSquad, + Name: "lokiMetricDataplane", + Description: "Changes metric responses from Loki to be compliant with the dataplane specification.", + Stage: FeatureStageGeneralAvailability, + Expression: "true", + Owner: grafanaObservabilityLogsSquad, + AllowSelfServe: falsePtr, }, { Name: "lokiLogsDataplane", @@ -362,12 +383,13 @@ var ( Owner: grafanaObservabilityLogsSquad, }, { - Name: "dataplaneFrontendFallback", - Description: "Support dataplane contract field name change for transformations and field name matchers where the name is different", - Stage: FeatureStageGeneralAvailability, - FrontendOnly: true, - Expression: "true", - Owner: grafanaObservabilityMetricsSquad, + Name: "dataplaneFrontendFallback", + Description: "Support dataplane contract field name change for transformations and field name matchers where the name is different", + Stage: FeatureStageGeneralAvailability, + FrontendOnly: true, + Expression: "true", + Owner: grafanaObservabilityMetricsSquad, + AllowSelfServe: falsePtr, }, { Name: "disableSSEDataplane", @@ -382,12 +404,13 @@ var ( Owner: grafanaAlertingSquad, }, { - Name: "alertingNotificationsPoliciesMatchingInstances", - Description: "Enables the preview of matching instances for notification policies", - Stage: FeatureStageGeneralAvailability, - FrontendOnly: true, - Expression: "true", // enabled by default - Owner: grafanaAlertingSquad, + Name: "alertingNotificationsPoliciesMatchingInstances", + Description: "Enables the preview of matching instances for notification policies", + Stage: FeatureStageGeneralAvailability, + FrontendOnly: true, + Expression: "true", // enabled by default + Owner: grafanaAlertingSquad, + AllowSelfServe: falsePtr, }, { Name: "alertStateHistoryLokiPrimary", @@ -433,21 +456,24 @@ var ( Owner: grafanaOperatorExperienceSquad, RequiresRestart: true, Expression: "true", // enabled by default + AllowSelfServe: falsePtr, }, { - Name: "enableElasticsearchBackendQuerying", - Description: "Enable the processing of queries and responses in the Elasticsearch data source through backend", - Stage: FeatureStageGeneralAvailability, - Owner: grafanaObservabilityLogsSquad, - Expression: "true", // enabled by default + Name: "enableElasticsearchBackendQuerying", + Description: "Enable the processing of queries and responses in the Elasticsearch data source through backend", + Stage: FeatureStageGeneralAvailability, + Owner: grafanaObservabilityLogsSquad, + Expression: "true", // enabled by default + AllowSelfServe: falsePtr, }, { - Name: "advancedDataSourcePicker", - Description: "Enable a new data source picker with contextual information, recently used order and advanced mode", - Stage: FeatureStageGeneralAvailability, - FrontendOnly: true, - Expression: "true", // enabled by default - Owner: grafanaDashboardsSquad, + Name: "advancedDataSourcePicker", + Description: "Enable a new data source picker with contextual information, recently used order and advanced mode", + Stage: FeatureStageGeneralAvailability, + FrontendOnly: true, + Expression: "true", // enabled by default + Owner: grafanaDashboardsSquad, + AllowSelfServe: falsePtr, }, { Name: "faroDatasourceSelector", @@ -520,12 +546,13 @@ var ( Owner: grafanaObservabilityLogsSquad, }, { - Name: "cloudWatchLogsMonacoEditor", - Description: "Enables the Monaco editor for CloudWatch Logs queries", - Stage: FeatureStageGeneralAvailability, - FrontendOnly: true, - Expression: "true", // enabled by default - Owner: awsDatasourcesSquad, + Name: "cloudWatchLogsMonacoEditor", + Description: "Enables the Monaco editor for CloudWatch Logs queries", + Stage: FeatureStageGeneralAvailability, + FrontendOnly: true, + Expression: "true", // enabled by default + Owner: awsDatasourcesSquad, + AllowSelfServe: falsePtr, }, { Name: "exploreScrollableLogsContainer", @@ -535,11 +562,12 @@ var ( Owner: grafanaObservabilityLogsSquad, }, { - Name: "recordedQueriesMulti", - Description: "Enables writing multiple items from a single query within Recorded Queries", - Stage: FeatureStageGeneralAvailability, - Expression: "true", - Owner: grafanaObservabilityMetricsSquad, + Name: "recordedQueriesMulti", + Description: "Enables writing multiple items from a single query within Recorded Queries", + Stage: FeatureStageGeneralAvailability, + Expression: "true", + Owner: grafanaObservabilityMetricsSquad, + AllowSelfServe: falsePtr, }, { Name: "pluginsDynamicAngularDetectionPatterns", @@ -576,12 +604,13 @@ var ( Owner: awsDatasourcesSquad, }, { - Name: "transformationsRedesign", - Description: "Enables the transformations redesign", - Stage: FeatureStageGeneralAvailability, - FrontendOnly: true, - Expression: "true", // enabled by default - Owner: grafanaObservabilityMetricsSquad, + Name: "transformationsRedesign", + Description: "Enables the transformations redesign", + Stage: FeatureStageGeneralAvailability, + FrontendOnly: true, + Expression: "true", // enabled by default + Owner: grafanaObservabilityMetricsSquad, + AllowSelfServe: falsePtr, }, { Name: "mlExpressions", @@ -641,11 +670,12 @@ var ( RequiresRestart: true, }, { - Name: "azureMonitorDataplane", - Description: "Adds dataplane compliant frame metadata in the Azure Monitor datasource", - Stage: FeatureStageGeneralAvailability, - Owner: grafanaPartnerPluginsSquad, - Expression: "true", // on by default + Name: "azureMonitorDataplane", + Description: "Adds dataplane compliant frame metadata in the Azure Monitor datasource", + Stage: FeatureStageGeneralAvailability, + Owner: grafanaPartnerPluginsSquad, + Expression: "true", // on by default + AllowSelfServe: falsePtr, }, { Name: "traceToProfiles", @@ -661,11 +691,12 @@ var ( Owner: grafanaBackendPlatformSquad, }, { - Name: "prometheusConfigOverhaulAuth", - Description: "Update the Prometheus configuration page with the new auth component", - Owner: grafanaObservabilityMetricsSquad, - Stage: FeatureStageGeneralAvailability, - Expression: "true", // on by default + Name: "prometheusConfigOverhaulAuth", + Description: "Update the Prometheus configuration page with the new auth component", + Owner: grafanaObservabilityMetricsSquad, + Stage: FeatureStageGeneralAvailability, + Expression: "true", // on by default + AllowSelfServe: falsePtr, }, { Name: "configurableSchedulerTick", @@ -701,12 +732,13 @@ var ( Owner: grafanaPluginsPlatformSquad, }, { - Name: "dashgpt", - Description: "Enable AI powered features in dashboards", - Stage: FeatureStageGeneralAvailability, - FrontendOnly: true, - Owner: grafanaDashboardsSquad, - Expression: "true", // on by default + Name: "dashgpt", + Description: "Enable AI powered features in dashboards", + Stage: FeatureStageGeneralAvailability, + FrontendOnly: true, + Owner: grafanaDashboardsSquad, + Expression: "true", // on by default + AllowSelfServe: falsePtr, }, { Name: "reportingRetries", @@ -717,12 +749,13 @@ var ( RequiresRestart: true, }, { - Name: "newBrowseDashboards", - Description: "New browse/manage dashboards UI", - Stage: FeatureStageGeneralAvailability, - Owner: grafanaFrontendPlatformSquad, - FrontendOnly: true, - Expression: "true", // on by default + Name: "newBrowseDashboards", + Description: "New browse/manage dashboards UI", + Stage: FeatureStageGeneralAvailability, + Owner: grafanaFrontendPlatformSquad, + FrontendOnly: true, + Expression: "true", // on by default + AllowSelfServe: falsePtr, }, { Name: "sseGroupByDatasource", @@ -760,12 +793,13 @@ var ( Owner: hostedGrafanaTeam, }, { - Name: "alertingInsights", - Description: "Show the new alerting insights landing page", - FrontendOnly: true, - Stage: FeatureStageGeneralAvailability, - Owner: grafanaAlertingSquad, - Expression: "true", // enabled by default + Name: "alertingInsights", + Description: "Show the new alerting insights landing page", + FrontendOnly: true, + Stage: FeatureStageGeneralAvailability, + Owner: grafanaAlertingSquad, + Expression: "true", // enabled by default + AllowSelfServe: falsePtr, }, { Name: "alertingContactPointsV2", @@ -803,11 +837,12 @@ var ( RequiresDevMode: true, }, { - Name: "cloudWatchWildCardDimensionValues", - Description: "Fetches dimension values from CloudWatch to correctly label wildcard dimensions", - Stage: FeatureStageGeneralAvailability, - Expression: "true", // enabled by default - Owner: awsDatasourcesSquad, + Name: "cloudWatchWildCardDimensionValues", + Description: "Fetches dimension values from CloudWatch to correctly label wildcard dimensions", + Stage: FeatureStageGeneralAvailability, + Expression: "true", // enabled by default + Owner: awsDatasourcesSquad, + AllowSelfServe: falsePtr, }, { Name: "externalServiceAccounts", @@ -991,3 +1026,7 @@ var ( }, } ) + +func boolPtr(b bool) *bool { + return &b +} diff --git a/pkg/services/featuremgmt/toggles_gen_test.go b/pkg/services/featuremgmt/toggles_gen_test.go index 5f7798195e66e..7d950b048bccf 100644 --- a/pkg/services/featuremgmt/toggles_gen_test.go +++ b/pkg/services/featuremgmt/toggles_gen_test.go @@ -45,6 +45,12 @@ func TestFeatureToggleFiles(t *testing.T) { if flag.Name != strings.TrimSpace(flag.Name) { t.Errorf("flag Name should not start/end with spaces. See: %s", flag.Name) } + if flag.Stage == FeatureStageGeneralAvailability && flag.AllowSelfServe == nil { + t.Errorf("feature stage FeatureStageGeneralAvailability should have the AllowSelfServe field defined") + } + if flag.AllowSelfServe != nil && flag.Stage != FeatureStageGeneralAvailability { + t.Errorf("only allow self-serving GA toggles") + } } }) From 0f4bb73fbcbb3d1dd018fb9eff3aed8ea0caf59d Mon Sep 17 00:00:00 2001 From: Alyssa Bull <58453566+alyssabull@users.noreply.github.com> Date: Fri, 3 Nov 2023 10:04:25 -0600 Subject: [PATCH 095/869] Azure Monitor: Select all Event Types by default (#77603) --- .../TracesQueryEditor/TraceTypeField.test.tsx | 3 +-- .../components/TracesQueryEditor/TraceTypeField.tsx | 13 ++++++++++++- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/public/app/plugins/datasource/azuremonitor/components/TracesQueryEditor/TraceTypeField.test.tsx b/public/app/plugins/datasource/azuremonitor/components/TracesQueryEditor/TraceTypeField.test.tsx index e5a7e076e5604..f590f6dc41302 100644 --- a/public/app/plugins/datasource/azuremonitor/components/TracesQueryEditor/TraceTypeField.test.tsx +++ b/public/app/plugins/datasource/azuremonitor/components/TracesQueryEditor/TraceTypeField.test.tsx @@ -24,11 +24,10 @@ describe('TraceTypeField', () => { ...props.query, azureTraces: { ...props.query.azureTraces, - traceTypes: [], }, }; + render(); - expect(screen.getByText('Choose event types')).toBeInTheDocument(); const menu = screen.getByLabelText(selectors.components.queryEditor.tracesQueryEditor.traceTypes.select); openMenu(menu); diff --git a/public/app/plugins/datasource/azuremonitor/components/TracesQueryEditor/TraceTypeField.tsx b/public/app/plugins/datasource/azuremonitor/components/TracesQueryEditor/TraceTypeField.tsx index 9d5ca517013ec..bd4303b5d5710 100644 --- a/public/app/plugins/datasource/azuremonitor/components/TracesQueryEditor/TraceTypeField.tsx +++ b/public/app/plugins/datasource/azuremonitor/components/TracesQueryEditor/TraceTypeField.tsx @@ -30,12 +30,23 @@ const TraceTypeField = ({ query, variableOptionGroup, onQueryChange }: AzureQuer const options = useMemo(() => [...tables, variableOptionGroup], [tables, variableOptionGroup]); + // Select all trace event ypes by default + const getDefaultOptions = () => { + const allEventTypes = tables.map((t) => t.value); + const defaultQuery = setTraceTypes(query, allEventTypes); + onQueryChange(defaultQuery); + return allEventTypes; + }; + return ( Date: Fri, 3 Nov 2023 10:06:18 -0600 Subject: [PATCH 096/869] CloudMonitoring: Warn users that query will be lost on switch (#76836) --- .../components/QueryEditor.tsx | 50 ++++++++++++++++++- 1 file changed, 48 insertions(+), 2 deletions(-) diff --git a/public/app/plugins/datasource/cloud-monitoring/components/QueryEditor.tsx b/public/app/plugins/datasource/cloud-monitoring/components/QueryEditor.tsx index 5005b98d03ce2..2c41fa66d1094 100644 --- a/public/app/plugins/datasource/cloud-monitoring/components/QueryEditor.tsx +++ b/public/app/plugins/datasource/cloud-monitoring/components/QueryEditor.tsx @@ -1,12 +1,15 @@ +import { isEqual } from 'lodash'; import React, { useEffect, useMemo, useState } from 'react'; import { QueryEditorProps, toOption } from '@grafana/data'; import { EditorRows } from '@grafana/experimental'; +import { ConfirmModal } from '@grafana/ui'; import CloudMonitoringDatasource from '../datasource'; import { CloudMonitoringQuery, PromQLQuery, QueryType, SLOQuery } from '../types/query'; import { CloudMonitoringOptions } from '../types/types'; +import { defaultTimeSeriesList, defaultTimeSeriesQuery } from './MetricQueryEditor'; import { PromQLQueryEditor } from './PromQLEditor'; import { QueryHeader } from './QueryHeader'; import { defaultQuery as defaultSLOQuery } from './SLOQueryEditor'; @@ -17,6 +20,7 @@ export type Props = QueryEditorProps { const { datasource, query: oldQ, onRunQuery, onChange } = props; + const [modalIsOpen, setModalIsOpen] = useState(false); // Migrate query if needed const [migrated, setMigrated] = useState(false); const query = useMemo(() => { @@ -29,6 +33,8 @@ export const QueryEditor = (props: Props) => { } return oldQ; }, [oldQ, datasource, onChange, migrated]); + const [currentQuery, setCurrentQuery] = useState(query); + const [queryHasBeenEdited, setQueryHasBeenEdited] = useState(false); const sloQuery = { ...defaultSLOQuery(datasource), ...query.sloQuery }; const onSLOQueryChange = (q: SLOQuery) => { @@ -44,6 +50,16 @@ export const QueryEditor = (props: Props) => { onChange({ ...query, promQLQuery: q }); }; + const onMetricQueryChange = (q: CloudMonitoringQuery) => { + if ( + (q.queryType === QueryType.TIME_SERIES_LIST && !isEqual(q.timeSeriesList, defaultTimeSeriesList(datasource))) || + (q.queryType === QueryType.TIME_SERIES_QUERY && !isEqual(q.timeSeriesQuery, defaultTimeSeriesQuery(datasource))) + ) { + setQueryHasBeenEdited(true); + } + onChange(q); + }; + const meta = props.data?.series.length ? props.data?.series[0].meta : {}; const customMetaData = meta?.custom ?? {}; const variableOptionGroup = { @@ -60,9 +76,39 @@ export const QueryEditor = (props: Props) => { }); const queryType = query.queryType; + const checkForModalDisplay = (q: CloudMonitoringQuery) => { + if ( + queryHasBeenEdited && + (currentQuery.queryType === QueryType.TIME_SERIES_LIST || currentQuery.queryType === QueryType.TIME_SERIES_QUERY) + ) { + if (currentQuery.queryType !== q.queryType) { + setModalIsOpen(true); + } + } else { + onChange(q); + } + setCurrentQuery(q); + }; + return ( - + { + setModalIsOpen(false); + onChange(currentQuery); + setQueryHasBeenEdited(false); + }} + confirmText="Confirm" + onDismiss={() => { + setModalIsOpen(false); + setCurrentQuery(query); + }} + /> + {queryType === QueryType.PROMQL && ( { refId={query.refId} variableOptionGroup={variableOptionGroup} customMetaData={customMetaData} - onChange={onChange} + onChange={onMetricQueryChange} onRunQuery={onRunQuery} datasource={datasource} query={query} From 224279fe0e5482c92799c8e3e058296e56bd0fee Mon Sep 17 00:00:00 2001 From: Kim Nylander <104772500+knylander-grafana@users.noreply.github.com> Date: Fri, 3 Nov 2023 12:17:15 -0400 Subject: [PATCH 097/869] [DOC] Add videos for Tempo data source (#77534) * Add videos for Tempo data source * Update docs/sources/datasources/tempo/span-filters.md * Update docs/sources/datasources/tempo/span-filters.md --- .../tempo/configure-tempo-data-source.md | 2 ++ docs/sources/explore/trace-integration.md | 12 ++++++----- .../datasources/tempo-editor-traceql.md | 20 +++++++++++++++++-- .../datasources/tempo-search-traceql.md | 18 +++++++++++++++-- 4 files changed, 43 insertions(+), 9 deletions(-) diff --git a/docs/sources/datasources/tempo/configure-tempo-data-source.md b/docs/sources/datasources/tempo/configure-tempo-data-source.md index 8cdc4264cb47b..3e57ab017dc21 100644 --- a/docs/sources/datasources/tempo/configure-tempo-data-source.md +++ b/docs/sources/datasources/tempo/configure-tempo-data-source.md @@ -113,6 +113,8 @@ If you use Grafana Cloud, open a [support ticket in the Cloud Portal](/profile/o The **Trace to metrics** setting configures the [trace to metrics feature](/blog/2022/08/18/new-in-grafana-9.1-trace-to-metrics-allows-users-to-navigate-from-a-trace-span-to-a-selected-data-source/) available when integrating Grafana with Tempo. +{{< youtube id="TkapvLeMMpc" >}} + To configure trace to metrics: 1. Select the target data source from the drop-down list. diff --git a/docs/sources/explore/trace-integration.md b/docs/sources/explore/trace-integration.md index 5b2d19b35fe38..855073d57fa66 100644 --- a/docs/sources/explore/trace-integration.md +++ b/docs/sources/explore/trace-integration.md @@ -39,7 +39,7 @@ For information on querying each data source, refer to their documentation: - [Zipkin query editor]({{< relref "../datasources/zipkin/#query-the-data-source" >}}) - [Azure Monitor Application Insights query editor]({{< relref "../datasources/azure-monitor/query-editor/#query-application-insights-traces" >}}) -## Trace View +## Trace view This section explains the elements of the Trace View. @@ -59,7 +59,7 @@ This section explains the elements of the Trace View. Shows condensed view or the trace timeline. Drag your mouse over the minimap to zoom into smaller time range. Zooming will also update the main timeline, so it is easy to see shorter spans. Hovering over the minimap, when zoomed, will show Reset Selection button which resets the zoom. -### Span Filters +### Span filters ![Screenshot of span filtering](/media/docs/tempo/screenshot-grafana-tempo-span-filters-v10-1.png) @@ -67,13 +67,15 @@ Using span filters, you can filter your spans in the trace timeline viewer. The You can add one or more of the following filters: -- Service name +- Resource service name - Span name - Duration - Tags (which include tags, process tags, and log fields) To only show the spans you have matched, you can press the `Show matches only` toggle. +{{< youtube id="VP2XV3IIc80" >}} + ### Timeline {{< figure src="/media/docs/tempo/screenshot-grafana-trace-view-timeline.png" class="docs-image--no-shadow" max-width= "900px" caption="Screenshot of the trace view timeline" >}} @@ -112,12 +114,12 @@ Click the document icon to open a split view in Explore with the configured data ### Trace to metrics {{% admonition type="note" %}} -This feature is currently in beta & behind the `traceToMetrics` feature toggle. +This feature is currently in beta and behind the `traceToMetrics` feature toggle. {{% /admonition %}} You can navigate from a span in a trace view directly to metrics relevant for that span. This feature is available for Tempo, Jaeger, and Zipkin data sources. Refer to their [relevant documentation](/docs/grafana/latest/datasources/tempo/#trace-to-metrics) for configuration instructions. -## Node Graph +## Node graph You can optionally expand the node graph for the displayed trace. Depending on the data source, this can show spans of the trace as nodes in the graph, or as some additional context like service graph based on the current trace. diff --git a/docs/sources/shared/datasources/tempo-editor-traceql.md b/docs/sources/shared/datasources/tempo-editor-traceql.md index d41fe3f770fba..24209ed6d6e44 100644 --- a/docs/sources/shared/datasources/tempo-editor-traceql.md +++ b/docs/sources/shared/datasources/tempo-editor-traceql.md @@ -46,6 +46,10 @@ To access the query editor, follow these steps: 1. Optional: Use the Time picker drop-down to change the time and range for the query (refer to the [documentation for instructions](https://grafana.com/docs/grafana/latest/dashboards/use-dashboards/#set-dashboard-time-range)). 1. Once you have finished your query, select **Run query**. +This video provides and example of creating a TraceQL query using the custom tag grouping. + +{{< youtube id="fraepWra00Y" >}} + ## Query by TraceID To query a particular trace by its trace ID: @@ -60,7 +64,7 @@ To query a particular trace by its trace ID: You can use the query editor’s autocomplete suggestions to write queries. The editor detects span sets to provide relevant autocomplete options. -It uses regular expressions (regex) to detect where it is inside a spanset and provide attribute names, scopes, intrinsic names, logic operators, or attribute values from Tempo's API, depending on what is expected for the current situation. +It uses regular expressions (regex) to detect where it's inside a spanset and provide attribute names, scopes, intrinsic names, logic operators, or attribute values from Tempo's API, depending on what's expected for the current situation. ![Query editor showing the auto-complete feature](/static/img/docs/tempo/screenshot-traceql-query-editor-auto-complete-v10.png) @@ -80,4 +84,16 @@ Query results for both the editor and the builder are returned in a table. Selec ![Query editor showing span results](/static/img/docs/tempo/screenshot-traceql-query-editor-results-v10.png) -Selecting the trace ID from the returned results will open a trace diagram. Selecting a span from the returned results opens a trace diagram and reveals the relevant span in the trace diagram (above, the highlighted blue line). +Selecting the trace ID from the returned results opens a trace diagram. Selecting a span from the returned results opens a trace diagram and reveals the relevant span in the trace diagram (above, the highlighted blue line). + +### Streaming results + +The Tempo data source supports streaming responses to TraceQL queries so you can see partial query results as they come in without waiting for the whole query to finish. + +{{% admonition type="note" %}} +To use this experimental feature, enable the `traceQLStreaming` feature toggle. If you’re using Grafana Cloud and would like to enable this feature, please contact customer support. +{{% /admonition %}} + +Streaming is available for both the **Search** and **TraceQL** query types, and you'll get immediate visibility of incoming traces on the results table. + +{{< video-embed src="/media/docs/grafana/data-sources/tempo-streaming-v2.mp4" >}} diff --git a/docs/sources/shared/datasources/tempo-search-traceql.md b/docs/sources/shared/datasources/tempo-search-traceql.md index 2547e15b42d10..15ce534aaa9ac 100644 --- a/docs/sources/shared/datasources/tempo-search-traceql.md +++ b/docs/sources/shared/datasources/tempo-search-traceql.md @@ -59,11 +59,11 @@ To access Search, use the following steps: 1. Sign into Grafana. 1. Select your Tempo data source. -1. From the menu, choose Explore and select Query type > Search. +1. From the menu, choose **Explore** and select **Query type > Search**. ## Define filters -Using filters, you refine the data returned from the query by selecting Resource Service Name, Span Name, or Duration. The **Duration** represents span time, calculated by subtracting the end time from the start time of the span. +Using filters, you refine the data returned from the query by selecting **Resource Service Name**, **Span Name**, or **Duration**. The **Duration** represents span time, calculated by subtracting the end time from the start time of the span. Grafana administrators can change the default filters using the Tempo data source configuration. Filters can be limited by the operators. The available operators are determined by the field type. @@ -110,6 +110,8 @@ Using **Aggregate by**, you can calculate RED metrics (total span count, percent This capability is based on the [metrics summary API](/docs/grafana-cloud/monitor-infrastructure/traces/metrics-summary-api/). Metrics summary only calculates summaries based on spans received within the last hour. +{{< youtube id="g97CjKOZqT4" >}} + When you use **Aggregate by**, the selections you make determine how the information is reported in the Table. Every combination that matches selections in your data is listed in the table. Each aggregate value, for example `intrinsic`:`name`, has a corresponding column in the results table. @@ -146,3 +148,15 @@ Select **Run query** to run the TraceQL query (1 in the screenshot). Queries can take a little while to return results. The results appear in a table underneath the query builder. Selecting a Trace ID (2 in the screenshot) displays more detailed information (3 in the screenshot). {{< figure src="/static/img/docs/queries/screenshot-tempods-query-results.png" class="docs-image--no-shadow" max-width="750px" caption="Tempo Search query type results" >}} + +#### Streaming results + +The Tempo data source supports streaming responses to TraceQL queries so you can see partial query results as they come in without waiting for the whole query to finish. + +{{% admonition type="note" %}} +To use this experimental feature, enable the `traceQLStreaming` feature toggle. If you’re using Grafana Cloud and would like to enable this feature, please contact customer support. +{{% /admonition %}} + +Streaming is available for both the **Search** and **TraceQL** query types, and you'll get immediate visibility of incoming traces on the results table. + +{{< video-embed src="/media/docs/grafana/data-sources/tempo-streaming-v2.mp4" >}} From 549787d4f91dd07393e0daa27d967a3423baa0b8 Mon Sep 17 00:00:00 2001 From: Ryan McKinley Date: Fri, 3 Nov 2023 09:25:29 -0700 Subject: [PATCH 098/869] Playlist: Implement the entire API with k8s client (#77596) --- pkg/api/playlist.go | 284 +++++++++++++--------- pkg/registry/apis/playlist/conversions.go | 21 ++ pkg/tests/apis/helper.go | 48 ++-- pkg/tests/apis/playlist/playlist_test.go | 41 +++- 4 files changed, 254 insertions(+), 140 deletions(-) diff --git a/pkg/api/playlist.go b/pkg/api/playlist.go index 3f1b95dc29fc6..8ec920ad1750e 100644 --- a/pkg/api/playlist.go +++ b/pkg/api/playlist.go @@ -16,127 +16,34 @@ import ( internalplaylist "github.com/grafana/grafana/pkg/registry/apis/playlist" contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/featuremgmt" + grafanaapiserver "github.com/grafana/grafana/pkg/services/grafana-apiserver" "github.com/grafana/grafana/pkg/services/grafana-apiserver/endpoints/request" "github.com/grafana/grafana/pkg/services/playlist" "github.com/grafana/grafana/pkg/util/errutil/errhttp" "github.com/grafana/grafana/pkg/web" ) -type playlistAPIHandler struct { - SearchPlaylists []web.Handler - GetPlaylist []web.Handler - GetPlaylistItems []web.Handler - DeletePlaylist []web.Handler - UpdatePlaylist []web.Handler - CreatePlaylist []web.Handler -} - -func chainHandlers(h ...web.Handler) []web.Handler { - return h -} - func (hs *HTTPServer) registerPlaylistAPI(apiRoute routing.RouteRegister) { - handler := playlistAPIHandler{ - SearchPlaylists: chainHandlers(routing.Wrap(hs.SearchPlaylists)), - GetPlaylist: chainHandlers(hs.validateOrgPlaylist, routing.Wrap(hs.GetPlaylist)), - GetPlaylistItems: chainHandlers(hs.validateOrgPlaylist, routing.Wrap(hs.GetPlaylistItems)), - DeletePlaylist: chainHandlers(middleware.ReqEditorRole, hs.validateOrgPlaylist, routing.Wrap(hs.DeletePlaylist)), - UpdatePlaylist: chainHandlers(middleware.ReqEditorRole, hs.validateOrgPlaylist, routing.Wrap(hs.UpdatePlaylist)), - CreatePlaylist: chainHandlers(middleware.ReqEditorRole, routing.Wrap(hs.CreatePlaylist)), - } - - // Alternative implementations for k8s - if hs.Features.IsEnabled(featuremgmt.FlagKubernetesPlaylistsAPI) { - namespacer := request.GetNamespaceMapper(hs.Cfg) - gvr := schema.GroupVersionResource{ - Group: internalplaylist.GroupName, - Version: internalplaylist.VersionID, - Resource: "playlists", - } - - clientGetter := func(c *contextmodel.ReqContext) (dynamic.ResourceInterface, bool) { - dyn, err := dynamic.NewForConfig(hs.clientConfigProvider.GetDirectRestConfig(c)) - if err != nil { - c.JsonApiErr(500, "client", err) - return nil, false - } - return dyn.Resource(gvr).Namespace(namespacer(c.OrgID)), true - } - - errorWriter := func(c *contextmodel.ReqContext, err error) { - //nolint:errorlint - statusError, ok := err.(*errors.StatusError) - if ok { - c.JsonApiErr(int(statusError.Status().Code), - statusError.Status().Message, err) - return - } - errhttp.Write(c.Req.Context(), err, c.Resp) - } - - handler.SearchPlaylists = []web.Handler{func(c *contextmodel.ReqContext) { - client, ok := clientGetter(c) - if !ok { - return // error is already sent - } - out, err := client.List(c.Req.Context(), v1.ListOptions{}) - if err != nil { - errorWriter(c, err) - return - } - - query := strings.ToUpper(c.Query("query")) - playlists := []playlist.Playlist{} - for _, item := range out.Items { - p := internalplaylist.UnstructuredToLegacyPlaylist(item) - if p == nil { - continue - } - if query != "" && !strings.Contains(strings.ToUpper(p.Name), query) { - continue // query filter - } - playlists = append(playlists, *p) - } - c.JSON(http.StatusOK, playlists) - }} - - handler.GetPlaylist = []web.Handler{func(c *contextmodel.ReqContext) { - client, ok := clientGetter(c) - if !ok { - return // error is already sent - } - uid := web.Params(c.Req)[":uid"] - out, err := client.Get(c.Req.Context(), uid, v1.GetOptions{}) - if err != nil { - errorWriter(c, err) - return - } - c.JSON(http.StatusOK, internalplaylist.UnstructuredToLegacyPlaylistDTO(*out)) - }} - - handler.GetPlaylistItems = []web.Handler{func(c *contextmodel.ReqContext) { - client, ok := clientGetter(c) - if !ok { - return // error is already sent - } - uid := web.Params(c.Req)[":uid"] - out, err := client.Get(c.Req.Context(), uid, v1.GetOptions{}) - if err != nil { - errorWriter(c, err) - return - } - c.JSON(http.StatusOK, internalplaylist.UnstructuredToLegacyPlaylistDTO(*out).Items) - }} - } - // Register the actual handlers apiRoute.Group("/playlists", func(playlistRoute routing.RouteRegister) { - playlistRoute.Get("/", handler.SearchPlaylists...) - playlistRoute.Get("/:uid", handler.GetPlaylist...) - playlistRoute.Get("/:uid/items", handler.GetPlaylistItems...) - playlistRoute.Delete("/:uid", handler.DeletePlaylist...) - playlistRoute.Put("/:uid", handler.UpdatePlaylist...) - playlistRoute.Post("/", handler.CreatePlaylist...) + if hs.Features.IsEnabled(featuremgmt.FlagKubernetesPlaylistsAPI) { + // Use k8s client to implement legacy API + handler := newPlaylistK8sHandler(hs) + playlistRoute.Get("/", handler.searchPlaylists) + playlistRoute.Get("/:uid", handler.getPlaylist) + playlistRoute.Get("/:uid/items", handler.getPlaylistItems) + playlistRoute.Delete("/:uid", handler.deletePlaylist) + playlistRoute.Put("/:uid", handler.updatePlaylist) + playlistRoute.Post("/", handler.createPlaylist) + } else { + // Legacy handlers + playlistRoute.Get("/", routing.Wrap(hs.SearchPlaylists)) + playlistRoute.Get("/:uid", hs.validateOrgPlaylist, routing.Wrap(hs.GetPlaylist)) + playlistRoute.Get("/:uid/items", hs.validateOrgPlaylist, routing.Wrap(hs.GetPlaylistItems)) + playlistRoute.Delete("/:uid", middleware.ReqEditorRole, hs.validateOrgPlaylist, routing.Wrap(hs.DeletePlaylist)) + playlistRoute.Put("/:uid", middleware.ReqEditorRole, hs.validateOrgPlaylist, routing.Wrap(hs.UpdatePlaylist)) + playlistRoute.Post("/", middleware.ReqEditorRole, routing.Wrap(hs.CreatePlaylist)) + } }) } @@ -409,3 +316,156 @@ type CreatePlaylistResponse struct { // in: body Body *playlist.Playlist `json:"body"` } + +type playlistK8sHandler struct { + namespacer request.NamespaceMapper + gvr schema.GroupVersionResource + clientConfigProvider grafanaapiserver.DirectRestConfigProvider +} + +//----------------------------------------------------------------------------------------- +// Playlist k8s wrapper functions +//----------------------------------------------------------------------------------------- + +func newPlaylistK8sHandler(hs *HTTPServer) *playlistK8sHandler { + return &playlistK8sHandler{ + gvr: schema.GroupVersionResource{ + Group: internalplaylist.GroupName, + Version: "v0alpha1", + Resource: "playlists", + }, + namespacer: request.GetNamespaceMapper(hs.Cfg), + clientConfigProvider: hs.clientConfigProvider, + } +} + +func (pk8s *playlistK8sHandler) searchPlaylists(c *contextmodel.ReqContext) { + client, ok := pk8s.getClient(c) + if !ok { + return // error is already sent + } + out, err := client.List(c.Req.Context(), v1.ListOptions{}) + if err != nil { + pk8s.writeError(c, err) + return + } + + query := strings.ToUpper(c.Query("query")) + playlists := []playlist.Playlist{} + for _, item := range out.Items { + p := internalplaylist.UnstructuredToLegacyPlaylist(item) + if p == nil { + continue + } + if query != "" && !strings.Contains(strings.ToUpper(p.Name), query) { + continue // query filter + } + playlists = append(playlists, *p) + } + c.JSON(http.StatusOK, playlists) +} + +func (pk8s *playlistK8sHandler) getPlaylist(c *contextmodel.ReqContext) { + client, ok := pk8s.getClient(c) + if !ok { + return // error is already sent + } + uid := web.Params(c.Req)[":uid"] + out, err := client.Get(c.Req.Context(), uid, v1.GetOptions{}) + if err != nil { + pk8s.writeError(c, err) + return + } + c.JSON(http.StatusOK, internalplaylist.UnstructuredToLegacyPlaylistDTO(*out)) +} + +func (pk8s *playlistK8sHandler) getPlaylistItems(c *contextmodel.ReqContext) { + client, ok := pk8s.getClient(c) + if !ok { + return // error is already sent + } + uid := web.Params(c.Req)[":uid"] + out, err := client.Get(c.Req.Context(), uid, v1.GetOptions{}) + if err != nil { + pk8s.writeError(c, err) + return + } + c.JSON(http.StatusOK, internalplaylist.UnstructuredToLegacyPlaylistDTO(*out).Items) +} + +func (pk8s *playlistK8sHandler) deletePlaylist(c *contextmodel.ReqContext) { + client, ok := pk8s.getClient(c) + if !ok { + return // error is already sent + } + uid := web.Params(c.Req)[":uid"] + err := client.Delete(c.Req.Context(), uid, v1.DeleteOptions{}) + if err != nil { + pk8s.writeError(c, err) + return + } + c.JSON(http.StatusOK, "") +} + +func (pk8s *playlistK8sHandler) updatePlaylist(c *contextmodel.ReqContext) { + client, ok := pk8s.getClient(c) + if !ok { + return // error is already sent + } + uid := web.Params(c.Req)[":uid"] + cmd := playlist.UpdatePlaylistCommand{} + if err := web.Bind(c.Req, &cmd); err != nil { + c.JsonApiErr(http.StatusBadRequest, "bad request data", err) + return + } + obj := internalplaylist.LegacyUpdateCommandToUnstructured(cmd) + obj.SetName(uid) + out, err := client.Update(c.Req.Context(), &obj, v1.UpdateOptions{}) + if err != nil { + pk8s.writeError(c, err) + return + } + c.JSON(http.StatusOK, internalplaylist.UnstructuredToLegacyPlaylistDTO(*out)) +} + +func (pk8s *playlistK8sHandler) createPlaylist(c *contextmodel.ReqContext) { + client, ok := pk8s.getClient(c) + if !ok { + return // error is already sent + } + cmd := playlist.UpdatePlaylistCommand{} + if err := web.Bind(c.Req, &cmd); err != nil { + c.JsonApiErr(http.StatusBadRequest, "bad request data", err) + return + } + obj := internalplaylist.LegacyUpdateCommandToUnstructured(cmd) + out, err := client.Create(c.Req.Context(), &obj, v1.CreateOptions{}) + if err != nil { + pk8s.writeError(c, err) + return + } + c.JSON(http.StatusOK, internalplaylist.UnstructuredToLegacyPlaylistDTO(*out)) +} + +//----------------------------------------------------------------------------------------- +// Utility functions +//----------------------------------------------------------------------------------------- + +func (pk8s *playlistK8sHandler) getClient(c *contextmodel.ReqContext) (dynamic.ResourceInterface, bool) { + dyn, err := dynamic.NewForConfig(pk8s.clientConfigProvider.GetDirectRestConfig(c)) + if err != nil { + c.JsonApiErr(500, "client", err) + return nil, false + } + return dyn.Resource(pk8s.gvr).Namespace(pk8s.namespacer(c.OrgID)), true +} + +func (pk8s *playlistK8sHandler) writeError(c *contextmodel.ReqContext, err error) { + //nolint:errorlint + statusError, ok := err.(*errors.StatusError) + if ok { + c.JsonApiErr(int(statusError.Status().Code), statusError.Status().Message, err) + return + } + errhttp.Write(c.Req.Context(), err, c.Resp) +} diff --git a/pkg/registry/apis/playlist/conversions.go b/pkg/registry/apis/playlist/conversions.go index 60f785a8a605b..19c6131621b8b 100644 --- a/pkg/registry/apis/playlist/conversions.go +++ b/pkg/registry/apis/playlist/conversions.go @@ -16,6 +16,27 @@ import ( playlistsvc "github.com/grafana/grafana/pkg/services/playlist" ) +func LegacyUpdateCommandToUnstructured(cmd playlistsvc.UpdatePlaylistCommand) unstructured.Unstructured { + items := []map[string]string{} + for _, item := range cmd.Items { + items = append(items, map[string]string{ + "type": item.Type, + "value": item.Value, + }) + } + obj := unstructured.Unstructured{ + Object: map[string]interface{}{ + "spec": map[string]interface{}{ + "title": cmd.Name, + "interval": cmd.Interval, + "items": items, + }, + }, + } + obj.SetName(cmd.UID) + return obj +} + func UnstructuredToLegacyPlaylist(item unstructured.Unstructured) *playlistsvc.Playlist { spec := item.Object["spec"].(map[string]any) return &playlistsvc.Playlist{ diff --git a/pkg/tests/apis/helper.go b/pkg/tests/apis/helper.go index b24fa49900d04..1ea0a38c8fe5e 100644 --- a/pkg/tests/apis/helper.go +++ b/pkg/tests/apis/helper.go @@ -26,7 +26,6 @@ import ( "github.com/grafana/grafana/pkg/server" "github.com/grafana/grafana/pkg/services/auth/identity" "github.com/grafana/grafana/pkg/services/datasources" - "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/grafana-apiserver/endpoints/request" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/org/orgimpl" @@ -43,24 +42,16 @@ type K8sTestHelper struct { env server.TestEnv namespacer request.NamespaceMapper - Org1 OrgUsers - Org2 OrgUsers + Org1 OrgUsers // default + OrgB OrgUsers // some other id // // Registered groups groups []metav1.APIGroup } -func NewK8sTestHelper(t *testing.T) *K8sTestHelper { +func NewK8sTestHelper(t *testing.T, opts testinfra.GrafanaOpts) *K8sTestHelper { t.Helper() - dir, path := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{ - AppModeProduction: true, // do not start extra port 6443 - DisableAnonymous: true, - EnableFeatureToggles: []string{ - featuremgmt.FlagGrafanaAPIServer, - featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs, - }, - }) - + dir, path := testinfra.CreateGrafDir(t, opts) _, env := testinfra.StartGrafanaEnv(t, dir, path) c := &K8sTestHelper{ env: *env, @@ -68,8 +59,8 @@ func NewK8sTestHelper(t *testing.T) *K8sTestHelper { namespacer: request.GetNamespaceMapper(nil), } - c.Org1 = c.createTestUsers(int64(1)) - c.Org2 = c.createTestUsers(int64(2)) + c.Org1 = c.createTestUsers("Org1") + c.OrgB = c.createTestUsers("OrgB") // Read the API groups rsp := DoRequest(c, RequestParams{ @@ -81,6 +72,12 @@ func NewK8sTestHelper(t *testing.T) *K8sTestHelper { return c } +func (c *K8sTestHelper) Shutdown() { + fmt.Printf("calling shutdown on: %s\n", c.env.Server.HTTPServer.Listener.Addr()) + err := c.env.Server.Shutdown(context.Background(), "done") + require.NoError(c.t, err) +} + type ResourceClientArgs struct { User User Namespace string @@ -327,20 +324,27 @@ func (c *K8sTestHelper) LoadYAMLOrJSON(body string) *unstructured.Unstructured { return &unstructured.Unstructured{Object: unstructuredMap} } -func (c K8sTestHelper) createTestUsers(orgId int64) OrgUsers { +func (c K8sTestHelper) createTestUsers(orgName string) OrgUsers { c.t.Helper() store := c.env.SQLStore - store.Cfg.AutoAssignOrg = true - store.Cfg.AutoAssignOrgId = int(orgId) + defer func() { + store.Cfg.AutoAssignOrg = false + store.Cfg.AutoAssignOrgId = 1 // the default + }() + quotaService := quotaimpl.ProvideService(store, store.Cfg) orgService, err := orgimpl.ProvideService(store, store.Cfg, quotaService) require.NoError(c.t, err) - gotID, err := orgService.GetOrCreate(context.Background(), fmt.Sprintf("Org%d", orgId)) - require.NoError(c.t, err) - require.Equal(c.t, orgId, gotID) + orgId := int64(1) + if orgName != "Org1" { + orgId, err = orgService.GetOrCreate(context.Background(), orgName) + require.NoError(c.t, err) + } + store.Cfg.AutoAssignOrg = true + store.Cfg.AutoAssignOrgId = int(orgId) teamSvc := teamimpl.ProvideService(store, store.Cfg) cache := localcache.ProvideService() @@ -354,7 +358,7 @@ func (c K8sTestHelper) createTestUsers(orgId int64) OrgUsers { u, err := userSvc.Create(context.Background(), &user.CreateUserCommand{ DefaultOrgRole: string(role), Password: key, - Login: fmt.Sprintf("%s%d", key, orgId), + Login: fmt.Sprintf("%s-%d", key, orgId), OrgID: orgId, }) require.NoError(c.t, err) diff --git a/pkg/tests/apis/playlist/playlist_test.go b/pkg/tests/apis/playlist/playlist_test.go index 59a47e8561231..fb6bdfce4a343 100644 --- a/pkg/tests/apis/playlist/playlist_test.go +++ b/pkg/tests/apis/playlist/playlist_test.go @@ -14,21 +14,50 @@ import ( "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime/schema" + "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/playlist" "github.com/grafana/grafana/pkg/tests/apis" + "github.com/grafana/grafana/pkg/tests/testinfra" ) func TestPlaylist(t *testing.T) { if testing.Short() { t.Skip("skipping integration test") } - helper := apis.NewK8sTestHelper(t) + + t.Run("default setup", func(t *testing.T) { + doPlaylistTests(t, apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{ + AppModeProduction: true, // do not start extra port 6443 + DisableAnonymous: true, + EnableFeatureToggles: []string{ + featuremgmt.FlagGrafanaAPIServer, + }, + })) + }) + + t.Run("with k8s api flag", func(t *testing.T) { + doPlaylistTests(t, apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{ + AppModeProduction: true, // do not start extra port 6443 + DisableAnonymous: true, + EnableFeatureToggles: []string{ + featuremgmt.FlagGrafanaAPIServer, + featuremgmt.FlagKubernetesPlaylistsAPI, // <<< The change we are testing! + }, + })) + }) +} + +func doPlaylistTests(t *testing.T, helper *apis.K8sTestHelper) { gvr := schema.GroupVersionResource{ Group: "playlist.grafana.app", Version: "v0alpha1", Resource: "playlists", } + defer func() { + helper.Shutdown() + }() + t.Run("Check direct List permissions from different org users", func(t *testing.T) { // Check view permissions rsp := helper.List(helper.Org1.Viewer, "default", gvr) @@ -38,13 +67,13 @@ func TestPlaylist(t *testing.T) { require.Nil(t, rsp.Status) // Check view permissions - rsp = helper.List(helper.Org2.Viewer, "default", gvr) - require.Equal(t, 403, rsp.Response.StatusCode) // Org2 can not see default namespace + rsp = helper.List(helper.OrgB.Viewer, "default", gvr) + require.Equal(t, 403, rsp.Response.StatusCode) // OrgB can not see default namespace require.Nil(t, rsp.Result) require.Equal(t, metav1.StatusReasonForbidden, rsp.Status.Reason) // Check view permissions - rsp = helper.List(helper.Org2.Viewer, "org-22", gvr) + rsp = helper.List(helper.OrgB.Viewer, "org-22", gvr) require.Equal(t, 403, rsp.Response.StatusCode) // Unknown/not a member require.Nil(t, rsp.Result) require.Equal(t, metav1.StatusReasonForbidden, rsp.Status.Reason) @@ -63,7 +92,7 @@ func TestPlaylist(t *testing.T) { // Check org2 viewer can not see org1 (default namespace) client = helper.GetResourceClient(apis.ResourceClientArgs{ - User: helper.Org2.Viewer, + User: helper.OrgB.Viewer, Namespace: "default", // actually org1 GVR: gvr, }) @@ -74,7 +103,7 @@ func TestPlaylist(t *testing.T) { // Check invalid namespace client = helper.GetResourceClient(apis.ResourceClientArgs{ - User: helper.Org2.Viewer, + User: helper.OrgB.Viewer, Namespace: "org-22", // org 22 does not exist GVR: gvr, }) From c98add6e5a1653f3c0b494704c63807bcbe744e3 Mon Sep 17 00:00:00 2001 From: kay delaney <45561153+kaydelaney@users.noreply.github.com> Date: Fri, 3 Nov 2023 16:33:59 +0000 Subject: [PATCH 099/869] Dashboards: Fix issue causing crashes when saving new dashboard (#77620) Closes #77593 --- .betterer.results | 7 ++++--- .../components/DashboardPrompt/DashboardPrompt.tsx | 5 +++++ .../dashboard/state/__fixtures__/dashboardFixtures.ts | 1 + 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/.betterer.results b/.betterer.results index 0e1dfd0c5a05b..140a1f0af245b 100644 --- a/.betterer.results +++ b/.betterer.results @@ -3000,9 +3000,10 @@ exports[`better eslint`] = { [0, 0, 0, "Do not use any type assertions.", "2"], [0, 0, 0, "Do not use any type assertions.", "3"], [0, 0, 0, "Do not use any type assertions.", "4"], - [0, 0, 0, "Unexpected any. Specify a different type.", "5"], - [0, 0, 0, "Do not use any type assertions.", "6"], - [0, 0, 0, "Unexpected any. Specify a different type.", "7"] + [0, 0, 0, "Do not use any type assertions.", "5"], + [0, 0, 0, "Unexpected any. Specify a different type.", "6"], + [0, 0, 0, "Do not use any type assertions.", "7"], + [0, 0, 0, "Unexpected any. Specify a different type.", "8"] ], "public/app/features/dashboard/components/DashboardRow/DashboardRow.test.tsx:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"] diff --git a/public/app/features/dashboard/components/DashboardPrompt/DashboardPrompt.tsx b/public/app/features/dashboard/components/DashboardPrompt/DashboardPrompt.tsx index c5602a8cc443f..8956e55afd2c1 100644 --- a/public/app/features/dashboard/components/DashboardPrompt/DashboardPrompt.tsx +++ b/public/app/features/dashboard/components/DashboardPrompt/DashboardPrompt.tsx @@ -146,6 +146,11 @@ export function ignoreChanges(current: DashboardModel | null, original: object | return true; } + // Ignore changes if original is unsaved + if ((original as DashboardModel).version === 0) { + return true; + } + // Ignore changes if the user has been signed out if (!contextSrv.isSignedIn) { return true; diff --git a/public/app/features/dashboard/state/__fixtures__/dashboardFixtures.ts b/public/app/features/dashboard/state/__fixtures__/dashboardFixtures.ts index d1c6f323c5afa..c4259b5eb0933 100644 --- a/public/app/features/dashboard/state/__fixtures__/dashboardFixtures.ts +++ b/public/app/features/dashboard/state/__fixtures__/dashboardFixtures.ts @@ -22,6 +22,7 @@ export function createDashboardModelFixture( editable: true, graphTooltip: defaultDashboardCursorSync, schemaVersion: 1, + version: 1, timezone: '', ...dashboardInput, }; From bf363b32347c2e2573f7c468fc9f221c98a6f312 Mon Sep 17 00:00:00 2001 From: Gabriel MABILLE Date: Fri, 3 Nov 2023 17:49:11 +0100 Subject: [PATCH 100/869] ServiceAccounts: Use `isManaged` in DTO instead of `isExternal` (#77634) * ServiceAccounts: Use IsManaged in DTO instead of isExternal * Revert omitempty * Modify the other DTO * Swagger --- pkg/services/serviceaccounts/models.go | 4 ++-- pkg/services/serviceaccounts/proxy/service.go | 4 ++-- pkg/services/serviceaccounts/proxy/service_test.go | 6 +++--- public/api-enterprise-spec.json | 4 ++-- public/api-merged.json | 4 ++-- public/openapi3.json | 4 ++-- 6 files changed, 13 insertions(+), 13 deletions(-) diff --git a/pkg/services/serviceaccounts/models.go b/pkg/services/serviceaccounts/models.go index 90b74ee663465..d126fe57f967f 100644 --- a/pkg/services/serviceaccounts/models.go +++ b/pkg/services/serviceaccounts/models.go @@ -85,7 +85,7 @@ type ServiceAccountDTO struct { // example: false IsDisabled bool `json:"isDisabled" xorm:"is_disabled"` // example: false - IsExternal bool `json:"isExternal,omitempty" xorm:"-"` + IsManaged bool `json:"isManaged,omitempty" xorm:"-"` // example: Viewer Role string `json:"role" xorm:"role"` // example: 0 @@ -157,7 +157,7 @@ type ServiceAccountProfileDTO struct { // example: [] Teams []string `json:"teams" xorm:"-"` // example: false - IsExternal bool `json:"isExternal,omitempty" xorm:"-"` + IsManaged bool `json:"isManaged,omitempty" xorm:"-"` Tokens int64 `json:"tokens,omitempty"` AccessControl map[string]bool `json:"accessControl,omitempty" xorm:"-"` diff --git a/pkg/services/serviceaccounts/proxy/service.go b/pkg/services/serviceaccounts/proxy/service.go index 62a3c9d05dcb6..875c4a3f48f58 100644 --- a/pkg/services/serviceaccounts/proxy/service.go +++ b/pkg/services/serviceaccounts/proxy/service.go @@ -138,7 +138,7 @@ func (s *ServiceAccountsProxy) RetrieveServiceAccount(ctx context.Context, orgID } if s.isProxyEnabled { - sa.IsExternal = isExternalServiceAccount(sa.Login) + sa.IsManaged = isExternalServiceAccount(sa.Login) } return sa, nil @@ -175,7 +175,7 @@ func (s *ServiceAccountsProxy) SearchOrgServiceAccounts(ctx context.Context, que if s.isProxyEnabled { for i := range sa.ServiceAccounts { - sa.ServiceAccounts[i].IsExternal = isExternalServiceAccount(sa.ServiceAccounts[i].Login) + sa.ServiceAccounts[i].IsManaged = isExternalServiceAccount(sa.ServiceAccounts[i].Login) } } return sa, nil diff --git a/pkg/services/serviceaccounts/proxy/service_test.go b/pkg/services/serviceaccounts/proxy/service_test.go index db0325a2172f8..b5e6b5cce6278 100644 --- a/pkg/services/serviceaccounts/proxy/service_test.go +++ b/pkg/services/serviceaccounts/proxy/service_test.go @@ -146,7 +146,7 @@ func TestProvideServiceAccount_crudServiceAccount(t *testing.T) { serviceMock.ExpectedServiceAccountProfile = tc.expectedServiceAccount sa, err := svc.RetrieveServiceAccount(context.Background(), testOrgId, testServiceAccountId) assert.NoError(t, err, tc.description) - assert.Equal(t, tc.expectedIsExternal, sa.IsExternal, tc.description) + assert.Equal(t, tc.expectedIsExternal, sa.IsManaged, tc.description) }) } }) @@ -164,8 +164,8 @@ func TestProvideServiceAccount_crudServiceAccount(t *testing.T) { res, err := svc.SearchOrgServiceAccounts(context.Background(), &serviceaccounts.SearchOrgServiceAccountsQuery{OrgID: 1}) require.Len(t, res.ServiceAccounts, 2) require.NoError(t, err) - require.False(t, res.ServiceAccounts[0].IsExternal) - require.True(t, res.ServiceAccounts[1].IsExternal) + require.False(t, res.ServiceAccounts[0].IsManaged) + require.True(t, res.ServiceAccounts[1].IsManaged) }) t.Run("should update service account", func(t *testing.T) { diff --git a/public/api-enterprise-spec.json b/public/api-enterprise-spec.json index 36113b6e63270..a1335f43f03cc 100644 --- a/public/api-enterprise-spec.json +++ b/public/api-enterprise-spec.json @@ -6825,7 +6825,7 @@ "type": "boolean", "example": false }, - "isExternal": { + "isManaged": { "type": "boolean", "example": false }, @@ -6880,7 +6880,7 @@ "type": "boolean", "example": false }, - "isExternal": { + "isManaged": { "type": "boolean", "example": false }, diff --git a/public/api-merged.json b/public/api-merged.json index bf8b1330c5279..ee7333b215295 100644 --- a/public/api-merged.json +++ b/public/api-merged.json @@ -18710,7 +18710,7 @@ "type": "boolean", "example": false }, - "isExternal": { + "isManaged": { "type": "boolean", "example": false }, @@ -18765,7 +18765,7 @@ "type": "boolean", "example": false }, - "isExternal": { + "isManaged": { "type": "boolean", "example": false }, diff --git a/public/openapi3.json b/public/openapi3.json index d56e135a89223..e77ed17cddb0e 100644 --- a/public/openapi3.json +++ b/public/openapi3.json @@ -9631,7 +9631,7 @@ "example": false, "type": "boolean" }, - "isExternal": { + "isManaged": { "example": false, "type": "boolean" }, @@ -9686,7 +9686,7 @@ "example": false, "type": "boolean" }, - "isExternal": { + "isManaged": { "example": false, "type": "boolean" }, From d68d31c63ca80651b0fe4071bc409778cb8ecf79 Mon Sep 17 00:00:00 2001 From: Ashley Harrison Date: Fri, 3 Nov 2023 17:07:15 +0000 Subject: [PATCH 101/869] Navigation: Hide `Undock menu` button when docked (#76965) * small dock menu exploration * fix e2e tests * revert back to web-section-alt --- .../grafana-e2e-selectors/src/selectors/components.ts | 2 +- .../components/AppChrome/DockedMegaMenu/MegaMenu.tsx | 8 ++------ .../components/AppChrome/NavToolbar/NavToolbar.tsx | 10 +++++++++- public/locales/de-DE/grafana.json | 6 +++--- public/locales/en-US/grafana.json | 6 +++--- public/locales/es-ES/grafana.json | 6 +++--- public/locales/fr-FR/grafana.json | 6 +++--- public/locales/pseudo-LOCALE/grafana.json | 6 +++--- public/locales/zh-Hans/grafana.json | 6 +++--- 9 files changed, 30 insertions(+), 26 deletions(-) diff --git a/packages/grafana-e2e-selectors/src/selectors/components.ts b/packages/grafana-e2e-selectors/src/selectors/components.ts index d8599280e4e72..3d1266e649a4a 100644 --- a/packages/grafana-e2e-selectors/src/selectors/components.ts +++ b/packages/grafana-e2e-selectors/src/selectors/components.ts @@ -257,7 +257,7 @@ export const Components = { button: 'Configuration', }, Toggle: { - button: 'Toggle menu', + button: 'data-testid Toggle menu', }, Reporting: { button: 'Reporting', diff --git a/public/app/core/components/AppChrome/DockedMegaMenu/MegaMenu.tsx b/public/app/core/components/AppChrome/DockedMegaMenu/MegaMenu.tsx index fae86e4e5761c..220967c06a585 100644 --- a/public/app/core/components/AppChrome/DockedMegaMenu/MegaMenu.tsx +++ b/public/app/core/components/AppChrome/DockedMegaMenu/MegaMenu.tsx @@ -60,14 +60,10 @@ export const MegaMenu = React.memo( onClick={state.megaMenu === 'open' ? onClose : undefined} activeItem={activeItem} /> - {index === 0 && ( + {index === 0 && Boolean(state.megaMenu === 'open') && ( state.navIndex)[HOME_NAV_ID]; const styles = useStyles2(getStyles); const breadcrumbs = buildBreadcrumbs(sectionNav, pageNav, homeNav); @@ -45,10 +48,15 @@ export function NavToolbar({
    diff --git a/public/locales/de-DE/grafana.json b/public/locales/de-DE/grafana.json index 75dbd521ff510..02ec36f904e3c 100644 --- a/public/locales/de-DE/grafana.json +++ b/public/locales/de-DE/grafana.json @@ -863,12 +863,12 @@ }, "megamenu": { "close": "", - "dock": "", - "undock": "" + "dock": "" }, "toolbar": { + "close-menu": "", "enable-kiosk": "Kiosk-Modus aktivieren", - "toggle-menu": "Menü umschalten", + "open-menu": "", "toggle-search-bar": "Obere Suchleiste umschalten" } }, diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index af01a8f1c48ef..b8f585003a17b 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -863,12 +863,12 @@ }, "megamenu": { "close": "Close menu", - "dock": "Dock menu", - "undock": "Undock menu" + "dock": "Dock menu" }, "toolbar": { + "close-menu": "Close menu", "enable-kiosk": "Enable kiosk mode", - "toggle-menu": "Toggle menu", + "open-menu": "Open menu", "toggle-search-bar": "Toggle top search bar" } }, diff --git a/public/locales/es-ES/grafana.json b/public/locales/es-ES/grafana.json index a858fddf4893f..5cf2a250f47bf 100644 --- a/public/locales/es-ES/grafana.json +++ b/public/locales/es-ES/grafana.json @@ -869,12 +869,12 @@ }, "megamenu": { "close": "", - "dock": "", - "undock": "" + "dock": "" }, "toolbar": { + "close-menu": "", "enable-kiosk": "Activar el modo de quiosco", - "toggle-menu": "Activar o desactivar menú", + "open-menu": "", "toggle-search-bar": "Activar o desactivar la barra de búsqueda superior" } }, diff --git a/public/locales/fr-FR/grafana.json b/public/locales/fr-FR/grafana.json index edfe34f9b5dd0..fc1698a3b6c49 100644 --- a/public/locales/fr-FR/grafana.json +++ b/public/locales/fr-FR/grafana.json @@ -869,12 +869,12 @@ }, "megamenu": { "close": "", - "dock": "", - "undock": "" + "dock": "" }, "toolbar": { + "close-menu": "", "enable-kiosk": "Activer le mode kiosque", - "toggle-menu": "Afficher/Masquer le menu", + "open-menu": "", "toggle-search-bar": "Afficher/Masquer la barre de recherche" } }, diff --git a/public/locales/pseudo-LOCALE/grafana.json b/public/locales/pseudo-LOCALE/grafana.json index 93993b5c1bf7f..306d52bc08514 100644 --- a/public/locales/pseudo-LOCALE/grafana.json +++ b/public/locales/pseudo-LOCALE/grafana.json @@ -863,12 +863,12 @@ }, "megamenu": { "close": "Cľőşę męʼnū", - "dock": "Đőčĸ męʼnū", - "undock": "Ůʼnđőčĸ męʼnū" + "dock": "Đőčĸ męʼnū" }, "toolbar": { + "close-menu": "Cľőşę męʼnū", "enable-kiosk": "Ēʼnäþľę ĸįőşĸ mőđę", - "toggle-menu": "Ŧőģģľę męʼnū", + "open-menu": "Øpęʼn męʼnū", "toggle-search-bar": "Ŧőģģľę ŧőp şęäřčĥ þäř" } }, diff --git a/public/locales/zh-Hans/grafana.json b/public/locales/zh-Hans/grafana.json index 2c3b6c26aa546..12307a5ed52fe 100644 --- a/public/locales/zh-Hans/grafana.json +++ b/public/locales/zh-Hans/grafana.json @@ -857,12 +857,12 @@ }, "megamenu": { "close": "", - "dock": "", - "undock": "" + "dock": "" }, "toolbar": { + "close-menu": "", "enable-kiosk": "启用 kiosk 模式", - "toggle-menu": "切换菜单", + "open-menu": "", "toggle-search-bar": "切换顶部搜索栏" } }, From ffcaef9c17f55ad98690d70bb5953e1131361729 Mon Sep 17 00:00:00 2001 From: Ashley Harrison Date: Fri, 3 Nov 2023 20:15:53 +0000 Subject: [PATCH 102/869] Update navigation e2e tests (#77654) update navigation e2e tests --- e2e/various-suite/navigation.spec.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/e2e/various-suite/navigation.spec.ts b/e2e/various-suite/navigation.spec.ts index 35ee5ee90f4ee..b068d56f3ad37 100644 --- a/e2e/various-suite/navigation.spec.ts +++ b/e2e/various-suite/navigation.spec.ts @@ -15,7 +15,7 @@ describe('Docked Navigation', () => { it('should remain docked when reloading the page', () => { // Expand, then dock the mega menu - cy.get('[aria-label="Toggle menu"]').click(); + cy.get('[aria-label="Open menu"]').click(); cy.get('[aria-label="Dock menu"]').click(); e2e.components.NavMenu.Menu().should('be.visible'); @@ -26,7 +26,7 @@ describe('Docked Navigation', () => { it('should remain docked when navigating to another page', () => { // Expand, then dock the mega menu - cy.get('[aria-label="Toggle menu"]').click(); + cy.get('[aria-label="Open menu"]').click(); cy.get('[aria-label="Dock menu"]').click(); cy.contains('a', 'Administration').click(); From c3962acf9837bc5d659375422acd27db9f2463a6 Mon Sep 17 00:00:00 2001 From: Julien Duchesne Date: Fri, 3 Nov 2023 18:32:06 -0400 Subject: [PATCH 103/869] Swagger: Rename annotations model (#77605) Currently, in the schema, it's a global definition called `ItemDTO`. It's hard to figure out what that is Renaming it to `Annotation` should be more helpful --- pkg/services/annotations/models.go | 1 + public/api-enterprise-spec.json | 186 +++++++++++++---------------- public/api-merged.json | 180 ++++++++++++---------------- public/openapi3.json | 184 ++++++++++++---------------- 4 files changed, 234 insertions(+), 317 deletions(-) diff --git a/pkg/services/annotations/models.go b/pkg/services/annotations/models.go index e1cef2fbe6ec2..1e24a75f40ef7 100644 --- a/pkg/services/annotations/models.go +++ b/pkg/services/annotations/models.go @@ -87,6 +87,7 @@ func (i Item) TableName() string { return "annotation" } +// swagger:model Annotation type ItemDTO struct { ID int64 `json:"id" xorm:"id"` AlertID int64 `json:"alertId" xorm:"alert_id"` diff --git a/public/api-enterprise-spec.json b/public/api-enterprise-spec.json index a1335f43f03cc..cdf6ab57f58fc 100644 --- a/public/api-enterprise-spec.json +++ b/public/api-enterprise-spec.json @@ -2621,6 +2621,80 @@ } } }, + "Annotation": { + "type": "object", + "properties": { + "alertId": { + "type": "integer", + "format": "int64" + }, + "alertName": { + "type": "string" + }, + "avatarUrl": { + "type": "string" + }, + "created": { + "type": "integer", + "format": "int64" + }, + "dashboardId": { + "type": "integer", + "format": "int64" + }, + "dashboardUID": { + "type": "string" + }, + "data": { + "$ref": "#/definitions/Json" + }, + "email": { + "type": "string" + }, + "id": { + "type": "integer", + "format": "int64" + }, + "login": { + "type": "string" + }, + "newState": { + "type": "string" + }, + "panelId": { + "type": "integer", + "format": "int64" + }, + "prevState": { + "type": "string" + }, + "tags": { + "type": "array", + "items": { + "type": "string" + } + }, + "text": { + "type": "string" + }, + "time": { + "type": "integer", + "format": "int64" + }, + "timeEnd": { + "type": "integer", + "format": "int64" + }, + "updated": { + "type": "integer", + "format": "int64" + }, + "userId": { + "type": "integer", + "format": "int64" + } + } + }, "AnnotationActions": { "type": "object", "properties": { @@ -3460,7 +3534,7 @@ "type": "object", "properties": { "folderId": { - "description": "ID of the folder where the library element is stored.", + "description": "ID of the folder where the library element is stored.\n\nDeprecated: use FolderUID instead", "type": "integer", "format": "int64" }, @@ -5034,80 +5108,6 @@ } } }, - "ItemDTO": { - "type": "object", - "properties": { - "alertId": { - "type": "integer", - "format": "int64" - }, - "alertName": { - "type": "string" - }, - "avatarUrl": { - "type": "string" - }, - "created": { - "type": "integer", - "format": "int64" - }, - "dashboardId": { - "type": "integer", - "format": "int64" - }, - "dashboardUID": { - "type": "string" - }, - "data": { - "$ref": "#/definitions/Json" - }, - "email": { - "type": "string" - }, - "id": { - "type": "integer", - "format": "int64" - }, - "login": { - "type": "string" - }, - "newState": { - "type": "string" - }, - "panelId": { - "type": "integer", - "format": "int64" - }, - "prevState": { - "type": "string" - }, - "tags": { - "type": "array", - "items": { - "type": "string" - } - }, - "text": { - "type": "string" - }, - "time": { - "type": "integer", - "format": "int64" - }, - "timeEnd": { - "type": "integer", - "format": "int64" - }, - "updated": { - "type": "integer", - "format": "int64" - }, - "userId": { - "type": "integer", - "format": "int64" - } - } - }, "JSONWebKey": { "type": "object", "title": "JSONWebKey represents a public or private key in JWK format.", @@ -7480,28 +7480,6 @@ "$ref": "#/definitions/Transformation" } }, - "TrimDashboardCommand": { - "type": "object", - "properties": { - "dashboard": { - "$ref": "#/definitions/Json" - }, - "meta": { - "$ref": "#/definitions/Json" - } - } - }, - "TrimDashboardFullWithMeta": { - "type": "object", - "properties": { - "dashboard": { - "$ref": "#/definitions/Json" - }, - "meta": { - "$ref": "#/definitions/Json" - } - } - }, "Type": { "type": "string" }, @@ -8686,7 +8664,7 @@ "getAnnotationByIDResponse": { "description": "", "schema": { - "$ref": "#/definitions/ItemDTO" + "$ref": "#/definitions/Annotation" } }, "getAnnotationTagsResponse": { @@ -8700,7 +8678,7 @@ "schema": { "type": "array", "items": { - "$ref": "#/definitions/ItemDTO" + "$ref": "#/definitions/Annotation" } } }, @@ -9316,6 +9294,10 @@ "url" ], "properties": { + "folderUid": { + "description": "FolderUID The unique identifier (uid) of the folder the dashboard belongs to.", + "type": "string" + }, "id": { "description": "ID The unique identifier (id) of the created/updated dashboard.", "type": "integer", @@ -9483,12 +9465,6 @@ "$ref": "#/definitions/AlertTestResult" } }, - "trimDashboardResponse": { - "description": "", - "schema": { - "$ref": "#/definitions/TrimDashboardFullWithMeta" - } - }, "unauthorisedError": { "description": "UnauthorizedError is returned when the request is not authenticated.", "schema": { diff --git a/public/api-merged.json b/public/api-merged.json index ee7333b215295..66dddbdf3c3b5 100644 --- a/public/api-merged.json +++ b/public/api-merged.json @@ -12027,6 +12027,80 @@ } } }, + "Annotation": { + "type": "object", + "properties": { + "alertId": { + "type": "integer", + "format": "int64" + }, + "alertName": { + "type": "string" + }, + "avatarUrl": { + "type": "string" + }, + "created": { + "type": "integer", + "format": "int64" + }, + "dashboardId": { + "type": "integer", + "format": "int64" + }, + "dashboardUID": { + "type": "string" + }, + "data": { + "$ref": "#/definitions/Json" + }, + "email": { + "type": "string" + }, + "id": { + "type": "integer", + "format": "int64" + }, + "login": { + "type": "string" + }, + "newState": { + "type": "string" + }, + "panelId": { + "type": "integer", + "format": "int64" + }, + "prevState": { + "type": "string" + }, + "tags": { + "type": "array", + "items": { + "type": "string" + } + }, + "text": { + "type": "string" + }, + "time": { + "type": "integer", + "format": "int64" + }, + "timeEnd": { + "type": "integer", + "format": "int64" + }, + "updated": { + "type": "integer", + "format": "int64" + }, + "userId": { + "type": "integer", + "format": "int64" + } + } + }, "AnnotationActions": { "type": "object", "properties": { @@ -15478,80 +15552,6 @@ } } }, - "ItemDTO": { - "type": "object", - "properties": { - "alertId": { - "type": "integer", - "format": "int64" - }, - "alertName": { - "type": "string" - }, - "avatarUrl": { - "type": "string" - }, - "created": { - "type": "integer", - "format": "int64" - }, - "dashboardId": { - "type": "integer", - "format": "int64" - }, - "dashboardUID": { - "type": "string" - }, - "data": { - "$ref": "#/definitions/Json" - }, - "email": { - "type": "string" - }, - "id": { - "type": "integer", - "format": "int64" - }, - "login": { - "type": "string" - }, - "newState": { - "type": "string" - }, - "panelId": { - "type": "integer", - "format": "int64" - }, - "prevState": { - "type": "string" - }, - "tags": { - "type": "array", - "items": { - "type": "string" - } - }, - "text": { - "type": "string" - }, - "time": { - "type": "integer", - "format": "int64" - }, - "timeEnd": { - "type": "integer", - "format": "int64" - }, - "updated": { - "type": "integer", - "format": "int64" - }, - "userId": { - "type": "integer", - "format": "int64" - } - } - }, "JSONWebKey": { "type": "object", "title": "JSONWebKey represents a public or private key in JWK format.", @@ -19843,28 +19843,6 @@ "$ref": "#/definitions/Transformation" } }, - "TrimDashboardCommand": { - "type": "object", - "properties": { - "dashboard": { - "$ref": "#/definitions/Json" - }, - "meta": { - "$ref": "#/definitions/Json" - } - } - }, - "TrimDashboardFullWithMeta": { - "type": "object", - "properties": { - "dashboard": { - "$ref": "#/definitions/Json" - }, - "meta": { - "$ref": "#/definitions/Json" - } - } - }, "Type": { "type": "string" }, @@ -21796,7 +21774,7 @@ "getAnnotationByIDResponse": { "description": "(empty)", "schema": { - "$ref": "#/definitions/ItemDTO" + "$ref": "#/definitions/Annotation" } }, "getAnnotationTagsResponse": { @@ -21810,7 +21788,7 @@ "schema": { "type": "array", "items": { - "$ref": "#/definitions/ItemDTO" + "$ref": "#/definitions/Annotation" } } }, @@ -22606,12 +22584,6 @@ "$ref": "#/definitions/AlertTestResult" } }, - "trimDashboardResponse": { - "description": "(empty)", - "schema": { - "$ref": "#/definitions/TrimDashboardFullWithMeta" - } - }, "unauthorisedError": { "description": "UnauthorizedError is returned when the request is not authenticated.", "schema": { diff --git a/public/openapi3.json b/public/openapi3.json index e77ed17cddb0e..64768646ea01c 100644 --- a/public/openapi3.json +++ b/public/openapi3.json @@ -674,7 +674,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ItemDTO" + "$ref": "#/components/schemas/Annotation" } } }, @@ -695,7 +695,7 @@ "application/json": { "schema": { "items": { - "$ref": "#/components/schemas/ItemDTO" + "$ref": "#/components/schemas/Annotation" }, "type": "array" } @@ -1855,16 +1855,6 @@ }, "description": "(empty)" }, - "trimDashboardResponse": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/TrimDashboardFullWithMeta" - } - } - }, - "description": "(empty)" - }, "unauthorisedError": { "content": { "application/json": { @@ -2951,6 +2941,80 @@ }, "type": "object" }, + "Annotation": { + "properties": { + "alertId": { + "format": "int64", + "type": "integer" + }, + "alertName": { + "type": "string" + }, + "avatarUrl": { + "type": "string" + }, + "created": { + "format": "int64", + "type": "integer" + }, + "dashboardId": { + "format": "int64", + "type": "integer" + }, + "dashboardUID": { + "type": "string" + }, + "data": { + "$ref": "#/components/schemas/Json" + }, + "email": { + "type": "string" + }, + "id": { + "format": "int64", + "type": "integer" + }, + "login": { + "type": "string" + }, + "newState": { + "type": "string" + }, + "panelId": { + "format": "int64", + "type": "integer" + }, + "prevState": { + "type": "string" + }, + "tags": { + "items": { + "type": "string" + }, + "type": "array" + }, + "text": { + "type": "string" + }, + "time": { + "format": "int64", + "type": "integer" + }, + "timeEnd": { + "format": "int64", + "type": "integer" + }, + "updated": { + "format": "int64", + "type": "integer" + }, + "userId": { + "format": "int64", + "type": "integer" + } + }, + "type": "object" + }, "AnnotationActions": { "properties": { "canAdd": { @@ -6402,80 +6466,6 @@ }, "type": "object" }, - "ItemDTO": { - "properties": { - "alertId": { - "format": "int64", - "type": "integer" - }, - "alertName": { - "type": "string" - }, - "avatarUrl": { - "type": "string" - }, - "created": { - "format": "int64", - "type": "integer" - }, - "dashboardId": { - "format": "int64", - "type": "integer" - }, - "dashboardUID": { - "type": "string" - }, - "data": { - "$ref": "#/components/schemas/Json" - }, - "email": { - "type": "string" - }, - "id": { - "format": "int64", - "type": "integer" - }, - "login": { - "type": "string" - }, - "newState": { - "type": "string" - }, - "panelId": { - "format": "int64", - "type": "integer" - }, - "prevState": { - "type": "string" - }, - "tags": { - "items": { - "type": "string" - }, - "type": "array" - }, - "text": { - "type": "string" - }, - "time": { - "format": "int64", - "type": "integer" - }, - "timeEnd": { - "format": "int64", - "type": "integer" - }, - "updated": { - "format": "int64", - "type": "integer" - }, - "userId": { - "format": "int64", - "type": "integer" - } - }, - "type": "object" - }, "JSONWebKey": { "properties": { "Algorithm": { @@ -10765,28 +10755,6 @@ }, "type": "array" }, - "TrimDashboardCommand": { - "properties": { - "dashboard": { - "$ref": "#/components/schemas/Json" - }, - "meta": { - "$ref": "#/components/schemas/Json" - } - }, - "type": "object" - }, - "TrimDashboardFullWithMeta": { - "properties": { - "dashboard": { - "$ref": "#/components/schemas/Json" - }, - "meta": { - "$ref": "#/components/schemas/Json" - } - }, - "type": "object" - }, "Type": { "type": "string" }, From f88a0f36ecf16a88124759b22367f9b50fbecad1 Mon Sep 17 00:00:00 2001 From: Sonia Aguilar <33540275+soniaAguilarPeiron@users.noreply.github.com> Date: Mon, 6 Nov 2023 08:35:42 +0100 Subject: [PATCH 104/869] Alerting: Avoid alert list view component being unmounted every time we fetch new data (#77631) * Avoid view component being unmounted any time we fetch new data * Render loading indicator only when loading data for the first time * Remove unnecessary useRef --- .../panel/alertlist/UnifiedAlertList.tsx | 61 +++++++++++-------- 1 file changed, 34 insertions(+), 27 deletions(-) diff --git a/public/app/plugins/panel/alertlist/UnifiedAlertList.tsx b/public/app/plugins/panel/alertlist/UnifiedAlertList.tsx index 544834f342205..890f8b2383392 100644 --- a/public/app/plugins/panel/alertlist/UnifiedAlertList.tsx +++ b/public/app/plugins/panel/alertlist/UnifiedAlertList.tsx @@ -213,6 +213,10 @@ export function UnifiedAlertList(props: PanelProps) { const noAlertsMessage = rules.length === 0 ? 'No alerts matching filters' : undefined; + const renderLoading = grafanaRulesLoading || (dispatched && loading && !haveResults); + + const havePreviousResults = Object.values(promRulesRequests).some((state) => state.result); + if ( !contextSrv.hasPermission(AccessControlAction.AlertingRuleRead) && !contextSrv.hasPermission(AccessControlAction.AlertingRuleExternalRead) @@ -225,33 +229,36 @@ export function UnifiedAlertList(props: PanelProps) { return (
    - {(grafanaRulesLoading || (dispatched && loading && !haveResults)) && } - {noAlertsMessage &&
    {noAlertsMessage}
    } -
    - {props.options.viewMode === ViewMode.Stat && haveResults && ( - - )} - {props.options.viewMode === ViewMode.List && props.options.groupMode === GroupMode.Custom && haveResults && ( - - )} - {props.options.viewMode === ViewMode.List && props.options.groupMode === GroupMode.Default && haveResults && ( - - )} -
    + {havePreviousResults && noAlertsMessage &&
    {noAlertsMessage}
    } + {havePreviousResults && ( +
    + {props.options.viewMode === ViewMode.Stat && ( + + )} + {props.options.viewMode === ViewMode.List && props.options.groupMode === GroupMode.Custom && ( + + )} + {props.options.viewMode === ViewMode.List && props.options.groupMode === GroupMode.Default && ( + + )} +
    + )} + {/* loading moved here to avoid twitching */} + {renderLoading && }
    ); From a1718aafce1599e2211a0a03d6301adab2d0e64f Mon Sep 17 00:00:00 2001 From: Ivana Huckova <30407135+ivanahuckova@users.noreply.github.com> Date: Mon, 6 Nov 2023 11:36:39 +0100 Subject: [PATCH 105/869] Elasticsearch: Add error source for DataQuery (#77386) * WIP * Refactor, plus update source of error in response_parser * Adjust test * Use methods and httpclient from errorsource * Update pkg/tsdb/elasticsearch/data_query.go Co-authored-by: Scott Lepper * Return nil error * Fix test * Fix integration test --------- Co-authored-by: Scott Lepper --- go.sum | 3 +++ pkg/tests/api/elasticsearch/elasticsearch_test.go | 2 +- pkg/tsdb/elasticsearch/data_query.go | 11 +++++++---- pkg/tsdb/elasticsearch/data_query_test.go | 7 +++++-- pkg/tsdb/elasticsearch/elasticsearch.go | 4 +++- pkg/tsdb/elasticsearch/error_handling_test.go | 5 +++-- pkg/tsdb/elasticsearch/response_parser.go | 5 ++--- public/dashboards/home.json | 4 ++-- 8 files changed, 26 insertions(+), 15 deletions(-) diff --git a/go.sum b/go.sum index 94188a7f9f96c..1df0223f53bd5 100644 --- a/go.sum +++ b/go.sum @@ -1754,6 +1754,7 @@ github.com/google/pprof v0.0.0-20230228050547-1710fef4ab10/go.mod h1:79YE0hCXdHa github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o= github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw= +github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= github.com/google/subcommands v1.0.1/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk= github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= @@ -1833,6 +1834,8 @@ github.com/grafana/gofpdf v0.0.0-20231002120153-857cc45be447 h1:jxJJ5z0GxqhWFbQU github.com/grafana/gofpdf v0.0.0-20231002120153-857cc45be447/go.mod h1:IxsY6mns6Q5sAnWcrptrgUrSglTZJXH/kXr9nbpb/9I= github.com/grafana/grafana-aws-sdk v0.19.1 h1:5GBiOv2AgdyjwlgAX+dtgPtXU4FgMTD9rfQUPQseEpQ= github.com/grafana/grafana-aws-sdk v0.19.1/go.mod h1:ntq2NDH12Y2Fkbc6fozpF8kYsJM9k6KNr+Xfo5w3/iM= +github.com/grafana/grafana-aws-sdk v0.19.2 h1:GCLdo3oz7gp/ZJvbgFktMP5LKdNLnhxh/nLGzuxnJPA= +github.com/grafana/grafana-aws-sdk v0.19.2/go.mod h1:IDhwY+LF6jD1zute5UvbZ5DY8aI4DQ+LjU8RjfayD20= github.com/grafana/grafana-azure-sdk-go v1.9.0 h1:4JRwlqgUtPRAQSoiV4DFZDQ3lbNsauHqj9kC6SMR9Ak= github.com/grafana/grafana-azure-sdk-go v1.9.0/go.mod h1:1vBa0KOl+/Kcm7V888OyMXDSFncmek14q7XhEkrcSaA= github.com/grafana/grafana-google-sdk-go v0.1.0 h1:LKGY8z2DSxKjYfr2flZsWgTRTZ6HGQbTqewE3JvRaNA= diff --git a/pkg/tests/api/elasticsearch/elasticsearch_test.go b/pkg/tests/api/elasticsearch/elasticsearch_test.go index 21eb70c8c1620..1bfc4ab9f26b3 100644 --- a/pkg/tests/api/elasticsearch/elasticsearch_test.go +++ b/pkg/tests/api/elasticsearch/elasticsearch_test.go @@ -95,7 +95,7 @@ func TestIntegrationElasticsearch(t *testing.T) { resp, err := http.Post(u, "application/json", buf1) require.NoError(t, err) - require.Equal(t, http.StatusInternalServerError, resp.StatusCode) + require.Equal(t, http.StatusBadRequest, resp.StatusCode) t.Cleanup(func() { err := resp.Body.Close() require.NoError(t, err) diff --git a/pkg/tsdb/elasticsearch/data_query.go b/pkg/tsdb/elasticsearch/data_query.go index 35fc092d8b71a..f737f746cc19b 100644 --- a/pkg/tsdb/elasticsearch/data_query.go +++ b/pkg/tsdb/elasticsearch/data_query.go @@ -10,6 +10,7 @@ import ( "time" "github.com/grafana/grafana-plugin-sdk-go/backend" + "github.com/grafana/grafana-plugin-sdk-go/experimental/errorsource" "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/infra/log" @@ -41,12 +42,13 @@ var newElasticsearchDataQuery = func(ctx context.Context, client es.Client, data func (e *elasticsearchDataQuery) execute() (*backend.QueryDataResponse, error) { start := time.Now() + response := backend.NewQueryDataResponse() e.logger.Debug("Parsing queries", "queriesLength", len(e.dataQueries)) queries, err := parseQuery(e.dataQueries, e.logger) if err != nil { mq, _ := json.Marshal(e.dataQueries) e.logger.Error("Failed to parse queries", "error", err, "queries", string(mq), "queriesLength", len(queries), "duration", time.Since(start), "stage", es.StagePrepareRequest) - return &backend.QueryDataResponse{}, err + return errorsource.AddPluginErrorToResponse(e.dataQueries[0].RefID, response, err), nil } ms := e.client.MultiSearch() @@ -57,7 +59,7 @@ func (e *elasticsearchDataQuery) execute() (*backend.QueryDataResponse, error) { if err := e.processQuery(q, ms, from, to); err != nil { mq, _ := json.Marshal(q) e.logger.Error("Failed to process query to multisearch request builder", "error", err, "query", string(mq), "queriesLength", len(queries), "duration", time.Since(start), "stage", es.StagePrepareRequest) - return &backend.QueryDataResponse{}, err + return errorsource.AddPluginErrorToResponse(q.RefID, response, err), nil } } @@ -65,13 +67,14 @@ func (e *elasticsearchDataQuery) execute() (*backend.QueryDataResponse, error) { if err != nil { mqs, _ := json.Marshal(e.dataQueries) e.logger.Error("Failed to build multisearch request", "error", err, "queriesLength", len(queries), "queries", string(mqs), "duration", time.Since(start), "stage", es.StagePrepareRequest) - return &backend.QueryDataResponse{}, err + return errorsource.AddPluginErrorToResponse(e.dataQueries[0].RefID, response, err), nil } e.logger.Info("Prepared request", "queriesLength", len(queries), "duration", time.Since(start), "stage", es.StagePrepareRequest) res, err := e.client.ExecuteMultisearch(req) if err != nil { - return &backend.QueryDataResponse{}, err + // We are returning error containing the source that was added trough errorsource.Middleware + return errorsource.AddErrorToResponse(e.dataQueries[0].RefID, response, err), nil } return parseResponse(e.ctx, res.Responses, queries, e.client.GetConfiguredFields(), e.logger, e.tracer) diff --git a/pkg/tsdb/elasticsearch/data_query_test.go b/pkg/tsdb/elasticsearch/data_query_test.go index 337dbe42fd034..bded5d6071da5 100644 --- a/pkg/tsdb/elasticsearch/data_query_test.go +++ b/pkg/tsdb/elasticsearch/data_query_test.go @@ -1428,10 +1428,12 @@ func TestExecuteElasticsearchDataQuery(t *testing.T) { t.Run("With invalid query should return error", (func(t *testing.T) { c := newFakeClient() - _, err := executeElasticsearchDataQuery(c, `{ + res, err := executeElasticsearchDataQuery(c, `{ "query": "foo", }`, from, to) - require.Error(t, err) + require.NoError(t, err) + require.Equal(t, res.Responses["A"].ErrorSource, backend.ErrorSourcePlugin) + require.Equal(t, res.Responses["A"].Error.Error(), "invalid character '}' looking for beginning of object key string") })) }) } @@ -1856,6 +1858,7 @@ func executeElasticsearchDataQuery(c es.Client, body string, from, to time.Time) { JSON: json.RawMessage(body), TimeRange: timeRange, + RefID: "A", }, }, } diff --git a/pkg/tsdb/elasticsearch/elasticsearch.go b/pkg/tsdb/elasticsearch/elasticsearch.go index cd25200b52d64..e6deb0c525b07 100644 --- a/pkg/tsdb/elasticsearch/elasticsearch.go +++ b/pkg/tsdb/elasticsearch/elasticsearch.go @@ -17,6 +17,7 @@ import ( "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana-plugin-sdk-go/backend/datasource" "github.com/grafana/grafana-plugin-sdk-go/backend/instancemgmt" + exphttpclient "github.com/grafana/grafana-plugin-sdk-go/experimental/errorsource/httpclient" "github.com/grafana/grafana/pkg/infra/httpclient" "github.com/grafana/grafana/pkg/infra/log" @@ -87,7 +88,8 @@ func newInstanceSettings(httpClientProvider httpclient.Provider) datasource.Inst httpCliOpts.SigV4.Service = "es" } - httpCli, err := httpClientProvider.New(httpCliOpts) + // enable experimental http client to support errors with source + httpCli, err := exphttpclient.New(httpCliOpts) if err != nil { return nil, err } diff --git a/pkg/tsdb/elasticsearch/error_handling_test.go b/pkg/tsdb/elasticsearch/error_handling_test.go index 20dddacda4caa..353c1e2409d2a 100644 --- a/pkg/tsdb/elasticsearch/error_handling_test.go +++ b/pkg/tsdb/elasticsearch/error_handling_test.go @@ -139,11 +139,12 @@ func TestNonElasticError(t *testing.T) { // to access the database for some reason. response := []byte(`Access to the database is forbidden`) - _, err := queryDataTestWithResponseCode(query, 403, response) + res, err := queryDataTestWithResponseCode(query, 403, response) // FIXME: we should return something better. // currently it returns the error-message about being unable to decode JSON // it is not 100% clear what we should return to the browser // (and what to debug-log for example), we could return // at least something like "unknown response, http status code 403" - require.ErrorContains(t, err, "invalid character") + require.NoError(t, err) + require.Contains(t, res.response.Responses["A"].Error.Error(), "invalid character") } diff --git a/pkg/tsdb/elasticsearch/response_parser.go b/pkg/tsdb/elasticsearch/response_parser.go index b95eb98de1dc4..3b332872a2585 100644 --- a/pkg/tsdb/elasticsearch/response_parser.go +++ b/pkg/tsdb/elasticsearch/response_parser.go @@ -13,6 +13,7 @@ import ( "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana-plugin-sdk-go/data" + "github.com/grafana/grafana-plugin-sdk-go/experimental/errorsource" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/codes" "go.opentelemetry.io/otel/trace" @@ -73,9 +74,7 @@ func parseResponse(ctx context.Context, responses []*es.SearchResponse, targets resSpan.End() logger.Error("Processing error response from Elasticsearch", "error", string(me), "query", string(mt)) errResult := getErrorFromElasticResponse(res) - result.Responses[target.RefID] = backend.DataResponse{ - Error: errors.New(errResult), - } + result.Responses[target.RefID] = errorsource.Response(errorsource.PluginError(errors.New(errResult), false)) continue } diff --git a/public/dashboards/home.json b/public/dashboards/home.json index d9e24324df7ec..718b6b52079be 100644 --- a/public/dashboards/home.json +++ b/public/dashboards/home.json @@ -32,9 +32,9 @@ "showHeadings": true, "folderId": 0, "maxItems": 30, - "tags":[], + "tags": [], "query": "" - }, + }, "title": "Dashboards", "type": "dashlist" }, From d1e52d4788ba496b862a9019f17fab0c935cea15 Mon Sep 17 00:00:00 2001 From: Ashley Harrison Date: Mon, 6 Nov 2023 11:28:44 +0000 Subject: [PATCH 106/869] Chore: type fixes (#77618) * type fixes * couple more * just a couple more * small fixes to prometheus typings * improve some more datasource types --- .betterer.results | 162 ++++-------------- .../plugins/datasource/graphite/datasource.ts | 18 +- .../app/plugins/datasource/graphite/gfunc.ts | 6 +- .../datasource/graphite/graphite_query.ts | 8 +- .../app/plugins/datasource/graphite/parser.ts | 4 +- .../datasource/graphite/state/store.ts | 6 +- .../app/plugins/datasource/graphite/utils.ts | 2 +- .../plugins/datasource/influxdb/datasource.ts | 4 +- .../datasource/influxdb/influx_query_model.ts | 14 +- .../plugins/datasource/influxdb/query_part.ts | 2 +- .../plugins/datasource/jaeger/datasource.ts | 14 +- public/app/plugins/datasource/jaeger/util.ts | 2 +- .../datasource/loki/LanguageProvider.ts | 2 +- .../app/plugins/datasource/loki/streaming.ts | 2 +- .../prometheus/components/PromQueryField.tsx | 4 +- .../components/PrometheusMetricsBrowser.tsx | 4 +- .../prometheus/configuration/PromSettings.tsx | 4 +- .../datasource/prometheus/datasource.ts | 25 ++- .../plugins/datasource/tempo/datasource.ts | 13 +- .../plugins/panel/geomap/utils/selection.ts | 2 +- .../app/plugins/panel/histogram/Histogram.tsx | 4 +- public/app/plugins/panel/nodeGraph/utils.ts | 4 +- public/app/types/templates.ts | 2 +- public/test/core/redux/mocks.ts | 2 +- public/test/core/redux/reducerTester.ts | 10 +- public/test/core/thunk/thunkTester.ts | 3 +- public/test/helpers/convertToStoreState.ts | 7 +- public/test/matchers/index.ts | 2 +- public/test/matchers/utils.ts | 5 +- public/test/mocks/workers.ts | 5 +- public/test/specs/helpers.ts | 4 +- 31 files changed, 131 insertions(+), 215 deletions(-) diff --git a/.betterer.results b/.betterer.results index 140a1f0af245b..0f0c8c76b2b21 100644 --- a/.betterer.results +++ b/.betterer.results @@ -5958,16 +5958,16 @@ exports[`better eslint`] = { [0, 0, 0, "Unexpected any. Specify a different type.", "0"], [0, 0, 0, "Unexpected any. Specify a different type.", "1"], [0, 0, 0, "Unexpected any. Specify a different type.", "2"], - [0, 0, 0, "Unexpected any. Specify a different type.", "3"], + [0, 0, 0, "Do not use any type assertions.", "3"], [0, 0, 0, "Do not use any type assertions.", "4"], - [0, 0, 0, "Do not use any type assertions.", "5"], + [0, 0, 0, "Unexpected any. Specify a different type.", "5"], [0, 0, 0, "Unexpected any. Specify a different type.", "6"], [0, 0, 0, "Unexpected any. Specify a different type.", "7"], [0, 0, 0, "Unexpected any. Specify a different type.", "8"], [0, 0, 0, "Unexpected any. Specify a different type.", "9"], - [0, 0, 0, "Unexpected any. Specify a different type.", "10"], + [0, 0, 0, "Do not use any type assertions.", "10"], [0, 0, 0, "Do not use any type assertions.", "11"], - [0, 0, 0, "Do not use any type assertions.", "12"], + [0, 0, 0, "Unexpected any. Specify a different type.", "12"], [0, 0, 0, "Unexpected any. Specify a different type.", "13"], [0, 0, 0, "Unexpected any. Specify a different type.", "14"], [0, 0, 0, "Unexpected any. Specify a different type.", "15"], @@ -6009,15 +6009,7 @@ exports[`better eslint`] = { [0, 0, 0, "Unexpected any. Specify a different type.", "51"], [0, 0, 0, "Unexpected any. Specify a different type.", "52"], [0, 0, 0, "Unexpected any. Specify a different type.", "53"], - [0, 0, 0, "Unexpected any. Specify a different type.", "54"], - [0, 0, 0, "Unexpected any. Specify a different type.", "55"], - [0, 0, 0, "Unexpected any. Specify a different type.", "56"], - [0, 0, 0, "Unexpected any. Specify a different type.", "57"], - [0, 0, 0, "Unexpected any. Specify a different type.", "58"], - [0, 0, 0, "Unexpected any. Specify a different type.", "59"], - [0, 0, 0, "Unexpected any. Specify a different type.", "60"], - [0, 0, 0, "Unexpected any. Specify a different type.", "61"], - [0, 0, 0, "Unexpected any. Specify a different type.", "62"] + [0, 0, 0, "Unexpected any. Specify a different type.", "54"] ], "public/app/plugins/datasource/graphite/gfunc.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"], @@ -6030,11 +6022,7 @@ exports[`better eslint`] = { [0, 0, 0, "Unexpected any. Specify a different type.", "7"], [0, 0, 0, "Unexpected any. Specify a different type.", "8"], [0, 0, 0, "Unexpected any. Specify a different type.", "9"], - [0, 0, 0, "Unexpected any. Specify a different type.", "10"], - [0, 0, 0, "Unexpected any. Specify a different type.", "11"], - [0, 0, 0, "Unexpected any. Specify a different type.", "12"], - [0, 0, 0, "Unexpected any. Specify a different type.", "13"], - [0, 0, 0, "Unexpected any. Specify a different type.", "14"] + [0, 0, 0, "Unexpected any. Specify a different type.", "10"] ], "public/app/plugins/datasource/graphite/graphite_query.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"], @@ -6052,15 +6040,11 @@ exports[`better eslint`] = { [0, 0, 0, "Unexpected any. Specify a different type.", "12"], [0, 0, 0, "Unexpected any. Specify a different type.", "13"], [0, 0, 0, "Unexpected any. Specify a different type.", "14"], - [0, 0, 0, "Unexpected any. Specify a different type.", "15"], + [0, 0, 0, "Do not use any type assertions.", "15"], [0, 0, 0, "Unexpected any. Specify a different type.", "16"], [0, 0, 0, "Unexpected any. Specify a different type.", "17"], [0, 0, 0, "Unexpected any. Specify a different type.", "18"], - [0, 0, 0, "Do not use any type assertions.", "19"], - [0, 0, 0, "Unexpected any. Specify a different type.", "20"], - [0, 0, 0, "Unexpected any. Specify a different type.", "21"], - [0, 0, 0, "Unexpected any. Specify a different type.", "22"], - [0, 0, 0, "Unexpected any. Specify a different type.", "23"] + [0, 0, 0, "Unexpected any. Specify a different type.", "19"] ], "public/app/plugins/datasource/graphite/lexer.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"], @@ -6074,11 +6058,6 @@ exports[`better eslint`] = { "public/app/plugins/datasource/graphite/migrations.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"] ], - "public/app/plugins/datasource/graphite/parser.ts:5381": [ - [0, 0, 0, "Unexpected any. Specify a different type.", "0"], - [0, 0, 0, "Unexpected any. Specify a different type.", "1"], - [0, 0, 0, "Unexpected any. Specify a different type.", "2"] - ], "public/app/plugins/datasource/graphite/specs/graphite_query.test.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"] ], @@ -6095,17 +6074,14 @@ exports[`better eslint`] = { ], "public/app/plugins/datasource/graphite/state/store.ts:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"], - [0, 0, 0, "Do not use any type assertions.", "1"], - [0, 0, 0, "Do not use any type assertions.", "2"], - [0, 0, 0, "Do not use any type assertions.", "3"] + [0, 0, 0, "Do not use any type assertions.", "1"] ], "public/app/plugins/datasource/graphite/types.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"], [0, 0, 0, "Unexpected any. Specify a different type.", "1"] ], "public/app/plugins/datasource/graphite/utils.ts:5381": [ - [0, 0, 0, "Unexpected any. Specify a different type.", "0"], - [0, 0, 0, "Unexpected any. Specify a different type.", "1"] + [0, 0, 0, "Unexpected any. Specify a different type.", "0"] ], "public/app/plugins/datasource/influxdb/components/editor/config/ConfigEditor.tsx:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"] @@ -6138,9 +6114,7 @@ exports[`better eslint`] = { [0, 0, 0, "Unexpected any. Specify a different type.", "12"], [0, 0, 0, "Unexpected any. Specify a different type.", "13"], [0, 0, 0, "Unexpected any. Specify a different type.", "14"], - [0, 0, 0, "Unexpected any. Specify a different type.", "15"], - [0, 0, 0, "Unexpected any. Specify a different type.", "16"], - [0, 0, 0, "Do not use any type assertions.", "17"] + [0, 0, 0, "Do not use any type assertions.", "15"] ], "public/app/plugins/datasource/influxdb/influx_query_model.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"], @@ -6155,14 +6129,7 @@ exports[`better eslint`] = { [0, 0, 0, "Unexpected any. Specify a different type.", "9"], [0, 0, 0, "Unexpected any. Specify a different type.", "10"], [0, 0, 0, "Unexpected any. Specify a different type.", "11"], - [0, 0, 0, "Unexpected any. Specify a different type.", "12"], - [0, 0, 0, "Unexpected any. Specify a different type.", "13"], - [0, 0, 0, "Unexpected any. Specify a different type.", "14"], - [0, 0, 0, "Unexpected any. Specify a different type.", "15"], - [0, 0, 0, "Unexpected any. Specify a different type.", "16"], - [0, 0, 0, "Unexpected any. Specify a different type.", "17"], - [0, 0, 0, "Unexpected any. Specify a different type.", "18"], - [0, 0, 0, "Unexpected any. Specify a different type.", "19"] + [0, 0, 0, "Unexpected any. Specify a different type.", "12"] ], "public/app/plugins/datasource/influxdb/influx_series.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"], @@ -6210,8 +6177,7 @@ exports[`better eslint`] = { [0, 0, 0, "Unexpected any. Specify a different type.", "12"], [0, 0, 0, "Unexpected any. Specify a different type.", "13"], [0, 0, 0, "Unexpected any. Specify a different type.", "14"], - [0, 0, 0, "Unexpected any. Specify a different type.", "15"], - [0, 0, 0, "Unexpected any. Specify a different type.", "16"] + [0, 0, 0, "Unexpected any. Specify a different type.", "15"] ], "public/app/plugins/datasource/influxdb/response_parser.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"], @@ -6232,14 +6198,8 @@ exports[`better eslint`] = { [0, 0, 0, "Styles should be written using objects.", "1"] ], "public/app/plugins/datasource/jaeger/datasource.ts:5381": [ - [0, 0, 0, "Unexpected any. Specify a different type.", "0"], - [0, 0, 0, "Unexpected any. Specify a different type.", "1"], - [0, 0, 0, "Do not use any type assertions.", "2"], - [0, 0, 0, "Unexpected any. Specify a different type.", "3"], - [0, 0, 0, "Unexpected any. Specify a different type.", "4"], - [0, 0, 0, "Unexpected any. Specify a different type.", "5"], - [0, 0, 0, "Unexpected any. Specify a different type.", "6"], - [0, 0, 0, "Unexpected any. Specify a different type.", "7"] + [0, 0, 0, "Do not use any type assertions.", "0"], + [0, 0, 0, "Unexpected any. Specify a different type.", "1"] ], "public/app/plugins/datasource/jaeger/testResponse.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"], @@ -6248,13 +6208,9 @@ exports[`better eslint`] = { "public/app/plugins/datasource/jaeger/types.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"] ], - "public/app/plugins/datasource/jaeger/util.ts:5381": [ - [0, 0, 0, "Unexpected any. Specify a different type.", "0"] - ], "public/app/plugins/datasource/loki/LanguageProvider.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"], - [0, 0, 0, "Unexpected any. Specify a different type.", "1"], - [0, 0, 0, "Unexpected any. Specify a different type.", "2"] + [0, 0, 0, "Unexpected any. Specify a different type.", "1"] ], "public/app/plugins/datasource/loki/LiveStreams.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"] @@ -6364,8 +6320,7 @@ exports[`better eslint`] = { ], "public/app/plugins/datasource/loki/streaming.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"], - [0, 0, 0, "Do not use any type assertions.", "1"], - [0, 0, 0, "Unexpected any. Specify a different type.", "2"] + [0, 0, 0, "Unexpected any. Specify a different type.", "1"] ], "public/app/plugins/datasource/opentsdb/components/OpenTsdbQueryEditor.tsx:5381": [ [0, 0, 0, "Styles should be written using objects.", "0"], @@ -6458,8 +6413,7 @@ exports[`better eslint`] = { [0, 0, 0, "Unexpected any. Specify a different type.", "0"] ], "public/app/plugins/datasource/prometheus/components/PromQueryField.tsx:5381": [ - [0, 0, 0, "Unexpected any. Specify a different type.", "0"], - [0, 0, 0, "Unexpected any. Specify a different type.", "1"] + [0, 0, 0, "Unexpected any. Specify a different type.", "0"] ], "public/app/plugins/datasource/prometheus/components/PrometheusMetricsBrowser.tsx:5381": [ [0, 0, 0, "Styles should be written using objects.", "0"], @@ -6473,9 +6427,7 @@ exports[`better eslint`] = { [0, 0, 0, "Styles should be written using objects.", "8"], [0, 0, 0, "Styles should be written using objects.", "9"], [0, 0, 0, "Styles should be written using objects.", "10"], - [0, 0, 0, "Styles should be written using objects.", "11"], - [0, 0, 0, "Do not use any type assertions.", "12"], - [0, 0, 0, "Do not use any type assertions.", "13"] + [0, 0, 0, "Styles should be written using objects.", "11"] ], "public/app/plugins/datasource/prometheus/components/monaco-query-field/MonacoQueryField.tsx:5381": [ [0, 0, 0, "Styles should be written using objects.", "0"], @@ -6532,9 +6484,6 @@ exports[`better eslint`] = { [0, 0, 0, "Use data-testid for E2E selectors instead of aria-label", "0"], [0, 0, 0, "Styles should be written using objects.", "1"] ], - "public/app/plugins/datasource/prometheus/configuration/PromSettings.tsx:5381": [ - [0, 0, 0, "Do not use any type assertions.", "0"] - ], "public/app/plugins/datasource/prometheus/datasource.test.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"], [0, 0, 0, "Unexpected any. Specify a different type.", "1"], @@ -6561,7 +6510,7 @@ exports[`better eslint`] = { [0, 0, 0, "Unexpected any. Specify a different type.", "8"], [0, 0, 0, "Unexpected any. Specify a different type.", "9"], [0, 0, 0, "Unexpected any. Specify a different type.", "10"], - [0, 0, 0, "Do not use any type assertions.", "11"], + [0, 0, 0, "Unexpected any. Specify a different type.", "11"], [0, 0, 0, "Unexpected any. Specify a different type.", "12"], [0, 0, 0, "Unexpected any. Specify a different type.", "13"], [0, 0, 0, "Unexpected any. Specify a different type.", "14"], @@ -6569,16 +6518,7 @@ exports[`better eslint`] = { [0, 0, 0, "Unexpected any. Specify a different type.", "16"], [0, 0, 0, "Unexpected any. Specify a different type.", "17"], [0, 0, 0, "Unexpected any. Specify a different type.", "18"], - [0, 0, 0, "Unexpected any. Specify a different type.", "19"], - [0, 0, 0, "Unexpected any. Specify a different type.", "20"], - [0, 0, 0, "Unexpected any. Specify a different type.", "21"], - [0, 0, 0, "Unexpected any. Specify a different type.", "22"], - [0, 0, 0, "Unexpected any. Specify a different type.", "23"], - [0, 0, 0, "Unexpected any. Specify a different type.", "24"], - [0, 0, 0, "Unexpected any. Specify a different type.", "25"], - [0, 0, 0, "Unexpected any. Specify a different type.", "26"], - [0, 0, 0, "Unexpected any. Specify a different type.", "27"], - [0, 0, 0, "Unexpected any. Specify a different type.", "28"] + [0, 0, 0, "Unexpected any. Specify a different type.", "19"] ], "public/app/plugins/datasource/prometheus/language_provider.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"], @@ -6855,17 +6795,13 @@ exports[`better eslint`] = { [0, 0, 0, "Do not use any type assertions.", "3"], [0, 0, 0, "Do not use any type assertions.", "4"], [0, 0, 0, "Unexpected any. Specify a different type.", "5"], - [0, 0, 0, "Unexpected any. Specify a different type.", "6"], - [0, 0, 0, "Unexpected any. Specify a different type.", "7"], - [0, 0, 0, "Unexpected any. Specify a different type.", "8"], + [0, 0, 0, "Do not use any type assertions.", "6"], + [0, 0, 0, "Do not use any type assertions.", "7"], + [0, 0, 0, "Do not use any type assertions.", "8"], [0, 0, 0, "Do not use any type assertions.", "9"], - [0, 0, 0, "Do not use any type assertions.", "10"], - [0, 0, 0, "Do not use any type assertions.", "11"], - [0, 0, 0, "Do not use any type assertions.", "12"], - [0, 0, 0, "Unexpected any. Specify a different type.", "13"], - [0, 0, 0, "Unexpected any. Specify a different type.", "14"], - [0, 0, 0, "Unexpected any. Specify a different type.", "15"], - [0, 0, 0, "Unexpected any. Specify a different type.", "16"] + [0, 0, 0, "Unexpected any. Specify a different type.", "10"], + [0, 0, 0, "Unexpected any. Specify a different type.", "11"], + [0, 0, 0, "Unexpected any. Specify a different type.", "12"] ], "public/app/plugins/datasource/tempo/language_provider.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"] @@ -7111,9 +7047,6 @@ exports[`better eslint`] = { [0, 0, 0, "Unexpected any. Specify a different type.", "0"], [0, 0, 0, "Do not use any type assertions.", "1"] ], - "public/app/plugins/panel/geomap/utils/selection.ts:5381": [ - [0, 0, 0, "Unexpected any. Specify a different type.", "0"] - ], "public/app/plugins/panel/geomap/utils/tooltip.ts:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"] ], @@ -7196,11 +7129,9 @@ exports[`better eslint`] = { [0, 0, 0, "Do not use any type assertions.", "16"] ], "public/app/plugins/panel/histogram/Histogram.tsx:5381": [ - [0, 0, 0, "Unexpected any. Specify a different type.", "0"], - [0, 0, 0, "Unexpected any. Specify a different type.", "1"], - [0, 0, 0, "Do not use any type assertions.", "2"], - [0, 0, 0, "Do not use any type assertions.", "3"], - [0, 0, 0, "Unexpected any. Specify a different type.", "4"] + [0, 0, 0, "Do not use any type assertions.", "0"], + [0, 0, 0, "Do not use any type assertions.", "1"], + [0, 0, 0, "Unexpected any. Specify a different type.", "2"] ], "public/app/plugins/panel/live/LiveChannelEditor.tsx:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"], @@ -7296,9 +7227,7 @@ exports[`better eslint`] = { ], "public/app/plugins/panel/nodeGraph/utils.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"], - [0, 0, 0, "Unexpected any. Specify a different type.", "1"], - [0, 0, 0, "Unexpected any. Specify a different type.", "2"], - [0, 0, 0, "Unexpected any. Specify a different type.", "3"] + [0, 0, 0, "Unexpected any. Specify a different type.", "1"] ], "public/app/plugins/panel/piechart/PieChart.tsx:5381": [ [0, 0, 0, "Use data-testid for E2E selectors instead of aria-label", "0"], @@ -7644,20 +7573,10 @@ exports[`better eslint`] = { [0, 0, 0, "Unexpected any. Specify a different type.", "0"], [0, 0, 0, "Do not use any type assertions.", "1"] ], - "public/app/types/templates.ts:5381": [ - [0, 0, 0, "Unexpected any. Specify a different type.", "0"] - ], "public/app/types/unified-alerting-dto.ts:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"], [0, 0, 0, "Do not use any type assertions.", "1"] ], - "public/test/core/redux/mocks.ts:5381": [ - [0, 0, 0, "Unexpected any. Specify a different type.", "0"] - ], - "public/test/core/redux/reducerTester.ts:5381": [ - [0, 0, 0, "Unexpected any. Specify a different type.", "0"], - [0, 0, 0, "Unexpected any. Specify a different type.", "1"] - ], "public/test/core/redux/reduxTester.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"], [0, 0, 0, "Unexpected any. Specify a different type.", "1"], @@ -7674,15 +7593,11 @@ exports[`better eslint`] = { [0, 0, 0, "Unexpected any. Specify a different type.", "4"], [0, 0, 0, "Unexpected any. Specify a different type.", "5"], [0, 0, 0, "Unexpected any. Specify a different type.", "6"], - [0, 0, 0, "Unexpected any. Specify a different type.", "7"], - [0, 0, 0, "Unexpected any. Specify a different type.", "8"] + [0, 0, 0, "Unexpected any. Specify a different type.", "7"] ], "public/test/global-jquery-shim.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"] ], - "public/test/helpers/convertToStoreState.ts:5381": [ - [0, 0, 0, "Unexpected any. Specify a different type.", "0"] - ], "public/test/helpers/getDashboardModel.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"] ], @@ -7695,20 +7610,11 @@ exports[`better eslint`] = { "public/test/lib/common.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"] ], - "public/test/matchers/index.ts:5381": [ - [0, 0, 0, "Unexpected any. Specify a different type.", "0"] - ], "public/test/matchers/toEmitValuesWith.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"], [0, 0, 0, "Unexpected any. Specify a different type.", "1"], [0, 0, 0, "Unexpected any. Specify a different type.", "2"] ], - "public/test/matchers/utils.ts:5381": [ - [0, 0, 0, "Unexpected any. Specify a different type.", "0"] - ], - "public/test/mocks/workers.ts:5381": [ - [0, 0, 0, "Unexpected any. Specify a different type.", "0"] - ], "public/test/specs/helpers.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"], [0, 0, 0, "Unexpected any. Specify a different type.", "1"], @@ -7723,9 +7629,7 @@ exports[`better eslint`] = { [0, 0, 0, "Unexpected any. Specify a different type.", "10"], [0, 0, 0, "Unexpected any. Specify a different type.", "11"], [0, 0, 0, "Unexpected any. Specify a different type.", "12"], - [0, 0, 0, "Unexpected any. Specify a different type.", "13"], - [0, 0, 0, "Unexpected any. Specify a different type.", "14"], - [0, 0, 0, "Unexpected any. Specify a different type.", "15"] + [0, 0, 0, "Unexpected any. Specify a different type.", "13"] ] }` }; diff --git a/public/app/plugins/datasource/graphite/datasource.ts b/public/app/plugins/datasource/graphite/datasource.ts index 2577d402e2f26..e99e02b85d594 100644 --- a/public/app/plugins/datasource/graphite/datasource.ts +++ b/public/app/plugins/datasource/graphite/datasource.ts @@ -28,7 +28,7 @@ import { getRollupNotice, getRuntimeConsolidationNotice } from 'app/plugins/data import { AnnotationEditor } from './components/AnnotationsEditor'; import { convertToGraphiteQueryObject } from './components/helpers'; -import gfunc, { FuncDefs, FuncInstance } from './gfunc'; +import gfunc, { FuncDef, FuncDefs, FuncInstance } from './gfunc'; import GraphiteQueryModel from './graphite_query'; import { prepareAnnotation } from './migrations'; // Types @@ -78,7 +78,7 @@ export class GraphiteDatasource cacheTimeout: any; withCredentials: boolean; funcDefs: FuncDefs | null = null; - funcDefsPromise: Promise | null = null; + funcDefsPromise: Promise | null = null; _seriesRefLetters: string; requestCounter = 100; private readonly metricMappings: GraphiteLokiMapping[]; @@ -371,7 +371,7 @@ export class GraphiteDatasource return lastValueFrom( this.query(graphiteQuery).pipe( - map((result: any) => { + map((result) => { const list = []; for (let i = 0; i < result.data.length; i++) { @@ -400,7 +400,7 @@ export class GraphiteDatasource } else { // Graphite event/tag as annotation const tags = this.templateSrv.replace(target.tags?.join(' ')); - return this.events({ range: range, tags: tags }).then((results: any) => { + return this.events({ range: range, tags: tags }).then((results) => { const list = []; if (!isArray(results.data)) { console.error(`Unable to get annotations from ${results.url}.`); @@ -827,7 +827,7 @@ export class GraphiteDatasource ); } - createFuncInstance(funcDef: any, options?: any): FuncInstance { + createFuncInstance(funcDef: string | FuncDef, options?: any): FuncInstance { return gfunc.createFuncInstance(funcDef, options, this.funcDefs); } @@ -870,7 +870,7 @@ export class GraphiteDatasource this.funcDefs = gfunc.parseFuncDefs(fixedData); return this.funcDefs; }), - catchError((error: any) => { + catchError((error) => { console.error('Fetching graphite functions error', error); this.funcDefs = gfunc.getFuncDefs(this.graphiteVersion); return of(this.funcDefs); @@ -924,7 +924,7 @@ export class GraphiteDatasource return getBackendSrv() .fetch(options) .pipe( - catchError((err: any) => { + catchError((err) => { return throwError(reduceError(err)); }) ); @@ -941,7 +941,7 @@ export class GraphiteDatasource options['format'] = 'json'; - function fixIntervalFormat(match: any) { + function fixIntervalFormat(match: string) { return match.replace('m', 'min').replace('M', 'mon'); } @@ -1007,7 +1007,7 @@ function supportsFunctionIndex(version: string): boolean { function mapToTags(): OperatorFunction> { return pipe( - map((results: any) => { + map((results) => { if (results.data) { return _map(results.data, (value) => { return { text: value }; diff --git a/public/app/plugins/datasource/graphite/gfunc.ts b/public/app/plugins/datasource/graphite/gfunc.ts index ffec5cfa70306..a8bc886e3e4df 100644 --- a/public/app/plugins/datasource/graphite/gfunc.ts +++ b/public/app/plugins/datasource/graphite/gfunc.ts @@ -1061,7 +1061,7 @@ export class FuncInstance { return str + parameters.join(', ') + ')'; } - _hasMultipleParamsInString(strValue: any, index: number) { + _hasMultipleParamsInString(strValue: string, index: number) { if (strValue.indexOf(',') === -1) { return false; } @@ -1077,7 +1077,7 @@ export class FuncInstance { return false; } - updateParam(strValue: any, index: any) { + updateParam(strValue: string, index: number) { // handle optional parameters // if string contains ',' and next param is optional, split and update both if (this._hasMultipleParamsInString(strValue, index)) { @@ -1109,7 +1109,7 @@ export class FuncInstance { } } -function createFuncInstance(funcDef: any, options?: { withDefaultParams: any }, idx?: any): FuncInstance { +function createFuncInstance(funcDef: FuncDef | string, options?: { withDefaultParams: any }, idx?: any): FuncInstance { if (isString(funcDef)) { funcDef = getFuncDef(funcDef, idx); } diff --git a/public/app/plugins/datasource/graphite/graphite_query.ts b/public/app/plugins/datasource/graphite/graphite_query.ts index c8e7254b78c47..fbf5f9bf32f51 100644 --- a/public/app/plugins/datasource/graphite/graphite_query.ts +++ b/public/app/plugins/datasource/graphite/graphite_query.ts @@ -159,11 +159,11 @@ export default class GraphiteQuery { this.segments.push({ value: 'select metric' }); } - addFunction(newFunc: any) { + addFunction(newFunc: FuncInstance) { this.functions.push(newFunc); } - addFunctionParameter(func: any, value: string) { + addFunctionParameter(func: FuncInstance, value: string) { if (func.params.length >= func.def.params.length && !get(last(func.def.params), 'multiple', false)) { throw { message: 'too many parameters for function ' + func.def.name }; } @@ -174,13 +174,13 @@ export default class GraphiteQuery { this.functions = without(this.functions, func); } - moveFunction(func: any, offset: number) { + moveFunction(func: FuncInstance, offset: number) { const index = this.functions.indexOf(func); arrayMove(this.functions, index, index + offset); } updateModelTarget(targets: any) { - const wrapFunction = (target: string, func: any) => { + const wrapFunction = (target: string, func: FuncInstance) => { return func.render(target, (value: string) => { return this.templateSrv.replace(value, this.scopedVars); }); diff --git a/public/app/plugins/datasource/graphite/parser.ts b/public/app/plugins/datasource/graphite/parser.ts index 5ee0e44332bdb..315d4509c08dc 100644 --- a/public/app/plugins/datasource/graphite/parser.ts +++ b/public/app/plugins/datasource/graphite/parser.ts @@ -276,12 +276,12 @@ export class Parser { return this.tokens[this.index - 1]; } - matchToken(type: any, index: number) { + matchToken(type: string, index: number) { const token = this.tokens[this.index + index]; return (token === undefined && type === '') || (token && token.type === type); } - match(token1: any, token2?: any) { + match(token1: string, token2?: string) { return this.matchToken(token1, 0) && (!token2 || this.matchToken(token2, 1)); } } diff --git a/public/app/plugins/datasource/graphite/state/store.ts b/public/app/plugins/datasource/graphite/state/store.ts index 2bd3842df780f..0081c2bd0eab6 100644 --- a/public/app/plugins/datasource/graphite/state/store.ts +++ b/public/app/plugins/datasource/graphite/state/store.ts @@ -8,7 +8,7 @@ import { TemplateSrv } from '../../../../features/templating/template_srv'; import { GraphiteDatasource } from '../datasource'; import { FuncDefs } from '../gfunc'; import GraphiteQuery, { GraphiteTarget } from '../graphite_query'; -import { GraphiteSegment, GraphiteTagOperator } from '../types'; +import { GraphiteSegment } from '../types'; import { actions } from './actions'; import { @@ -91,7 +91,7 @@ const reducer = async (action: Action, state: GraphiteQueryEditorState): Promise fake: false, }; } else { - segment = segmentOrString as GraphiteSegment; + segment = segmentOrString; } state.error = null; @@ -131,7 +131,7 @@ const reducer = async (action: Action, state: GraphiteQueryEditorState): Promise if (actions.addNewTag.match(action)) { const segment = action.payload.segment; const newTagKey = segment.value; - const newTag = { key: newTagKey, operator: '=' as GraphiteTagOperator, value: '' }; + const newTag = { key: newTagKey, operator: '=' as const, value: '' }; state.queryModel.addTag(newTag); handleTargetChanged(state); } diff --git a/public/app/plugins/datasource/graphite/utils.ts b/public/app/plugins/datasource/graphite/utils.ts index 88f0a5332cd7e..c8de42fac29d7 100644 --- a/public/app/plugins/datasource/graphite/utils.ts +++ b/public/app/plugins/datasource/graphite/utils.ts @@ -8,7 +8,7 @@ import { GraphiteParserError } from './types'; * This function removes all HTML tags and keeps only the last line from the stack * trace which should be the most meaningful. */ -export function reduceError(error: any): any { +export function reduceError(error: any) { if (error && error.status === 500 && error.data?.message?.startsWith('( diff --git a/public/app/plugins/datasource/influxdb/datasource.ts b/public/app/plugins/datasource/influxdb/datasource.ts index 78467ba3a0f5d..91bd95a038277 100644 --- a/public/app/plugins/datasource/influxdb/datasource.ts +++ b/public/app/plugins/datasource/influxdb/datasource.ts @@ -590,7 +590,7 @@ export default class InfluxDatasource extends DataSourceWithBackend { + map((data) => { if (!data || !data.results) { return { data: [] }; } @@ -690,7 +690,7 @@ export default class InfluxDatasource extends DataSourceWithBackend { + return lastValueFrom(this._seriesQuery(query, options)).then((data) => { if (!data || !data.results || !data.results[0]) { throw { message: 'No results in response from InfluxDB' }; } diff --git a/public/app/plugins/datasource/influxdb/influx_query_model.ts b/public/app/plugins/datasource/influxdb/influx_query_model.ts index 676fa04eccd6a..3356e2d40edbe 100644 --- a/public/app/plugins/datasource/influxdb/influx_query_model.ts +++ b/public/app/plugins/datasource/influxdb/influx_query_model.ts @@ -39,7 +39,7 @@ export default class InfluxQueryModel { } updateProjection() { - this.selectModels = map(this.target.select, (parts: any) => { + this.selectModels = map(this.target.select, (parts) => { return map(parts, queryPart.create); }); this.groupByParts = map(this.target.groupBy, queryPart.create); @@ -47,18 +47,18 @@ export default class InfluxQueryModel { updatePersistedParts() { this.target.select = map(this.selectModels, (selectParts) => { - return map(selectParts, (part: any) => { + return map(selectParts, (part) => { return { type: part.def.type, params: part.params }; }); }); } hasGroupByTime() { - return find(this.target.groupBy, (g: any) => g.type === 'time'); + return find(this.target.groupBy, (g) => g.type === 'time'); } hasFill() { - return find(this.target.groupBy, (g: any) => g.type === 'fill'); + return find(this.target.groupBy, (g) => g.type === 'fill'); } addGroupBy(value: string) { @@ -95,10 +95,10 @@ export default class InfluxQueryModel { if (part.def.type === 'time') { // remove fill - this.target.groupBy = filter(this.target.groupBy, (g: any) => g.type !== 'fill'); + this.target.groupBy = filter(this.target.groupBy, (g) => g.type !== 'fill'); // remove aggregations - this.target.select = map(this.target.select, (s: any) => { - return filter(s, (part: any) => { + this.target.select = map(this.target.select, (s) => { + return filter(s, (part) => { const partModel = queryPart.create(part); if (partModel.def.category === categories.Aggregations) { return false; diff --git a/public/app/plugins/datasource/influxdb/query_part.ts b/public/app/plugins/datasource/influxdb/query_part.ts index d37adbafa7fe8..a2bb8f2cd1094 100644 --- a/public/app/plugins/datasource/influxdb/query_part.ts +++ b/public/app/plugins/datasource/influxdb/query_part.ts @@ -140,7 +140,7 @@ function addAliasStrategy(selectParts: any[], partModel: any) { function addFieldStrategy(selectParts: any, partModel: any, query: { selectModels: any[][] }) { // copy all parts - const parts = map(selectParts, (part: any) => { + const parts = map(selectParts, (part) => { return createPart({ type: part.def.type, params: clone(part.params) }); }); diff --git a/public/app/plugins/datasource/jaeger/datasource.ts b/public/app/plugins/datasource/jaeger/datasource.ts index a2d10cc0f8d6a..5333b60f0c5e1 100644 --- a/public/app/plugins/datasource/jaeger/datasource.ts +++ b/public/app/plugins/datasource/jaeger/datasource.ts @@ -47,7 +47,7 @@ export class JaegerDatasource extends DataSourceApi this.traceIdTimeParams = instanceSettings.jsonData.traceIdTimeParams; } - async metadataRequest(url: string, params?: Record): Promise { + async metadataRequest(url: string, params?: Record) { const res = await lastValueFrom(this._request(url, params, { hideFromInspector: true })); return res.data.data; } @@ -180,11 +180,11 @@ export class JaegerDatasource extends DataSourceApi }; } - async testDatasource(): Promise { + async testDatasource() { return lastValueFrom( this._request('/api/services').pipe( map((res) => { - const values: any[] = res?.data?.data || []; + const values = res?.data?.data || []; const testResult = values.length > 0 ? { status: 'success', message: 'Data source connected and services found.' } @@ -195,7 +195,7 @@ export class JaegerDatasource extends DataSourceApi }; return testResult; }), - catchError((err: any) => { + catchError((err) => { let message = 'Jaeger: '; if (err.statusText) { message += err.statusText; @@ -230,7 +230,11 @@ export class JaegerDatasource extends DataSourceApi return query.query || ''; } - private _request(apiUrl: string, data?: any, options?: Partial): Observable> { + private _request( + apiUrl: string, + data?: Record, + options?: Partial + ): Observable> { const params = data ? serializeParams(data) : ''; const url = `${this.instanceSettings.url}${apiUrl}${params.length ? `?${params}` : ''}`; const req = { diff --git a/public/app/plugins/datasource/jaeger/util.ts b/public/app/plugins/datasource/jaeger/util.ts index 51f01fa929a33..f025ed9df5572 100644 --- a/public/app/plugins/datasource/jaeger/util.ts +++ b/public/app/plugins/datasource/jaeger/util.ts @@ -4,7 +4,7 @@ export function convertTagsLogfmt(tags: string | undefined) { if (!tags) { return ''; } - const data: any = logfmt.parse(tags); + const data = logfmt.parse(tags); Object.keys(data).forEach((key) => { const value = data[key]; if (typeof value !== 'string') { diff --git a/public/app/plugins/datasource/loki/LanguageProvider.ts b/public/app/plugins/datasource/loki/LanguageProvider.ts index 960dfcbfa894c..7cf74f9811334 100644 --- a/public/app/plugins/datasource/loki/LanguageProvider.ts +++ b/public/app/plugins/datasource/loki/LanguageProvider.ts @@ -37,7 +37,7 @@ export default class LokiLanguageProvider extends LanguageProvider { Object.assign(this, initialValues); } - request = async (url: string, params?: any): Promise => { + request = async (url: string, params?: any) => { try { return await this.datasource.metadataRequest(url, params); } catch (error) { diff --git a/public/app/plugins/datasource/loki/streaming.ts b/public/app/plugins/datasource/loki/streaming.ts index edd2878311877..953e74337dffa 100644 --- a/public/app/plugins/datasource/loki/streaming.ts +++ b/public/app/plugins/datasource/loki/streaming.ts @@ -45,7 +45,7 @@ export function doLokiChannelStream( let frame: StreamingDataFrame | undefined = undefined; const updateFrame = (msg: any) => { if (msg?.message) { - const p = msg.message as DataFrameJSON; + const p: DataFrameJSON = msg.message; if (!frame) { frame = StreamingDataFrame.fromDataFrameJSON(p, { maxLength, diff --git a/public/app/plugins/datasource/prometheus/components/PromQueryField.tsx b/public/app/plugins/datasource/prometheus/components/PromQueryField.tsx index 97e010b0b5fd9..1986901cca36c 100644 --- a/public/app/plugins/datasource/prometheus/components/PromQueryField.tsx +++ b/public/app/plugins/datasource/prometheus/components/PromQueryField.tsx @@ -50,8 +50,8 @@ interface PromQueryFieldState { class PromQueryField extends React.PureComponent { declare languageProviderInitializationPromise: CancelablePromise; - constructor(props: PromQueryFieldProps, context: React.Context) { - super(props, context); + constructor(props: PromQueryFieldProps) { + super(props); this.state = { labelBrowserVisible: false, diff --git a/public/app/plugins/datasource/prometheus/components/PrometheusMetricsBrowser.tsx b/public/app/plugins/datasource/prometheus/components/PrometheusMetricsBrowser.tsx index becc8b401f285..2ef3b2694ee1b 100644 --- a/public/app/plugins/datasource/prometheus/components/PrometheusMetricsBrowser.tsx +++ b/public/app/plugins/datasource/prometheus/components/PrometheusMetricsBrowser.tsx @@ -500,7 +500,7 @@ export class UnthemedPrometheusMetricsBrowser extends React.Component (metrics!.values as FacettableValue[])[i].name} + itemKey={(i) => metrics!.values![i].name} width={300} className={styles.valueList} > @@ -589,7 +589,7 @@ export class UnthemedPrometheusMetricsBrowser extends React.Component (label.values as FacettableValue[])[i].name} + itemKey={(i) => label.values![i].name} width={200} className={styles.valueList} > diff --git a/public/app/plugins/datasource/prometheus/configuration/PromSettings.tsx b/public/app/plugins/datasource/prometheus/configuration/PromSettings.tsx index d310c8044180b..2eb17508cfcea 100644 --- a/public/app/plugins/datasource/prometheus/configuration/PromSettings.tsx +++ b/public/app/plugins/datasource/prometheus/configuration/PromSettings.tsx @@ -549,11 +549,11 @@ export const getValueFromEventItem = (eventItem: SyntheticEvent).value; + return eventItem.value; }; const onChangeHandler = diff --git a/public/app/plugins/datasource/prometheus/datasource.ts b/public/app/plugins/datasource/prometheus/datasource.ts index ccecbe67f29cb..02218a579e0b3 100644 --- a/public/app/plugins/datasource/prometheus/datasource.ts +++ b/public/app/plugins/datasource/prometheus/datasource.ts @@ -227,7 +227,7 @@ export class PrometheusDatasource * request. Any processing done here needs to be also copied on the backend as this goes through data source proxy * but not through the same code as alerting. */ - _request( + _request( url: string, data: Record | null, overrides: Partial = {} @@ -369,7 +369,7 @@ export class PrometheusDatasource delete instantTarget.maxDataPoints; // Create range target - const rangeTarget: any = cloneDeep(target); + const rangeTarget = cloneDeep(target); rangeTarget.format = 'time_series'; rangeTarget.instant = false; instantTarget.range = true; @@ -398,7 +398,7 @@ export class PrometheusDatasource ); // If running only instant query in Explore, format as table } else if (target.instant && options.app === CoreApp.Explore) { - const instantTarget: any = cloneDeep(target); + const instantTarget = cloneDeep(target); instantTarget.format = 'table'; queries.push(this.createQuery(instantTarget, options, start, end)); activeTargets.push(instantTarget); @@ -536,18 +536,19 @@ export class PrometheusDatasource // (should hold until there is some streaming requests involved). tap(() => runningQueriesCount--), filter((response: any) => (response.cancelled ? false : true)), - map((response: any) => { + map((response) => { const data = transform(response, { query, target, responseListLength: queries.length, exemplarTraceIdDestinations: this.exemplarTraceIdDestinations, }); - return { + const result: DataQueryResponse = { data, key: query.requestId, state: runningQueriesCount === 0 ? LoadingState.Done : LoadingState.Loading, - } as DataQueryResponse; + }; + return result; }) ); @@ -569,7 +570,7 @@ export class PrometheusDatasource const filterAndMapResponse = pipe( filter((response: any) => (response.cancelled ? false : true)), - map((response: any) => { + map((response) => { const data = transform(response, { query, target, @@ -1174,11 +1175,7 @@ export class PrometheusDatasource } // Used when running queries through backend - applyTemplateVariables( - target: PromQuery, - scopedVars: ScopedVars, - filters?: AdHocVariableFilter[] - ): Record { + applyTemplateVariables(target: PromQuery, scopedVars: ScopedVars, filters?: AdHocVariableFilter[]) { const variables = cloneDeep(scopedVars); // We want to interpolate these variables on backend @@ -1302,10 +1299,10 @@ export function extractRuleMappingFromGroups(groups: any[]) { // NOTE: these two functions are very similar to the escapeLabelValueIn* functions // in language_utils.ts, but they are not exactly the same algorithm, and we found // no way to reuse one in the another or vice versa. -export function prometheusRegularEscape(value: any) { +export function prometheusRegularEscape(value: unknown) { return typeof value === 'string' ? value.replace(/\\/g, '\\\\').replace(/'/g, "\\\\'") : value; } -export function prometheusSpecialRegexEscape(value: any) { +export function prometheusSpecialRegexEscape(value: unknown) { return typeof value === 'string' ? value.replace(/\\/g, '\\\\\\\\').replace(/[$^*{}\[\]\'+?.()|]/g, '\\\\$&') : value; } diff --git a/public/app/plugins/datasource/tempo/datasource.ts b/public/app/plugins/datasource/tempo/datasource.ts index 476d2d46a5851..73898ceb31bb0 100644 --- a/public/app/plugins/datasource/tempo/datasource.ts +++ b/public/app/plugins/datasource/tempo/datasource.ts @@ -18,6 +18,7 @@ import { LoadingState, rangeUtil, ScopedVars, + TestDataSourceResponse, } from '@grafana/data'; import { BackendSrvRequest, @@ -496,7 +497,7 @@ export class TempoDatasource extends DataSourceWithBackend { + applyTemplateVariables(query: TempoQuery, scopedVars: ScopedVars) { return this.applyVariables(query, scopedVars); } @@ -685,7 +686,11 @@ export class TempoDatasource extends DataSourceWithBackend): Observable> { + private _request( + apiUrl: string, + data?: unknown, + options?: Partial + ): Observable> { const params = data ? serializeParams(data) : ''; const url = `${this.instanceSettings.url}${apiUrl}${params.length ? `?${params}` : ''}`; const req = { ...options, url }; @@ -693,7 +698,7 @@ export class TempoDatasource extends DataSourceWithBackend { + async testDatasource(): Promise { const options: BackendSrvRequest = { headers: {}, method: 'GET', @@ -1297,7 +1302,7 @@ export function getRateAlignedValues( return values; } -export function makeServiceGraphViewRequest(metrics: any[]) { +export function makeServiceGraphViewRequest(metrics: string[]): PromQuery[] { return metrics.map((metric) => { return { refId: metric, diff --git a/public/app/plugins/panel/geomap/utils/selection.ts b/public/app/plugins/panel/geomap/utils/selection.ts index b164c7fe9bfe5..a666ef0049e44 100644 --- a/public/app/plugins/panel/geomap/utils/selection.ts +++ b/public/app/plugins/panel/geomap/utils/selection.ts @@ -1,6 +1,6 @@ import { SelectableValue } from '@grafana/data'; -export interface SelectionInfo { +export interface SelectionInfo { options: Array>; current?: SelectableValue; } diff --git a/public/app/plugins/panel/histogram/Histogram.tsx b/public/app/plugins/panel/histogram/Histogram.tsx index 77d3c3127bd29..29852eb572874 100644 --- a/public/app/plugins/panel/histogram/Histogram.tsx +++ b/public/app/plugins/panel/histogram/Histogram.tsx @@ -157,8 +157,8 @@ const prepConfig = (frame: DataFrame, theme: GrafanaTheme2) => { incrs: isOrdinalX ? [1] : useLogScale ? undefined : histogramBucketSizes, splits: useLogScale || isOrdinalX ? undefined : xSplits, values: isOrdinalX - ? (u: uPlot, splits: any[]) => splits - : (u: uPlot, splits: any[]) => { + ? (u, splits) => splits + : (u, splits) => { const tickLabels = splits.map(xAxisFormatter); const maxWidth = tickLabels.reduce( diff --git a/public/app/plugins/panel/nodeGraph/utils.ts b/public/app/plugins/panel/nodeGraph/utils.ts index 97fb11e30618c..728026b95d0a8 100644 --- a/public/app/plugins/panel/nodeGraph/utils.ts +++ b/public/app/plugins/panel/nodeGraph/utils.ts @@ -251,8 +251,8 @@ function computableField(field?: Field) { * @param edgeFields */ function normalizeStatsForNodes(nodesMap: { [id: string]: NodeDatumFromEdge }, edgeFields: EdgeFields): NodeDatum[] { - const secondaryStatValues: any[] = []; - const mainStatValues: any[] = []; + const secondaryStatValues: Array = []; + const mainStatValues: Array = []; const secondaryStatField = computableField(edgeFields.secondaryStat) ? { ...edgeFields.secondaryStat!, diff --git a/public/app/types/templates.ts b/public/app/types/templates.ts index 897044a93fe5b..17bb0edb3fbf9 100644 --- a/public/app/types/templates.ts +++ b/public/app/types/templates.ts @@ -1,5 +1,5 @@ export interface Variable { name: string; type: string; - current: any; + current: unknown; } diff --git a/public/test/core/redux/mocks.ts b/public/test/core/redux/mocks.ts index a65bf971c7f05..e6ad9c835ec46 100644 --- a/public/test/core/redux/mocks.ts +++ b/public/test/core/redux/mocks.ts @@ -1,6 +1,6 @@ import { ActionCreatorWithoutPayload, PayloadActionCreator } from '@reduxjs/toolkit'; -export const mockToolkitActionCreator = (creator: PayloadActionCreator) => { +export const mockToolkitActionCreator =

    (creator: PayloadActionCreator) => { return Object.assign(jest.fn(), creator); }; diff --git a/public/test/core/redux/reducerTester.ts b/public/test/core/redux/reducerTester.ts index 145dadb0e9a61..55d5378cff199 100644 --- a/public/test/core/redux/reducerTester.ts +++ b/public/test/core/redux/reducerTester.ts @@ -2,7 +2,9 @@ import { AnyAction } from '@reduxjs/toolkit'; import { cloneDeep } from 'lodash'; import { Action } from 'redux'; -type GrafanaReducer = (state: S, action: A) => S; +import { StoreState } from 'app/types'; + +type GrafanaReducer = (state: S, action: A) => S; export interface Given { givenReducer: ( @@ -23,10 +25,6 @@ export interface Then { whenActionIsDispatched: (action: AnyAction) => Then; } -interface ObjectType extends Object { - [key: string]: any; -} - export const deepFreeze = (obj: T): T => { Object.freeze(obj); @@ -37,7 +35,7 @@ export const deepFreeze = (obj: T): T => { const hasOwnProp = Object.prototype.hasOwnProperty; if (obj && obj instanceof Object) { - const object: ObjectType = obj; + const object: Record = obj; Object.getOwnPropertyNames(object).forEach((propertyName) => { const objectProperty = object[propertyName]; if ( diff --git a/public/test/core/thunk/thunkTester.ts b/public/test/core/thunk/thunkTester.ts index 8afedcb53bf76..9e228630d2ec4 100644 --- a/public/test/core/thunk/thunkTester.ts +++ b/public/test/core/thunk/thunkTester.ts @@ -1,4 +1,3 @@ -// @ts-ignore import { PayloadAction } from '@reduxjs/toolkit'; import configureMockStore from 'redux-mock-store'; import thunk from 'redux-thunk'; @@ -13,7 +12,7 @@ export interface ThunkWhen { whenThunkIsDispatched: (...args: any) => Promise>>; } -export const thunkTester = (initialState: any, debug?: boolean): ThunkGiven => { +export const thunkTester = (initialState: unknown, debug?: boolean): ThunkGiven => { const store = mockStore(initialState); let thunkUnderTest: any = null; let dispatchedActions: Array> = []; diff --git a/public/test/helpers/convertToStoreState.ts b/public/test/helpers/convertToStoreState.ts index 8a93487ae02e3..9f696b0e45a6a 100644 --- a/public/test/helpers/convertToStoreState.ts +++ b/public/test/helpers/convertToStoreState.ts @@ -1,8 +1,11 @@ +import { TypedVariableModel } from '@grafana/data'; + import { getPreloadedState } from '../../app/features/variables/state/helpers'; +import { VariablesState } from '../../app/features/variables/state/types'; import { StoreState } from '../../app/types'; -export const convertToStoreState = (key: string, models: any[]): StoreState => { - const variables = models.reduce((byName, variable) => { +export const convertToStoreState = (key: string, models: TypedVariableModel[]): StoreState => { + const variables = models.reduce((byName, variable) => { byName[variable.name] = variable; return byName; }, {}); diff --git a/public/test/matchers/index.ts b/public/test/matchers/index.ts index b6f8a091aaa51..b62de8d6974e9 100644 --- a/public/test/matchers/index.ts +++ b/public/test/matchers/index.ts @@ -4,7 +4,7 @@ import { toEmitValues } from './toEmitValues'; import { toEmitValuesWith } from './toEmitValuesWith'; import { ObservableMatchers } from './types'; -export const matchers: ObservableMatchers> = { +export const matchers: ObservableMatchers> = { toEmitValues, toEmitValuesWith, }; diff --git a/public/test/matchers/utils.ts b/public/test/matchers/utils.ts index e7758bb917786..08a57f060b8f9 100644 --- a/public/test/matchers/utils.ts +++ b/public/test/matchers/utils.ts @@ -3,7 +3,10 @@ import { asapScheduler, Subscription, timer, isObservable } from 'rxjs'; import { OBSERVABLE_TEST_TIMEOUT_IN_MS } from './types'; -export function forceObservableCompletion(subscription: Subscription, resolve: (args: any) => void) { +export function forceObservableCompletion( + subscription: Subscription, + resolve: (args: jest.CustomMatcherResult | PromiseLike) => void +) { const timeoutObservable = timer(OBSERVABLE_TEST_TIMEOUT_IN_MS, asapScheduler); subscription.add( diff --git a/public/test/mocks/workers.ts b/public/test/mocks/workers.ts index 2b7802a4a1f80..ddba6e1e386f7 100644 --- a/public/test/mocks/workers.ts +++ b/public/test/mocks/workers.ts @@ -1,9 +1,12 @@ +import { Config } from 'app/plugins/panel/nodeGraph/layout'; +import { EdgeDatum, NodeDatum } from 'app/plugins/panel/nodeGraph/types'; + const { layout } = jest.requireActual('../../app/plugins/panel/nodeGraph/layout.worker.js'); class LayoutMockWorker { timeout: number | undefined; constructor() {} - postMessage(data: any) { + postMessage(data: { nodes: NodeDatum[]; edges: EdgeDatum[]; config: Config }) { const { nodes, edges, config } = data; this.timeout = window.setTimeout(() => { this.timeout = undefined; diff --git a/public/test/specs/helpers.ts b/public/test/specs/helpers.ts index 303421c4584a1..d094d019b6aed 100644 --- a/public/test/specs/helpers.ts +++ b/public/test/specs/helpers.ts @@ -129,7 +129,7 @@ export class TimeSrvStub { }; } - setTime(time: any) { + setTime(time: RawTimeRange) { this.time = time; } } @@ -161,7 +161,7 @@ export function TemplateSrvStub(this: any) { return template(text, this.templateSettings)(this.data); }; this.init = () => {}; - this.getAdhocFilters = (): any => { + this.getAdhocFilters = () => { return []; }; this.fillVariableValuesForUrl = () => {}; From 20b4cebc475d2d2debd01278dda16a77272c4dfb Mon Sep 17 00:00:00 2001 From: Jack Baldry Date: Mon, 6 Nov 2023 11:58:10 +0000 Subject: [PATCH 107/869] Use versioned action to update `make-docs` procedure (#77694) https://github.com/grafana/writers-toolkit/blob/main/update-make-docs/action.yml Signed-off-by: Jack Baldry --- .github/workflows/update-make-docs.yml | 33 ++++++++------------------ 1 file changed, 10 insertions(+), 23 deletions(-) diff --git a/.github/workflows/update-make-docs.yml b/.github/workflows/update-make-docs.yml index fd223de30687d..09159af49e718 100644 --- a/.github/workflows/update-make-docs.yml +++ b/.github/workflows/update-make-docs.yml @@ -2,31 +2,18 @@ name: Update `make docs` procedure on: schedule: - cron: '0 7 * * 1-5' + workflow_dispatch: jobs: main: if: github.repository == 'grafana/grafana' runs-on: ubuntu-latest steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Update procedure - run: | - BRANCH=update-make-docs - git checkout -b "${BRANCH}" - curl -s -Lo docs/docs.mk https://raw.githubusercontent.com/grafana/writers-toolkit/main/docs/docs.mk - curl -s -Lo docs/make-docs https://raw.githubusercontent.com/grafana/writers-toolkit/main/docs/make-docs - if git diff --exit-code; then exit 0; fi - git add . - git config --local user.email "bot@grafana.com" - git config --local user.name "grafanabot" - git commit --message "Update \`make docs\` procedure" - git push -v origin "refs/heads/${BRANCH}" - gh pr create --fill \ - --label 'backport v10.0.x' \ - --label 'backport v10.1.x' \ - --label 'backport v10.2.x' \ - --label no-changelog \ - --label type/docs || true - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - uses: actions/checkout@v4 + - uses: grafana/writers-toolkit/update-make-docs@update-make-docs/v1 + with: + pr_options: > + --label 'backport v10.0.x' + --label 'backport v10.1.x' + --label 'backport v10.2.x' + --label no-changelog + --label type/docs From f999fe3d12cecb4e5be462cea823ffd17d9a2582 Mon Sep 17 00:00:00 2001 From: Sofia Papagiannaki <1632407+papagian@users.noreply.github.com> Date: Mon, 6 Nov 2023 15:16:23 +0200 Subject: [PATCH 108/869] Search: Modify query for better performance (#77576) * Add missing `org_id` in query condition * Update benchmarks --- pkg/api/folder_bench_test.go | 64 +++++++- .../sqlstore/permissions/dashboard.go | 20 ++- .../dashboard_filter_no_subquery.go | 12 +- .../sqlstore/searchstore/search_test.go | 152 +++++++++++++----- 4 files changed, 184 insertions(+), 64 deletions(-) diff --git a/pkg/api/folder_bench_test.go b/pkg/api/folder_bench_test.go index 9a52eec53e42a..407addf94b58a 100644 --- a/pkg/api/folder_bench_test.go +++ b/pkg/api/folder_bench_test.go @@ -96,49 +96,97 @@ func BenchmarkFolderListAndSearch(b *testing.B) { features *featuremgmt.FeatureManager }{ { - desc: "get root folders with nested folders feature enabled", + desc: "impl=default nested_folders=on get root folders", + url: "/api/folders", + expectedLen: LEVEL0_FOLDER_NUM, + features: featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders), + }, + { + desc: "impl=permissionsFilterRemoveSubquery nested_folders=on get root folders", url: "/api/folders", expectedLen: LEVEL0_FOLDER_NUM, features: featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders, featuremgmt.FlagPermissionsFilterRemoveSubquery), }, { - desc: "get subfolders with nested folders feature enabled", + desc: "impl=default nested_folders=on get subfolders", + url: "/api/folders?parentUid=folder0", + expectedLen: LEVEL1_FOLDER_NUM, + features: featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders), + }, + { + desc: "impl=permissionsFilterRemoveSubquery nested_folders=on get subfolders", url: "/api/folders?parentUid=folder0", expectedLen: LEVEL1_FOLDER_NUM, features: featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders, featuremgmt.FlagPermissionsFilterRemoveSubquery), }, { - desc: "list all inherited dashboards with nested folders feature enabled", + desc: "impl=default nested_folders=on list all inherited dashboards", + url: "/api/search?type=dash-db&limit=5000", + expectedLen: withLimit(all), + features: featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders), + }, + { + desc: "impl=permissionsFilterRemoveSubquery nested_folders=on list all inherited dashboards", url: "/api/search?type=dash-db&limit=5000", expectedLen: withLimit(all), features: featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders, featuremgmt.FlagPermissionsFilterRemoveSubquery), }, { - desc: "search for pattern with nested folders feature enabled", + desc: "impl=default nested_folders=on search for pattern", + url: "/api/search?type=dash-db&query=dashboard_0_0&limit=5000", + expectedLen: withLimit(1 + LEVEL1_DASHBOARD_NUM + LEVEL2_FOLDER_NUM*LEVEL2_DASHBOARD_NUM), + features: featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders), + }, + { + desc: "impl=permissionsFilterRemoveSubquery nested_folders=on search for pattern", url: "/api/search?type=dash-db&query=dashboard_0_0&limit=5000", expectedLen: withLimit(1 + LEVEL1_DASHBOARD_NUM + LEVEL2_FOLDER_NUM*LEVEL2_DASHBOARD_NUM), features: featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders, featuremgmt.FlagPermissionsFilterRemoveSubquery), }, { - desc: "search for specific dashboard nested folders feature enabled", + desc: "impl=default nested_folders=on search for specific dashboard", + url: "/api/search?type=dash-db&query=dashboard_0_0_0_0", + expectedLen: 1, + features: featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders), + }, + { + desc: "impl=permissionsFilterRemoveSubquery nested_folders=on search for specific dashboard", url: "/api/search?type=dash-db&query=dashboard_0_0_0_0", expectedLen: 1, features: featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders, featuremgmt.FlagPermissionsFilterRemoveSubquery), }, { - desc: "get root folders with nested folders feature disabled", + desc: "impl=default nested_folders=off get root folders", + url: "/api/folders?limit=5000", + expectedLen: withLimit(LEVEL0_FOLDER_NUM), + features: featuremgmt.WithFeatures(), + }, + { + desc: "impl=permissionsFilterRemoveSubquery nested_folders=off get root folders", url: "/api/folders?limit=5000", expectedLen: withLimit(LEVEL0_FOLDER_NUM), features: featuremgmt.WithFeatures(featuremgmt.FlagPermissionsFilterRemoveSubquery), }, { - desc: "list all dashboards with nested folders feature disabled", + desc: "impl=default nested_folders=off list all dashboards", + url: "/api/search?type=dash-db&limit=5000", + expectedLen: withLimit(LEVEL0_FOLDER_NUM * LEVEL0_DASHBOARD_NUM), + features: featuremgmt.WithFeatures(), + }, + { + desc: "impl=permissionsFilterRemoveSubquery nested_folders=off list all dashboards", url: "/api/search?type=dash-db&limit=5000", expectedLen: withLimit(LEVEL0_FOLDER_NUM * LEVEL0_DASHBOARD_NUM), features: featuremgmt.WithFeatures(featuremgmt.FlagPermissionsFilterRemoveSubquery), }, { - desc: "search specific dashboard with nested folders feature disabled", + desc: "impl=default nested_folders=off search specific dashboard", + url: "/api/search?type=dash-db&query=dashboard_0_0", + expectedLen: 1, + features: featuremgmt.WithFeatures(), + }, + { + desc: "impl=permissionsFilterRemoveSubquery nested_folders=off search specific dashboard", url: "/api/search?type=dash-db&query=dashboard_0_0", expectedLen: 1, features: featuremgmt.WithFeatures(featuremgmt.FlagPermissionsFilterRemoveSubquery), diff --git a/pkg/services/sqlstore/permissions/dashboard.go b/pkg/services/sqlstore/permissions/dashboard.go index 27cc1235e8bc4..af8852d930f4a 100644 --- a/pkg/services/sqlstore/permissions/dashboard.go +++ b/pkg/services/sqlstore/permissions/dashboard.go @@ -41,7 +41,7 @@ type PermissionsFilter interface { Where() (string, []any) buildClauses() - nestedFoldersSelectors(permSelector string, permSelectorArgs []any, leftTableCol string, rightTableCol string) (string, []any) + nestedFoldersSelectors(permSelector string, permSelectorArgs []any, leftTableCol string, rightTableCol string, orgID int64) (string, []any) } // NewAccessControlDashboardPermissionFilter creates a new AccessControlDashboardPermissionFilter that is configured with specific actions calculated based on the dashboards.PermissionType and query type @@ -126,7 +126,8 @@ func (f *accessControlDashboardPermissionFilter) buildClauses() { userID, _ = identity.IntIdentifier(namespaceID, identifier) } - filter, params := accesscontrol.UserRolesFilter(f.user.GetOrgID(), userID, f.user.GetTeams(), accesscontrol.GetOrgRoles(f.user)) + orgID := f.user.GetOrgID() + filter, params := accesscontrol.UserRolesFilter(orgID, userID, f.user.GetTeams(), accesscontrol.GetOrgRoles(f.user)) rolesFilter := " AND role_id IN(SELECT id FROM role " + filter + ") " var args []any builder := strings.Builder{} @@ -208,9 +209,10 @@ func (f *accessControlDashboardPermissionFilter) buildClauses() { builder.WriteString("(dashboard.folder_id IN (SELECT d.id FROM dashboard as d ") recQueryName := fmt.Sprintf("RecQry%d", len(f.recQueries)) f.addRecQry(recQueryName, permSelector.String(), permSelectorArgs) - builder.WriteString(fmt.Sprintf("WHERE d.uid IN (SELECT uid FROM %s)", recQueryName)) + builder.WriteString(fmt.Sprintf("WHERE d.org_id = ? AND d.uid IN (SELECT uid FROM %s)", recQueryName)) + args = append(args, orgID) default: - nestedFoldersSelectors, nestedFoldersArgs := f.nestedFoldersSelectors(permSelector.String(), permSelectorArgs, "dashboard.folder_id", "d.id") + nestedFoldersSelectors, nestedFoldersArgs := f.nestedFoldersSelectors(permSelector.String(), permSelectorArgs, "dashboard.folder_id", "d.id", orgID) builder.WriteRune('(') builder.WriteString(nestedFoldersSelectors) args = append(args, nestedFoldersArgs...) @@ -222,7 +224,8 @@ func (f *accessControlDashboardPermissionFilter) buildClauses() { default: builder.WriteString("(dashboard.folder_id IN (SELECT d.id FROM dashboard as d ") if len(permSelectorArgs) > 0 { - builder.WriteString("WHERE d.uid IN ") + builder.WriteString("WHERE d.org_id = ? AND d.uid IN ") + args = append(args, orgID) builder.WriteString(permSelector.String()) args = append(args, permSelectorArgs...) } else { @@ -287,7 +290,7 @@ func (f *accessControlDashboardPermissionFilter) buildClauses() { builder.WriteString("(dashboard.uid IN ") builder.WriteString(fmt.Sprintf("(SELECT uid FROM %s)", recQueryName)) default: - nestedFoldersSelectors, nestedFoldersArgs := f.nestedFoldersSelectors(permSelector.String(), permSelectorArgs, "dashboard.uid", "d.uid") + nestedFoldersSelectors, nestedFoldersArgs := f.nestedFoldersSelectors(permSelector.String(), permSelectorArgs, "dashboard.uid", "d.uid", orgID) builder.WriteRune('(') builder.WriteString(nestedFoldersSelectors) builder.WriteRune(')') @@ -372,7 +375,7 @@ func actionsToCheck(actions []string, permissions map[string][]string, wildcards return toCheck } -func (f *accessControlDashboardPermissionFilter) nestedFoldersSelectors(permSelector string, permSelectorArgs []any, leftTableCol string, rightTableCol string) (string, []any) { +func (f *accessControlDashboardPermissionFilter) nestedFoldersSelectors(permSelector string, permSelectorArgs []any, leftTableCol string, rightTableCol string, orgID int64) (string, []any) { wheres := make([]string, 0, folder.MaxNestedFolderDepth+1) args := make([]any, 0, len(permSelectorArgs)*(folder.MaxNestedFolderDepth+1)) @@ -387,7 +390,8 @@ func (f *accessControlDashboardPermissionFilter) nestedFoldersSelectors(permSele s := fmt.Sprintf(tmpl, t, prev, onCol, t, prev, t) joins = append(joins, s) - wheres = append(wheres, fmt.Sprintf("(%s IN (SELECT %s FROM dashboard d %s WHERE %s.uid IN %s)", leftTableCol, rightTableCol, strings.Join(joins, " "), t, permSelector)) + wheres = append(wheres, fmt.Sprintf("(%s IN (SELECT %s FROM dashboard d %s WHERE %s.org_id = ? AND %s.uid IN %s)", leftTableCol, rightTableCol, strings.Join(joins, " "), t, t, permSelector)) + args = append(args, orgID) args = append(args, permSelectorArgs...) prev = t diff --git a/pkg/services/sqlstore/permissions/dashboard_filter_no_subquery.go b/pkg/services/sqlstore/permissions/dashboard_filter_no_subquery.go index 2507c21fba932..e4b13db9c4428 100644 --- a/pkg/services/sqlstore/permissions/dashboard_filter_no_subquery.go +++ b/pkg/services/sqlstore/permissions/dashboard_filter_no_subquery.go @@ -40,7 +40,8 @@ func (f *accessControlDashboardPermissionFilterNoFolderSubquery) buildClauses() userID, _ = identity.IntIdentifier(namespaceID, identifier) } - filter, params := accesscontrol.UserRolesFilter(f.user.GetOrgID(), userID, f.user.GetTeams(), accesscontrol.GetOrgRoles(f.user)) + orgID := f.user.GetOrgID() + filter, params := accesscontrol.UserRolesFilter(orgID, userID, f.user.GetTeams(), accesscontrol.GetOrgRoles(f.user)) rolesFilter := " AND role_id IN(SELECT id FROM role " + filter + ") " var args []any builder := strings.Builder{} @@ -124,7 +125,7 @@ func (f *accessControlDashboardPermissionFilterNoFolderSubquery) buildClauses() f.addRecQry(recQueryName, permSelector.String(), permSelectorArgs) builder.WriteString("(folder.uid IN (SELECT uid FROM " + recQueryName) default: - nestedFoldersSelectors, nestedFoldersArgs := f.nestedFoldersSelectors(permSelector.String(), permSelectorArgs, "folder.uid", "") + nestedFoldersSelectors, nestedFoldersArgs := f.nestedFoldersSelectors(permSelector.String(), permSelectorArgs, "folder.uid", "", orgID) builder.WriteRune('(') builder.WriteString(nestedFoldersSelectors) args = append(args, nestedFoldersArgs...) @@ -202,7 +203,7 @@ func (f *accessControlDashboardPermissionFilterNoFolderSubquery) buildClauses() builder.WriteString("(dashboard.uid IN ") builder.WriteString(fmt.Sprintf("(SELECT uid FROM %s)", recQueryName)) default: - nestedFoldersSelectors, nestedFoldersArgs := f.nestedFoldersSelectors(permSelector.String(), permSelectorArgs, "dashboard.uid", "") + nestedFoldersSelectors, nestedFoldersArgs := f.nestedFoldersSelectors(permSelector.String(), permSelectorArgs, "dashboard.uid", "", orgID) builder.WriteRune('(') builder.WriteString(nestedFoldersSelectors) builder.WriteRune(')') @@ -230,7 +231,7 @@ func (f *accessControlDashboardPermissionFilterNoFolderSubquery) buildClauses() f.where = clause{string: builder.String(), params: args} } -func (f *accessControlDashboardPermissionFilterNoFolderSubquery) nestedFoldersSelectors(permSelector string, permSelectorArgs []any, leftTableCol string, _ string) (string, []any) { +func (f *accessControlDashboardPermissionFilterNoFolderSubquery) nestedFoldersSelectors(permSelector string, permSelectorArgs []any, leftTableCol string, _ string, orgID int64) (string, []any) { wheres := make([]string, 0, folder.MaxNestedFolderDepth+1) args := make([]any, 0, len(permSelectorArgs)*(folder.MaxNestedFolderDepth+1)) @@ -247,7 +248,8 @@ func (f *accessControlDashboardPermissionFilterNoFolderSubquery) nestedFoldersSe s := fmt.Sprintf(tmpl, t, prev, t, prev, t) joins = append(joins, s) - wheres = append(wheres, fmt.Sprintf("(%s IN (SELECT f1.uid FROM folder f1 %s WHERE %s.uid IN %s)", leftTableCol, strings.Join(joins, " "), t, permSelector)) + wheres = append(wheres, fmt.Sprintf("(%s IN (SELECT f1.uid FROM folder f1 %s WHERE %s.org_id = ? AND %s.uid IN %s)", leftTableCol, strings.Join(joins, " "), t, t, permSelector)) + args = append(args, orgID) args = append(args, permSelectorArgs...) prev = t diff --git a/pkg/services/sqlstore/searchstore/search_test.go b/pkg/services/sqlstore/searchstore/search_test.go index b36b0323b0f53..1dfa9c328e360 100644 --- a/pkg/services/sqlstore/searchstore/search_test.go +++ b/pkg/services/sqlstore/searchstore/search_test.go @@ -3,7 +3,6 @@ package searchstore_test import ( "context" - "strings" "testing" "github.com/stretchr/testify/assert" @@ -120,12 +119,12 @@ func TestBuilder_RBAC(t *testing.T) { testsCases := []struct { desc string userPermissions []accesscontrol.Permission - features []any + features featuremgmt.FeatureToggles expectedParams []any }{ { desc: "no user permissions", - features: []any{}, + features: featuremgmt.WithFeatures(), expectedParams: []any{ int64(1), }, @@ -135,7 +134,7 @@ func TestBuilder_RBAC(t *testing.T) { userPermissions: []accesscontrol.Permission{ {Action: dashboards.ActionDashboardsRead, Scope: "dashboards:uid:1"}, }, - features: []any{}, + features: featuremgmt.WithFeatures(), expectedParams: []any{ int64(1), int64(1), @@ -149,6 +148,7 @@ func TestBuilder_RBAC(t *testing.T) { 2, int64(1), int64(1), + int64(1), 0, "Viewer", int64(1), @@ -172,7 +172,82 @@ func TestBuilder_RBAC(t *testing.T) { userPermissions: []accesscontrol.Permission{ {Action: dashboards.ActionDashboardsRead, Scope: "dashboards:uid:1"}, }, - features: []any{featuremgmt.FlagNestedFolders}, + features: featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders), + expectedParams: []any{ + int64(1), + int64(1), + 0, + "Viewer", + int64(1), + 0, + "dashboards:read", + "dashboards:write", + 2, + int64(1), + int64(1), + 0, + "Viewer", + int64(1), + 0, + "folders:read", + "dashboards:create", + 2, + int64(1), + int64(1), + int64(1), + 0, + "Viewer", + int64(1), + 0, + "dashboards:read", + "dashboards:write", + 2, + int64(1), + }, + }, + { + desc: "user with view permission with remove subquery", + userPermissions: []accesscontrol.Permission{ + {Action: dashboards.ActionDashboardsRead, Scope: "dashboards:uid:1"}, + }, + features: featuremgmt.WithFeatures(featuremgmt.FlagPermissionsFilterRemoveSubquery), + expectedParams: []any{ + int64(1), + int64(1), + int64(1), + 0, + "Viewer", + int64(1), + 0, + "dashboards:read", + "dashboards:write", + 2, + int64(1), + int64(1), + 0, + "Viewer", + int64(1), + 0, + "dashboards:read", + "dashboards:write", + 2, + int64(1), + int64(1), + 0, + "Viewer", + int64(1), + 0, + "folders:read", + "dashboards:create", + 2, + }, + }, + { + desc: "user with view permission with nesting and remove subquery", + userPermissions: []accesscontrol.Permission{ + {Action: dashboards.ActionDashboardsRead, Scope: "dashboards:uid:1"}, + }, + features: featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders, featuremgmt.FlagPermissionsFilterRemoveSubquery), expectedParams: []any{ int64(1), int64(1), @@ -219,48 +294,39 @@ func TestBuilder_RBAC(t *testing.T) { require.NoError(t, err) for _, tc := range testsCases { - for _, features := range []*featuremgmt.FeatureManager{featuremgmt.WithFeatures(tc.features...), featuremgmt.WithFeatures(append(tc.features, featuremgmt.FlagPermissionsFilterRemoveSubquery)...)} { - m := features.GetEnabled(context.Background()) - keys := make([]string, 0, len(m)) - for k := range m { - keys = append(keys, k) + t.Run(tc.desc, func(t *testing.T) { + if len(tc.userPermissions) > 0 { + user.Permissions = map[int64]map[string][]string{1: accesscontrol.GroupScopesByAction(tc.userPermissions)} } - t.Run(tc.desc+" with features "+strings.Join(keys, ","), func(t *testing.T) { - if len(tc.userPermissions) > 0 { - user.Permissions = map[int64]map[string][]string{1: accesscontrol.GroupScopesByAction(tc.userPermissions)} - } - - level := dashboards.PERMISSION_EDIT - - builder := &searchstore.Builder{ - Filters: []any{ - searchstore.OrgFilter{OrgId: user.OrgID}, - searchstore.TitleSorter{}, - permissions.NewAccessControlDashboardPermissionFilter( - user, - level, - "", - features, - recursiveQueriesAreSupported, - ), - }, - Dialect: store.GetDialect(), - Features: features, - } - - res := []dashboards.DashboardSearchProjection{} - err := store.WithDbSession(context.Background(), func(sess *db.Session) error { - sql, params := builder.ToSQL(limit, page) - // TODO: replace with a proper test - assert.Equal(t, tc.expectedParams, params) - return sess.SQL(sql, params...).Find(&res) - }) - require.NoError(t, err) + level := dashboards.PERMISSION_EDIT + + builder := &searchstore.Builder{ + Filters: []any{ + searchstore.OrgFilter{OrgId: user.OrgID}, + searchstore.TitleSorter{}, + permissions.NewAccessControlDashboardPermissionFilter( + user, + level, + "", + tc.features, + recursiveQueriesAreSupported, + ), + }, + Dialect: store.GetDialect(), + Features: tc.features, + } - assert.Len(t, res, 0) + res := []dashboards.DashboardSearchProjection{} + err := store.WithDbSession(context.Background(), func(sess *db.Session) error { + sql, params := builder.ToSQL(limit, page) + assert.Equal(t, tc.expectedParams, params) + return sess.SQL(sql, params...).Find(&res) }) - } + require.NoError(t, err) + + assert.Len(t, res, 0) + }) } } From 95b48339f89c7d267bce7d894404d26ccd75d0e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Agn=C3=A8s=20Toulet?= <35176601+AgnesToulet@users.noreply.github.com> Date: Mon, 6 Nov 2023 14:39:22 +0100 Subject: [PATCH 109/869] Feature Flag: Add pdfTables (#76452) Feature Toggles: Add PDF Tables --- packages/grafana-data/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 ++++ 4 files changed, 13 insertions(+) diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index ed59e657fa2d2..5b089ec12bc6d 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -159,4 +159,5 @@ export interface FeatureToggles { extractFieldsNameDeduplication?: boolean; dashboardSceneForViewers?: boolean; panelFilterVariable?: boolean; + pdfTables?: boolean; } diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index 02e4f0b7c0ea1..7cebf32c162ab 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -1024,6 +1024,13 @@ var ( Owner: grafanaDashboardsSquad, HideFromDocs: true, }, + { + Name: "pdfTables", + Description: "Enables generating table data as PDF in reporting", + Stage: FeatureStagePrivatePreview, + FrontendOnly: false, + Owner: grafanaSharingSquad, + }, } ) diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv index 4fd2c1e0b811e..e5a35d8af1104 100644 --- a/pkg/services/featuremgmt/toggles_gen.csv +++ b/pkg/services/featuremgmt/toggles_gen.csv @@ -140,3 +140,4 @@ annotationPermissionUpdate,experimental,@grafana/identity-access-team,false,fals extractFieldsNameDeduplication,experimental,@grafana/grafana-bi-squad,false,false,false,true dashboardSceneForViewers,experimental,@grafana/dashboards-squad,false,false,false,true panelFilterVariable,experimental,@grafana/dashboards-squad,false,false,false,true +pdfTables,privatePreview,@grafana/sharing-squad,false,false,false,false diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go index 15be0d1570f40..f9780d2669921 100644 --- a/pkg/services/featuremgmt/toggles_gen.go +++ b/pkg/services/featuremgmt/toggles_gen.go @@ -570,4 +570,8 @@ const ( // FlagPanelFilterVariable // Enables use of the `systemPanelFilterVar` variable to filter panels in a dashboard FlagPanelFilterVariable = "panelFilterVariable" + + // FlagPdfTables + // Enables generating table data as PDF in reporting + FlagPdfTables = "pdfTables" ) From c0afa74876044d36c01fa82e061ff93111d3eec7 Mon Sep 17 00:00:00 2001 From: Ashley Harrison Date: Mon, 6 Nov 2023 13:52:50 +0000 Subject: [PATCH 110/869] QueryRows: Fix being able to reorder rows with keyboard (#77355) * Revert "Explore: A tooltip for reorder icon in query operation (#75978)" This reverts commit 76e102d1a3df1b4fce52f9d0e34c2d27e0d9ac0e. * update type * CI driven development --- .../QueryOperationRowHeader.tsx | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/public/app/core/components/QueryOperationRow/QueryOperationRowHeader.tsx b/public/app/core/components/QueryOperationRow/QueryOperationRowHeader.tsx index bcdfb7cf42bd1..83431260f3a3a 100644 --- a/public/app/core/components/QueryOperationRow/QueryOperationRowHeader.tsx +++ b/public/app/core/components/QueryOperationRow/QueryOperationRowHeader.tsx @@ -4,7 +4,7 @@ import { DraggableProvided } from 'react-beautiful-dnd'; import { GrafanaTheme2 } from '@grafana/data'; import { Stack } from '@grafana/experimental'; -import { IconButton, useStyles2 } from '@grafana/ui'; +import { Icon, IconButton, useStyles2 } from '@grafana/ui'; export interface QueryOperationRowHeaderProps { actionsElement?: React.ReactNode; @@ -15,7 +15,7 @@ export interface QueryOperationRowHeaderProps { headerElement?: React.ReactNode; isContentVisible: boolean; onRowToggle: () => void; - reportDragMousePosition: MouseEventHandler; + reportDragMousePosition: MouseEventHandler; title?: string; id: string; expanderMessages?: ExpanderMessages; @@ -76,16 +76,9 @@ export const QueryOperationRowHeader = ({ {actionsElement} {draggable && ( - +

    + +
    )}
    From df7b760f375bd6173861ec15b33a559ad6269b10 Mon Sep 17 00:00:00 2001 From: Sonia Aguilar <33540275+soniaAguilarPeiron@users.noreply.github.com> Date: Mon, 6 Nov 2023 15:15:53 +0100 Subject: [PATCH 111/869] Alerting: Disable cache in rktq when fetching export data. (#77678) * Disable cache in rktq when fecthing export rules group * Fix export rules data being cached * Disable cache for the rest of export endpoints --- public/app/features/alerting/unified/api/alertRuleApi.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/public/app/features/alerting/unified/api/alertRuleApi.ts b/public/app/features/alerting/unified/api/alertRuleApi.ts index 85c07792765a1..edb861399392c 100644 --- a/public/app/features/alerting/unified/api/alertRuleApi.ts +++ b/public/app/features/alerting/unified/api/alertRuleApi.ts @@ -201,6 +201,7 @@ export const alertRuleApi = alertingApi.injectEndpoints({ params: { format: format, folderUid: folderUid, group: group, ruleUid: ruleUid }, responseType: 'text', }), + keepUnusedDataFor: 0, }), exportReceiver: build.query({ query: ({ receiverName, decrypt, format }) => ({ @@ -208,6 +209,7 @@ export const alertRuleApi = alertingApi.injectEndpoints({ params: { format: format, decrypt: decrypt, name: receiverName }, responseType: 'text', }), + keepUnusedDataFor: 0, }), exportReceivers: build.query({ query: ({ decrypt, format }) => ({ @@ -215,6 +217,7 @@ export const alertRuleApi = alertingApi.injectEndpoints({ params: { format: format, decrypt: decrypt }, responseType: 'text', }), + keepUnusedDataFor: 0, }), exportPolicies: build.query({ query: ({ format }) => ({ @@ -222,6 +225,7 @@ export const alertRuleApi = alertingApi.injectEndpoints({ params: { format: format }, responseType: 'text', }), + keepUnusedDataFor: 0, }), exportModifiedRuleGroup: build.mutation< string, From b6e2db7d913255df84c0f35aa710c2231e311c6c Mon Sep 17 00:00:00 2001 From: Ashley Harrison Date: Mon, 6 Nov 2023 14:32:29 +0000 Subject: [PATCH 112/869] Navigation: Report megamenu state when clicking a navigation item (#77705) report megamenu state when clicking a navigation item --- public/app/core/components/AppChrome/AppChromeService.tsx | 4 +++- .../core/components/AppChrome/DockedMegaMenu/MegaMenu.tsx | 2 +- .../app/core/components/AppChrome/DockedMegaMenu/utils.ts | 7 ++++--- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/public/app/core/components/AppChrome/AppChromeService.tsx b/public/app/core/components/AppChrome/AppChromeService.tsx index 83032e0dd631a..a7a89bfceb2d1 100644 --- a/public/app/core/components/AppChrome/AppChromeService.tsx +++ b/public/app/core/components/AppChrome/AppChromeService.tsx @@ -11,13 +11,15 @@ import { KioskMode } from 'app/types'; import { RouteDescriptor } from '../../navigation/types'; +export type MegaMenuState = 'open' | 'closed' | 'docked'; + export interface AppChromeState { chromeless?: boolean; sectionNav: NavModel; pageNav?: NavModelItem; actions?: React.ReactNode; searchBarHidden?: boolean; - megaMenu: 'open' | 'closed' | 'docked'; + megaMenu: MegaMenuState; kioskMode: KioskMode | null; layout: PageLayoutType; } diff --git a/public/app/core/components/AppChrome/DockedMegaMenu/MegaMenu.tsx b/public/app/core/components/AppChrome/DockedMegaMenu/MegaMenu.tsx index 220967c06a585..1a15b5c01c5ba 100644 --- a/public/app/core/components/AppChrome/DockedMegaMenu/MegaMenu.tsx +++ b/public/app/core/components/AppChrome/DockedMegaMenu/MegaMenu.tsx @@ -30,7 +30,7 @@ export const MegaMenu = React.memo( // Remove profile + help from tree const navItems = navTree .filter((item) => item.id !== 'profile' && item.id !== 'help') - .map((item) => enrichWithInteractionTracking(item, true)); + .map((item) => enrichWithInteractionTracking(item, state.megaMenu)); const activeItem = getActiveItem(navItems, location.pathname); diff --git a/public/app/core/components/AppChrome/DockedMegaMenu/utils.ts b/public/app/core/components/AppChrome/DockedMegaMenu/utils.ts index 89384f0eb6c9c..a7ef5ee3a50d4 100644 --- a/public/app/core/components/AppChrome/DockedMegaMenu/utils.ts +++ b/public/app/core/components/AppChrome/DockedMegaMenu/utils.ts @@ -6,6 +6,7 @@ import { ShowModalReactEvent } from '../../../../types/events'; import appEvents from '../../../app_events'; import { getFooterLinks } from '../../Footer/Footer'; import { HelpModal } from '../../help/HelpModal'; +import { MegaMenuState } from '../AppChromeService'; export const enrichHelpItem = (helpItem: NavModelItem) => { let menuItems = helpItem.children || []; @@ -29,19 +30,19 @@ export const enrichHelpItem = (helpItem: NavModelItem) => { return helpItem; }; -export const enrichWithInteractionTracking = (item: NavModelItem, expandedState: boolean) => { +export const enrichWithInteractionTracking = (item: NavModelItem, megaMenuState: MegaMenuState) => { // creating a new object here to not mutate the original item object const newItem = { ...item }; const onClick = newItem.onClick; newItem.onClick = () => { reportInteraction('grafana_navigation_item_clicked', { path: newItem.url ?? newItem.id, - state: expandedState ? 'expanded' : 'collapsed', + state: megaMenuState, }); onClick?.(); }; if (newItem.children) { - newItem.children = newItem.children.map((item) => enrichWithInteractionTracking(item, expandedState)); + newItem.children = newItem.children.map((item) => enrichWithInteractionTracking(item, megaMenuState)); } return newItem; }; From a558d287a70268e09729c0338165a13b8c775e1d Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Mon, 6 Nov 2023 15:33:58 +0100 Subject: [PATCH 113/869] Role picker: Remove unnecessary table wrapper (#77707) --- .../features/admin/Users/OrgUsersTable.tsx | 17 +++-------- .../app/features/admin/Users/TableWrapper.tsx | 30 ------------------- public/app/features/teams/TeamList.tsx | 26 ++++++---------- 3 files changed, 13 insertions(+), 60 deletions(-) delete mode 100644 public/app/features/admin/Users/TableWrapper.tsx diff --git a/public/app/features/admin/Users/OrgUsersTable.tsx b/public/app/features/admin/Users/OrgUsersTable.tsx index 34a58210fab0b..174a881c4571f 100644 --- a/public/app/features/admin/Users/OrgUsersTable.tsx +++ b/public/app/features/admin/Users/OrgUsersTable.tsx @@ -26,8 +26,6 @@ import { AccessControlAction, OrgUser, Role } from 'app/types'; import { OrgRolePicker } from '../OrgRolePicker'; -import { TableWrapper } from './TableWrapper'; - type Cell = CellProps; const disabledRoleMessage = `This user's role is not editable because it is synchronized from your auth provider. @@ -220,17 +218,10 @@ export const OrgUsersTable = ({ return ( - - String(user.userId)} - fetchData={fetchData} - /> - - - - + String(user.userId)} fetchData={fetchData} /> + + + {Boolean(userToRemove) && ( { - const styles = useStyles2(getStyles); - return
    {children}
    ; -}; - -const getStyles = (theme: GrafanaTheme2) => ({ - // Enable RolePicker overflow - wrapper: css({ - display: 'flex', - flexDirection: 'column', - overflowX: 'auto', - overflowY: 'hidden', - minHeight: '100vh', - width: '100%', - '& > div': { - overflowX: 'unset', - marginBottom: theme.spacing(2), - }, - }), -}); diff --git a/public/app/features/teams/TeamList.tsx b/public/app/features/teams/TeamList.tsx index 4211a462ec18f..fe57446c47615 100644 --- a/public/app/features/teams/TeamList.tsx +++ b/public/app/features/teams/TeamList.tsx @@ -22,7 +22,6 @@ import { contextSrv } from 'app/core/services/context_srv'; import { AccessControlAction, Role, StoreState, Team } from 'app/types'; import { TeamRolePicker } from '../../core/components/RolePicker/TeamRolePicker'; -import { TableWrapper } from '../admin/Users/TableWrapper'; import { deleteTeam, loadTeams, changePage, changeQuery, changeSort } from './state/actions'; @@ -173,22 +172,15 @@ export const TeamList = ({
    - - String(team.id)} - fetchData={changeSort} - /> - - - - + String(team.id)} + fetchData={changeSort} + /> + + + )} From 901a6bfa697be4434d49ac3ada7e0ecd7ab24a6b Mon Sep 17 00:00:00 2001 From: Gilles De Mey Date: Mon, 6 Nov 2023 15:50:29 +0100 Subject: [PATCH 114/869] Alerting: Use correct URL for modify export (#77714) --- .../unified/components/rules/RuleActionsButtons.tsx | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/public/app/features/alerting/unified/components/rules/RuleActionsButtons.tsx b/public/app/features/alerting/unified/components/rules/RuleActionsButtons.tsx index 80e42a858decb..4325ec0174b0b 100644 --- a/public/app/features/alerting/unified/components/rules/RuleActionsButtons.tsx +++ b/public/app/features/alerting/unified/components/rules/RuleActionsButtons.tsx @@ -5,7 +5,6 @@ import { useLocation } from 'react-router-dom'; import { GrafanaTheme2 } from '@grafana/data'; import { Stack } from '@grafana/experimental'; -import { locationService } from '@grafana/runtime'; import { Button, ClipboardButton, @@ -148,13 +147,9 @@ export const RuleActionsButtons = ({ rule, rulesSource }: Props) => { - locationService.push( - createUrl(`/alerting/${encodeURIComponent(ruleId.stringifyIdentifier(identifier))}/modify-export`, { - returnTo: location.pathname + location.search, - }) - ) - } + url={createUrl(`/alerting/${encodeURIComponent(ruleId.stringifyIdentifier(identifier))}/modify-export`, { + returnTo: location.pathname + location.search, + })} /> ); } From e61ebd4b0623537e97ab90c4ff8a4923e9966aa8 Mon Sep 17 00:00:00 2001 From: Gilles De Mey Date: Mon, 6 Nov 2023 15:51:36 +0100 Subject: [PATCH 115/869] Alerting: Do not show missing integration while loading oncall plugin status (#77710) --- .../components/contact-points/useContactPoints.tsx | 14 +++++++++++--- .../unified/components/contact-points/utils.ts | 2 +- .../grafanaAppReceivers/useReceiversMetadata.ts | 9 +++++++-- 3 files changed, 19 insertions(+), 6 deletions(-) diff --git a/public/app/features/alerting/unified/components/contact-points/useContactPoints.tsx b/public/app/features/alerting/unified/components/contact-points/useContactPoints.tsx index 555341a9f7675..4b70e5d6e2fd5 100644 --- a/public/app/features/alerting/unified/components/contact-points/useContactPoints.tsx +++ b/public/app/features/alerting/unified/components/contact-points/useContactPoints.tsx @@ -7,7 +7,7 @@ import { produce } from 'immer'; import { remove } from 'lodash'; import { alertmanagerApi } from '../../api/alertmanagerApi'; -import { onCallApi } from '../../api/onCallApi'; +import { onCallApi, OnCallIntegrationDTO } from '../../api/onCallApi'; import { usePluginBridge } from '../../hooks/usePluginBridge'; import { useAlertmanager } from '../../state/AlertmanagerContext'; import { SupportedPlugin } from '../../types/pluginBridges'; @@ -29,7 +29,7 @@ const RECEIVER_STATUS_POLLING_INTERVAL = 10 * 1000; // 10 seconds */ export function useContactPointsWithStatus() { const { selectedAlertmanager, isGrafanaAlertmanager } = useAlertmanager(); - const { installed: onCallPluginInstalled = false, loading: onCallPluginStatusLoading } = usePluginBridge( + const { installed: onCallPluginInstalled, loading: onCallPluginStatusLoading } = usePluginBridge( SupportedPlugin.OnCall ); @@ -55,6 +55,14 @@ export function useContactPointsWithStatus() { skip: !onCallPluginInstalled || !isGrafanaAlertmanager, }); + // null = no installed, undefined = loading, [n] is installed with integrations + let onCallMetadata: null | undefined | OnCallIntegrationDTO[] = undefined; + if (onCallPluginInstalled) { + onCallMetadata = onCallIntegrations ?? []; + } else if (onCallPluginInstalled === false) { + onCallMetadata = null; + } + // fetch the latest config from the Alertmanager const fetchAlertmanagerConfiguration = alertmanagerApi.endpoints.getAlertmanagerConfiguration.useQuery( selectedAlertmanager!, @@ -68,7 +76,7 @@ export function useContactPointsWithStatus() { result.data, fetchContactPointsStatus.data, fetchReceiverMetadata.data, - onCallPluginInstalled ? onCallIntegrations ?? [] : null + onCallMetadata ) : [], }), diff --git a/public/app/features/alerting/unified/components/contact-points/utils.ts b/public/app/features/alerting/unified/components/contact-points/utils.ts index 414d7bbc00cf6..79a71733fb7e0 100644 --- a/public/app/features/alerting/unified/components/contact-points/utils.ts +++ b/public/app/features/alerting/unified/components/contact-points/utils.ts @@ -96,7 +96,7 @@ export function enhanceContactPointsWithMetadata( result: AlertManagerCortexConfig, status: ReceiversStateDTO[] = [], notifiers: NotifierDTO[] = [], - onCallIntegrations: OnCallIntegrationDTO[] | null + onCallIntegrations: OnCallIntegrationDTO[] | undefined | null ): ContactPointWithMetadata[] { const contactPoints = result.alertmanager_config.receivers ?? []; diff --git a/public/app/features/alerting/unified/components/receivers/grafanaAppReceivers/useReceiversMetadata.ts b/public/app/features/alerting/unified/components/receivers/grafanaAppReceivers/useReceiversMetadata.ts index 8f00947029ed2..13a85df9d6ccd 100644 --- a/public/app/features/alerting/unified/components/receivers/grafanaAppReceivers/useReceiversMetadata.ts +++ b/public/app/features/alerting/unified/components/receivers/grafanaAppReceivers/useReceiversMetadata.ts @@ -48,13 +48,18 @@ export const useReceiversMetadata = (receivers: Receiver[]): Map Date: Mon, 6 Nov 2023 15:51:59 +0100 Subject: [PATCH 116/869] instrumentation: remove live endpoints from slo (#77706) Signed-off-by: bergquist --- pkg/services/live/live.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pkg/services/live/live.go b/pkg/services/live/live.go index eb802c8b2362f..4017af850929d 100644 --- a/pkg/services/live/live.go +++ b/pkg/services/live/live.go @@ -29,6 +29,7 @@ import ( "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/usagestats" "github.com/grafana/grafana/pkg/middleware" + "github.com/grafana/grafana/pkg/middleware/requestmeta" "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/annotations" @@ -333,12 +334,12 @@ func ProvideService(plugCtxProvider *plugincontext.Provider, cfg *setting.Cfg, r g.RouteRegister.Group("/api/live", func(group routing.RouteRegister) { group.Get("/ws", g.websocketHandler) - }, middleware.ReqSignedIn) + }, middleware.ReqSignedIn, requestmeta.SetSLOGroup(requestmeta.SLOGroupNone)) g.RouteRegister.Group("/api/live", func(group routing.RouteRegister) { group.Get("/push/:streamId", g.pushWebsocketHandler) group.Get("/pipeline/push/*", g.pushPipelineWebsocketHandler) - }, middleware.ReqOrgAdmin) + }, middleware.ReqOrgAdmin, requestmeta.SetSLOGroup(requestmeta.SLOGroupNone)) g.registerUsageMetrics() From 5e7f6e86998c0dcdc8663db5df7370a050db1f6d Mon Sep 17 00:00:00 2001 From: Ryan McKinley Date: Mon, 6 Nov 2023 06:53:52 -0800 Subject: [PATCH 117/869] Snapshots: Do not return internal database ids (#77672) --- pkg/services/dashboardsnapshots/models.go | 6 +++--- public/api-enterprise-spec.json | 12 ------------ public/api-merged.json | 12 ------------ public/app/features/manage-dashboards/types.ts | 3 --- public/openapi3.json | 12 ------------ 5 files changed, 3 insertions(+), 42 deletions(-) diff --git a/pkg/services/dashboardsnapshots/models.go b/pkg/services/dashboardsnapshots/models.go index 00e5b4582e316..36c155b52ead3 100644 --- a/pkg/services/dashboardsnapshots/models.go +++ b/pkg/services/dashboardsnapshots/models.go @@ -29,11 +29,11 @@ type DashboardSnapshot struct { // DashboardSnapshotDTO without dashboard map type DashboardSnapshotDTO struct { - ID int64 `json:"id" xorm:"id"` + ID int64 `json:"-" xorm:"id"` Name string `json:"name"` Key string `json:"key"` - OrgID int64 `json:"orgId" xorm:"org_id"` - UserID int64 `json:"userId" xorm:"user_id"` + OrgID int64 `json:"-" xorm:"org_id"` + UserID int64 `json:"-" xorm:"user_id"` External bool `json:"external"` ExternalURL string `json:"externalUrl" xorm:"external_url"` diff --git a/public/api-enterprise-spec.json b/public/api-enterprise-spec.json index cdf6ab57f58fc..d575e97116a3d 100644 --- a/public/api-enterprise-spec.json +++ b/public/api-enterprise-spec.json @@ -4046,27 +4046,15 @@ "externalUrl": { "type": "string" }, - "id": { - "type": "integer", - "format": "int64" - }, "key": { "type": "string" }, "name": { "type": "string" }, - "orgId": { - "type": "integer", - "format": "int64" - }, "updated": { "type": "string", "format": "date-time" - }, - "userId": { - "type": "integer", - "format": "int64" } } }, diff --git a/public/api-merged.json b/public/api-merged.json index 66dddbdf3c3b5..c3dd7a61e0bec 100644 --- a/public/api-merged.json +++ b/public/api-merged.json @@ -13629,27 +13629,15 @@ "externalUrl": { "type": "string" }, - "id": { - "type": "integer", - "format": "int64" - }, "key": { "type": "string" }, "name": { "type": "string" }, - "orgId": { - "type": "integer", - "format": "int64" - }, "updated": { "type": "string", "format": "date-time" - }, - "userId": { - "type": "integer", - "format": "int64" } } }, diff --git a/public/app/features/manage-dashboards/types.ts b/public/app/features/manage-dashboards/types.ts index 48bd056afb797..a7f5a9749205c 100644 --- a/public/app/features/manage-dashboards/types.ts +++ b/public/app/features/manage-dashboards/types.ts @@ -7,13 +7,10 @@ export interface Snapshot { expires: string; external: boolean; externalUrl: string; - id: number; key: string; name: string; - orgId: number; updated: string; url?: string; - userId: number; } export type DeleteDashboardResponse = { diff --git a/public/openapi3.json b/public/openapi3.json index 64768646ea01c..a379c2b120165 100644 --- a/public/openapi3.json +++ b/public/openapi3.json @@ -4542,27 +4542,15 @@ "externalUrl": { "type": "string" }, - "id": { - "format": "int64", - "type": "integer" - }, "key": { "type": "string" }, "name": { "type": "string" }, - "orgId": { - "format": "int64", - "type": "integer" - }, "updated": { "format": "date-time", "type": "string" - }, - "userId": { - "format": "int64", - "type": "integer" } }, "type": "object" From 7e1110f1f9f560d4e70266227189836aa7c060da Mon Sep 17 00:00:00 2001 From: Matias Chomicki Date: Mon, 6 Nov 2023 15:59:48 +0100 Subject: [PATCH 118/869] Log Row: memoize row processing (#77716) * LogRow: memoize row processing * Rename method --- public/app/features/logs/components/LogRow.tsx | 12 ++++++++---- public/app/features/logs/utils.test.ts | 10 ++++++++++ 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/public/app/features/logs/components/LogRow.tsx b/public/app/features/logs/components/LogRow.tsx index 390cbd04704d1..f4b8697ae5612 100644 --- a/public/app/features/logs/components/LogRow.tsx +++ b/public/app/features/logs/components/LogRow.tsx @@ -1,5 +1,6 @@ import { cx } from '@emotion/css'; import { debounce } from 'lodash'; +import memoizeOne from 'memoize-one'; import React, { PureComponent } from 'react'; import { Field, LinkModel, LogRowModel, LogsSortOrder, dateTimeFormat, CoreApp, DataFrame } from '@grafana/data'; @@ -157,6 +158,12 @@ class UnThemedLogRow extends PureComponent { } }; + escapeRow = memoizeOne((row: LogRowModel, forceEscape: boolean | undefined) => { + return row.hasUnescapedContent && forceEscape + ? { ...row, entry: escapeUnescapedString(row.entry), raw: escapeUnescapedString(row.raw) } + : row; + }); + render() { const { getRows, @@ -191,10 +198,7 @@ class UnThemedLogRow extends PureComponent { [styles.highlightBackground]: permalinked && !this.state.showDetails, }); - const processedRow = - row.hasUnescapedContent && forceEscape - ? { ...row, entry: escapeUnescapedString(row.entry), raw: escapeUnescapedString(row.raw) } - : row; + const processedRow = this.escapeRow(row, forceEscape); return ( <> diff --git a/public/app/features/logs/utils.test.ts b/public/app/features/logs/utils.test.ts index ed58cfa8c0482..f73a0d43f6ea7 100644 --- a/public/app/features/logs/utils.test.ts +++ b/public/app/features/logs/utils.test.ts @@ -14,6 +14,7 @@ import { calculateLogsLabelStats, calculateStats, checkLogsError, + escapeUnescapedString, getLogLevel, getLogLevelFromKey, getLogsVolumeMaximumRange, @@ -469,3 +470,12 @@ describe('getLogsVolumeDimensions', () => { expect(maximumRange).toEqual({ from: 5, to: 25 }); }); }); + +describe('escapeUnescapedString', () => { + it('does not modify strings without unescaped characters', () => { + expect(escapeUnescapedString('a simple string')).toBe('a simple string'); + }); + it('escapes unescaped strings', () => { + expect(escapeUnescapedString(`\\r\\n|\\n|\\t|\\r`)).toBe(`\n|\n|\t|\n`); + }); +}); From 5458ea18e73d2ddff886fafbea765b70d8f162bc Mon Sep 17 00:00:00 2001 From: Serge Zaitsev Date: Mon, 6 Nov 2023 16:09:49 +0100 Subject: [PATCH 119/869] Chore: Use vendored xorm instead of a fork (#77388) * use vendored xorm instead of a fork * bring back mssql driver * backport azuresql driver * restore NoAutoTime --- go.mod | 3 +- pkg/util/xorm/dialect_mssql.go | 567 +++++++++++++++++++++++++++++++++ pkg/util/xorm/engine.go | 9 + pkg/util/xorm/go.mod | 2 +- pkg/util/xorm/session_cols.go | 7 + pkg/util/xorm/xorm.go | 2 + 6 files changed, 588 insertions(+), 2 deletions(-) create mode 100644 pkg/util/xorm/dialect_mssql.go diff --git a/go.mod b/go.mod index cf38d15efaff4..923a165176648 100644 --- a/go.mod +++ b/go.mod @@ -496,7 +496,8 @@ replace github.com/hashicorp/go-hclog => github.com/hashicorp/go-hclog v0.16.1 // happen, for example, during a read when the sqlite db is under heavy write load. // This patch cherry picks compatible fixes from upstream xorm PR#1998 and can be reverted on upgrade to xorm v1.2.0+. // This has also been patched to support the azuresql driver that is a thin wrapper for the mssql driver with azure authentication support. -replace xorm.io/xorm => github.com/grafana/xorm v0.8.3-0.20230627081928-d04aa38aa209 +//replace xorm.io/xorm => github.com/grafana/xorm v0.8.3-0.20230627081928-d04aa38aa209 +replace xorm.io/xorm => ./pkg/util/xorm // Use our fork of the upstream alertmanagers. // This is required in order to get notification delivery errors from the receivers API. diff --git a/pkg/util/xorm/dialect_mssql.go b/pkg/util/xorm/dialect_mssql.go new file mode 100644 index 0000000000000..29070da2fbec3 --- /dev/null +++ b/pkg/util/xorm/dialect_mssql.go @@ -0,0 +1,567 @@ +// Copyright 2015 The Xorm Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package xorm + +import ( + "errors" + "fmt" + "net/url" + "strconv" + "strings" + + "xorm.io/core" +) + +var ( + mssqlReservedWords = map[string]bool{ + "ADD": true, + "EXTERNAL": true, + "PROCEDURE": true, + "ALL": true, + "FETCH": true, + "PUBLIC": true, + "ALTER": true, + "FILE": true, + "RAISERROR": true, + "AND": true, + "FILLFACTOR": true, + "READ": true, + "ANY": true, + "FOR": true, + "READTEXT": true, + "AS": true, + "FOREIGN": true, + "RECONFIGURE": true, + "ASC": true, + "FREETEXT": true, + "REFERENCES": true, + "AUTHORIZATION": true, + "FREETEXTTABLE": true, + "REPLICATION": true, + "BACKUP": true, + "FROM": true, + "RESTORE": true, + "BEGIN": true, + "FULL": true, + "RESTRICT": true, + "BETWEEN": true, + "FUNCTION": true, + "RETURN": true, + "BREAK": true, + "GOTO": true, + "REVERT": true, + "BROWSE": true, + "GRANT": true, + "REVOKE": true, + "BULK": true, + "GROUP": true, + "RIGHT": true, + "BY": true, + "HAVING": true, + "ROLLBACK": true, + "CASCADE": true, + "HOLDLOCK": true, + "ROWCOUNT": true, + "CASE": true, + "IDENTITY": true, + "ROWGUIDCOL": true, + "CHECK": true, + "IDENTITY_INSERT": true, + "RULE": true, + "CHECKPOINT": true, + "IDENTITYCOL": true, + "SAVE": true, + "CLOSE": true, + "IF": true, + "SCHEMA": true, + "CLUSTERED": true, + "IN": true, + "SECURITYAUDIT": true, + "COALESCE": true, + "INDEX": true, + "SELECT": true, + "COLLATE": true, + "INNER": true, + "SEMANTICKEYPHRASETABLE": true, + "COLUMN": true, + "INSERT": true, + "SEMANTICSIMILARITYDETAILSTABLE": true, + "COMMIT": true, + "INTERSECT": true, + "SEMANTICSIMILARITYTABLE": true, + "COMPUTE": true, + "INTO": true, + "SESSION_USER": true, + "CONSTRAINT": true, + "IS": true, + "SET": true, + "CONTAINS": true, + "JOIN": true, + "SETUSER": true, + "CONTAINSTABLE": true, + "KEY": true, + "SHUTDOWN": true, + "CONTINUE": true, + "KILL": true, + "SOME": true, + "CONVERT": true, + "LEFT": true, + "STATISTICS": true, + "CREATE": true, + "LIKE": true, + "SYSTEM_USER": true, + "CROSS": true, + "LINENO": true, + "TABLE": true, + "CURRENT": true, + "LOAD": true, + "TABLESAMPLE": true, + "CURRENT_DATE": true, + "MERGE": true, + "TEXTSIZE": true, + "CURRENT_TIME": true, + "NATIONAL": true, + "THEN": true, + "CURRENT_TIMESTAMP": true, + "NOCHECK": true, + "TO": true, + "CURRENT_USER": true, + "NONCLUSTERED": true, + "TOP": true, + "CURSOR": true, + "NOT": true, + "TRAN": true, + "DATABASE": true, + "NULL": true, + "TRANSACTION": true, + "DBCC": true, + "NULLIF": true, + "TRIGGER": true, + "DEALLOCATE": true, + "OF": true, + "TRUNCATE": true, + "DECLARE": true, + "OFF": true, + "TRY_CONVERT": true, + "DEFAULT": true, + "OFFSETS": true, + "TSEQUAL": true, + "DELETE": true, + "ON": true, + "UNION": true, + "DENY": true, + "OPEN": true, + "UNIQUE": true, + "DESC": true, + "OPENDATASOURCE": true, + "UNPIVOT": true, + "DISK": true, + "OPENQUERY": true, + "UPDATE": true, + "DISTINCT": true, + "OPENROWSET": true, + "UPDATETEXT": true, + "DISTRIBUTED": true, + "OPENXML": true, + "USE": true, + "DOUBLE": true, + "OPTION": true, + "USER": true, + "DROP": true, + "OR": true, + "VALUES": true, + "DUMP": true, + "ORDER": true, + "VARYING": true, + "ELSE": true, + "OUTER": true, + "VIEW": true, + "END": true, + "OVER": true, + "WAITFOR": true, + "ERRLVL": true, + "PERCENT": true, + "WHEN": true, + "ESCAPE": true, + "PIVOT": true, + "WHERE": true, + "EXCEPT": true, + "PLAN": true, + "WHILE": true, + "EXEC": true, + "PRECISION": true, + "WITH": true, + "EXECUTE": true, + "PRIMARY": true, + "WITHIN": true, + "EXISTS": true, + "PRINT": true, + "WRITETEXT": true, + "EXIT": true, + "PROC": true, + } +) + +type mssql struct { + core.Base +} + +func (db *mssql) Init(d *core.DB, uri *core.Uri, drivername, dataSourceName string) error { + return db.Base.Init(d, db, uri, drivername, dataSourceName) +} + +func (db *mssql) SqlType(c *core.Column) string { + var res string + switch t := c.SQLType.Name; t { + case core.Bool: + res = core.Bit + if strings.EqualFold(c.Default, "true") { + c.Default = "1" + } else if strings.EqualFold(c.Default, "false") { + c.Default = "0" + } + case core.Serial: + c.IsAutoIncrement = true + c.IsPrimaryKey = true + c.Nullable = false + res = core.Int + case core.BigSerial: + c.IsAutoIncrement = true + c.IsPrimaryKey = true + c.Nullable = false + res = core.BigInt + case core.Bytea, core.Blob, core.Binary, core.TinyBlob, core.MediumBlob, core.LongBlob: + res = core.VarBinary + if c.Length == 0 { + c.Length = 50 + } + case core.TimeStamp: + res = core.DateTime + case core.TimeStampz: + res = "DATETIMEOFFSET" + c.Length = 7 + case core.MediumInt: + res = core.Int + case core.Text, core.MediumText, core.TinyText, core.LongText, core.Json: + res = core.Varchar + "(MAX)" + case core.Double: + res = core.Real + case core.Uuid: + res = core.Varchar + c.Length = 40 + case core.TinyInt: + res = core.TinyInt + c.Length = 0 + case core.BigInt: + res = core.BigInt + c.Length = 0 + default: + res = t + } + + if res == core.Int { + return core.Int + } + + hasLen1 := (c.Length > 0) + hasLen2 := (c.Length2 > 0) + + if hasLen2 { + res += "(" + strconv.Itoa(c.Length) + "," + strconv.Itoa(c.Length2) + ")" + } else if hasLen1 { + res += "(" + strconv.Itoa(c.Length) + ")" + } + return res +} + +func (db *mssql) SupportInsertMany() bool { + return true +} + +func (db *mssql) IsReserved(name string) bool { + _, ok := mssqlReservedWords[name] + return ok +} + +func (db *mssql) Quote(name string) string { + return "\"" + name + "\"" +} + +func (db *mssql) SupportEngine() bool { + return false +} + +func (db *mssql) AutoIncrStr() string { + return "IDENTITY" +} + +func (db *mssql) DropTableSql(tableName string) string { + return fmt.Sprintf("IF EXISTS (SELECT * FROM sysobjects WHERE id = "+ + "object_id(N'%s') and OBJECTPROPERTY(id, N'IsUserTable') = 1) "+ + "DROP TABLE \"%s\"", tableName, tableName) +} + +func (db *mssql) SupportCharset() bool { + return false +} + +func (db *mssql) IndexOnTable() bool { + return true +} + +func (db *mssql) IndexCheckSql(tableName, idxName string) (string, []interface{}) { + args := []interface{}{idxName} + sql := "select name from sysindexes where id=object_id('" + tableName + "') and name=?" + return sql, args +} + +/*func (db *mssql) ColumnCheckSql(tableName, colName string) (string, []interface{}) { + args := []interface{}{tableName, colName} + sql := `SELECT "COLUMN_NAME" FROM "INFORMATION_SCHEMA"."COLUMNS" WHERE "TABLE_NAME" = ? AND "COLUMN_NAME" = ?` + return sql, args +}*/ + +func (db *mssql) IsColumnExist(tableName, colName string) (bool, error) { + query := `SELECT "COLUMN_NAME" FROM "INFORMATION_SCHEMA"."COLUMNS" WHERE "TABLE_NAME" = ? AND "COLUMN_NAME" = ?` + + return db.HasRecords(query, tableName, colName) +} + +func (db *mssql) TableCheckSql(tableName string) (string, []interface{}) { + args := []interface{}{} + sql := "select * from sysobjects where id = object_id(N'" + tableName + "') and OBJECTPROPERTY(id, N'IsUserTable') = 1" + return sql, args +} + +func (db *mssql) GetColumns(tableName string) ([]string, map[string]*core.Column, error) { + args := []interface{}{} + s := `select a.name as name, b.name as ctype,a.max_length,a.precision,a.scale,a.is_nullable as nullable, + "default_is_null" = (CASE WHEN c.text is null THEN 1 ELSE 0 END), + replace(replace(isnull(c.text,''),'(',''),')','') as vdefault, + ISNULL(i.is_primary_key, 0), a.is_identity as is_identity + from sys.columns a + left join sys.types b on a.user_type_id=b.user_type_id + left join sys.syscomments c on a.default_object_id=c.id + LEFT OUTER JOIN + sys.index_columns ic ON ic.object_id = a.object_id AND ic.column_id = a.column_id + LEFT OUTER JOIN + sys.indexes i ON ic.object_id = i.object_id AND ic.index_id = i.index_id + where a.object_id=object_id('` + tableName + `')` + db.LogSQL(s, args) + + rows, err := db.DB().Query(s, args...) + if err != nil { + return nil, nil, err + } + defer rows.Close() + + cols := make(map[string]*core.Column) + colSeq := make([]string, 0) + for rows.Next() { + var name, ctype, vdefault string + var maxLen, precision, scale int + var nullable, isPK, defaultIsNull, isIncrement bool + err = rows.Scan(&name, &ctype, &maxLen, &precision, &scale, &nullable, &defaultIsNull, &vdefault, &isPK, &isIncrement) + if err != nil { + return nil, nil, err + } + + col := new(core.Column) + col.Indexes = make(map[string]int) + col.Name = strings.Trim(name, "` ") + col.Nullable = nullable + col.DefaultIsEmpty = defaultIsNull + if !defaultIsNull { + col.Default = vdefault + } + col.IsPrimaryKey = isPK + col.IsAutoIncrement = isIncrement + ct := strings.ToUpper(ctype) + if ct == "DECIMAL" { + col.Length = precision + col.Length2 = scale + } else { + col.Length = maxLen + } + switch ct { + case "DATETIMEOFFSET": + col.SQLType = core.SQLType{Name: core.TimeStampz, DefaultLength: 0, DefaultLength2: 0} + case "NVARCHAR": + col.SQLType = core.SQLType{Name: core.NVarchar, DefaultLength: 0, DefaultLength2: 0} + case "IMAGE": + col.SQLType = core.SQLType{Name: core.VarBinary, DefaultLength: 0, DefaultLength2: 0} + default: + if _, ok := core.SqlTypes[ct]; ok { + col.SQLType = core.SQLType{Name: ct, DefaultLength: 0, DefaultLength2: 0} + } else { + return nil, nil, fmt.Errorf("Unknown colType %v for %v - %v", ct, tableName, col.Name) + } + } + + cols[col.Name] = col + colSeq = append(colSeq, col.Name) + } + return colSeq, cols, nil +} + +func (db *mssql) GetTables() ([]*core.Table, error) { + args := []interface{}{} + s := `select name from sysobjects where xtype ='U'` + db.LogSQL(s, args) + + rows, err := db.DB().Query(s, args...) + if err != nil { + return nil, err + } + defer rows.Close() + + tables := make([]*core.Table, 0) + for rows.Next() { + table := core.NewEmptyTable() + var name string + err = rows.Scan(&name) + if err != nil { + return nil, err + } + table.Name = strings.Trim(name, "` ") + tables = append(tables, table) + } + return tables, nil +} + +func (db *mssql) GetIndexes(tableName string) (map[string]*core.Index, error) { + args := []interface{}{tableName} + s := `SELECT +IXS.NAME AS [INDEX_NAME], +C.NAME AS [COLUMN_NAME], +IXS.is_unique AS [IS_UNIQUE] +FROM SYS.INDEXES IXS +INNER JOIN SYS.INDEX_COLUMNS IXCS +ON IXS.OBJECT_ID=IXCS.OBJECT_ID AND IXS.INDEX_ID = IXCS.INDEX_ID +INNER JOIN SYS.COLUMNS C ON IXS.OBJECT_ID=C.OBJECT_ID +AND IXCS.COLUMN_ID=C.COLUMN_ID +WHERE IXS.TYPE_DESC='NONCLUSTERED' and OBJECT_NAME(IXS.OBJECT_ID) =? +` + db.LogSQL(s, args) + + rows, err := db.DB().Query(s, args...) + if err != nil { + return nil, err + } + defer rows.Close() + + indexes := make(map[string]*core.Index, 0) + for rows.Next() { + var indexType int + var indexName, colName, isUnique string + + err = rows.Scan(&indexName, &colName, &isUnique) + if err != nil { + return nil, err + } + + i, err := strconv.ParseBool(isUnique) + if err != nil { + return nil, err + } + + if i { + indexType = core.UniqueType + } else { + indexType = core.IndexType + } + + colName = strings.Trim(colName, "` ") + var isRegular bool + if strings.HasPrefix(indexName, "IDX_"+tableName) || strings.HasPrefix(indexName, "UQE_"+tableName) { + indexName = indexName[5+len(tableName):] + isRegular = true + } + + var index *core.Index + var ok bool + if index, ok = indexes[indexName]; !ok { + index = new(core.Index) + index.Type = indexType + index.Name = indexName + index.IsRegular = isRegular + indexes[indexName] = index + } + index.AddColumn(colName) + } + return indexes, nil +} + +func (db *mssql) CreateTableSql(table *core.Table, tableName, storeEngine, charset string) string { + var sql string + if tableName == "" { + tableName = table.Name + } + + sql = "IF NOT EXISTS (SELECT [name] FROM sys.tables WHERE [name] = '" + tableName + "' ) CREATE TABLE " + + sql += db.Quote(tableName) + " (" + + pkList := table.PrimaryKeys + + for _, colName := range table.ColumnsSeq() { + col := table.GetColumn(colName) + if col.IsPrimaryKey && len(pkList) == 1 { + sql += col.String(db) + } else { + sql += col.StringNoPk(db) + } + sql = strings.TrimSpace(sql) + sql += ", " + } + + if len(pkList) > 1 { + sql += "PRIMARY KEY ( " + sql += strings.Join(pkList, ",") + sql += " ), " + } + + sql = sql[:len(sql)-2] + ")" + sql += ";" + return sql +} + +func (db *mssql) ForUpdateSql(query string) string { + return query +} + +func (db *mssql) Filters() []core.Filter { + return []core.Filter{&core.IdFilter{}, &core.QuoteFilter{}} +} + +type odbcDriver struct { +} + +func (p *odbcDriver) Parse(driverName, dataSourceName string) (*core.Uri, error) { + var dbName string + + if strings.HasPrefix(dataSourceName, "sqlserver://") { + u, err := url.Parse(dataSourceName) + if err != nil { + return nil, err + } + dbName = u.Query().Get("database") + } else { + kv := strings.Split(dataSourceName, ";") + for _, c := range kv { + vv := strings.Split(strings.TrimSpace(c), "=") + if len(vv) == 2 { + switch strings.ToLower(vv[0]) { + case "database": + dbName = vv[1] + } + } + } + } + if dbName == "" { + return nil, errors.New("no db name provided") + } + return &core.Uri{DbName: dbName, DbType: core.MSSQL}, nil +} diff --git a/pkg/util/xorm/engine.go b/pkg/util/xorm/engine.go index 3556dc9325c4c..ba2136414b190 100644 --- a/pkg/util/xorm/engine.go +++ b/pkg/util/xorm/engine.go @@ -259,6 +259,15 @@ func (engine *Engine) SQL(query any, args ...any) *Session { return session.SQL(query, args...) } +// NoAutoTime Default if your struct has "created" or "updated" filed tag, the fields +// will automatically be filled with current time when Insert or Update +// invoked. Call NoAutoTime if you dont' want to fill automatically. +func (engine *Engine) NoAutoTime() *Session { + session := engine.NewSession() + session.isAutoClose = true + return session.NoAutoTime() +} + func (engine *Engine) loadTableInfo(table *core.Table) error { colSeq, cols, err := engine.dialect.GetColumns(table.Name) if err != nil { diff --git a/pkg/util/xorm/go.mod b/pkg/util/xorm/go.mod index 95cdb5d2b6b74..667b88a32f06a 100644 --- a/pkg/util/xorm/go.mod +++ b/pkg/util/xorm/go.mod @@ -1,6 +1,6 @@ module xorm.io/xorm -go 1.11 +go 1.20 require ( github.com/denisenkom/go-mssqldb v0.0.0-20190707035753-2be1aa521ff4 diff --git a/pkg/util/xorm/session_cols.go b/pkg/util/xorm/session_cols.go index 184cfb982c239..f9622f00b8f70 100644 --- a/pkg/util/xorm/session_cols.go +++ b/pkg/util/xorm/session_cols.go @@ -123,3 +123,10 @@ func (session *Session) Nullable(columns ...string) *Session { session.statement.Nullable(columns...) return session } + +// NoAutoTime means do not automatically give created field and updated field +// the current time on the current session temporarily +func (session *Session) NoAutoTime() *Session { + session.statement.UseAutoTime = false + return session +} diff --git a/pkg/util/xorm/xorm.go b/pkg/util/xorm/xorm.go index b31787735181d..c330c8215a00b 100644 --- a/pkg/util/xorm/xorm.go +++ b/pkg/util/xorm/xorm.go @@ -35,6 +35,8 @@ func regDrvsNDialects() bool { "postgres": {"postgres", func() core.Driver { return &pqDriver{} }, func() core.Dialect { return &postgres{} }}, "pgx": {"postgres", func() core.Driver { return &pqDriverPgx{} }, func() core.Dialect { return &postgres{} }}, "sqlite3": {"sqlite3", func() core.Driver { return &sqlite3Driver{} }, func() core.Dialect { return &sqlite3{} }}, + "mssql": {"mssql", func() core.Driver { return &odbcDriver{} }, func() core.Dialect { return &mssql{} }}, + "azuresql": {"azuresql", func() core.Driver { return &odbcDriver{} }, func() core.Dialect { return &mssql{} }}, } for driverName, v := range providedDrvsNDialects { From e6bdf0029d5d98ebb26b491f09ffb0828024314c Mon Sep 17 00:00:00 2001 From: Andrei Golubkov Date: Mon, 6 Nov 2023 19:12:46 +0400 Subject: [PATCH 120/869] Loki: Add tests to cover NestedQueryList.tsx (#77331) Loki: Add tests to cover NestedQueryList.tsx. Covered cases: rendering list, shows explanations and call onChange when remove button click. Co-authored-by: Ivana Huckova --- .../components/NestedQueryList.test.tsx | 89 +++++++++++++++++++ 1 file changed, 89 insertions(+) create mode 100644 public/app/plugins/datasource/loki/querybuilder/components/NestedQueryList.test.tsx diff --git a/public/app/plugins/datasource/loki/querybuilder/components/NestedQueryList.test.tsx b/public/app/plugins/datasource/loki/querybuilder/components/NestedQueryList.test.tsx new file mode 100644 index 0000000000000..d1b2f3fb4b759 --- /dev/null +++ b/public/app/plugins/datasource/loki/querybuilder/components/NestedQueryList.test.tsx @@ -0,0 +1,89 @@ +import { render, screen, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import React from 'react'; + +import { LokiDatasource } from '../../datasource'; +import { createLokiDatasource } from '../../mocks'; +import { LokiVisualQuery, LokiVisualQueryBinary } from '../types'; + +import { EXPLAIN_LABEL_FILTER_CONTENT } from './LokiQueryBuilderExplained'; +import { NestedQueryList, Props as NestedQueryListProps } from './NestedQueryList'; + +const X_BUTTON_LABEL = 'Remove nested query'; +function createMockProps(nestedQueriesCount = 1, showExplain = false): NestedQueryListProps { + const operators: string[] = ['+', '-', '*', '/']; + const nestedQueries: LokiVisualQueryBinary[] = [...Array(nestedQueriesCount).keys()].map((i) => ({ + operator: operators[i % operators.length], + query: { + labels: [], + operations: [], + }, + })); + + const query: LokiVisualQuery = { + labels: [], + operations: [], + binaryQueries: nestedQueries, + }; + const datasource: LokiDatasource = createLokiDatasource(); + + const props: NestedQueryListProps = { + query: query, + datasource: datasource, + showExplain: showExplain, + onChange: jest.fn(), + onRunQuery: jest.fn(), + }; + + return props; +} + +describe('render nested queries', () => { + it.each([1, 3])('%i nested queries can be rendered', async (queriesCount) => { + const props = createMockProps(queriesCount, false); + render(); + + const nestedQueryCloseButtons = await screen.findAllByLabelText(X_BUTTON_LABEL); + expect(nestedQueryCloseButtons).toHaveLength(queriesCount); + }); + + it('shows explanations for all nested queries', async () => { + const props = createMockProps(3, true); + render(); + + const nestedQueryCloseButtons = await screen.findAllByText(EXPLAIN_LABEL_FILTER_CONTENT); + expect(nestedQueryCloseButtons).toHaveLength(3); + }); +}); + +describe('events from nested queries', () => { + it('onChange is called when user removes one', async () => { + const props: NestedQueryListProps = createMockProps(3, false); + render(); + + const xButton: HTMLElement[] = await screen.findAllByLabelText(X_BUTTON_LABEL); + await userEvent.click(xButton[0]); + + const removedQuery: LokiVisualQueryBinary[] = [props.query.binaryQueries![0]]; + await waitFor(() => { + expect(props.onChange).toHaveBeenCalledWith({ + ...props.query, + binaryQueries: expect.not.arrayContaining(removedQuery), + }); + }); + }); + + it('onChange is called from sub-query', async () => { + const props = createMockProps(4, false); + const subQuery = { ...props.query.binaryQueries![3] }; + props.query.binaryQueries![0].query.binaryQueries = [subQuery]; + render(); + + const xButton: HTMLElement[] = await screen.findAllByLabelText(X_BUTTON_LABEL); + await userEvent.click(xButton[1]); + + await waitFor(async () => { + expect(props.onChange).toHaveBeenCalledTimes(1); + }); + }); +}); From dff8dc86d60378707e973ce1f03fedb3d74989b6 Mon Sep 17 00:00:00 2001 From: Dominik Prokop Date: Mon, 6 Nov 2023 16:35:20 +0100 Subject: [PATCH 121/869] PanelInspector: Always use the latest panel data (#77732) PanelInspector: Always use the latest panel data --- .../features/dashboard/components/Inspector/PanelInspector.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/app/features/dashboard/components/Inspector/PanelInspector.tsx b/public/app/features/dashboard/components/Inspector/PanelInspector.tsx index 57a31c2a54078..3c9a56d9637b1 100644 --- a/public/app/features/dashboard/components/Inspector/PanelInspector.tsx +++ b/public/app/features/dashboard/components/Inspector/PanelInspector.tsx @@ -35,7 +35,7 @@ const PanelInspectorUnconnected = ({ panel, dashboard, plugin }: Props) => { withFieldConfig: true, }); - const { data, isLoading, hasError } = usePanelLatestData(panel, dataOptions, true); + const { data, isLoading, hasError } = usePanelLatestData(panel, dataOptions, false); const metaDs = useDatasourceMetadata(data); const tabs = useInspectTabs(panel, dashboard, plugin, hasError, metaDs); From 7322f98b9c156aad98bc735a519cdbdbc7f90218 Mon Sep 17 00:00:00 2001 From: Yasir Ekinci Date: Mon, 6 Nov 2023 16:40:11 +0100 Subject: [PATCH 122/869] DashGPT: Fix dashboard description use in panel generation (#75740) Dashboard description was using dashboard.title, this PR fixes it to use dashboard.description instead From c0d8a7132e38a4a1ff5cc9cfbfb56b96526b0506 Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Mon, 6 Nov 2023 17:08:11 +0100 Subject: [PATCH 123/869] Teams: Search by team ids (#77730) * Teams: Search by team ids * Add tests * Fix tests --- pkg/services/team/model.go | 1 + pkg/services/team/teamapi/team.go | 10 ++++++++++ pkg/services/team/teamimpl/store.go | 7 +++++++ pkg/services/team/teamimpl/store_test.go | 20 ++++++++++++++++++++ 4 files changed, 38 insertions(+) diff --git a/pkg/services/team/model.go b/pkg/services/team/model.go index 3ab72eace9ae5..63cfcce3ba5d2 100644 --- a/pkg/services/team/model.go +++ b/pkg/services/team/model.go @@ -92,6 +92,7 @@ type SearchTeamsQuery struct { Page int OrgID int64 `xorm:"org_id"` SortOpts []model.SortOption + TeamIds []int64 SignedInUser identity.Requester HiddenUsers map[string]struct{} } diff --git a/pkg/services/team/teamapi/team.go b/pkg/services/team/teamapi/team.go index ebcde9222d9e1..ab4b8b33c090e 100644 --- a/pkg/services/team/teamapi/team.go +++ b/pkg/services/team/teamapi/team.go @@ -154,10 +154,20 @@ func (tapi *TeamAPI) searchTeams(c *contextmodel.ReqContext) response.Response { return response.Err(err) } + stringTeamIDs := c.QueryStrings("teamId") + queryTeamIDs := make([]int64, 0) + for _, id := range stringTeamIDs { + teamID, err := strconv.ParseInt(id, 10, 64) + if err == nil { + queryTeamIDs = append(queryTeamIDs, teamID) + } + } + query := team.SearchTeamsQuery{ OrgID: c.SignedInUser.GetOrgID(), Query: c.Query("query"), Name: c.Query("name"), + TeamIds: queryTeamIDs, Page: page, Limit: perPage, SignedInUser: c.SignedInUser, diff --git a/pkg/services/team/teamimpl/store.go b/pkg/services/team/teamimpl/store.go index c600ecfb443a6..b91da0f3a5099 100644 --- a/pkg/services/team/teamimpl/store.go +++ b/pkg/services/team/teamimpl/store.go @@ -212,6 +212,13 @@ func (ss *xormStore) Search(ctx context.Context, query *team.SearchTeamsQuery) ( params = append(params, query.Name) } + if len(query.TeamIds) > 0 { + sql.WriteString(` and team.id IN (?` + strings.Repeat(",?", len(query.TeamIds)-1) + ")") + for _, id := range query.TeamIds { + params = append(params, id) + } + } + acFilter, err := ac.Filter(query.SignedInUser, "team.id", "teams:id:", ac.ActionTeamsRead) if err != nil { return err diff --git a/pkg/services/team/teamimpl/store_test.go b/pkg/services/team/teamimpl/store_test.go index 67d0937771754..78d91645bd54c 100644 --- a/pkg/services/team/teamimpl/store_test.go +++ b/pkg/services/team/teamimpl/store_test.go @@ -268,6 +268,26 @@ func TestIntegrationTeamCommandsAndQueries(t *testing.T) { require.Equal(t, queryResult.Teams[1].Name, team1.Name) }) + t.Run("Should be able to query teams by ids", func(t *testing.T) { + allTeamsQuery := &team.SearchTeamsQuery{OrgID: testOrgID, Query: "", SignedInUser: testUser} + allTeamsQueryResult, err := teamSvc.SearchTeams(context.Background(), allTeamsQuery) + require.NoError(t, err) + require.Equal(t, len(allTeamsQueryResult.Teams), 2) + + teamIds := make([]int64, 0) + for _, team := range allTeamsQueryResult.Teams { + teamIds = append(teamIds, team.ID) + } + + query := &team.SearchTeamsQuery{OrgID: testOrgID, SignedInUser: testUser, TeamIds: teamIds} + queryResult, err := teamSvc.SearchTeams(context.Background(), query) + require.NoError(t, err) + require.Equal(t, len(queryResult.Teams), 2) + require.EqualValues(t, queryResult.TotalCount, 2) + require.Equal(t, queryResult.Teams[0].ID, teamIds[0]) + require.Equal(t, queryResult.Teams[1].ID, teamIds[1]) + }) + t.Run("Should be able to return all teams a user is member of", func(t *testing.T) { sqlStore = db.InitTestDB(t) setup() From 25779bb6e5cedef712db15b9cc77ab4af68c31dd Mon Sep 17 00:00:00 2001 From: Alex Khomenko Date: Mon, 6 Nov 2023 17:15:52 +0100 Subject: [PATCH 124/869] Stack: Use the component from grafana/ui (#77543) * grafana/ui: Move Stack out of unstable * grafana/ui: Replace imports * Replace the import from experimental * Cleanup * Remove invalid prop * Add flexGrow * Remove Stack used in Field * Remove import --- .../src/components/Layout/Stack/Stack.tsx | 23 +- .../AccessControl/AddPermission.tsx | 3 +- .../QueryOperationRowHeader.tsx | 5 +- .../alerting/unified/AlertsFolderView.tsx | 3 +- .../unified/GrafanaRuleQueryViewer.tsx | 3 +- .../alerting/unified/NotificationPolicies.tsx | 3 +- .../features/alerting/unified/RuleList.tsx | 3 +- .../alerting/unified/components/HoverCard.tsx | 3 +- .../alerting/unified/components/Label.tsx | 5 +- .../alerting/unified/components/MetaText.tsx | 5 +- .../unified/components/MoreButton.tsx | 3 +- .../components/alert-groups/AlertGroup.tsx | 3 +- .../components/alert-groups/MatcherFilter.tsx | 3 +- .../contact-points/ContactPoints.v2.tsx | 2 +- .../components/expressions/Expression.tsx | 5 +- .../mute-timings/MuteTimingTimeInterval.tsx | 3 +- .../mute-timings/MuteTimingsTable.tsx | 3 +- .../AlertGroupsSummary.tsx | 3 +- .../ContactPointSelector.tsx | 5 +- .../EditNotificationPolicyForm.tsx | 2 +- .../notification-policies/Filters.tsx | 3 +- .../notification-policies/Matchers.tsx | 5 +- .../notification-policies/Modals.tsx | 5 +- .../notification-policies/Policy.tsx | 14 +- .../receivers/ReceiversAndTemplatesView.tsx | 3 +- .../components/receivers/ReceiversSection.tsx | 3 +- .../components/receivers/ReceiversTable.tsx | 3 +- .../components/receivers/TemplateDataDocs.tsx | 5 +- .../components/receivers/TemplateForm.tsx | 2 +- .../receivers/form/GenerateAlertDataModal.tsx | 3 +- .../ReceiverMetadataBadge.tsx | 3 +- .../rule-editor/AnnotationHeaderField.tsx | 3 +- .../rule-editor/AnnotationsStep.tsx | 3 +- .../components/rule-editor/FolderAndGroup.tsx | 224 +++++++++--------- .../rule-editor/GrafanaEvaluationBehavior.tsx | 15 +- .../components/rule-editor/LabelsField.tsx | 2 +- .../components/rule-editor/NeedHelpInfo.tsx | 3 +- .../rule-editor/NotificationsStep.tsx | 3 +- .../components/rule-editor/QueryRows.tsx | 3 +- .../components/rule-editor/QueryWrapper.tsx | 3 +- .../rule-editor/RuleEditorSection.tsx | 3 +- .../rule-editor/RuleFolderPicker.tsx | 3 +- .../alert-rule-form/AlertRuleForm.tsx | 3 +- .../alert-rule-form/ModifyExportRuleForm.tsx | 3 +- .../QueryAndExpressionsStep.tsx | 15 +- .../SmartAlertTypeDetector.tsx | 3 +- .../rule-editor/rule-types/RuleTypePicker.tsx | 3 +- .../components/rule-viewer/RuleViewer.v1.tsx | 15 +- .../rule-viewer/v2/RuleViewer.v2.tsx | 3 +- .../components/rules/EditRuleGroupModal.tsx | 59 +++-- .../unified/components/rules/NoRulesCTA.tsx | 5 +- .../components/rules/RuleActionsButtons.tsx | 2 +- .../unified/components/rules/RuleState.tsx | 3 +- .../unified/components/rules/RuleStats.tsx | 3 +- .../unified/components/rules/RulesFilter.tsx | 3 +- .../unified/components/rules/RulesGroup.tsx | 3 +- .../rules/state-history/LogRecordViewer.tsx | 9 +- .../rules/state-history/LokiStateHistory.tsx | 3 +- .../rules/state-history/StateHistory.tsx | 3 +- .../components/silences/SilencesFilter.tsx | 3 +- .../components/silences/SilencesTable.tsx | 3 +- .../alerting/unified/home/GettingStarted.tsx | 5 +- .../Forms/TransformationsEditor.tsx | 4 +- .../AnnotationSettingsEdit.tsx | 2 +- .../components/HelpWizard/HelpWizard.tsx | 2 +- .../components/PanelEditor/PanelEditor.tsx | 2 +- .../SaveDashboard/forms/SaveDashboardForm.tsx | 3 +- .../forms/SaveProvisionedDashboardForm.tsx | 3 +- .../VersionHistory/VersionHistoryButtons.tsx | 3 +- .../expressions/components/Condition.tsx | 5 +- .../features/expressions/components/Math.tsx | 5 +- .../app/features/inspector/QueryInspector.tsx | 3 +- public/app/features/org/UserInviteForm.tsx | 2 +- .../PluginDetailsHeaderDependencies.tsx | 3 +- .../configuration/TLSSecretsConfig.tsx | 3 +- public/app/features/scenes/SceneListPage.tsx | 3 +- .../editors/GroupByTransformerEditor.tsx | 5 +- .../LabelsToFieldsTransformerEditor.tsx | 5 +- .../variables/editor/VariableEditorList.tsx | 3 +- .../MetaDataInspector.tsx | 2 +- .../components/LokiQueryBuilderExplained.tsx | 2 +- .../components/NestedQueryList.tsx | 2 +- .../components/NestedQueryList.tsx | 2 +- .../components/PromQueryBuilderExplained.tsx | 2 +- .../components/metrics-modal/FeedbackLink.tsx | 3 +- .../querybuilder/shared/OperationEditor.tsx | 5 +- .../querybuilder/shared/OperationList.tsx | 3 +- .../shared/OperationParamEditor.tsx | 5 +- .../shared/OperationsEditorRow.tsx | 3 +- .../querybuilder/shared/QueryHeaderSwitch.tsx | 3 +- .../querybuilder/shared/QueryOptionGroup.tsx | 5 +- .../unified-alerting/UngroupedView.tsx | 5 +- .../table/cells/BarGaugeCellOptionsEditor.tsx | 3 +- 93 files changed, 317 insertions(+), 335 deletions(-) diff --git a/packages/grafana-ui/src/components/Layout/Stack/Stack.tsx b/packages/grafana-ui/src/components/Layout/Stack/Stack.tsx index 27baa49dfc5b7..d966ce2bf75c0 100644 --- a/packages/grafana-ui/src/components/Layout/Stack/Stack.tsx +++ b/packages/grafana-ui/src/components/Layout/Stack/Stack.tsx @@ -1,5 +1,5 @@ import { css } from '@emotion/css'; -import React from 'react'; +import React, { CSSProperties } from 'react'; import { GrafanaTheme2, ThemeSpacingTokens } from '@grafana/data'; @@ -40,11 +40,12 @@ interface StackProps extends Omit, 'className' direction?: ResponsiveProp; wrap?: ResponsiveProp; children?: React.ReactNode; + flexGrow?: ResponsiveProp; } export const Stack = React.forwardRef( - ({ gap = 1, alignItems, justifyContent, direction, wrap, children, ...rest }, ref) => { - const styles = useStyles2(getStyles, gap, alignItems, justifyContent, direction, wrap); + ({ gap = 1, alignItems, justifyContent, direction, wrap, children, flexGrow, ...rest }, ref) => { + const styles = useStyles2(getStyles, gap, alignItems, justifyContent, direction, wrap, flexGrow); return (
    @@ -62,28 +63,32 @@ const getStyles = ( alignItems: StackProps['alignItems'], justifyContent: StackProps['justifyContent'], direction: StackProps['direction'], - wrap: StackProps['wrap'] + wrap: StackProps['wrap'], + flexGrow: StackProps['flexGrow'] ) => { return { flex: css([ { display: 'flex', }, - getResponsiveStyle(theme, direction, (val) => ({ + getResponsiveStyle(theme, direction, (val) => ({ flexDirection: val, })), - getResponsiveStyle(theme, wrap, (val) => ({ + getResponsiveStyle(theme, wrap, (val) => ({ flexWrap: val, })), - getResponsiveStyle(theme, alignItems, (val) => ({ + getResponsiveStyle(theme, alignItems, (val) => ({ alignItems: val, })), - getResponsiveStyle(theme, justifyContent, (val) => ({ + getResponsiveStyle(theme, justifyContent, (val) => ({ justifyContent: val, })), - getResponsiveStyle(theme, gap, (val) => ({ + getResponsiveStyle(theme, gap, (val) => ({ gap: theme.spacing(val), })), + getResponsiveStyle(theme, flexGrow, (val) => ({ + flexGrow: val, + })), ]), }; }; diff --git a/public/app/core/components/AccessControl/AddPermission.tsx b/public/app/core/components/AccessControl/AddPermission.tsx index 740e0acc041ee..6098f20a13e70 100644 --- a/public/app/core/components/AccessControl/AddPermission.tsx +++ b/public/app/core/components/AccessControl/AddPermission.tsx @@ -1,7 +1,6 @@ import React, { useEffect, useMemo, useState } from 'react'; -import { Stack } from '@grafana/experimental'; -import { Button, Form, Select } from '@grafana/ui'; +import { Button, Form, Select, Stack } from '@grafana/ui'; import { CloseButton } from 'app/core/components/CloseButton/CloseButton'; import { ServiceAccountPicker } from 'app/core/components/Select/ServiceAccountPicker'; import { TeamPicker } from 'app/core/components/Select/TeamPicker'; diff --git a/public/app/core/components/QueryOperationRow/QueryOperationRowHeader.tsx b/public/app/core/components/QueryOperationRow/QueryOperationRowHeader.tsx index 83431260f3a3a..97cd26f64a0b0 100644 --- a/public/app/core/components/QueryOperationRow/QueryOperationRowHeader.tsx +++ b/public/app/core/components/QueryOperationRow/QueryOperationRowHeader.tsx @@ -3,8 +3,7 @@ import React, { MouseEventHandler } from 'react'; import { DraggableProvided } from 'react-beautiful-dnd'; import { GrafanaTheme2 } from '@grafana/data'; -import { Stack } from '@grafana/experimental'; -import { Icon, IconButton, useStyles2 } from '@grafana/ui'; +import { Icon, IconButton, useStyles2, Stack } from '@grafana/ui'; export interface QueryOperationRowHeaderProps { actionsElement?: React.ReactNode; @@ -73,7 +72,7 @@ export const QueryOperationRowHeader = ({ {headerElement}
    - + {actionsElement} {draggable && (
    diff --git a/public/app/features/alerting/unified/AlertsFolderView.tsx b/public/app/features/alerting/unified/AlertsFolderView.tsx index 060ed9b370810..bb121f0f25d05 100644 --- a/public/app/features/alerting/unified/AlertsFolderView.tsx +++ b/public/app/features/alerting/unified/AlertsFolderView.tsx @@ -4,8 +4,7 @@ import React, { useEffect, useState } from 'react'; import { useDebounce } from 'react-use'; import { GrafanaTheme2, SelectableValue } from '@grafana/data'; -import { Stack } from '@grafana/experimental'; -import { Card, FilterInput, Icon, Pagination, Select, TagList, useStyles2 } from '@grafana/ui'; +import { Card, FilterInput, Icon, Pagination, Select, TagList, useStyles2, Stack } from '@grafana/ui'; import { DEFAULT_PER_PAGE_PAGINATION } from 'app/core/constants'; import { getQueryParamValue } from 'app/core/utils/query'; import { FolderState, useDispatch } from 'app/types'; diff --git a/public/app/features/alerting/unified/GrafanaRuleQueryViewer.tsx b/public/app/features/alerting/unified/GrafanaRuleQueryViewer.tsx index 6fdab6e5aefd0..d6c17ded2490c 100644 --- a/public/app/features/alerting/unified/GrafanaRuleQueryViewer.tsx +++ b/public/app/features/alerting/unified/GrafanaRuleQueryViewer.tsx @@ -4,9 +4,8 @@ import { keyBy, startCase } from 'lodash'; import React from 'react'; import { DataSourceInstanceSettings, GrafanaTheme2, PanelData, RelativeTimeRange } from '@grafana/data'; -import { Stack } from '@grafana/experimental'; import { config } from '@grafana/runtime'; -import { Badge, useStyles2 } from '@grafana/ui'; +import { Badge, useStyles2, Stack } from '@grafana/ui'; import { mapRelativeTimeRangeToOption } from '@grafana/ui/src/components/DateTimePickers/RelativeTimeRangePicker/utils'; import { AlertQuery } from '../../../types/unified-alerting-dto'; diff --git a/public/app/features/alerting/unified/NotificationPolicies.tsx b/public/app/features/alerting/unified/NotificationPolicies.tsx index 861c5658d9ec2..d31c34a36d0c0 100644 --- a/public/app/features/alerting/unified/NotificationPolicies.tsx +++ b/public/app/features/alerting/unified/NotificationPolicies.tsx @@ -4,8 +4,7 @@ import React, { useEffect, useMemo, useState } from 'react'; import { useAsyncFn } from 'react-use'; import { GrafanaTheme2, UrlQueryMap } from '@grafana/data'; -import { Stack } from '@grafana/experimental'; -import { Alert, LoadingPlaceholder, Tab, TabContent, TabsBar, useStyles2, withErrorBoundary } from '@grafana/ui'; +import { Alert, LoadingPlaceholder, Tab, TabContent, TabsBar, useStyles2, withErrorBoundary, Stack } from '@grafana/ui'; import { useQueryParams } from 'app/core/hooks/useQueryParams'; import { ObjectMatcher, Route, RouteWithID } from 'app/plugins/datasource/alertmanager/types'; import { useDispatch } from 'app/types'; diff --git a/public/app/features/alerting/unified/RuleList.tsx b/public/app/features/alerting/unified/RuleList.tsx index a36a7eccff3d0..de55f35e0cd5c 100644 --- a/public/app/features/alerting/unified/RuleList.tsx +++ b/public/app/features/alerting/unified/RuleList.tsx @@ -3,8 +3,7 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { useAsyncFn, useInterval } from 'react-use'; import { GrafanaTheme2 } from '@grafana/data'; -import { Stack } from '@grafana/experimental'; -import { Button, useStyles2, withErrorBoundary } from '@grafana/ui'; +import { Button, useStyles2, withErrorBoundary, Stack } from '@grafana/ui'; import { useQueryParams } from 'app/core/hooks/useQueryParams'; import { useDispatch } from 'app/types'; diff --git a/public/app/features/alerting/unified/components/HoverCard.tsx b/public/app/features/alerting/unified/components/HoverCard.tsx index dfe470be13ea5..5d592b1113de4 100644 --- a/public/app/features/alerting/unified/components/HoverCard.tsx +++ b/public/app/features/alerting/unified/components/HoverCard.tsx @@ -4,8 +4,7 @@ import classnames from 'classnames'; import React, { ReactElement, ReactNode, useRef } from 'react'; import { GrafanaTheme2 } from '@grafana/data'; -import { Stack } from '@grafana/experimental'; -import { Popover as GrafanaPopover, PopoverController, useStyles2 } from '@grafana/ui'; +import { Popover as GrafanaPopover, PopoverController, useStyles2, Stack } from '@grafana/ui'; export interface HoverCardProps { children: ReactElement; diff --git a/public/app/features/alerting/unified/components/Label.tsx b/public/app/features/alerting/unified/components/Label.tsx index 42bd58d00409d..6c3788d77abc8 100644 --- a/public/app/features/alerting/unified/components/Label.tsx +++ b/public/app/features/alerting/unified/components/Label.tsx @@ -3,8 +3,7 @@ import React, { ReactNode } from 'react'; import tinycolor2 from 'tinycolor2'; import { GrafanaTheme2, IconName } from '@grafana/data'; -import { Stack } from '@grafana/experimental'; -import { Icon, useStyles2 } from '@grafana/ui'; +import { Icon, useStyles2, Stack } from '@grafana/ui'; export type LabelSize = 'md' | 'sm'; @@ -22,7 +21,7 @@ const Label = ({ label, value, icon, color, size = 'md' }: Props) => { return (
    - +
    {icon && } {label ?? ''} diff --git a/public/app/features/alerting/unified/components/MetaText.tsx b/public/app/features/alerting/unified/components/MetaText.tsx index beb183c82308e..f2f5764dd82f1 100644 --- a/public/app/features/alerting/unified/components/MetaText.tsx +++ b/public/app/features/alerting/unified/components/MetaText.tsx @@ -1,8 +1,7 @@ import { css, cx } from '@emotion/css'; import React, { ComponentProps, HTMLAttributes } from 'react'; -import { Stack } from '@grafana/experimental'; -import { Icon, IconName, useStyles2, Text } from '@grafana/ui'; +import { Icon, IconName, useStyles2, Text, Stack } from '@grafana/ui'; interface Props extends HTMLAttributes { icon?: IconName; @@ -22,7 +21,7 @@ const MetaText = ({ children, icon, color = 'secondary', ...rest }: Props) => { {...rest} > - + {icon && } {children} diff --git a/public/app/features/alerting/unified/components/MoreButton.tsx b/public/app/features/alerting/unified/components/MoreButton.tsx index a3f824d65050e..2fea06edbc370 100644 --- a/public/app/features/alerting/unified/components/MoreButton.tsx +++ b/public/app/features/alerting/unified/components/MoreButton.tsx @@ -1,7 +1,6 @@ import React, { forwardRef, Ref } from 'react'; -import { Stack } from '@grafana/experimental'; -import { Button, ButtonProps, Icon } from '@grafana/ui'; +import { Button, ButtonProps, Icon, Stack } from '@grafana/ui'; const MoreButton = forwardRef(function MoreButton(props: ButtonProps, ref: Ref) { return ( diff --git a/public/app/features/alerting/unified/components/alert-groups/AlertGroup.tsx b/public/app/features/alerting/unified/components/alert-groups/AlertGroup.tsx index f85d126d7c8e3..a50ff011d468d 100644 --- a/public/app/features/alerting/unified/components/alert-groups/AlertGroup.tsx +++ b/public/app/features/alerting/unified/components/alert-groups/AlertGroup.tsx @@ -2,8 +2,7 @@ import { css } from '@emotion/css'; import React, { useState } from 'react'; import { GrafanaTheme2 } from '@grafana/data'; -import { Stack } from '@grafana/experimental'; -import { useStyles2 } from '@grafana/ui'; +import { useStyles2, Stack } from '@grafana/ui'; import { AlertmanagerGroup, AlertState } from 'app/plugins/datasource/alertmanager/types'; import { AlertLabels } from '../AlertLabels'; diff --git a/public/app/features/alerting/unified/components/alert-groups/MatcherFilter.tsx b/public/app/features/alerting/unified/components/alert-groups/MatcherFilter.tsx index 7c6a51d20e5c2..66d7f580c56f4 100644 --- a/public/app/features/alerting/unified/components/alert-groups/MatcherFilter.tsx +++ b/public/app/features/alerting/unified/components/alert-groups/MatcherFilter.tsx @@ -3,8 +3,7 @@ import { debounce } from 'lodash'; import React, { FormEvent, useEffect, useMemo } from 'react'; import { GrafanaTheme2 } from '@grafana/data'; -import { Stack } from '@grafana/experimental'; -import { Label, Tooltip, Input, Icon, useStyles2 } from '@grafana/ui'; +import { Label, Tooltip, Input, Icon, useStyles2, Stack } from '@grafana/ui'; import { logInfo, LogMessages } from '../../Analytics'; diff --git a/public/app/features/alerting/unified/components/contact-points/ContactPoints.v2.tsx b/public/app/features/alerting/unified/components/contact-points/ContactPoints.v2.tsx index 90d8c227b5292..97a6c7cafc20f 100644 --- a/public/app/features/alerting/unified/components/contact-points/ContactPoints.v2.tsx +++ b/public/app/features/alerting/unified/components/contact-points/ContactPoints.v2.tsx @@ -7,7 +7,6 @@ import { Link } from 'react-router-dom'; import { useToggle } from 'react-use'; import { dateTime, GrafanaTheme2 } from '@grafana/data'; -import { Stack } from '@grafana/experimental'; import { Alert, Dropdown, @@ -23,6 +22,7 @@ import { Tab, Pagination, Button, + Stack, } from '@grafana/ui'; import ConditionalWrap from 'app/features/alerting/components/ConditionalWrap'; import { receiverTypeNames } from 'app/plugins/datasource/alertmanager/consts'; diff --git a/public/app/features/alerting/unified/components/expressions/Expression.tsx b/public/app/features/alerting/unified/components/expressions/Expression.tsx index efc5bbd6b8bd5..b748e0308f2bf 100644 --- a/public/app/features/alerting/unified/components/expressions/Expression.tsx +++ b/public/app/features/alerting/unified/components/expressions/Expression.tsx @@ -3,8 +3,7 @@ import { uniqueId } from 'lodash'; import React, { FC, useCallback, useState } from 'react'; import { DataFrame, dateTimeFormat, GrafanaTheme2, isTimeSeriesFrames, LoadingState, PanelData } from '@grafana/data'; -import { Stack } from '@grafana/experimental'; -import { AutoSizeInput, Button, clearButtonStyles, IconButton, useStyles2 } from '@grafana/ui'; +import { AutoSizeInput, Button, clearButtonStyles, IconButton, useStyles2, Stack } from '@grafana/ui'; import { ClassicConditions } from 'app/features/expressions/components/ClassicConditions'; import { Math } from 'app/features/expressions/components/Math'; import { Reduce } from 'app/features/expressions/components/Reduce'; @@ -288,7 +287,7 @@ const Header: FC = ({ return (
    - + {!editingRefId && ( - - )) ||
    Creating new folder...
    } -
    - - } - {isCreatingFolder && ( - setIsCreatingFolder(false)} /> - )} -
    - -
    + + Folder + + } className={styles.formInput} - error={errors.group?.message} - invalid={!!errors.group?.message} + error={errors.folder?.message} + invalid={!!errors.folder?.message} + data-testid="folder-picker" > - + {(!isCreatingFolder && ( ( + render={({ field: { ref, ...field } }) => (
    - { - field.onChange(group.label ?? ''); + enableReset={true} + onChange={({ title, uid }) => { + field.onChange({ title, uid }); + resetGroup(); }} - isLoading={loading} - invalid={Boolean(folder) && !group && Boolean(fieldState.error)} - loadOptions={debouncedSearch} - cacheOptions - loadingMessage={'Loading groups...'} - defaultValue={defaultGroupValue} - defaultOptions={groupOptions} - getOptionLabel={(option: SelectableValue) => ( -
    - {option.label} - {option['isProvisioned'] && ( - <> - {' '} - - - )} -
    - )} - placeholder={'Select an evaluation group...'} />
    )} + name="folder" + rules={{ + required: { value: true, message: 'Select a folder' }, + validate: { + pathSeparator: (folder: Folder) => checkForPathSeparator(folder.title), + }, + }} + /> + )) ||
    Creating new folder...
    } +
    + + or + + +
    + + {isCreatingFolder && ( + setIsCreatingFolder(false)} /> + )} + + +
    + + ( + { + field.onChange(group.label ?? ''); + }} + isLoading={loading} + invalid={Boolean(folder) && !group && Boolean(fieldState.error)} + loadOptions={debouncedSearch} + cacheOptions + loadingMessage={'Loading groups...'} + defaultValue={defaultGroupValue} + defaultOptions={groupOptions} + getOptionLabel={(option: SelectableValue) => ( +
    + {option.label} + {option['isProvisioned'] && ( + <> + {' '} + + + )} +
    + )} + placeholder={'Select an evaluation group...'} + /> + )} name="group" control={control} rules={{ @@ -245,19 +251,21 @@ export function FolderAndGroup({ }, }} /> - or - - -
    + +
    + + or + + {isCreatingEvaluationGroup && ( )} -
    +
    ); } 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 0d640760a14c4..a1f88222c2b61 100644 --- a/public/app/features/alerting/unified/components/rule-editor/GrafanaEvaluationBehavior.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/GrafanaEvaluationBehavior.tsx @@ -3,8 +3,19 @@ import React, { useCallback, useEffect, useState } from 'react'; import { RegisterOptions, useFormContext } from 'react-hook-form'; import { GrafanaTheme2, SelectableValue } from '@grafana/data'; -import { Stack } from '@grafana/experimental'; -import { Field, Icon, IconButton, Input, InputControl, Label, Switch, Text, Tooltip, useStyles2 } from '@grafana/ui'; +import { + Field, + Icon, + IconButton, + Input, + InputControl, + Label, + Stack, + Switch, + Text, + Tooltip, + useStyles2, +} from '@grafana/ui'; import { CombinedRuleGroup, CombinedRuleNamespace } from '../../../../../types/unified-alerting'; import { logInfo, LogMessages } from '../../Analytics'; diff --git a/public/app/features/alerting/unified/components/rule-editor/LabelsField.tsx b/public/app/features/alerting/unified/components/rule-editor/LabelsField.tsx index 6e8b82d7a4bc7..fdeec0525ab8b 100644 --- a/public/app/features/alerting/unified/components/rule-editor/LabelsField.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/LabelsField.tsx @@ -3,7 +3,6 @@ import React, { FC, useCallback, useEffect, useMemo, useState } from 'react'; import { FieldArrayMethodProps, useFieldArray, useFormContext } from 'react-hook-form'; import { GrafanaTheme2, SelectableValue } from '@grafana/data'; -import { Stack } from '@grafana/experimental'; import { Button, Field, @@ -15,6 +14,7 @@ import { Icon, Input, LoadingPlaceholder, + Stack, } from '@grafana/ui'; import { useDispatch } from 'app/types'; diff --git a/public/app/features/alerting/unified/components/rule-editor/NeedHelpInfo.tsx b/public/app/features/alerting/unified/components/rule-editor/NeedHelpInfo.tsx index 70e27b0a2c6b4..7d1cc340c08d5 100644 --- a/public/app/features/alerting/unified/components/rule-editor/NeedHelpInfo.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/NeedHelpInfo.tsx @@ -2,8 +2,7 @@ import { css } from '@emotion/css'; import React from 'react'; import { GrafanaTheme2 } from '@grafana/data'; -import { Stack } from '@grafana/experimental'; -import { Icon, Text, Toggletip, useStyles2 } from '@grafana/ui'; +import { Icon, Text, Toggletip, useStyles2, Stack } from '@grafana/ui'; interface NeedHelpInfoProps { contentText: string | JSX.Element; diff --git a/public/app/features/alerting/unified/components/rule-editor/NotificationsStep.tsx b/public/app/features/alerting/unified/components/rule-editor/NotificationsStep.tsx index 7ce8541a99fb5..8f141328cc5b1 100644 --- a/public/app/features/alerting/unified/components/rule-editor/NotificationsStep.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/NotificationsStep.tsx @@ -1,8 +1,7 @@ import React from 'react'; import { useFormContext } from 'react-hook-form'; -import { Stack } from '@grafana/experimental'; -import { Icon, Text } from '@grafana/ui'; +import { Icon, Text, Stack } from '@grafana/ui'; import { RuleFormType, RuleFormValues } from '../../types/rule-form'; import { GRAFANA_RULES_SOURCE_NAME } from '../../utils/datasource'; diff --git a/public/app/features/alerting/unified/components/rule-editor/QueryRows.tsx b/public/app/features/alerting/unified/components/rule-editor/QueryRows.tsx index ed5941fd90c17..fc17b0b323efd 100644 --- a/public/app/features/alerting/unified/components/rule-editor/QueryRows.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/QueryRows.tsx @@ -10,9 +10,8 @@ import { rangeUtil, RelativeTimeRange, } from '@grafana/data'; -import { Stack } from '@grafana/experimental'; import { getDataSourceSrv } from '@grafana/runtime'; -import { Button, Card, Icon } from '@grafana/ui'; +import { Button, Card, Icon, Stack } from '@grafana/ui'; import { QueryOperationRow } from 'app/core/components/QueryOperationRow/QueryOperationRow'; import { getDatasourceSrv } from 'app/features/plugins/datasource_srv'; import { AlertDataQuery, AlertQuery } from 'app/types/unified-alerting-dto'; diff --git a/public/app/features/alerting/unified/components/rule-editor/QueryWrapper.tsx b/public/app/features/alerting/unified/components/rule-editor/QueryWrapper.tsx index 45c06c5afa557..a7ef27be7e083 100644 --- a/public/app/features/alerting/unified/components/rule-editor/QueryWrapper.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/QueryWrapper.tsx @@ -12,9 +12,8 @@ import { RelativeTimeRange, ThresholdsConfig, } from '@grafana/data'; -import { Stack } from '@grafana/experimental'; import { DataQuery } from '@grafana/schema'; -import { GraphTresholdsStyleMode, Icon, InlineField, Input, Tooltip, useStyles2 } from '@grafana/ui'; +import { GraphTresholdsStyleMode, Icon, InlineField, Input, Tooltip, useStyles2, Stack } from '@grafana/ui'; import { QueryEditorRow } from 'app/features/query/components/QueryEditorRow'; import { AlertQuery } from 'app/types/unified-alerting-dto'; diff --git a/public/app/features/alerting/unified/components/rule-editor/RuleEditorSection.tsx b/public/app/features/alerting/unified/components/rule-editor/RuleEditorSection.tsx index f291440327d9d..cf3b0676adbaa 100644 --- a/public/app/features/alerting/unified/components/rule-editor/RuleEditorSection.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/RuleEditorSection.tsx @@ -2,8 +2,7 @@ import { css, cx } from '@emotion/css'; import React, { ReactElement } from 'react'; import { GrafanaTheme2 } from '@grafana/data'; -import { Stack } from '@grafana/experimental'; -import { FieldSet, Text, useStyles2 } from '@grafana/ui'; +import { FieldSet, Text, useStyles2, Stack } from '@grafana/ui'; export interface RuleEditorSectionProps { title: string; diff --git a/public/app/features/alerting/unified/components/rule-editor/RuleFolderPicker.tsx b/public/app/features/alerting/unified/components/rule-editor/RuleFolderPicker.tsx index 98318fd3a8bf9..0afb7361a841d 100644 --- a/public/app/features/alerting/unified/components/rule-editor/RuleFolderPicker.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/RuleFolderPicker.tsx @@ -2,8 +2,7 @@ import { css } from '@emotion/css'; import React from 'react'; import { GrafanaTheme2 } from '@grafana/data'; -import { Stack } from '@grafana/experimental'; -import { Icon, Tooltip, useStyles2 } from '@grafana/ui'; +import { Icon, Tooltip, useStyles2, Stack } from '@grafana/ui'; import { OldFolderPicker, Props as FolderPickerProps } from 'app/core/components/Select/OldFolderPicker'; import { PermissionLevelString, SearchQueryType } from 'app/types'; 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 d4a7990ba92a9..628dac968074a 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 @@ -4,9 +4,8 @@ import { DeepMap, FieldError, FormProvider, useForm, UseFormWatch } from 'react- import { Link, useParams } from 'react-router-dom'; import { GrafanaTheme2 } from '@grafana/data'; -import { Stack } from '@grafana/experimental'; import { config } from '@grafana/runtime'; -import { Button, ConfirmModal, CustomScrollbar, HorizontalGroup, Spinner, useStyles2 } from '@grafana/ui'; +import { Button, ConfirmModal, CustomScrollbar, HorizontalGroup, Spinner, useStyles2, Stack } from '@grafana/ui'; import { AppChromeUpdate } from 'app/core/components/AppChrome/AppChromeUpdate'; import { useAppNotification } from 'app/core/copy/appNotification'; import { contextSrv } from 'app/core/core'; 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 c503301017179..f5506787546c5 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 @@ -2,8 +2,7 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { FormProvider, useForm } from 'react-hook-form'; import { useAsync } from 'react-use'; -import { Stack } from '@grafana/experimental'; -import { Button, CustomScrollbar, LinkButton, LoadingPlaceholder } from '@grafana/ui'; +import { Button, CustomScrollbar, LinkButton, LoadingPlaceholder, Stack } from '@grafana/ui'; import { useAppNotification } from 'app/core/copy/appNotification'; import { useQueryParams } from 'app/core/hooks/useQueryParams'; 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 b1b56b46e48ee..2031db493afcc 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 @@ -5,9 +5,20 @@ import { useFormContext } from 'react-hook-form'; import { getDefaultRelativeTimeRange, GrafanaTheme2 } from '@grafana/data'; import { selectors } from '@grafana/e2e-selectors'; -import { Stack } from '@grafana/experimental'; import { config, getDataSourceSrv } from '@grafana/runtime'; -import { Alert, Button, Dropdown, Field, Icon, InputControl, Menu, MenuItem, Tooltip, useStyles2 } from '@grafana/ui'; +import { + Alert, + Button, + Dropdown, + Field, + Icon, + InputControl, + Menu, + MenuItem, + Stack, + Tooltip, + useStyles2, +} from '@grafana/ui'; import { Text } from '@grafana/ui/src/components/Text/Text'; import { isExpressionQuery } from 'app/features/expressions/guards'; import { ExpressionDatasourceUID, ExpressionQueryType, expressionTypes } from 'app/features/expressions/types'; diff --git a/public/app/features/alerting/unified/components/rule-editor/query-and-alert-condition/SmartAlertTypeDetector.tsx b/public/app/features/alerting/unified/components/rule-editor/query-and-alert-condition/SmartAlertTypeDetector.tsx index 86144503a02da..5a526fc8443da 100644 --- a/public/app/features/alerting/unified/components/rule-editor/query-and-alert-condition/SmartAlertTypeDetector.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/query-and-alert-condition/SmartAlertTypeDetector.tsx @@ -2,9 +2,8 @@ import React from 'react'; import { useFormContext } from 'react-hook-form'; import { DataSourceInstanceSettings } from '@grafana/data'; -import { Stack } from '@grafana/experimental'; import { DataSourceJsonData } from '@grafana/schema'; -import { RadioButtonGroup, Text } from '@grafana/ui'; +import { RadioButtonGroup, Text, Stack } from '@grafana/ui'; import { contextSrv } from 'app/core/core'; import { ExpressionDatasourceUID } from 'app/features/expressions/types'; import { AccessControlAction } from 'app/types'; diff --git a/public/app/features/alerting/unified/components/rule-editor/rule-types/RuleTypePicker.tsx b/public/app/features/alerting/unified/components/rule-editor/rule-types/RuleTypePicker.tsx index 9f51d74e4f164..60d54a0df97c8 100644 --- a/public/app/features/alerting/unified/components/rule-editor/rule-types/RuleTypePicker.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/rule-types/RuleTypePicker.tsx @@ -3,8 +3,7 @@ import { isEmpty } from 'lodash'; import React, { useEffect } from 'react'; import { GrafanaTheme2 } from '@grafana/data/src'; -import { Stack } from '@grafana/experimental'; -import { useStyles2 } from '@grafana/ui'; +import { useStyles2, Stack } from '@grafana/ui'; import { dispatch } from 'app/store/store'; import { useRulesSourcesWithRuler } from '../../../hooks/useRuleSourcesWithRuler'; diff --git a/public/app/features/alerting/unified/components/rule-viewer/RuleViewer.v1.tsx b/public/app/features/alerting/unified/components/rule-viewer/RuleViewer.v1.tsx index 6081d1b1a0757..4861a87a1f555 100644 --- a/public/app/features/alerting/unified/components/rule-viewer/RuleViewer.v1.tsx +++ b/public/app/features/alerting/unified/components/rule-viewer/RuleViewer.v1.tsx @@ -4,9 +4,18 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { useObservable, useToggle } from 'react-use'; import { GrafanaTheme2, LoadingState, PanelData, RelativeTimeRange } from '@grafana/data'; -import { Stack } from '@grafana/experimental'; import { config, isFetchError } from '@grafana/runtime'; -import { Alert, Button, Collapse, Icon, IconButton, LoadingPlaceholder, useStyles2, VerticalGroup } from '@grafana/ui'; +import { + Alert, + Button, + Collapse, + Icon, + IconButton, + LoadingPlaceholder, + Stack, + VerticalGroup, + useStyles2, +} from '@grafana/ui'; import { GrafanaRouteComponentProps } from 'app/core/navigation/types'; import { DEFAULT_PER_PAGE_PAGINATION } from '../../../../../core/constants'; @@ -164,7 +173,7 @@ export function RuleViewer({ match }: RuleViewerProps) { {isProvisioned && }
    - + {rule.name} diff --git a/public/app/features/alerting/unified/components/rule-viewer/v2/RuleViewer.v2.tsx b/public/app/features/alerting/unified/components/rule-viewer/v2/RuleViewer.v2.tsx index 9ddc8761e97a2..52c29a2367fb0 100644 --- a/public/app/features/alerting/unified/components/rule-viewer/v2/RuleViewer.v2.tsx +++ b/public/app/features/alerting/unified/components/rule-viewer/v2/RuleViewer.v2.tsx @@ -1,7 +1,6 @@ import React, { useMemo, useState } from 'react'; -import { Stack } from '@grafana/experimental'; -import { Alert, Button, Icon, LoadingPlaceholder, Tab, TabContent, TabsBar, Text } from '@grafana/ui'; +import { Alert, Button, Icon, LoadingPlaceholder, Tab, TabContent, TabsBar, Text, Stack } from '@grafana/ui'; import { GrafanaRouteComponentProps } from 'app/core/navigation/types'; import { GrafanaAlertState } from 'app/types/unified-alerting-dto'; diff --git a/public/app/features/alerting/unified/components/rules/EditRuleGroupModal.tsx b/public/app/features/alerting/unified/components/rules/EditRuleGroupModal.tsx index 421720fc86520..395d7d33f0208 100644 --- a/public/app/features/alerting/unified/components/rules/EditRuleGroupModal.tsx +++ b/public/app/features/alerting/unified/components/rules/EditRuleGroupModal.tsx @@ -4,8 +4,7 @@ import React, { useEffect, useMemo } from 'react'; import { FormProvider, RegisterOptions, useForm, useFormContext } from 'react-hook-form'; import { GrafanaTheme2 } from '@grafana/data'; -import { Stack } from '@grafana/experimental'; -import { Badge, Button, Field, Input, Label, LinkButton, Modal, useStyles2 } from '@grafana/ui'; +import { Badge, Button, Field, Input, Label, LinkButton, Modal, useStyles2, Stack } from '@grafana/ui'; import { useAppNotification } from 'app/core/copy/appNotification'; import { useCleanup } from 'app/core/hooks/useCleanup'; import { useDispatch } from 'app/types'; @@ -235,41 +234,41 @@ export function EditCloudGroupModal(props: ModalProps): React.ReactElement { e.preventDefault()} key={JSON.stringify(defaultValues)}> <> {!props.hideFolder && ( - - {nameSpaceLabel} - - } - invalid={!!errors.namespaceName} - error={errors.namespaceName?.message} - > - + + + {nameSpaceLabel} + + } + invalid={!!errors.namespaceName} + error={errors.namespaceName?.message} + > - {isGrafanaManagedGroup && props.folderUrl && ( - - )} - - + + {isGrafanaManagedGroup && props.folderUrl && ( + + )} + )} Evaluation group name} diff --git a/public/app/features/alerting/unified/components/rules/NoRulesCTA.tsx b/public/app/features/alerting/unified/components/rules/NoRulesCTA.tsx index ee64d3795d97d..c1104a10a5d8c 100644 --- a/public/app/features/alerting/unified/components/rules/NoRulesCTA.tsx +++ b/public/app/features/alerting/unified/components/rules/NoRulesCTA.tsx @@ -2,8 +2,7 @@ import { css } from '@emotion/css'; import React from 'react'; import { GrafanaTheme2 } from '@grafana/data/src/themes'; -import { Stack } from '@grafana/experimental'; -import { CallToActionCard, useStyles2 } from '@grafana/ui'; +import { CallToActionCard, useStyles2, Stack } from '@grafana/ui'; import EmptyListCTA from 'app/core/components/EmptyListCTA/EmptyListCTA'; import { logInfo, LogMessages } from '../../Analytics'; @@ -16,7 +15,7 @@ export const NoRulesSplash = () => { return (

    {"You haven't created any alert rules yet"}

    - +
    - {line.values && } + {line.values && }
    {line.labels && ( - {line.values && } + {line.values && }
    {dateTimeFormat(timestamp)}
    ))} @@ -134,7 +133,7 @@ const Timestamp = ({ time }: TimestampProps) => { return (
    - + {dateTimeFormat(dateTime)} ({formatDistanceToNowStrict(dateTime)} ago) diff --git a/public/app/features/alerting/unified/components/rules/state-history/LokiStateHistory.tsx b/public/app/features/alerting/unified/components/rules/state-history/LokiStateHistory.tsx index 115a127d65e6d..30c476bf8d5cb 100644 --- a/public/app/features/alerting/unified/components/rules/state-history/LokiStateHistory.tsx +++ b/public/app/features/alerting/unified/components/rules/state-history/LokiStateHistory.tsx @@ -4,8 +4,7 @@ import React, { useCallback, useMemo, useRef, useState } from 'react'; import { useForm } from 'react-hook-form'; import { DataFrame, dateTime, GrafanaTheme2, TimeRange } from '@grafana/data'; -import { Stack } from '@grafana/experimental'; -import { Alert, Button, Field, Icon, Input, Label, TagList, Tooltip, useStyles2 } from '@grafana/ui'; +import { Alert, Button, Field, Icon, Input, Label, TagList, Tooltip, useStyles2, Stack } from '@grafana/ui'; import { stateHistoryApi } from '../../../api/stateHistoryApi'; import { combineMatcherStrings } from '../../../utils/alertmanager'; diff --git a/public/app/features/alerting/unified/components/rules/state-history/StateHistory.tsx b/public/app/features/alerting/unified/components/rules/state-history/StateHistory.tsx index 1ba413adf6c9c..816722aa86075 100644 --- a/public/app/features/alerting/unified/components/rules/state-history/StateHistory.tsx +++ b/public/app/features/alerting/unified/components/rules/state-history/StateHistory.tsx @@ -3,8 +3,7 @@ import { groupBy } from 'lodash'; import React, { FormEvent, useCallback, useState } from 'react'; import { AlertState, dateTimeFormat, GrafanaTheme2 } from '@grafana/data'; -import { Stack } from '@grafana/experimental'; -import { Alert, Field, Icon, Input, Label, LoadingPlaceholder, Tooltip, useStyles2 } from '@grafana/ui'; +import { Alert, Field, Icon, Input, Label, LoadingPlaceholder, Tooltip, useStyles2, Stack } from '@grafana/ui'; import { StateHistoryItem, StateHistoryItemData } from 'app/types/unified-alerting'; import { GrafanaAlertStateWithReason, PromAlertingRuleState } from 'app/types/unified-alerting-dto'; diff --git a/public/app/features/alerting/unified/components/silences/SilencesFilter.tsx b/public/app/features/alerting/unified/components/silences/SilencesFilter.tsx index acb6b61aa6625..c8bbd93926370 100644 --- a/public/app/features/alerting/unified/components/silences/SilencesFilter.tsx +++ b/public/app/features/alerting/unified/components/silences/SilencesFilter.tsx @@ -3,8 +3,7 @@ import { debounce, uniqueId } from 'lodash'; import React, { FormEvent, useState } from 'react'; import { GrafanaTheme2 } from '@grafana/data'; -import { Stack } from '@grafana/experimental'; -import { Button, Field, Icon, Input, Label, Tooltip, useStyles2 } from '@grafana/ui'; +import { Button, Field, Icon, Input, Label, Tooltip, useStyles2, Stack } from '@grafana/ui'; import { useQueryParams } from 'app/core/hooks/useQueryParams'; import { parseMatchers } from '../../utils/alertmanager'; diff --git a/public/app/features/alerting/unified/components/silences/SilencesTable.tsx b/public/app/features/alerting/unified/components/silences/SilencesTable.tsx index 402df1507c37d..5b33f85197bf3 100644 --- a/public/app/features/alerting/unified/components/silences/SilencesTable.tsx +++ b/public/app/features/alerting/unified/components/silences/SilencesTable.tsx @@ -2,8 +2,7 @@ import { css } from '@emotion/css'; import React, { useMemo } from 'react'; import { dateMath, GrafanaTheme2 } from '@grafana/data'; -import { Stack } from '@grafana/experimental'; -import { CollapsableSection, Icon, Link, LinkButton, useStyles2 } from '@grafana/ui'; +import { CollapsableSection, Icon, Link, LinkButton, useStyles2, Stack } from '@grafana/ui'; import { useQueryParams } from 'app/core/hooks/useQueryParams'; import { AlertmanagerAlert, Silence, SilenceState } from 'app/plugins/datasource/alertmanager/types'; import { useDispatch } from 'app/types'; diff --git a/public/app/features/alerting/unified/home/GettingStarted.tsx b/public/app/features/alerting/unified/home/GettingStarted.tsx index c768ad97b32c1..bc06edb7e9f4d 100644 --- a/public/app/features/alerting/unified/home/GettingStarted.tsx +++ b/public/app/features/alerting/unified/home/GettingStarted.tsx @@ -3,9 +3,8 @@ import React from 'react'; import SVG from 'react-inlinesvg'; import { GrafanaTheme2 } from '@grafana/data'; -import { Stack } from '@grafana/experimental'; import { EmbeddedScene, SceneFlexLayout, SceneFlexItem, SceneReactObject } from '@grafana/scenes'; -import { Icon, useStyles2, useTheme2 } from '@grafana/ui'; +import { Icon, useStyles2, useTheme2, Stack } from '@grafana/ui'; export const getOverviewScene = () => { return new EmbeddedScene({ @@ -48,7 +47,7 @@ export default function GettingStarted({ showWelcomeHeader }: { showWelcomeHeade

    Get started

    - +
    • Create an alert rule by adding queries and expressions from multiple data sources. diff --git a/public/app/features/correlations/Forms/TransformationsEditor.tsx b/public/app/features/correlations/Forms/TransformationsEditor.tsx index 8003ee2836c63..5740ff38736e5 100644 --- a/public/app/features/correlations/Forms/TransformationsEditor.tsx +++ b/public/app/features/correlations/Forms/TransformationsEditor.tsx @@ -4,7 +4,6 @@ import React, { useState } from 'react'; import { useFormContext } from 'react-hook-form'; import { GrafanaTheme2 } from '@grafana/data'; -import { Stack } from '@grafana/experimental'; import { Button, Field, @@ -17,6 +16,7 @@ import { Select, Tooltip, useStyles2, + Stack, } from '@grafana/ui'; import { getSupportedTransTypeDetails, getTransformOptions } from './types'; @@ -56,7 +56,7 @@ export const TransformationsEditor = (props: Props) => {
      {fields.map((fieldVal, index) => { return ( - + diff --git a/public/app/features/dashboard/components/AnnotationSettings/AnnotationSettingsEdit.tsx b/public/app/features/dashboard/components/AnnotationSettings/AnnotationSettingsEdit.tsx index 7e656629641ee..adc52f64b2134 100644 --- a/public/app/features/dashboard/components/AnnotationSettings/AnnotationSettingsEdit.tsx +++ b/public/app/features/dashboard/components/AnnotationSettings/AnnotationSettingsEdit.tsx @@ -10,7 +10,6 @@ import { SelectableValue, } from '@grafana/data'; import { selectors } from '@grafana/e2e-selectors'; -import { Stack } from '@grafana/experimental'; import { getDataSourceSrv, locationService } from '@grafana/runtime'; import { AnnotationPanelFilter } from '@grafana/schema/src/raw/dashboard/x/dashboard_types.gen'; import { @@ -23,6 +22,7 @@ import { MultiSelect, Select, useStyles2, + Stack, } from '@grafana/ui'; import { ColorValueEditor } from 'app/core/components/OptionsUI/color'; import config from 'app/core/config'; diff --git a/public/app/features/dashboard/components/HelpWizard/HelpWizard.tsx b/public/app/features/dashboard/components/HelpWizard/HelpWizard.tsx index b0fd9dc7a57c8..cb94d8c67b4d2 100644 --- a/public/app/features/dashboard/components/HelpWizard/HelpWizard.tsx +++ b/public/app/features/dashboard/components/HelpWizard/HelpWizard.tsx @@ -3,7 +3,6 @@ import React, { useMemo, useEffect } from 'react'; import AutoSizer from 'react-virtualized-auto-sizer'; import { PanelPlugin, GrafanaTheme2, FeatureState } from '@grafana/data'; -import { Stack } from '@grafana/experimental'; import { config } from '@grafana/runtime'; import { Drawer, @@ -21,6 +20,7 @@ import { Select, ClipboardButton, Icon, + Stack, } from '@grafana/ui'; import { contextSrv } from 'app/core/services/context_srv'; import { PanelModel } from 'app/features/dashboard/state'; diff --git a/public/app/features/dashboard/components/PanelEditor/PanelEditor.tsx b/public/app/features/dashboard/components/PanelEditor/PanelEditor.tsx index 4a5b75a58ff65..eb31dae7c041e 100644 --- a/public/app/features/dashboard/components/PanelEditor/PanelEditor.tsx +++ b/public/app/features/dashboard/components/PanelEditor/PanelEditor.tsx @@ -6,7 +6,6 @@ import { Subscription } from 'rxjs'; import { FieldConfigSource, GrafanaTheme2, NavModel, NavModelItem, PageLayoutType } from '@grafana/data'; import { selectors } from '@grafana/e2e-selectors'; -import { Stack } from '@grafana/experimental'; import { locationService } from '@grafana/runtime'; import { Button, @@ -19,6 +18,7 @@ import { ToolbarButton, ToolbarButtonRow, withTheme2, + Stack, } from '@grafana/ui'; import { AppChromeUpdate } from 'app/core/components/AppChrome/AppChromeUpdate'; import { Page } from 'app/core/components/Page/Page'; diff --git a/public/app/features/dashboard/components/SaveDashboard/forms/SaveDashboardForm.tsx b/public/app/features/dashboard/components/SaveDashboard/forms/SaveDashboardForm.tsx index 622eef95c90c8..f2faf077c86e4 100644 --- a/public/app/features/dashboard/components/SaveDashboard/forms/SaveDashboardForm.tsx +++ b/public/app/features/dashboard/components/SaveDashboard/forms/SaveDashboardForm.tsx @@ -3,10 +3,9 @@ import React, { useMemo, useState } from 'react'; import { GrafanaTheme2 } from '@grafana/data'; import { selectors } from '@grafana/e2e-selectors'; -import { Stack } from '@grafana/experimental'; import { config } from '@grafana/runtime'; import { Dashboard } from '@grafana/schema'; -import { Button, Checkbox, Form, TextArea, useStyles2 } from '@grafana/ui'; +import { Button, Checkbox, Form, TextArea, useStyles2, Stack } from '@grafana/ui'; import { DashboardModel } from 'app/features/dashboard/state'; import { GenAIDashboardChangesButton } from '../../GenAI/GenAIDashboardChangesButton'; diff --git a/public/app/features/dashboard/components/SaveDashboard/forms/SaveProvisionedDashboardForm.tsx b/public/app/features/dashboard/components/SaveDashboard/forms/SaveProvisionedDashboardForm.tsx index 984d5b466838c..785b568528ccd 100644 --- a/public/app/features/dashboard/components/SaveDashboard/forms/SaveProvisionedDashboardForm.tsx +++ b/public/app/features/dashboard/components/SaveDashboard/forms/SaveProvisionedDashboardForm.tsx @@ -2,8 +2,7 @@ import { css } from '@emotion/css'; import { saveAs } from 'file-saver'; import React, { useCallback, useState } from 'react'; -import { Stack } from '@grafana/experimental'; -import { Button, ClipboardButton, HorizontalGroup, TextArea } from '@grafana/ui'; +import { Button, ClipboardButton, HorizontalGroup, TextArea, Stack } from '@grafana/ui'; import { SaveDashboardFormProps } from '../types'; diff --git a/public/app/features/dashboard/components/VersionHistory/VersionHistoryButtons.tsx b/public/app/features/dashboard/components/VersionHistory/VersionHistoryButtons.tsx index e39fc70ac7a25..073b9c64d8dde 100644 --- a/public/app/features/dashboard/components/VersionHistory/VersionHistoryButtons.tsx +++ b/public/app/features/dashboard/components/VersionHistory/VersionHistoryButtons.tsx @@ -1,7 +1,6 @@ import React from 'react'; -import { Stack } from '@grafana/experimental'; -import { Tooltip, Button } from '@grafana/ui'; +import { Tooltip, Button, Stack } from '@grafana/ui'; type VersionsButtonsType = { hasMore: boolean; diff --git a/public/app/features/expressions/components/Condition.tsx b/public/app/features/expressions/components/Condition.tsx index 82dbb93c68285..72fc74027fe71 100644 --- a/public/app/features/expressions/components/Condition.tsx +++ b/public/app/features/expressions/components/Condition.tsx @@ -2,8 +2,7 @@ import { css, cx } from '@emotion/css'; import React, { FormEvent } from 'react'; import { GrafanaTheme2, SelectableValue } from '@grafana/data'; -import { Stack } from '@grafana/experimental'; -import { Button, ButtonSelect, Icon, InlineFieldRow, Input, Select, useStyles2 } from '@grafana/ui'; +import { Button, ButtonSelect, Icon, InlineFieldRow, Input, Select, useStyles2, Stack } from '@grafana/ui'; import alertDef, { EvalFunction } from '../../alerting/state/alertDef'; import { ClassicCondition, ReducerType } from '../types'; @@ -73,7 +72,7 @@ export const Condition = ({ condition, index, onChange, onRemoveCondition, refId condition.evaluator.type === EvalFunction.IsWithinRange || condition.evaluator.type === EvalFunction.IsOutsideRange; return ( - +
      {index === 0 ? ( diff --git a/public/app/features/expressions/components/Math.tsx b/public/app/features/expressions/components/Math.tsx index 4a5a34f9ff30a..ecebcf2f6755d 100644 --- a/public/app/features/expressions/components/Math.tsx +++ b/public/app/features/expressions/components/Math.tsx @@ -2,8 +2,7 @@ import { css } from '@emotion/css'; import React, { ChangeEvent } from 'react'; import { GrafanaTheme2 } from '@grafana/data'; -import { Stack } from '@grafana/experimental'; -import { Icon, InlineField, InlineLabel, TextArea, Toggletip, useStyles2 } from '@grafana/ui'; +import { Icon, InlineField, InlineLabel, TextArea, Toggletip, useStyles2, Stack } from '@grafana/ui'; import { ExpressionQuery } from '../types'; @@ -32,7 +31,7 @@ export const Math = ({ labelWidth, onChange, query, onRunQuery }: Props) => { }; return ( - + diff --git a/public/app/features/inspector/QueryInspector.tsx b/public/app/features/inspector/QueryInspector.tsx index 81df3f209f6cf..c09f999d44da2 100644 --- a/public/app/features/inspector/QueryInspector.tsx +++ b/public/app/features/inspector/QueryInspector.tsx @@ -4,9 +4,8 @@ import { Subscription } from 'rxjs'; import { LoadingState, PanelData } from '@grafana/data'; import { selectors } from '@grafana/e2e-selectors'; -import { Stack } from '@grafana/experimental'; import { config } from '@grafana/runtime'; -import { Button, ClipboardButton, JSONFormatter, LoadingPlaceholder } from '@grafana/ui'; +import { Button, ClipboardButton, JSONFormatter, LoadingPlaceholder, Stack } from '@grafana/ui'; import { Trans } from 'app/core/internationalization'; import { backendSrv } from 'app/core/services/backend_srv'; diff --git a/public/app/features/org/UserInviteForm.tsx b/public/app/features/org/UserInviteForm.tsx index 6332c41e180e0..c2b14b4cb9bd5 100644 --- a/public/app/features/org/UserInviteForm.tsx +++ b/public/app/features/org/UserInviteForm.tsx @@ -1,7 +1,6 @@ import React from 'react'; import { locationUtil, SelectableValue } from '@grafana/data'; -import { Stack } from '@grafana/experimental'; import { locationService } from '@grafana/runtime'; import { Button, @@ -17,6 +16,7 @@ import { TextLink, Tooltip, Label, + Stack, } from '@grafana/ui'; import { getConfig } from 'app/core/config'; import { OrgRole, useDispatch } from 'app/types'; diff --git a/public/app/features/plugins/admin/components/PluginDetailsHeaderDependencies.tsx b/public/app/features/plugins/admin/components/PluginDetailsHeaderDependencies.tsx index 570fe47a5534b..c0c840d7e7560 100644 --- a/public/app/features/plugins/admin/components/PluginDetailsHeaderDependencies.tsx +++ b/public/app/features/plugins/admin/components/PluginDetailsHeaderDependencies.tsx @@ -2,8 +2,7 @@ import { css } from '@emotion/css'; import React from 'react'; import { GrafanaTheme2 } from '@grafana/data'; -import { Stack } from '@grafana/experimental'; -import { useStyles2, Icon } from '@grafana/ui'; +import { useStyles2, Icon, Stack } from '@grafana/ui'; import { Version, CatalogPlugin, PluginIconName } from '../types'; diff --git a/public/app/features/plugins/sql/components/configuration/TLSSecretsConfig.tsx b/public/app/features/plugins/sql/components/configuration/TLSSecretsConfig.tsx index f6d1b02a97e6f..a40a49db7e574 100644 --- a/public/app/features/plugins/sql/components/configuration/TLSSecretsConfig.tsx +++ b/public/app/features/plugins/sql/components/configuration/TLSSecretsConfig.tsx @@ -7,8 +7,7 @@ import { onUpdateDatasourceSecureJsonDataOption, updateDatasourcePluginResetOption, } from '@grafana/data'; -import { Stack } from '@grafana/experimental'; -import { Field, Icon, Label, SecretTextArea, Tooltip } from '@grafana/ui'; +import { Field, Icon, Label, SecretTextArea, Tooltip, Stack } from '@grafana/ui'; export interface Props { editorProps: DataSourcePluginOptionsEditorProps; diff --git a/public/app/features/scenes/SceneListPage.tsx b/public/app/features/scenes/SceneListPage.tsx index 9310fd2802b69..643a38a31c9b0 100644 --- a/public/app/features/scenes/SceneListPage.tsx +++ b/public/app/features/scenes/SceneListPage.tsx @@ -2,8 +2,7 @@ import React from 'react'; import { useAsync } from 'react-use'; -import { Stack } from '@grafana/experimental'; -import { Card } from '@grafana/ui'; +import { Card, Stack } from '@grafana/ui'; import { Page } from 'app/core/components/Page/Page'; // Types diff --git a/public/app/features/transformers/editors/GroupByTransformerEditor.tsx b/public/app/features/transformers/editors/GroupByTransformerEditor.tsx index 41dac28ea971c..e5b8bb6b5ecdc 100644 --- a/public/app/features/transformers/editors/GroupByTransformerEditor.tsx +++ b/public/app/features/transformers/editors/GroupByTransformerEditor.tsx @@ -16,8 +16,7 @@ import { GroupByOperationID, GroupByTransformerOptions, } from '@grafana/data/src/transformations/transformers/groupBy'; -import { Stack } from '@grafana/experimental'; -import { useTheme2, Select, StatsPicker, InlineField } from '@grafana/ui'; +import { useTheme2, Select, StatsPicker, InlineField, Stack } from '@grafana/ui'; import { useAllFieldNamesFromDataFrames } from '../utils'; @@ -84,7 +83,7 @@ export const GroupByFieldConfiguration = ({ fieldName, config, onConfigChange }: return ( - +
      , 'value' | 'ref'> { value?: boolean; diff --git a/public/app/plugins/datasource/prometheus/querybuilder/shared/QueryOptionGroup.tsx b/public/app/plugins/datasource/prometheus/querybuilder/shared/QueryOptionGroup.tsx index 4963de23d99ce..1f3b089dfb58b 100644 --- a/public/app/plugins/datasource/prometheus/querybuilder/shared/QueryOptionGroup.tsx +++ b/public/app/plugins/datasource/prometheus/querybuilder/shared/QueryOptionGroup.tsx @@ -3,9 +3,8 @@ import React from 'react'; import { useToggle } from 'react-use'; import { getValueFormat, GrafanaTheme2 } from '@grafana/data'; -import { Stack } from '@grafana/experimental'; import { config } from '@grafana/runtime'; -import { Collapse, Icon, Tooltip, useStyles2 } from '@grafana/ui'; +import { Collapse, Icon, Tooltip, useStyles2, Stack } from '@grafana/ui'; import { QueryStats } from 'app/plugins/datasource/loki/types'; export interface Props { @@ -27,7 +26,7 @@ export function QueryOptionGroup({ title, children, collapsedInfo, queryStats }: isOpen={isOpen} onToggle={toggleOpen} label={ - +
      {title}
      {!isOpen && (
      diff --git a/public/app/plugins/panel/alertlist/unified-alerting/UngroupedView.tsx b/public/app/plugins/panel/alertlist/unified-alerting/UngroupedView.tsx index 6868e620a4241..935e452676258 100644 --- a/public/app/plugins/panel/alertlist/unified-alerting/UngroupedView.tsx +++ b/public/app/plugins/panel/alertlist/unified-alerting/UngroupedView.tsx @@ -3,8 +3,7 @@ import React from 'react'; import { useLocation } from 'react-use'; import { GrafanaTheme2, intervalToAbbreviatedDurationString } from '@grafana/data'; -import { Stack } from '@grafana/experimental'; -import { Icon, useStyles2 } from '@grafana/ui'; +import { Icon, useStyles2, Stack } from '@grafana/ui'; import alertDef from 'app/features/alerting/state/alertDef'; import { Spacer } from 'app/features/alerting/unified/components/Spacer'; import { fromCombinedRule, stringifyIdentifier } from 'app/features/alerting/unified/utils/rule-id'; @@ -84,7 +83,7 @@ const UngroupedModeView = ({ rules, options, handleInstancesLimit, limitInstance
      - +
      {ruleWithLocation.name}
      diff --git a/public/app/plugins/panel/table/cells/BarGaugeCellOptionsEditor.tsx b/public/app/plugins/panel/table/cells/BarGaugeCellOptionsEditor.tsx index 02797d3bf6dfd..59bf1e617a79a 100644 --- a/public/app/plugins/panel/table/cells/BarGaugeCellOptionsEditor.tsx +++ b/public/app/plugins/panel/table/cells/BarGaugeCellOptionsEditor.tsx @@ -1,9 +1,8 @@ import React from 'react'; import { SelectableValue } from '@grafana/data'; -import { Stack } from '@grafana/experimental'; import { BarGaugeDisplayMode, BarGaugeValueMode, TableBarGaugeCellOptions } from '@grafana/schema'; -import { Field, RadioButtonGroup } from '@grafana/ui'; +import { Field, RadioButtonGroup, Stack } from '@grafana/ui'; import { TableCellEditorProps } from '../TableCellOptionEditor'; From 8aa5d470ca34831757e7aa4b47822210505a6db5 Mon Sep 17 00:00:00 2001 From: Andre Pereira Date: Mon, 6 Nov 2023 16:29:59 +0000 Subject: [PATCH 125/869] Tempo: Fix streaming query restart after Grafana server reboot (#77614) * Fix streaming query restart after Grafana server reboot * TraceQL Search filter name improvements * Add flag to enable streaming in tempo docker block --------- Co-authored-by: Andrej Ocenas --- devenv/docker/blocks/tempo/tempo.yaml | 2 + .../SpanFilters/SpanFilters.tsx | 4 +- .../SearchTraceQLEditor/TraceQLSearch.tsx | 2 +- .../tempo/SearchTraceQLEditor/utils.ts | 4 + .../app/plugins/datasource/tempo/streaming.ts | 111 ++++++++++-------- 5 files changed, 68 insertions(+), 55 deletions(-) diff --git a/devenv/docker/blocks/tempo/tempo.yaml b/devenv/docker/blocks/tempo/tempo.yaml index 6e94ff11a8d6b..e8378e9b9a68c 100644 --- a/devenv/docker/blocks/tempo/tempo.yaml +++ b/devenv/docker/blocks/tempo/tempo.yaml @@ -52,3 +52,5 @@ storage: overrides: metrics_generator_processors: [local-blocks, service-graphs, span-metrics] + +stream_over_http_enabled: true diff --git a/public/app/features/explore/TraceView/components/TracePageHeader/SpanFilters/SpanFilters.tsx b/public/app/features/explore/TraceView/components/TracePageHeader/SpanFilters/SpanFilters.tsx index f29caaca8b6e4..7a27e19a20c3a 100644 --- a/public/app/features/explore/TraceView/components/TracePageHeader/SpanFilters/SpanFilters.tsx +++ b/public/app/features/explore/TraceView/components/TracePageHeader/SpanFilters/SpanFilters.tsx @@ -342,9 +342,9 @@ export const SpanFilters = memo((props: SpanFilterProps) => {