From ec48b0dfcbfede17853b48768d077e599e698387 Mon Sep 17 00:00:00 2001 From: Alex Khomenko Date: Thu, 22 May 2025 12:03:58 +0300 Subject: [PATCH 01/73] grafana/ui: Add noMargin prop to Card (#105604) grafana/ui: Add noMargin to Card --- .../src/components/Button/Button.story.tsx | 2 +- .../src/components/Card/Card.story.tsx | 38 +++++++++---------- .../src/components/Card/Card.test.tsx | 18 ++++----- .../src/components/ThemeDemos/ThemeDemo.tsx | 6 +-- 4 files changed, 32 insertions(+), 32 deletions(-) diff --git a/packages/grafana-ui/src/components/Button/Button.story.tsx b/packages/grafana-ui/src/components/Button/Button.story.tsx index 708facda0a3..76fec86e1e2 100644 --- a/packages/grafana-ui/src/components/Button/Button.story.tsx +++ b/packages/grafana-ui/src/components/Button/Button.story.tsx @@ -100,7 +100,7 @@ export const Examples: StoryFn = () => { @@ -37,7 +37,7 @@ describe('Card', () => { expect(screen.getByRole('button', { name: 'Delete' })).toBeEnabled(); rerender( - + Test Heading @@ -54,7 +54,7 @@ describe('Card', () => { it('Children should be independently enabled or disabled if explicitly set', () => { const { rerender } = render( - + Test Heading @@ -69,7 +69,7 @@ describe('Card', () => { expect(screen.getByRole('button', { name: 'Delete' })).toBeDisabled(); rerender( - + Test Heading @@ -87,7 +87,7 @@ describe('Card', () => { it('Children should be conditional', () => { const shouldNotRender = false; render( - + Test Heading @@ -105,7 +105,7 @@ describe('Card', () => { it('Should allow selectable cards', () => { const { rerender } = render( - + My Option ); @@ -114,7 +114,7 @@ describe('Card', () => { expect(screen.getByRole('radio')).toBeChecked(); rerender( - + My Option ); @@ -123,7 +123,7 @@ describe('Card', () => { expect(screen.getByRole('radio')).not.toBeChecked(); rerender( - + My Option ); diff --git a/packages/grafana-ui/src/components/ThemeDemos/ThemeDemo.tsx b/packages/grafana-ui/src/components/ThemeDemos/ThemeDemo.tsx index cc243bee735..ab36eabca9e 100644 --- a/packages/grafana-ui/src/components/ThemeDemos/ThemeDemo.tsx +++ b/packages/grafana-ui/src/components/ThemeDemos/ThemeDemo.tsx @@ -226,7 +226,7 @@ export const ThemeDemo = () => { Disabled - + Button inside card {allButtonVariants.map((variant) => ( @@ -259,8 +259,8 @@ export function VizHuesDemo({ theme, color }: VizHuesDemoProps) { return ( {color.name} - {color.shades.map((shade) => ( - + {color.shades.map((shade, index) => ( +
Date: Thu, 22 May 2025 12:04:09 +0300 Subject: [PATCH 02/73] Provisioning: Skip settings request if the feature toggle not set (#105800) --- .../features/provisioning/hooks/useIsProvisionedInstance.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/public/app/features/provisioning/hooks/useIsProvisionedInstance.ts b/public/app/features/provisioning/hooks/useIsProvisionedInstance.ts index f218fe855e9..9e158ed3597 100644 --- a/public/app/features/provisioning/hooks/useIsProvisionedInstance.ts +++ b/public/app/features/provisioning/hooks/useIsProvisionedInstance.ts @@ -1,9 +1,12 @@ import { skipToken } from '@reduxjs/toolkit/query'; +import { config } from '@grafana/runtime'; import { RepositoryViewList, useGetFrontendSettingsQuery } from 'app/api/clients/provisioning'; export function useIsProvisionedInstance(settings?: RepositoryViewList) { - const settingsQuery = useGetFrontendSettingsQuery(settings ? skipToken : undefined); + const settingsQuery = useGetFrontendSettingsQuery( + settings || !config.featureToggles.provisioning ? skipToken : undefined + ); if (!settings) { settings = settingsQuery.data; } From e841674745640d68c873618dccc5290455181bb5 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 22 May 2025 09:06:46 +0000 Subject: [PATCH 03/73] Update dependency react-select to v5.10.1 (#104907) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package.json | 2 +- .../grafana-o11y-ds-frontend/package.json | 2 +- packages/grafana-sql/package.json | 2 +- packages/grafana-ui/package.json | 2 +- .../datasource/azuremonitor/package.json | 2 +- .../datasource/cloud-monitoring/package.json | 2 +- .../grafana-testdata-datasource/package.json | 2 +- .../plugins/datasource/jaeger/package.json | 2 +- .../app/plugins/datasource/tempo/package.json | 2 +- .../plugins/datasource/zipkin/package.json | 2 +- yarn.lock | 28 +++++++++---------- 11 files changed, 24 insertions(+), 24 deletions(-) diff --git a/package.json b/package.json index 94f2da51f20..d08b3f4c16e 100644 --- a/package.json +++ b/package.json @@ -390,7 +390,7 @@ "react-router": "5.3.4", "react-router-dom": "5.3.4", "react-router-dom-v5-compat": "^6.26.1", - "react-select": "5.10.0", + "react-select": "5.10.1", "react-split-pane": "0.1.92", "react-table": "7.8.0", "react-transition-group": "4.4.5", diff --git a/packages/grafana-o11y-ds-frontend/package.json b/packages/grafana-o11y-ds-frontend/package.json index 894cf4c919a..0cdcec2db12 100644 --- a/packages/grafana-o11y-ds-frontend/package.json +++ b/packages/grafana-o11y-ds-frontend/package.json @@ -24,7 +24,7 @@ "@grafana/runtime": "12.1.0-pre", "@grafana/schema": "12.1.0-pre", "@grafana/ui": "12.1.0-pre", - "react-select": "5.10.0", + "react-select": "5.10.1", "react-use": "17.6.0", "rxjs": "7.8.2", "tslib": "2.8.1" diff --git a/packages/grafana-sql/package.json b/packages/grafana-sql/package.json index 02301c0ae08..5af3c3fa16f 100644 --- a/packages/grafana-sql/package.json +++ b/packages/grafana-sql/package.json @@ -25,7 +25,7 @@ "lodash": "4.17.21", "react": "18.3.1", "react-dom": "18.3.1", - "react-select": "5.10.0", + "react-select": "5.10.1", "react-use": "17.6.0", "react-virtualized-auto-sizer": "1.0.25", "rxjs": "7.8.2", diff --git a/packages/grafana-ui/package.json b/packages/grafana-ui/package.json index 37cb20ead94..3e8f2bfb31f 100644 --- a/packages/grafana-ui/package.json +++ b/packages/grafana-ui/package.json @@ -117,7 +117,7 @@ "react-loading-skeleton": "3.5.0", "react-router-dom": "5.3.4", "react-router-dom-v5-compat": "^6.26.1", - "react-select": "5.10.0", + "react-select": "5.10.1", "react-table": "7.8.0", "react-transition-group": "4.4.5", "react-use": "17.6.0", diff --git a/public/app/plugins/datasource/azuremonitor/package.json b/public/app/plugins/datasource/azuremonitor/package.json index 3fb4f5bbbff..0534f946682 100644 --- a/public/app/plugins/datasource/azuremonitor/package.json +++ b/public/app/plugins/datasource/azuremonitor/package.json @@ -20,7 +20,7 @@ "prismjs": "1.30.0", "react": "18.3.1", "react-dom": "18.3.1", - "react-select": "5.10.0", + "react-select": "5.10.1", "react-use": "17.6.0", "rxjs": "7.8.2", "tslib": "2.8.1" diff --git a/public/app/plugins/datasource/cloud-monitoring/package.json b/public/app/plugins/datasource/cloud-monitoring/package.json index 8409d313695..e5f4fe75054 100644 --- a/public/app/plugins/datasource/cloud-monitoring/package.json +++ b/public/app/plugins/datasource/cloud-monitoring/package.json @@ -20,7 +20,7 @@ "prismjs": "1.30.0", "react": "18.3.1", "react-dom": "18.3.1", - "react-select": "5.10.0", + "react-select": "5.10.1", "react-use": "17.6.0", "rxjs": "7.8.2", "tslib": "2.8.1" diff --git a/public/app/plugins/datasource/grafana-testdata-datasource/package.json b/public/app/plugins/datasource/grafana-testdata-datasource/package.json index 5f330c9ca34..d2231408551 100644 --- a/public/app/plugins/datasource/grafana-testdata-datasource/package.json +++ b/public/app/plugins/datasource/grafana-testdata-datasource/package.json @@ -14,7 +14,7 @@ "micro-memoize": "^4.1.2", "react": "18.3.1", "react-dom": "18.3.1", - "react-select": "5.10.0", + "react-select": "5.10.1", "react-use": "17.6.0", "rxjs": "7.8.2", "tslib": "2.8.1", diff --git a/public/app/plugins/datasource/jaeger/package.json b/public/app/plugins/datasource/jaeger/package.json index 12bbc8fc4e7..68bd3647eee 100644 --- a/public/app/plugins/datasource/jaeger/package.json +++ b/public/app/plugins/datasource/jaeger/package.json @@ -15,7 +15,7 @@ "logfmt": "^1.3.2", "react": "18.3.1", "react-dom": "18.3.1", - "react-select": "5.10.0", + "react-select": "5.10.1", "react-window": "1.8.11", "rxjs": "7.8.2", "stream-browserify": "3.0.0", diff --git a/public/app/plugins/datasource/tempo/package.json b/public/app/plugins/datasource/tempo/package.json index f7b5eb30b85..3898c6f9def 100644 --- a/public/app/plugins/datasource/tempo/package.json +++ b/public/app/plugins/datasource/tempo/package.json @@ -28,7 +28,7 @@ "prismjs": "1.30.0", "react": "18.3.1", "react-dom": "18.3.1", - "react-select": "5.10.0", + "react-select": "5.10.1", "react-use": "17.6.0", "rxjs": "7.8.2", "semver": "7.7.2", diff --git a/public/app/plugins/datasource/zipkin/package.json b/public/app/plugins/datasource/zipkin/package.json index 86e4910decc..60bfa62383a 100644 --- a/public/app/plugins/datasource/zipkin/package.json +++ b/public/app/plugins/datasource/zipkin/package.json @@ -14,7 +14,7 @@ "lodash": "4.17.21", "react": "18.3.1", "react-dom": "18.3.1", - "react-select": "5.10.0", + "react-select": "5.10.1", "react-use": "17.6.0", "rxjs": "7.8.2", "tslib": "2.8.1" diff --git a/yarn.lock b/yarn.lock index 1c86aa5e13f..fb79a552b96 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2520,7 +2520,7 @@ __metadata: prismjs: "npm:1.30.0" react: "npm:18.3.1" react-dom: "npm:18.3.1" - react-select: "npm:5.10.0" + react-select: "npm:5.10.1" react-select-event: "npm:5.5.1" react-use: "npm:17.6.0" rxjs: "npm:7.8.2" @@ -2630,7 +2630,7 @@ __metadata: micro-memoize: "npm:^4.1.2" react: "npm:18.3.1" react-dom: "npm:18.3.1" - react-select: "npm:5.10.0" + react-select: "npm:5.10.1" react-use: "npm:17.6.0" rxjs: "npm:7.8.2" ts-node: "npm:10.9.2" @@ -2671,7 +2671,7 @@ __metadata: logfmt: "npm:^1.3.2" react: "npm:18.3.1" react-dom: "npm:18.3.1" - react-select: "npm:5.10.0" + react-select: "npm:5.10.1" react-window: "npm:1.8.11" rxjs: "npm:7.8.2" stream-browserify: "npm:3.0.0" @@ -2814,7 +2814,7 @@ __metadata: prismjs: "npm:1.30.0" react: "npm:18.3.1" react-dom: "npm:18.3.1" - react-select: "npm:5.10.0" + react-select: "npm:5.10.1" react-select-event: "npm:5.5.1" react-use: "npm:17.6.0" rxjs: "npm:7.8.2" @@ -2869,7 +2869,7 @@ __metadata: prismjs: "npm:1.30.0" react: "npm:18.3.1" react-dom: "npm:18.3.1" - react-select: "npm:5.10.0" + react-select: "npm:5.10.1" react-select-event: "npm:5.5.1" react-use: "npm:17.6.0" rxjs: "npm:7.8.2" @@ -2909,7 +2909,7 @@ __metadata: lodash: "npm:4.17.21" react: "npm:18.3.1" react-dom: "npm:18.3.1" - react-select: "npm:5.10.0" + react-select: "npm:5.10.1" react-use: "npm:17.6.0" rxjs: "npm:7.8.2" ts-node: "npm:10.9.2" @@ -3271,7 +3271,7 @@ __metadata: "@types/systemjs": "npm:6.15.1" jest: "npm:^29.6.4" react: "npm:18.3.1" - react-select: "npm:5.10.0" + react-select: "npm:5.10.1" react-use: "npm:17.6.0" rxjs: "npm:7.8.2" ts-jest: "npm:29.2.5" @@ -3580,7 +3580,7 @@ __metadata: lodash: "npm:4.17.21" react: "npm:18.3.1" react-dom: "npm:18.3.1" - react-select: "npm:5.10.0" + react-select: "npm:5.10.1" react-use: "npm:17.6.0" react-virtualized-auto-sizer: "npm:1.0.25" rxjs: "npm:7.8.2" @@ -3728,7 +3728,7 @@ __metadata: react-loading-skeleton: "npm:3.5.0" react-router-dom: "npm:5.3.4" react-router-dom-v5-compat: "npm:^6.26.1" - react-select: "npm:5.10.0" + react-select: "npm:5.10.1" react-select-event: "npm:^5.1.0" react-table: "npm:7.8.0" react-transition-group: "npm:4.4.5" @@ -17974,7 +17974,7 @@ __metadata: react-router: "npm:5.3.4" react-router-dom: "npm:5.3.4" react-router-dom-v5-compat: "npm:^6.26.1" - react-select: "npm:5.10.0" + react-select: "npm:5.10.1" react-select-event: "npm:5.5.1" react-split-pane: "npm:0.1.92" react-table: "npm:7.8.0" @@ -26472,9 +26472,9 @@ __metadata: languageName: node linkType: hard -"react-select@npm:5.10.0": - version: 5.10.0 - resolution: "react-select@npm:5.10.0" +"react-select@npm:5.10.1": + version: 5.10.1 + resolution: "react-select@npm:5.10.1" dependencies: "@babel/runtime": "npm:^7.12.0" "@emotion/cache": "npm:^11.4.0" @@ -26488,7 +26488,7 @@ __metadata: peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - checksum: 10/70ccb74ef31a1fe24c3f7cb94459dd33289999ceda104b1b75256758b7f25ead5dcb4a1877d513f9e22d9ac62d2656414c51dd57b9436d49bd6834a7c8b1c727 + checksum: 10/c0c4d798e9b1f654b56c26bffb149ab811d24ca0950c1e8c3675438065628561eb132c6a5982cea893a288a0e9af6f9d9db290f4e53da71570dada0c1147f146 languageName: node linkType: hard From ca0ac05b39ab384061af423d2b4db0f42372d69f Mon Sep 17 00:00:00 2001 From: Denis Vodopianov Date: Thu, 22 May 2025 11:55:35 +0200 Subject: [PATCH 04/73] Chore: add missing build-id to the rgm-promotion step (#105814) add missing build-id to the rgm-promotion step --- .drone.yml | 5 +++-- scripts/drone/rgm.star | 1 + 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.drone.yml b/.drone.yml index b6bc050aad3..298efd04173 100644 --- a/.drone.yml +++ b/.drone.yml @@ -4343,7 +4343,8 @@ steps: - apk add docker - export GITHUB_TOKEN=$(cat /github-app/token) - dagger run --silent go run ./pkg/build/cmd artifacts -a $${ARTIFACTS} --grafana-ref=$${GRAFANA_REF} - --enterprise-ref=$${ENTERPRISE_REF} --grafana-repo=$${GRAFANA_REPO} --version=$${VERSION} + --enterprise-ref=$${ENTERPRISE_REF} --grafana-repo=$${GRAFANA_REPO} --build-id=$${DRONE_BUILD_NUMBER} + --version=$${VERSION} depends_on: - github-app-generate-token environment: @@ -5205,6 +5206,6 @@ kind: secret name: gcr_credentials --- kind: signature -hmac: 1da5c9da34dc5dbffef8bef4ef84282997cb2363f929fdf4b897d2ee87a33703 +hmac: e430c51190ba46dbd426d13f47c13c275f4d05b6b1a26d1d9f67b5a8f09ecb41 ... diff --git a/scripts/drone/rgm.star b/scripts/drone/rgm.star index f112a39ab42..32d3cfc9d05 100644 --- a/scripts/drone/rgm.star +++ b/scripts/drone/rgm.star @@ -351,6 +351,7 @@ def rgm_promotion_pipeline(): "--grafana-ref=$${GRAFANA_REF} " + "--enterprise-ref=$${ENTERPRISE_REF} " + "--grafana-repo=$${GRAFANA_REPO} " + + "--build-id=$${DRONE_BUILD_NUMBER} " + "--version=$${VERSION}", ], dagger_version), "environment": rgm_env_secrets(env), From 30398b65918e105f59441062baa62347144b4052 Mon Sep 17 00:00:00 2001 From: Gilles De Mey Date: Thu, 22 May 2025 12:09:40 +0200 Subject: [PATCH 05/73] Alerting: Make rules clickable in rule group detail view (#105622) --- .../alerting/unified/api/alertRuleApi.ts | 22 +++++--- .../group-details/GroupDetailsPage.test.tsx | 18 ++++--- .../group-details/GroupDetailsPage.tsx | 51 +++++++++++++++---- 3 files changed, 69 insertions(+), 22 deletions(-) diff --git a/public/app/features/alerting/unified/api/alertRuleApi.ts b/public/app/features/alerting/unified/api/alertRuleApi.ts index fd2d6758dec..9d5d02d9ffb 100644 --- a/public/app/features/alerting/unified/api/alertRuleApi.ts +++ b/public/app/features/alerting/unified/api/alertRuleApi.ts @@ -1,5 +1,3 @@ -import { set } from 'lodash'; - import { RelativeTimeRange } from '@grafana/data'; import { t } from '@grafana/i18n/internal'; import { Matcher } from 'app/plugins/datasource/alertmanager/types'; @@ -49,11 +47,18 @@ export interface Datasource { export const PREVIEW_URL = '/api/v1/rule/test/grafana'; export const PROM_RULES_URL = 'api/prometheus/grafana/api/v1/rules'; +// for some reason vanilla Prometheus uses param notation with [] appended export enum PrometheusAPIFilters { + RuleName = 'rule_name', + RuleNameVanilla = 'rule_name[]', RuleGroup = 'rule_group', + RuleGroupVanilla = 'rule_group[]', Namespace = 'file', + NamespaceVanilla = 'file[]', FolderUID = 'folder_uid', LimitAlerts = 'limit_alerts', + MaxGroups = 'max_groups', + ExcludeAlerts = 'exclude_alerts', } export interface Data { @@ -146,7 +151,9 @@ export const alertRuleApi = alertingApi.injectEndpoints({ if (identifier && (isPrometheusRuleIdentifier(identifier) || isCloudRuleIdentifier(identifier))) { searchParams.set(PrometheusAPIFilters.Namespace, identifier.namespace); + searchParams.set(PrometheusAPIFilters.NamespaceVanilla, identifier.namespace); searchParams.set(PrometheusAPIFilters.RuleGroup, identifier.groupName); + searchParams.set(PrometheusAPIFilters.RuleGroupVanilla, identifier.groupName); } const filterParams = getRulesFilterSearchParams(filter); @@ -193,22 +200,23 @@ export const alertRuleApi = alertingApi.injectEndpoints({ if (namespace) { if (isGrafanaRulesSource(ruleSourceName)) { - set(queryParams, PrometheusAPIFilters.FolderUID, namespace); + queryParams[PrometheusAPIFilters.FolderUID] = namespace; } else { - set(queryParams, PrometheusAPIFilters.Namespace, namespace); + queryParams[PrometheusAPIFilters.Namespace] = namespace; + queryParams[PrometheusAPIFilters.NamespaceVanilla] = namespace; } } if (limitAlerts !== undefined) { - set(queryParams, PrometheusAPIFilters.LimitAlerts, String(limitAlerts)); + queryParams[PrometheusAPIFilters.LimitAlerts] = String(PrometheusAPIFilters.LimitAlerts); } if (maxGroups) { - set(queryParams, 'max_groups', maxGroups); + queryParams[PrometheusAPIFilters.MaxGroups] = String(maxGroups); } if (excludeAlerts) { - set(queryParams, 'exclude_alerts', 'true'); + queryParams[PrometheusAPIFilters.ExcludeAlerts] = 'true'; } return { diff --git a/public/app/features/alerting/unified/group-details/GroupDetailsPage.test.tsx b/public/app/features/alerting/unified/group-details/GroupDetailsPage.test.tsx index 9e172532ed7..93a9e332588 100644 --- a/public/app/features/alerting/unified/group-details/GroupDetailsPage.test.tsx +++ b/public/app/features/alerting/unified/group-details/GroupDetailsPage.test.tsx @@ -1,7 +1,7 @@ import { HttpResponse } from 'msw'; import { Route, Routes } from 'react-router-dom-v5-compat'; import { Props } from 'react-virtualized-auto-sizer'; -import { render, screen, waitFor } from 'test/test-utils'; +import { render, screen, waitFor, within } from 'test/test-utils'; import { byRole, byTestId } from 'testing-library-selector'; import { AccessControlAction } from 'app/types'; @@ -100,7 +100,7 @@ describe('GroupDetailsPage', () => { // Assert expect(header).toHaveTextContent('test-group-cpu'); expect(await screen.findByRole('link', { name: /test-folder-title/ })).toBeInTheDocument(); - expect(await screen.findByText(/5m/)).toBeInTheDocument(); + expect(await screen.findByText(group.interval!)).toBeInTheDocument(); expect(editLink).toHaveAttribute( 'href', '/alerting/grafana/namespaces/test-folder-uid/groups/test-group-cpu/edit?returnTo=%2Falerting%2Fgrafana%2Fnamespaces%2Ftest-folder-uid%2Fgroups%2Ftest-group-cpu%2Fview' @@ -109,12 +109,18 @@ describe('GroupDetailsPage', () => { const tableRows = await ui.tableRow.findAll(await ui.rowsTable.find()); expect(tableRows).toHaveLength(2); - expect(tableRows[0]).toHaveTextContent('High CPU Usage'); - expect(tableRows[0]).toHaveTextContent('10m'); + expect(within(tableRows[0]).getByRole('link', { name: rule1.grafana_alert.title })).toHaveAttribute( + 'href', + `/alerting/grafana/${rule1.grafana_alert.uid}/view` + ); + expect(tableRows[0]).toHaveTextContent(String(rule1.for)); expect(tableRows[0]).toHaveTextContent('5'); - expect(tableRows[1]).toHaveTextContent('Memory Pressure'); - expect(tableRows[1]).toHaveTextContent('5m'); + expect(within(tableRows[1]).getByRole('link', { name: rule2.grafana_alert.title })).toHaveAttribute( + 'href', + `/alerting/grafana/${rule2.grafana_alert.uid}/view` + ); + expect(tableRows[1]).toHaveTextContent(String(rule2.for)); expect(tableRows[1]).toHaveTextContent('3'); }); diff --git a/public/app/features/alerting/unified/group-details/GroupDetailsPage.tsx b/public/app/features/alerting/unified/group-details/GroupDetailsPage.tsx index 4ad7c88b070..505e893fed5 100644 --- a/public/app/features/alerting/unified/group-details/GroupDetailsPage.tsx +++ b/public/app/features/alerting/unified/group-details/GroupDetailsPage.tsx @@ -16,10 +16,12 @@ import { DynamicTable, DynamicTableColumnProps } from '../components/DynamicTabl import { GrafanaRuleGroupExporter } from '../components/export/GrafanaRuleGroupExporter'; import { useFolder } from '../hooks/useFolder'; import { DEFAULT_GROUP_EVALUATION_INTERVAL } from '../rule-editor/formDefaults'; +import { createViewLinkFromIdentifier } from '../rule-list/DataSourceRuleListItem'; import { useRulesAccess } from '../utils/accessControlHooks'; -import { GRAFANA_RULES_SOURCE_NAME } from '../utils/datasource'; +import { GRAFANA_RULES_SOURCE_NAME, getDataSourceByUid } from '../utils/datasource'; import { makeFolderLink, stringifyErrorLike } from '../utils/misc'; import { createListFilterLink, groups } from '../utils/navigation'; +import { fromRule, fromRulerRule } from '../utils/rule-id'; import { calcRuleEvalsToStartAlerting, getRuleName, @@ -78,6 +80,7 @@ function GroupDetailsPage() { ); const { t } = useTranslate(); + const ruleSourceName = isGrafanaRuleGroup ? GRAFANA_RULES_SOURCE_NAME : getDataSourceByUid(dataSourceUid)?.name; const isLoading = isFolderLoading || isDsFeaturesLoading || isRuleNamespacesLoading || isRuleGroupLoading; const groupInterval = promGroup?.interval @@ -148,8 +151,12 @@ function GroupDetailsPage() {
{stringifyErrorLike(ruleNamespacesError || ruleGroupError)}
)} - {promGroup && } - {rulerGroup && } + {promGroup && ruleSourceName && ( + + )} + {rulerGroup && ruleSourceName && ( + + )} {!promGroup && !rulerGroup && } @@ -213,12 +220,14 @@ interface RuleGroupDetails { interface AlertingRuleDetails { name: string; + href?: string; type: 'alerting'; pendingPeriod: string; evaluationsToFire: number; } interface RecordingRuleDetails { name: string; + href?: string; type: 'recording'; } @@ -248,8 +257,16 @@ function RulesTable({ rules }: { rules: RuleDetails[] }) { { id: 'alertName', label: t('alerting.group-details.rule-name', 'Rule name'), - renderCell: ({ data }) => { - return {data.name}; + renderCell: ({ data: { name, href } }) => { + if (href) { + return ( + + {name} + + ); + } + + return {name}; }, size: 0.4, }, @@ -285,29 +302,41 @@ function RulesTable({ rules }: { rules: RuleDetails[] }) { return ; } -function promRuleGroupToRuleGroupDetails(group: RuleGroup): RuleGroupDetails { +function promRuleGroupToRuleGroupDetails( + ruleSourceName: string, + namespaceName: string, + group: RuleGroup +): RuleGroupDetails { const groupIntervalMs = group.interval * 1000; return { name: group.name, interval: formatPrometheusDuration(group.interval * 1000), rules: group.rules.map((rule) => { + const ruleIdentifier = fromRule(ruleSourceName, namespaceName, group.name, rule); + const href = ruleIdentifier ? createViewLinkFromIdentifier(ruleIdentifier) : undefined; + switch (rule.type) { case PromRuleType.Alerting: return { name: rule.name, + href, type: 'alerting', pendingPeriod: formatPrometheusDuration(rule.duration ? rule.duration * 1000 : 0), evaluationsToFire: calcRuleEvalsToStartAlerting(rule.duration ? rule.duration * 1000 : 0, groupIntervalMs), }; case PromRuleType.Recording: - return { name: rule.name, type: 'recording' }; + return { name: rule.name, href, type: 'recording' }; } }), }; } -function rulerRuleGroupToRuleGroupDetails(group: RulerRuleGroupDTO): RuleGroupDetails { +function rulerRuleGroupToRuleGroupDetails( + ruleSourceName: string, + namespaceName: string, + group: RulerRuleGroupDTO +): RuleGroupDetails { const groupIntervalMs = safeParsePrometheusDuration(group.interval ?? DEFAULT_GROUP_EVALUATION_INTERVAL); return { @@ -316,9 +345,13 @@ function rulerRuleGroupToRuleGroupDetails(group: RulerRuleGroupDTO): RuleGroupDe rules: group.rules.map((rule) => { const name = getRuleName(rule); + const ruleIdentifier = fromRulerRule(ruleSourceName, namespaceName, group.name, rule); + const href = createViewLinkFromIdentifier(ruleIdentifier); + if (rulerRuleType.any.alertingRule(rule)) { return { name, + href, type: 'alerting', pendingPeriod: rule.for ?? '0s', evaluationsToFire: calcRuleEvalsToStartAlerting( @@ -328,7 +361,7 @@ function rulerRuleGroupToRuleGroupDetails(group: RulerRuleGroupDTO): RuleGroupDe }; } - return { name, type: 'recording' }; + return { name, href, type: 'recording' }; }), }; } From 9729c4cf60911d27458cfbaf35b27e50eadc8d04 Mon Sep 17 00:00:00 2001 From: kay delaney <45561153+kaydelaney@users.noreply.github.com> Date: Thu, 22 May 2025 11:39:52 +0100 Subject: [PATCH 06/73] Dashboards/E2E: Add test validating panel duplication (#105663) * Dashboards/E2E: Add test validating panel duplication * Validate duplicates remain after save/reload --- .../dashboard-duplicate-panel.spec.ts | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 e2e/dashboard-new-layouts/dashboard-duplicate-panel.spec.ts diff --git a/e2e/dashboard-new-layouts/dashboard-duplicate-panel.spec.ts b/e2e/dashboard-new-layouts/dashboard-duplicate-panel.spec.ts new file mode 100644 index 00000000000..43ac0576f47 --- /dev/null +++ b/e2e/dashboard-new-layouts/dashboard-duplicate-panel.spec.ts @@ -0,0 +1,30 @@ +import { e2e } from '../utils'; + +import { flows } from './dashboard-edit-flows'; + +describe('Dashboard panels', () => { + beforeEach(() => { + e2e.flows.login(Cypress.env('USERNAME'), Cypress.env('PASSWORD')); + }); + + it('can duplicate a panel', () => { + e2e.flows.scenes.importV2Dashboard({ title: 'Paste tab' }); + + e2e.flows.scenes.toggleEditMode(); + const panelTitle = 'Unique'; + flows.changePanelTitle('New panel', panelTitle); + + e2e.components.Panels.Panel.title(panelTitle).should('have.length', 1); + + e2e.components.Panels.Panel.menu(panelTitle).click({ force: true }); + e2e.components.Panels.Panel.menuItems('More...').trigger('mouseover'); + e2e.components.Panels.Panel.menuItems('Duplicate').click(); + + e2e.components.Panels.Panel.title(panelTitle).should('have.length', 2); + + // Save, reload, and ensure duplicate has persisted + e2e.flows.scenes.saveDashboard(); + cy.reload(); + e2e.components.Panels.Panel.title(panelTitle).should('have.length', 2); + }); +}); From 2ce1c67d92f04f406068f22c9106ef3ed86d1ba7 Mon Sep 17 00:00:00 2001 From: Matt Cowley Date: Thu, 22 May 2025 12:06:50 +0100 Subject: [PATCH 07/73] Home: update setup guide link/title to Getting Started Guide (#105692) * Home: update setup guide link/title to Getting Started Guide * Home: check plugin presence to show getting started guide * Home: remove now unused homeSetupGuide feature toggle * Home: use sentence case for Getting started guide item --- packages/grafana-data/src/types/featureToggles.gen.ts | 4 ---- pkg/services/featuremgmt/registry.go | 7 ------- pkg/services/featuremgmt/toggles_gen.csv | 1 - pkg/services/featuremgmt/toggles_gen.go | 4 ---- pkg/services/featuremgmt/toggles_gen.json | 3 ++- pkg/services/navtree/navtreeimpl/navtree.go | 6 +++--- public/app/core/utils/navBarItem-translations.ts | 2 +- public/locales/en-US/grafana.json | 2 +- 8 files changed, 7 insertions(+), 22 deletions(-) diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index 69175808369..a3544cf6938 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -679,10 +679,6 @@ export interface FeatureToggles { */ exploreLogsLimitedTimeRange?: boolean; /** - * Used in Home for users who want to return to the onboarding flow or quickly find popular config pages - */ - homeSetupGuide?: boolean; - /** * Enables the gRPC client to authenticate with the App Platform by using ID & access tokens */ appPlatformGrpcClientAuth?: boolean; diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index 7437565e76d..a72ae57b7f6 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -1163,13 +1163,6 @@ var ( FrontendOnly: true, Owner: grafanaObservabilityLogsSquad, }, - { - Name: "homeSetupGuide", - Description: "Used in Home for users who want to return to the onboarding flow or quickly find popular config pages", - Stage: FeatureStageExperimental, - FrontendOnly: true, - Owner: growthAndOnboarding, - }, { Name: "appPlatformGrpcClientAuth", Description: "Enables the gRPC client to authenticate with the App Platform by using ID & access tokens", diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv index 5b5dd86153f..9e359fef7f9 100644 --- a/pkg/services/featuremgmt/toggles_gen.csv +++ b/pkg/services/featuremgmt/toggles_gen.csv @@ -151,7 +151,6 @@ alertingPrometheusRulesPrimary,experimental,@grafana/alerting-squad,false,false, exploreLogsShardSplitting,experimental,@grafana/observability-logs,false,false,true exploreLogsAggregatedMetrics,experimental,@grafana/observability-logs,false,false,true exploreLogsLimitedTimeRange,experimental,@grafana/observability-logs,false,false,true -homeSetupGuide,experimental,@grafana/growth-and-onboarding,false,false,true appPlatformGrpcClientAuth,experimental,@grafana/identity-access-team,false,false,false groupAttributeSync,privatePreview,@grafana/identity-access-team,false,false,false alertingQueryAndExpressionsStepMode,GA,@grafana/alerting-squad,false,false,true diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go index 7157f7fa236..7b30101fb72 100644 --- a/pkg/services/featuremgmt/toggles_gen.go +++ b/pkg/services/featuremgmt/toggles_gen.go @@ -615,10 +615,6 @@ const ( // Used in Logs Drilldown to limit the time range FlagExploreLogsLimitedTimeRange = "exploreLogsLimitedTimeRange" - // FlagHomeSetupGuide - // Used in Home for users who want to return to the onboarding flow or quickly find popular config pages - FlagHomeSetupGuide = "homeSetupGuide" - // FlagAppPlatformGrpcClientAuth // Enables the gRPC client to authenticate with the App Platform by using ID & access tokens FlagAppPlatformGrpcClientAuth = "appPlatformGrpcClientAuth" diff --git a/pkg/services/featuremgmt/toggles_gen.json b/pkg/services/featuremgmt/toggles_gen.json index a99defd2459..bcbe952df23 100644 --- a/pkg/services/featuremgmt/toggles_gen.json +++ b/pkg/services/featuremgmt/toggles_gen.json @@ -1517,7 +1517,8 @@ "metadata": { "name": "homeSetupGuide", "resourceVersion": "1743693517832", - "creationTimestamp": "2024-09-25T17:20:04Z" + "creationTimestamp": "2024-09-25T17:20:04Z", + "deletionTimestamp": "2025-05-20T16:08:36Z" }, "spec": { "description": "Used in Home for users who want to return to the onboarding flow or quickly find popular config pages", diff --git a/pkg/services/navtree/navtreeimpl/navtree.go b/pkg/services/navtree/navtreeimpl/navtree.go index 8a31154d8fb..5f31b4b7205 100644 --- a/pkg/services/navtree/navtreeimpl/navtree.go +++ b/pkg/services/navtree/navtreeimpl/navtree.go @@ -225,13 +225,13 @@ func (s *ServiceImpl) getHomeNode(c *contextmodel.ReqContext, prefs *pref.Prefer SortWeight: navtree.WeightHome, } ctx := c.Req.Context() - if s.features.IsEnabled(ctx, featuremgmt.FlagHomeSetupGuide) { + if _, exists := s.pluginStore.Plugin(ctx, "grafana-setupguide-app"); exists { var children []*navtree.NavLink // setup guide (a submenu item under Home) children = append(children, &navtree.NavLink{ Id: "home-setup-guide", - Text: "Setup guide", - Url: homeUrl + "/setup-guide", + Text: "Getting started guide", + Url: "/a/grafana-setupguide-app/getting-started", SortWeight: navtree.WeightHome, }) homeNode.Children = children diff --git a/public/app/core/utils/navBarItem-translations.ts b/public/app/core/utils/navBarItem-translations.ts index 6c78e1de1e6..d32ffcc1840 100644 --- a/public/app/core/utils/navBarItem-translations.ts +++ b/public/app/core/utils/navBarItem-translations.ts @@ -12,7 +12,7 @@ export function getNavTitle(navId: string | undefined) { case 'home': return t('nav.home.title', 'Home'); case 'home-setup-guide': - return t('nav.setup-guide.title', 'Setup guide'); + return t('nav.setup-guide.title', 'Getting started guide'); case 'new': return t('nav.new.title', 'New'); case 'create': diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index a23feb9ab67..612002cf70d 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -6866,7 +6866,7 @@ "title": "Service accounts" }, "setup-guide": { - "title": "Setup guide" + "title": "Getting started guide" }, "shared-dashboard": { "subtitle": "Manage your organization's externally shared dashboards", From 267e3ffb7c99e1021f3d87f4f48f03ab1a3d028d Mon Sep 17 00:00:00 2001 From: Adela Almasan <88068998+adela-almasan@users.noreply.github.com> Date: Thu, 22 May 2025 06:07:24 -0500 Subject: [PATCH 08/73] Actions: Add support for custom variables (#105639) Co-authored-by: Leon Sorokin --- packages/grafana-data/src/index.ts | 4 + packages/grafana-data/src/types/action.ts | 20 ++- .../src/components/Actions/ActionButton.tsx | 35 +++- .../Actions/VariablesInputModal.tsx | 72 +++++++++ .../Table/DataLinksActionsTooltip.test.tsx | 1 + .../uPlot/plugins/TooltipPlugin2.tsx | 5 +- public/app/features/actions/ActionEditor.tsx | 25 ++- .../actions/ActionVariablesEditor.test.tsx | 110 +++++++++++++ .../actions/ActionVariablesEditor.tsx | 107 +++++++++++++ public/app/features/actions/utils.test.ts | 151 ++++++++++++++++++ public/app/features/actions/utils.ts | 132 +++++++++------ .../app/features/canvas/runtime/element.tsx | 2 +- public/locales/en-US/grafana.json | 11 +- 13 files changed, 611 insertions(+), 64 deletions(-) create mode 100644 packages/grafana-ui/src/components/Actions/VariablesInputModal.tsx create mode 100644 public/app/features/actions/ActionVariablesEditor.test.tsx create mode 100644 public/app/features/actions/ActionVariablesEditor.tsx create mode 100644 public/app/features/actions/utils.test.ts diff --git a/packages/grafana-data/src/index.ts b/packages/grafana-data/src/index.ts index 631b4374ab6..2a190301d1d 100644 --- a/packages/grafana-data/src/index.ts +++ b/packages/grafana-data/src/index.ts @@ -814,7 +814,11 @@ export { export { type Action, type ActionModel, + type ActionVariable, + type ActionVariableInput, + ActionType, HttpRequestMethod, + ActionVariableType, defaultActionConfig, contentTypeOptions, httpMethodOptions, diff --git a/packages/grafana-data/src/types/action.ts b/packages/grafana-data/src/types/action.ts index 06a61e53ecd..3a637ffa071 100644 --- a/packages/grafana-data/src/types/action.ts +++ b/packages/grafana-data/src/types/action.ts @@ -1,4 +1,4 @@ -import { CSSProperties } from 'react'; +import { CSSProperties, ReactNode } from 'react'; import { SelectableValue } from './select'; @@ -18,6 +18,7 @@ export interface Action { [ActionType.Fetch]: FetchOptions; confirmation?: string; oneClick?: boolean; + variables?: ActionVariable[]; style?: ActionButtonCssProperties; } @@ -26,10 +27,21 @@ export interface Action { */ export interface ActionModel { title: string; - onClick: (event: any, origin?: any) => void; - confirmation?: string; + onClick: (event: any, origin?: any, actionVars?: ActionVariableInput) => void; + confirmation: (actionVars?: ActionVariableInput) => ReactNode; oneClick?: boolean; style: ActionButtonCssProperties; + variables?: ActionVariable[]; +} + +export type ActionVariable = { + key: string; + name: string; + type: ActionVariableType; +}; + +export enum ActionVariableType { + String = 'string', } interface FetchOptions { @@ -70,3 +82,5 @@ export const defaultActionConfig: Action = { headers: [['Content-Type', 'application/json']], }, }; + +export type ActionVariableInput = { [key: string]: string }; diff --git a/packages/grafana-ui/src/components/Actions/ActionButton.tsx b/packages/grafana-ui/src/components/Actions/ActionButton.tsx index 9d2987dc47d..55b8d3baf32 100644 --- a/packages/grafana-ui/src/components/Actions/ActionButton.tsx +++ b/packages/grafana-ui/src/components/Actions/ActionButton.tsx @@ -1,12 +1,14 @@ import { useState } from 'react'; -import { ActionModel, Field } from '@grafana/data'; +import { ActionModel, Field, ActionVariableInput } from '@grafana/data'; import { useTheme2 } from '../../themes'; import { t } from '../../utils/i18n'; import { Button, ButtonProps } from '../Button'; import { ConfirmModal } from '../ConfirmModal/ConfirmModal'; +import { VariablesInputModal } from './VariablesInputModal'; + type ActionButtonProps = ButtonProps & { action: ActionModel; }; @@ -21,27 +23,52 @@ export function ActionButton({ action, ...buttonProps }: ActionButtonProps) { const [showConfirm, setShowConfirm] = useState(false); + // Action variables + const [showVarsModal, setShowVarsModal] = useState(false); + const [actionVars, setActionVars] = useState({}); + + const actionHasVariables = action.variables && action.variables.length > 0; + + const onClick = () => { + if (actionHasVariables) { + setShowVarsModal(true); + } else { + setShowConfirm(true); + } + }; + return ( <> + + {actionHasVariables && showVarsModal && ( + setShowVarsModal(false)} + action={action} + onShowConfirm={() => setShowConfirm(true)} + variables={actionVars} + setVariables={setActionVars} + /> + )} + {showConfirm && ( { setShowConfirm(false); - action.onClick(new MouseEvent('click')); + action.onClick(new MouseEvent('click'), null, actionVars); }} onDismiss={() => { setShowConfirm(false); diff --git a/packages/grafana-ui/src/components/Actions/VariablesInputModal.tsx b/packages/grafana-ui/src/components/Actions/VariablesInputModal.tsx new file mode 100644 index 00000000000..73be7916e72 --- /dev/null +++ b/packages/grafana-ui/src/components/Actions/VariablesInputModal.tsx @@ -0,0 +1,72 @@ +import { css } from '@emotion/css'; + +import { ActionModel, ActionVariableInput } from '@grafana/data'; + +import { useStyles2 } from '../../themes'; +import { t } from '../../utils/i18n'; +import { Button } from '../Button/Button'; +import { Field } from '../Forms/Field'; +import { FieldSet } from '../Forms/FieldSet'; +import { Input } from '../Input/Input'; +import { Modal } from '../Modal/Modal'; + +interface Props { + action: ActionModel; + onDismiss: () => void; + onShowConfirm: () => void; + variables: ActionVariableInput; + setVariables: (vars: ActionVariableInput) => void; +} + +/** + * @internal + */ +export function VariablesInputModal({ action, onDismiss, onShowConfirm, variables, setVariables }: Props) { + const styles = useStyles2(getStyles); + + const onModalContinue = () => { + onDismiss(); + onShowConfirm(); + }; + + return ( + +
+ {action.variables!.map((variable) => ( + + { + setVariables({ ...variables, [variable.key]: e.currentTarget.value }); + }} + placeholder={t('grafana-ui.action-editor.button.variable-value-placeholder', 'Value')} + width={20} + /> + + ))} +
+ + + + +
+ ); +} + +const getStyles = () => { + return { + variablesModal: css({ + zIndex: 10000, + }), + }; +}; diff --git a/packages/grafana-ui/src/components/Table/DataLinksActionsTooltip.test.tsx b/packages/grafana-ui/src/components/Table/DataLinksActionsTooltip.test.tsx index 53bb472f54b..e6918901f2b 100644 --- a/packages/grafana-ui/src/components/Table/DataLinksActionsTooltip.test.tsx +++ b/packages/grafana-ui/src/components/Table/DataLinksActionsTooltip.test.tsx @@ -19,6 +19,7 @@ describe('DataLinksActionsTooltip', () => { const mockAction: ActionModel = { title: 'Action1', onClick: jest.fn(), + confirmation: jest.fn(), style: { backgroundColor: '#ff0000' }, }; diff --git a/packages/grafana-ui/src/components/uPlot/plugins/TooltipPlugin2.tsx b/packages/grafana-ui/src/components/uPlot/plugins/TooltipPlugin2.tsx index b5b0bcef2c4..649e1a377d2 100644 --- a/packages/grafana-ui/src/components/uPlot/plugins/TooltipPlugin2.tsx +++ b/packages/grafana-ui/src/components/uPlot/plugins/TooltipPlugin2.tsx @@ -228,8 +228,11 @@ export const TooltipPlugin2 = ({ // in some ways this is similar to ClickOutsideWrapper.tsx const downEventOutside = (e: Event) => { + // this tooltip is Portaled, but actions inside it create forms in Modals + const isModalOrPortaled = '[role="dialog"], #grafana-portal-container'; + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions - if (!domRef.current!.contains(e.target as Node)) { + if ((e.target as HTMLElement).closest(isModalOrPortaled) == null) { dismiss(); } }; diff --git a/public/app/features/actions/ActionEditor.tsx b/public/app/features/actions/ActionEditor.tsx index b9595c8d16a..9e94c088574 100644 --- a/public/app/features/actions/ActionEditor.tsx +++ b/public/app/features/actions/ActionEditor.tsx @@ -1,7 +1,14 @@ import { css } from '@emotion/css'; import { memo } from 'react'; -import { Action, GrafanaTheme2, httpMethodOptions, HttpRequestMethod, VariableSuggestion } from '@grafana/data'; +import { + Action, + GrafanaTheme2, + httpMethodOptions, + HttpRequestMethod, + VariableSuggestion, + ActionVariable, +} from '@grafana/data'; import { useTranslate } from '@grafana/i18n'; import { Switch, @@ -17,6 +24,7 @@ import { import { HTMLElementType, SuggestionsInput } from '../transformers/suggestionsInput/SuggestionsInput'; +import { ActionVariablesEditor } from './ActionVariablesEditor'; import { ParamsEditor } from './ParamsEditor'; interface ActionEditorProps { @@ -75,6 +83,13 @@ export const ActionEditor = memo(({ index, value, onChange, suggestions, showOne }); }; + const onVariablesChange = (variables: ActionVariable[]) => { + onChange(index, { + ...value, + variables, + }); + }; + const onQueryParamsChange = (queryParams: Array<[string, string]>) => { onChange(index, { ...value, @@ -205,6 +220,14 @@ export const ActionEditor = memo(({ index, value, onChange, suggestions, showOne + + + + { + const mockOnChange = jest.fn(); + + beforeEach(() => { + mockOnChange.mockClear(); + }); + + it('renders empty state correctly', () => { + render(); + + expect(screen.getByPlaceholderText('Key')).toBeInTheDocument(); + expect(screen.getByPlaceholderText('Name')).toBeInTheDocument(); + expect(screen.getByPlaceholderText('Type')).toBeInTheDocument(); + expect(screen.getByRole('button', { name: 'Add' })).toBeDisabled(); + }); + + it('enables add button when both key and name are filled', async () => { + render(); + + const keyInput = screen.getByPlaceholderText('Key'); + const nameInput = screen.getByPlaceholderText('Name'); + const addButton = screen.getByRole('button', { name: 'Add' }); + + expect(addButton).toBeDisabled(); + + await userEvent.type(keyInput, 'testKey'); + await userEvent.type(nameInput, 'testName'); + + expect(addButton).not.toBeDisabled(); + }); + + it('adds a new variable when add button is clicked', async () => { + render(); + + const keyInput = screen.getByPlaceholderText('Key'); + const nameInput = screen.getByPlaceholderText('Name'); + const addButton = screen.getByRole('button', { name: 'Add' }); + + await userEvent.type(keyInput, 'testKey'); + await userEvent.type(nameInput, 'testName'); + await userEvent.click(addButton); + + expect(mockOnChange).toHaveBeenCalledWith([ + { + key: 'testKey', + name: 'testName', + type: ActionVariableType.String, + }, + ]); + + expect(keyInput).toHaveValue(''); + expect(nameInput).toHaveValue(''); + }); + + it('removes a variable when delete button is clicked', async () => { + const existingVariables = [ + { key: 'key1', name: 'name1', type: ActionVariableType.String }, + { key: 'key2', name: 'name2', type: ActionVariableType.String }, + ]; + + render(); + + const deleteButtons = screen.getAllByRole('button', { name: 'Delete' }); + await userEvent.click(deleteButtons[0]); + + expect(mockOnChange).toHaveBeenCalledWith([{ key: 'key2', name: 'name2', type: ActionVariableType.String }]); + }); + + it('sorts variables by key when adding new ones', async () => { + const existingVariables = [{ key: 'key2', name: 'name2', type: ActionVariableType.String }]; + + render(); + + const keyInput = screen.getByPlaceholderText('Key'); + const nameInput = screen.getByPlaceholderText('Name'); + const addButton = screen.getByRole('button', { name: 'Add' }); + + await userEvent.type(keyInput, 'key1'); + await userEvent.type(nameInput, 'name1'); + await userEvent.click(addButton); + + expect(mockOnChange).toHaveBeenCalledWith([ + { key: 'key1', name: 'name1', type: ActionVariableType.String }, + { key: 'key2', name: 'name2', type: ActionVariableType.String }, + ]); + }); + + it('updates existing variable when adding with same key', async () => { + const existingVariables = [{ key: 'key1', name: 'oldName', type: ActionVariableType.String }]; + + render(); + + const keyInput = screen.getByPlaceholderText('Key'); + const nameInput = screen.getByPlaceholderText('Name'); + const addButton = screen.getByRole('button', { name: 'Add' }); + + await userEvent.type(keyInput, 'key1'); + await userEvent.type(nameInput, 'newName'); + await userEvent.click(addButton); + + expect(mockOnChange).toHaveBeenCalledWith([{ key: 'key1', name: 'newName', type: ActionVariableType.String }]); + }); +}); diff --git a/public/app/features/actions/ActionVariablesEditor.tsx b/public/app/features/actions/ActionVariablesEditor.tsx new file mode 100644 index 00000000000..7c264b53a6c --- /dev/null +++ b/public/app/features/actions/ActionVariablesEditor.tsx @@ -0,0 +1,107 @@ +import { useState } from 'react'; + +import { ActionVariable, ActionVariableType } from '@grafana/data'; +import { useTranslate } from '@grafana/i18n'; +import { IconButton, Input, Stack, Combobox, ComboboxOption } from '@grafana/ui'; + +interface Props { + onChange: (v: ActionVariable[]) => void; + value: ActionVariable[]; +} + +export const ActionVariablesEditor = ({ value, onChange }: Props) => { + const [key, setKey] = useState(''); + const [name, setName] = useState(''); + const [type, setType] = useState(ActionVariableType.String); + + const { t } = useTranslate(); + + const changeKey = (key: string) => { + setKey(key); + }; + + const changeName = (name: string) => { + setName(name); + }; + + const changeType = (type: ComboboxOption) => { + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions + setType(type.value as ActionVariableType); + }; + + const addVariable = () => { + let newVariables: ActionVariable[]; + + if (value) { + newVariables = value.filter((e) => e.key !== key); + } else { + newVariables = []; + } + + newVariables.push({ key, name, type }); + newVariables.sort((a, b) => a.key.localeCompare(b.key)); + onChange(newVariables); + + setKey(''); + setName(''); + setType(ActionVariableType.String); + }; + + const removeVariable = (key: string) => () => { + const updatedVariables = value.filter((variable) => variable.key !== key); + onChange(updatedVariables); + }; + + const isAddButtonDisabled = name === '' || key === ''; + + const variableTypeOptions: ComboboxOption[] = [{ label: 'string', value: ActionVariableType.String }]; + + return ( +
+ + changeKey(e.currentTarget.value)} + placeholder={t('actions.params-editor.placeholder-key', 'Key')} + width={300} + /> + changeName(e.currentTarget.value)} + placeholder={t('actions.params-editor.placeholder-name', 'Name')} + width={300} + /> + + addVariable()} + disabled={isAddButtonDisabled} + /> + + + + {value.map((entry) => ( + + + + + + + ))} + +
+ ); +}; diff --git a/public/app/features/actions/utils.test.ts b/public/app/features/actions/utils.test.ts new file mode 100644 index 00000000000..959128f908c --- /dev/null +++ b/public/app/features/actions/utils.test.ts @@ -0,0 +1,151 @@ +import { Action, ActionType, ActionVariableInput, ActionVariableType } from '@grafana/data'; + +import { HttpRequestMethod } from '../../plugins/panel/canvas/panelcfg.gen'; + +import { buildActionRequest, genReplaceActionVars } from './utils'; + +describe('interpolateActionVariables', () => { + const actionMock = (): Action => ({ + title: 'Thermostat Control', + type: ActionType.Fetch, + variables: [ + { key: 'thermostat1', name: 'First Floor Thermostat', type: ActionVariableType.String }, + { key: 'thermostat2', name: 'Second Floor Thermostat', type: ActionVariableType.String }, + ], + fetch: { + url: 'http://test.com/api/thermostats/$thermostat1/sync/$thermostat2', + method: HttpRequestMethod.POST, + body: JSON.stringify({ + primary: 'Device-$thermostat1', + data: { + secondary: 'Room-$thermostat2', + settings: { + syncMode: 'Mode-$thermostat1', + }, + }, + }), + headers: [ + ['Device-ID', 'Thermostat-$thermostat1'], + ['Content-Type', 'application/json'], + ], + queryParams: [ + ['primary', 'Device-$thermostat1'], + ['secondary', 'Room-$thermostat2'], + ['mode', 'sync'], + ], + }, + }); + + it('should return original action if no variables or actionVars are provided', () => { + const action: Action = { + title: 'Test', + type: ActionType.Fetch, + [ActionType.Fetch]: { + url: 'http://test.com/api/', + method: HttpRequestMethod.GET, + }, + }; + const actionVars: ActionVariableInput = {}; + + const request = buildActionRequest( + action, + genReplaceActionVars((str) => str, action, actionVars) + ); + expect(request).toEqual({ + headers: { + 'X-Grafana-Action': '1', + }, + method: 'GET', + url: 'http://test.com/api/', + }); + }); + + it('should interpolate variables in URL', () => { + const action = actionMock(); + const actionVars: ActionVariableInput = { + thermostat1: 'T-001', + thermostat2: 'T-002', + }; + + const request = buildActionRequest( + action, + genReplaceActionVars((str) => str, action, actionVars) + ); + expect(request.url).toBe( + 'http://test.com/api/thermostats/T-001/sync/T-002?primary=Device-T-001&secondary=Room-T-002&mode=sync' + ); + }); + + it('should interpolate variables in request body', () => { + const action = actionMock(); + const actionVars: ActionVariableInput = { + thermostat1: 'T-001', + thermostat2: 'T-002', + }; + + const request = buildActionRequest( + action, + genReplaceActionVars((str) => str, action, actionVars) + ); + expect(JSON.parse(request.data)).toEqual({ + primary: 'Device-T-001', + data: { + secondary: 'Room-T-002', + settings: { + syncMode: 'Mode-T-001', + }, + }, + }); + }); + + it('should interpolate variables in headers', () => { + const action = actionMock(); + const actionVars: ActionVariableInput = { + thermostat1: 'T-001', + thermostat2: 'T-002', + }; + + const request = buildActionRequest( + action, + genReplaceActionVars((str) => str, action, actionVars) + ); + expect(request.headers).toEqual({ + 'Content-Type': 'application/json', + 'Device-ID': 'Thermostat-T-001', + 'X-Grafana-Action': '1', + }); + }); + + it('should interpolate variables in query params', () => { + const action = actionMock(); + const actionVars: ActionVariableInput = { + thermostat1: 'T-001', + thermostat2: 'T-002', + }; + + const request = buildActionRequest( + action, + genReplaceActionVars((str) => str, action, actionVars) + ); + expect(request.url).toEqual( + 'http://test.com/api/thermostats/T-001/sync/T-002?primary=Device-T-001&secondary=Room-T-002&mode=sync' + ); + }); + + it('should only interpolate provided variables', () => { + const action = actionMock(); + const actionVars: ActionVariableInput = { + thermostat1: 'T-001', + // thermostat2 is not provided + }; + + const request = buildActionRequest( + action, + genReplaceActionVars((str) => str, action, actionVars) + ); + expect(request.url).toBe( + 'http://test.com/api/thermostats/T-001/sync/$thermostat2?primary=Device-T-001&secondary=Room-%24thermostat2&mode=sync' + ); + expect(JSON.parse(request.data).data.secondary).toBe('Room-$thermostat2'); + }); +}); diff --git a/public/app/features/actions/utils.ts b/public/app/features/actions/utils.ts index e8771075178..8c78f5ce6a9 100644 --- a/public/app/features/actions/utils.ts +++ b/public/app/features/actions/utils.ts @@ -1,6 +1,7 @@ import { Action, ActionModel, + ActionVariableInput, AppEvents, DataContextScopedVar, DataFrame, @@ -13,12 +14,35 @@ import { textUtil, ValueLinkConfig, } from '@grafana/data'; -import { BackendSrvRequest, getBackendSrv, config as grafanaConfig } from '@grafana/runtime'; +import { BackendSrvRequest, config as grafanaConfig, getBackendSrv } from '@grafana/runtime'; import { appEvents } from 'app/core/core'; import { HttpRequestMethod } from '../../plugins/panel/canvas/panelcfg.gen'; import { createAbsoluteUrl, RelativeUrl } from '../alerting/unified/utils/url'; +/** @internal */ +export const genReplaceActionVars = ( + boundReplaceVariables: InterpolateFunction, + action: Action, + actionVars?: ActionVariableInput +): InterpolateFunction => { + return (value, scopedVars, format) => { + if (action.variables && actionVars) { + value = value.replace(/\$\w+/g, (matched) => { + const name = matched.slice(1); + + if (action.variables!.some((action) => action.key === name) && actionVars[name] != null) { + return actionVars[name]; + } + + return matched; + }); + } + + return boundReplaceVariables(value, scopedVars, format); + }; +}; + /** @internal */ export const getActions = ( frame: DataFrame, @@ -50,22 +74,40 @@ export const getActions = ( dataContext.value.calculatedValue = config.calculatedValue; } - const title = replaceVariables(action.title, actionScopedVars); - const confirmation = replaceVariables( - action.confirmation || `Are you sure you want to ${action.title}?`, - actionScopedVars - ); - const actionModel: ActionModel = { - title, - confirmation, - onClick: (evt: MouseEvent, origin: Field) => { - buildActionOnClick(action, boundReplaceVariables); + title: replaceVariables(action.title, actionScopedVars), + confirmation: (actionVars?: ActionVariableInput) => + genReplaceActionVars( + boundReplaceVariables, + action, + actionVars + )(action.confirmation || `Are you sure you want to ${action.title}?`), + onClick: (evt: MouseEvent, origin: Field, actionVars?: ActionVariableInput) => { + let request = buildActionRequest(action, genReplaceActionVars(boundReplaceVariables, action, actionVars)); + + try { + getBackendSrv() + .fetch(request) + .subscribe({ + error: (error) => { + appEvents.emit(AppEvents.alertError, ['An error has occurred. Check console output for more details.']); + console.error(error); + }, + complete: () => { + appEvents.emit(AppEvents.alertSuccess, ['API call was successful']); + }, + }); + } catch (error) { + appEvents.emit(AppEvents.alertError, ['An error has occurred. Check console output for more details.']); + console.error(error); + return; + } }, oneClick: action.oneClick ?? false, style: { backgroundColor: action.style?.backgroundColor ?? grafanaConfig.theme2.colors.secondary.main, }, + variables: action.variables, }; return actionModel; @@ -75,52 +117,36 @@ export const getActions = ( }; /** @internal */ -const buildActionOnClick = (action: Action, replaceVariables: InterpolateFunction) => { - try { - const url = new URL(getUrl(replaceVariables(action.fetch.url))); +export const buildActionRequest = (action: Action, replaceVariables: InterpolateFunction) => { + const url = new URL(getUrl(replaceVariables(action.fetch.url))); - const requestHeaders: Record = {}; + const requestHeaders: Record = {}; - let request: BackendSrvRequest = { - url: url.toString(), - method: action.fetch.method, - data: getData(action, replaceVariables), - headers: requestHeaders, - }; + let request: BackendSrvRequest = { + url: url.toString(), + method: action.fetch.method, + data: getData(action, replaceVariables), + headers: requestHeaders, + }; - if (action.fetch.headers) { - action.fetch.headers.forEach(([name, value]) => { - requestHeaders[replaceVariables(name)] = replaceVariables(value); - }); - } - - if (action.fetch.queryParams) { - action.fetch.queryParams?.forEach(([name, value]) => { - url.searchParams.append(replaceVariables(name), replaceVariables(value)); - }); - - request.url = url.toString(); - } - - requestHeaders['X-Grafana-Action'] = '1'; - request.headers = requestHeaders; - - getBackendSrv() - .fetch(request) - .subscribe({ - error: (error) => { - appEvents.emit(AppEvents.alertError, ['An error has occurred. Check console output for more details.']); - console.error(error); - }, - complete: () => { - appEvents.emit(AppEvents.alertSuccess, ['API call was successful']); - }, - }); - } catch (error) { - appEvents.emit(AppEvents.alertError, ['An error has occurred. Check console output for more details.']); - console.error(error); - return; + if (action.fetch.headers) { + action.fetch.headers.forEach(([name, value]) => { + requestHeaders[replaceVariables(name)] = replaceVariables(value); + }); } + + if (action.fetch.queryParams) { + action.fetch.queryParams?.forEach(([name, value]) => { + url.searchParams.append(replaceVariables(name), replaceVariables(value)); + }); + + request.url = url.toString(); + } + + requestHeaders['X-Grafana-Action'] = '1'; + request.headers = requestHeaders; + + return request; }; /** @internal */ diff --git a/public/app/features/canvas/runtime/element.tsx b/public/app/features/canvas/runtime/element.tsx index 9264d6fd985..a94a46c122d 100644 --- a/public/app/features/canvas/runtime/element.tsx +++ b/public/app/features/canvas/runtime/element.tsx @@ -752,7 +752,7 @@ export class ElementState implements LayerElement { { diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index 612002cf70d..f80a38f77f9 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -57,7 +57,11 @@ "aria-label-add": "Add", "aria-label-delete": "Delete", "placeholder-key": "Key", + "placeholder-name": "Name", "placeholder-value": "Value" + }, + "variables-editor": { + "placeholder-type": "Type" } }, "admin": { @@ -5404,10 +5408,14 @@ "grafana-ui": { "action-editor": { "button": { + "action-variables-title": "Action variables", "confirm": "Confirm", "confirm-action": "Confirm action", - "style": "Button style" + "style": "Button style", + "variable-value-placeholder": "Value" }, + "close": "Close", + "continue": "Continue", "inline": { "add-action": "Add action", "edit-action": "Edit action" @@ -5418,6 +5426,7 @@ "action-query-params": "Query parameters", "action-title": "Title", "action-title-placeholder": "Action title", + "action-variables": "Variables", "one-click-description": "Only one link or action can have one click enabled at a time" } }, From e05ccb822a27ed438a50666c28b941bd875512c4 Mon Sep 17 00:00:00 2001 From: Syerikjan Kh Date: Thu, 22 May 2025 08:23:33 -0400 Subject: [PATCH 09/73] feat: map GF_INSTALL_PLUGINS to use preinstall plugins feature (#105145) * feat: map GF_INSTALL_PLUGINS to use preinstall plugins feature * ref: process GF_INSTALL_PLUGINS as sync install * fix: check GF_INSTALL_PLUGINS_FORCE in the docker run * ref: use preinstall_sync for GF_INSTALL_PLUGINS * ref: logs and deprecation msg for GF_INSTALL_PLUGINS * chore: deprecated log formatting --- .../setup-grafana/start-restart-grafana.md | 2 +- packaging/docker/run.sh | 27 ++-- pkg/setting/setting_plugins.go | 60 +++++++- pkg/setting/setting_plugins_test.go | 145 +++++++++++++++++- pkg/setting/setting_test.go | 12 ++ 5 files changed, 225 insertions(+), 21 deletions(-) diff --git a/docs/sources/setup-grafana/start-restart-grafana.md b/docs/sources/setup-grafana/start-restart-grafana.md index 0cba9899590..f0c4e570b87 100644 --- a/docs/sources/setup-grafana/start-restart-grafana.md +++ b/docs/sources/setup-grafana/start-restart-grafana.md @@ -128,7 +128,7 @@ services: restart: unless-stopped environment: - TERM=linux - - GF_INSTALL_PLUGINS=grafana-clock-panel,grafana-polystat-panel + - GF_PLUGINS_PREINSTALL=grafana-clock-panel,grafana-polystat-panel ports: - '3000:3000' volumes: diff --git a/packaging/docker/run.sh b/packaging/docker/run.sh index ac1f9ea45a6..a4c91b49379 100755 --- a/packaging/docker/run.sh +++ b/packaging/docker/run.sh @@ -63,18 +63,21 @@ done export HOME="$GF_PATHS_HOME" if [ ! -z "${GF_INSTALL_PLUGINS}" ]; then - OLDIFS=$IFS - IFS=',' - for plugin in ${GF_INSTALL_PLUGINS}; do - IFS=$OLDIFS - if [[ $plugin =~ .*\;.* ]]; then - pluginUrl=$(echo "$plugin" | cut -d';' -f 1) - pluginInstallFolder=$(echo "$plugin" | cut -d';' -f 2) - grafana cli --pluginUrl ${pluginUrl} --pluginsDir "${GF_PATHS_PLUGINS}" plugins install "${pluginInstallFolder}" - else - grafana cli --pluginsDir "${GF_PATHS_PLUGINS}" plugins install ${plugin} - fi - done + >&2 echo "\033[0;33mWARN\033[0m: GF_INSTALL_PLUGINS is deprecated. Use GF_PLUGINS_PREINSTALL or GF_PLUGINS_PREINSTALL_SYNC instead. Checkout the documentation for more info." + if [ "${GF_INSTALL_PLUGINS_FORCE}" = "true" ]; then + OLDIFS=$IFS + IFS=',' + for plugin in ${GF_INSTALL_PLUGINS}; do + IFS=$OLDIFS + if [[ $plugin =~ .*\;.* ]]; then + pluginUrl=$(echo "$plugin" | cut -d';' -f 1) + pluginInstallFolder=$(echo "$plugin" | cut -d';' -f 2) + grafana cli --pluginUrl ${pluginUrl} --pluginsDir "${GF_PATHS_PLUGINS}" plugins install "${pluginInstallFolder}" + else + grafana cli --pluginsDir "${GF_PATHS_PLUGINS}" plugins install ${plugin} + fi + done + fi fi exec grafana server \ diff --git a/pkg/setting/setting_plugins.go b/pkg/setting/setting_plugins.go index 3fb980c38da..005950e9611 100644 --- a/pkg/setting/setting_plugins.go +++ b/pkg/setting/setting_plugins.go @@ -1,6 +1,8 @@ package setting import ( + "os" + "regexp" "strings" "gopkg.in/ini.v1" @@ -34,13 +36,62 @@ func extractPluginSettings(sections []*ini.Section) PluginSettings { var ( defaultPreinstallPlugins = map[string]InstallPlugin{ // Default preinstalled plugins - "grafana-lokiexplore-app": {"grafana-lokiexplore-app", "", ""}, - "grafana-pyroscope-app": {"grafana-pyroscope-app", "", ""}, - "grafana-exploretraces-app": {"grafana-exploretraces-app", "", ""}, - "grafana-metricsdrilldown-app": {"grafana-metricsdrilldown-app", "", ""}, + "grafana-lokiexplore-app": {ID: "grafana-lokiexplore-app"}, + "grafana-pyroscope-app": {ID: "grafana-pyroscope-app"}, + "grafana-exploretraces-app": {ID: "grafana-exploretraces-app"}, + "grafana-metricsdrilldown-app": {ID: "grafana-metricsdrilldown-app"}, } ) +func (cfg *Cfg) migrateInstallPluginsToPreinstallPluginsSync(rawInstallPlugins, installPluginsForce string, preinstallPluginsSync map[string]InstallPlugin) { + if strings.ToLower(installPluginsForce) == "true" || rawInstallPlugins == "" { + cfg.Logger.Debug("GF_INSTALL_PLUGINS_FORCE is set to true, skipping migration of GF_INSTALL_PLUGINS to GF_PLUGINS_PREINSTALL_SYNC") + return + } + installPluginsEntries := strings.Split(rawInstallPlugins, ",") + + // Format 1: ID only (e.g., "grafana-clock-panel") + // Format 2: ID with version (e.g., "grafana-clock-panel 1.0.1") + // Format 3: URL with folder (e.g., "https://grafana.com/api/plugins/grafana-clock-panel/versions/latest/download;grafana-clock-panel") + pluginRegex := regexp.MustCompile(`(?:([^;]+);)?([^;\s]+)(?:\s+(.+))?`) + for _, entry := range installPluginsEntries { + trimmedEntry := strings.TrimSpace(entry) + if trimmedEntry == "" { + continue + } + + matches := pluginRegex.FindStringSubmatch(trimmedEntry) + + if matches == nil { + cfg.Logger.Debug("No match found for entry: ", trimmedEntry) + continue + } + + url := "" + if len(matches) > 1 { + url = strings.TrimSpace(matches[1]) + } + + id := "" + if len(matches) > 2 { + id = strings.TrimSpace(matches[2]) + } + if _, exists := preinstallPluginsSync[id]; exists { + continue + } + + version := "" + if len(matches) > 3 { + version = strings.TrimSpace(matches[3]) + } + if id != "" { + preinstallPluginsSync[id] = InstallPlugin{ID: id, Version: version, URL: url} + } else { + cfg.Logger.Debug("No ID found for entry: ", trimmedEntry, "matches: ", matches) + } + } +} + func (cfg *Cfg) processPreinstallPlugins(rawInstallPlugins []string, preinstallPlugins map[string]InstallPlugin) { // Add the plugins defined in the configuration for _, plugin := range rawInstallPlugins { @@ -88,6 +139,7 @@ func (cfg *Cfg) readPluginSettings(iniFile *ini.File) error { rawInstallPluginsSync := util.SplitString(pluginsSection.Key("preinstall_sync").MustString("")) preinstallPluginsSync := make(map[string]InstallPlugin) cfg.processPreinstallPlugins(rawInstallPluginsSync, preinstallPluginsSync) + cfg.migrateInstallPluginsToPreinstallPluginsSync(os.Getenv("GF_INSTALL_PLUGINS"), os.Getenv("GF_INSTALL_PLUGINS_FORCE"), preinstallPluginsSync) // Remove from the list the plugins that have been disabled for _, disabledPlugin := range cfg.DisablePlugins { delete(preinstallPluginsAsync, disabledPlugin) diff --git a/pkg/setting/setting_plugins_test.go b/pkg/setting/setting_plugins_test.go index 1af9c95270f..74425c95a0e 100644 --- a/pkg/setting/setting_plugins_test.go +++ b/pkg/setting/setting_plugins_test.go @@ -172,12 +172,12 @@ func Test_readPluginSettings(t *testing.T) { { name: "should add the default preinstalled plugin and the one defined", rawInput: "plugin1", - expected: append(defaultPreinstallPluginsList, InstallPlugin{"plugin1", "", ""}), + expected: append(defaultPreinstallPluginsList, InstallPlugin{ID: "plugin1", Version: "", URL: ""}), }, { name: "should add the default preinstalled plugin and the one defined with version", rawInput: "plugin1@1.0.0", - expected: append(defaultPreinstallPluginsList, InstallPlugin{"plugin1", "1.0.0", ""}), + expected: append(defaultPreinstallPluginsList, InstallPlugin{ID: "plugin1", Version: "1.0.0", URL: ""}), }, { name: "it should remove the disabled plugin", @@ -207,12 +207,12 @@ func Test_readPluginSettings(t *testing.T) { { name: "should parse a plugin with version and URL", rawInput: "plugin1@1.0.1@https://example.com/plugin1.tar.gz", - expected: append(defaultPreinstallPluginsList, InstallPlugin{"plugin1", "1.0.1", "https://example.com/plugin1.tar.gz"}), + expected: append(defaultPreinstallPluginsList, InstallPlugin{ID: "plugin1", Version: "1.0.1", URL: "https://example.com/plugin1.tar.gz"}), }, { name: "should parse a plugin with URL", rawInput: "plugin1@@https://example.com/plugin1.tar.gz", - expected: append(defaultPreinstallPluginsList, InstallPlugin{"plugin1", "", "https://example.com/plugin1.tar.gz"}), + expected: append(defaultPreinstallPluginsList, InstallPlugin{ID: "plugin1", Version: "", URL: "https://example.com/plugin1.tar.gz"}), }, { name: "when preinstall_async is false, should add all plugins to preinstall_sync", @@ -272,3 +272,140 @@ func Test_readPluginSettings(t *testing.T) { } }) } + +func Test_migrateInstallPluginsToPreinstallPluginsSync(t *testing.T) { + tests := []struct { + name string + installPluginsVal string + installPluginsForce string + preinstallPlugins map[string]InstallPlugin + expectedPlugins map[string]InstallPlugin + }{ + { + name: "should return empty map when GF_INSTALL_PLUGINS is not set", + installPluginsVal: "", + preinstallPlugins: map[string]InstallPlugin{}, + expectedPlugins: map[string]InstallPlugin{}, + }, + { + name: "should parse URL with folder format", + installPluginsVal: "https://grafana.com/grafana/plugins/grafana-piechart-panel/;grafana-piechart-panel", + preinstallPlugins: map[string]InstallPlugin{}, + expectedPlugins: map[string]InstallPlugin{ + "grafana-piechart-panel": { + ID: "grafana-piechart-panel", + Version: "", + URL: "https://grafana.com/grafana/plugins/grafana-piechart-panel/", + }, + }, + }, + { + name: "should parse mixed formats", + installPluginsVal: "https://github.com/VolkovLabs/business-links/releases/download/v1.2.1/volkovlabs-links-panel-1.2.1.zip;volkovlabs-links-panel,marcusolsson-static-datasource,volkovlabs-variable-panel", + preinstallPlugins: map[string]InstallPlugin{}, + expectedPlugins: map[string]InstallPlugin{ + "volkovlabs-links-panel": { + ID: "volkovlabs-links-panel", + Version: "", + URL: "https://github.com/VolkovLabs/business-links/releases/download/v1.2.1/volkovlabs-links-panel-1.2.1.zip", + }, + "marcusolsson-static-datasource": { + ID: "marcusolsson-static-datasource", + Version: "", + URL: "", + }, + "volkovlabs-variable-panel": { + ID: "volkovlabs-variable-panel", + Version: "", + URL: "", + }, + }, + }, + { + name: "should parse ID with version format", + installPluginsVal: "volkovlabs-links-panel 1.2.1,marcusolsson-static-datasource 1.0.0,volkovlabs-variable-panel", + preinstallPlugins: map[string]InstallPlugin{}, + expectedPlugins: map[string]InstallPlugin{ + "volkovlabs-links-panel": { + ID: "volkovlabs-links-panel", + Version: "1.2.1", + URL: "", + }, + "marcusolsson-static-datasource": { + ID: "marcusolsson-static-datasource", + Version: "1.0.0", + URL: "", + }, + "volkovlabs-variable-panel": { + ID: "volkovlabs-variable-panel", + Version: "", + URL: "", + }, + }, + }, + { + name: "should return empty map when GF_INSTALL_PLUGINS_FORCE is true", + installPluginsVal: "grafana-piechart-panel", + installPluginsForce: "true", + preinstallPlugins: map[string]InstallPlugin{}, + expectedPlugins: map[string]InstallPlugin{}, + }, + { + name: "should skip plugins that are already configured", + installPluginsVal: "plugin1 1.0.0,plugin2,plugin3", + preinstallPlugins: map[string]InstallPlugin{ + "plugin1": {ID: "plugin1", Version: "1.0.1"}, + "plugin3": {ID: "plugin3"}, + }, + expectedPlugins: map[string]InstallPlugin{ + "plugin2": { + ID: "plugin2", + }, + "plugin3": { + ID: "plugin3", + }, + "plugin1": { + ID: "plugin1", + Version: "1.0.1", + }, + }, + }, + { + name: "should trim the space in the input", + installPluginsVal: " plugin1 1.0.0, plugin2, plugin3 ", + preinstallPlugins: map[string]InstallPlugin{}, + expectedPlugins: map[string]InstallPlugin{ + "plugin2": { + ID: "plugin2", + }, + "plugin3": { + ID: "plugin3", + }, + "plugin1": { + ID: "plugin1", + Version: "1.0.0", + }, + }, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + cfg := NewCfg() + + cfg.migrateInstallPluginsToPreinstallPluginsSync(tc.installPluginsVal, tc.installPluginsForce, tc.preinstallPlugins) + assert.Equal(t, len(tc.expectedPlugins), len(tc.preinstallPlugins), "Number of plugins doesn't match") + + // Check each expected plugin exists with correct values + for id, expectedPlugin := range tc.expectedPlugins { + actualPlugin, exists := tc.preinstallPlugins[id] + assert.True(t, exists, "Expected plugin %s not found", id) + if exists { + assert.Equal(t, expectedPlugin.ID, actualPlugin.ID, "Plugin ID mismatch for %s", id) + assert.Equal(t, expectedPlugin.Version, actualPlugin.Version, "Plugin version mismatch for %s", id) + assert.Equal(t, expectedPlugin.URL, actualPlugin.URL, "Plugin URL mismatch for %s", id) + } + } + }) + } +} diff --git a/pkg/setting/setting_test.go b/pkg/setting/setting_test.go index 9a434a7e2ff..c40f761cad4 100644 --- a/pkg/setting/setting_test.go +++ b/pkg/setting/setting_test.go @@ -78,6 +78,18 @@ func TestLoadingSettings(t *testing.T) { require.Equal(t, filepath.Join(cfg.DataPath, "log"), cfg.LogsPath) }) + t.Run("Should be able to override via plugins.preinstall with GF_INSTALL_PLUGINS env var when GF_PLUGINS_PREINSTALL and cfg.plugins.preinstall are not set", func(t *testing.T) { + t.Setenv("GF_INSTALL_PLUGINS", "https://grafana.com/grafana/plugins/grafana-piechart-panel/;grafana-piechart-panel") + + cfg := NewCfg() + err := cfg.Load(CommandLineArgs{HomePath: "../../"}) + require.Nil(t, err) + + require.Equal(t, filepath.Join(cfg.HomePath, "data"), cfg.DataPath) + require.Equal(t, filepath.Join(cfg.DataPath, "log"), cfg.LogsPath) + require.Equal(t, cfg.PreinstallPluginsSync, []InstallPlugin{{ID: "grafana-piechart-panel", Version: "", URL: "https://grafana.com/grafana/plugins/grafana-piechart-panel/"}}) + }) + t.Run("Should be able to expand parameter from environment variables", func(t *testing.T) { t.Setenv("DEFAULT_IDP_URL", "grafana.com") t.Setenv("GF_AUTH_GENERIC_OAUTH_AUTH_URL", "${DEFAULT_IDP_URL}/auth") From ec3f46766673e9c86ade73b59c76d438ca84daac Mon Sep 17 00:00:00 2001 From: Sarah Zinger Date: Thu, 22 May 2025 09:04:02 -0400 Subject: [PATCH 10/73] ds-querier: small renames (#105701) --- pkg/registry/apis/query/query.go | 2 +- pkg/registry/apis/query/query_test.go | 4 ++-- pkg/registry/apis/query/register.go | 18 +++++++++--------- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/pkg/registry/apis/query/query.go b/pkg/registry/apis/query/query.go index 9c01c9266a7..ded201ec0f4 100644 --- a/pkg/registry/apis/query/query.go +++ b/pkg/registry/apis/query/query.go @@ -254,7 +254,7 @@ func (b *QueryAPIBuilder) handleQuerySingleDatasource(ctx context.Context, req d return &backend.QueryDataResponse{}, nil } - client, err := b.client.GetDataSourceClient( + client, err := b.clientSupplier.GetDataSourceClient( ctx, v0alpha1.DataSourceRef{ Type: req.PluginId, diff --git a/pkg/registry/apis/query/query_test.go b/pkg/registry/apis/query/query_test.go index fbe1a2766e7..ea6ad1e1bd0 100644 --- a/pkg/registry/apis/query/query_test.go +++ b/pkg/registry/apis/query/query_test.go @@ -24,7 +24,7 @@ import ( func TestQueryRestConnectHandler(t *testing.T) { b := &QueryAPIBuilder{ - client: mockClient{ + clientSupplier: mockClient{ lastCalledWithHeaders: &map[string]string{}, }, tracer: tracing.InitializeTracerForTest(), @@ -80,7 +80,7 @@ func TestQueryRestConnectHandler(t *testing.T) { "X-Rule-Type": "type-1", "X-Rule-Version": "version-1", "X-Grafana-Org-Id": "1", - }, *b.client.(mockClient).lastCalledWithHeaders) + }, *b.clientSupplier.(mockClient).lastCalledWithHeaders) } func TestInstantQueryFromAlerting(t *testing.T) { diff --git a/pkg/registry/apis/query/register.go b/pkg/registry/apis/query/register.go index 9692c21d2c7..bd034976727 100644 --- a/pkg/registry/apis/query/register.go +++ b/pkg/registry/apis/query/register.go @@ -43,17 +43,17 @@ type QueryAPIBuilder struct { authorizer authorizer.Authorizer - tracer tracing.Tracer - metrics *metrics.ExprMetrics - parser *queryParser - client clientapi.DataSourceClientSupplier - registry query.DataSourceApiServerRegistry - converter *expr.ResultConverter - queryTypes *query.QueryTypeDefinitionList + tracer tracing.Tracer + metrics *metrics.ExprMetrics + parser *queryParser + clientSupplier clientapi.DataSourceClientSupplier + registry query.DataSourceApiServerRegistry + converter *expr.ResultConverter + queryTypes *query.QueryTypeDefinitionList } func NewQueryAPIBuilder(features featuremgmt.FeatureToggles, - client clientapi.DataSourceClientSupplier, + clientSupplier clientapi.DataSourceClientSupplier, ar authorizer.Authorizer, registry query.DataSourceApiServerRegistry, legacy service.LegacyDataSourceLookup, @@ -80,7 +80,7 @@ func NewQueryAPIBuilder(features featuremgmt.FeatureToggles, return &QueryAPIBuilder{ concurrentQueryLimit: 4, log: log.New("query_apiserver"), - client: client, + clientSupplier: clientSupplier, authorizer: ar, registry: registry, parser: newQueryParser(reader, legacy, tracer, log.New("query_parser")), From e56f196c2ea4a916a5ee8afcb2d6191ac753bb51 Mon Sep 17 00:00:00 2001 From: Victor Marin <36818606+mdvictor@users.noreply.github.com> Date: Thu, 22 May 2025 16:05:31 +0300 Subject: [PATCH 11/73] Revert "Remove scope dependency" (#105830) Revert "Remove scope dependency (#105768)" This reverts commit 75d983ccc8d08eb950988663f073ae7a808ff86a. --- .../features/dashboard-scene/scene/DashboardReloadBehavior.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/public/app/features/dashboard-scene/scene/DashboardReloadBehavior.ts b/public/app/features/dashboard-scene/scene/DashboardReloadBehavior.ts index 24475c6df59..bd75781f091 100644 --- a/public/app/features/dashboard-scene/scene/DashboardReloadBehavior.ts +++ b/public/app/features/dashboard-scene/scene/DashboardReloadBehavior.ts @@ -42,6 +42,7 @@ export class DashboardReloadBehavior extends SceneObjectBase Date: Thu, 22 May 2025 06:09:24 -0700 Subject: [PATCH 12/73] Chore: update triage project for Elasticsearch (#105825) --- .github/commands.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/commands.json b/.github/commands.json index 861c9a86707..da2c16a918d 100644 --- a/.github/commands.json +++ b/.github/commands.json @@ -160,7 +160,7 @@ "name": "datasource/Elasticsearch", "action": "addToProject", "addToProject": { - "url": "https://github.com/orgs/grafana/projects/97" + "url": "https://github.com/orgs/grafana/projects/190" } }, { From 7f2923d4ed72182fd2a21c2747cd8fcabc00a5ab Mon Sep 17 00:00:00 2001 From: Dana Axinte <53751979+dana-axinte@users.noreply.github.com> Date: Thu, 22 May 2025 14:26:47 +0100 Subject: [PATCH 13/73] SecretsManager: Introduce keeper store (#105557) * SecretsManager: Introduce secret database wrapper Co-authored-by: PoorlyDefinedBehaviour Co-authored-by: Leandro Deveikis Co-authored-by: Dana Axinte <53751979+dana-axinte@users.noreply.github.com> Co-authored-by: Matheus Macabu * SecretsManager: Introduce db migrator with keeper table Co-authored-by: PoorlyDefinedBehaviour Co-authored-by: Leandro Deveikis Co-authored-by: Dana Axinte <53751979+dana-axinte@users.noreply.github.com> Co-authored-by: Matheus Macabu * SecretsManager: Introduce keeper store Co-authored-by: Leandro Deveikis Co-authored-by: Dana Axinte <53751979+dana-axinte@users.noreply.github.com> Co-authored-by: Matheus Macabu * new line * without query listByNameSecureValue * remove unused extractSecureValues for now * SecretsManager: Add keeper integration tests Co-authored-by: Leandro Deveikis Co-authored-by: Dana Axinte <53751979+dana-axinte@users.noreply.github.com> Co-authored-by: Matheus Macabu --------- Co-authored-by: PoorlyDefinedBehaviour Co-authored-by: Leandro Deveikis Co-authored-by: Matheus Macabu --- .github/CODEOWNERS | 1 + .../secret/metadata/data/keeper_create.sql | 27 + .../secret/metadata/data/keeper_delete.sql | 4 + .../secret/metadata/data/keeper_list.sql | 18 + .../metadata/data/keeper_listByName.sql | 10 + .../secret/metadata/data/keeper_read.sql | 21 + .../secret/metadata/data/keeper_update.sql | 18 + pkg/storage/secret/metadata/keeper_model.go | 248 ++++++++ pkg/storage/secret/metadata/keeper_store.go | 256 +++++++- .../secret/metadata/keeper_store_test.go | 344 +++++++++++ pkg/storage/secret/metadata/query.go | 106 ++++ pkg/storage/secret/metadata/query_test.go | 108 ++++ .../testdata/mysql--keeper_create-create.sql | 27 + .../testdata/mysql--keeper_delete-delete.sql | 4 + .../testdata/mysql--keeper_list-list.sql | 18 + .../mysql--keeper_listByName-list.sql | 8 + .../mysql--keeper_read-read-for-update.sql | 19 + .../testdata/mysql--keeper_read-read.sql | 18 + .../testdata/mysql--keeper_update-update.sql | 18 + .../postgres--keeper_create-create.sql | 27 + .../postgres--keeper_delete-delete.sql | 4 + .../testdata/postgres--keeper_list-list.sql | 18 + .../postgres--keeper_listByName-list.sql | 8 + .../postgres--keeper_read-read-for-update.sql | 19 + .../testdata/postgres--keeper_read-read.sql | 18 + .../postgres--keeper_update-update.sql | 18 + .../testdata/sqlite--keeper_create-create.sql | 27 + .../testdata/sqlite--keeper_delete-delete.sql | 4 + .../testdata/sqlite--keeper_list-list.sql | 18 + .../sqlite--keeper_listByName-list.sql | 7 + .../sqlite--keeper_read-read-for-update.sql | 18 + .../testdata/sqlite--keeper_read-read.sql | 18 + .../testdata/sqlite--keeper_update-update.sql | 18 + pkg/tests/apis/secret/keeper_test.go | 569 ++++++++++++++++++ pkg/tests/apis/secret/main_test.go | 150 +++++ .../secret/testdata/keeper-aws-generate.yaml | 15 + .../secret/testdata/keeper-gcp-generate.yaml | 14 + 37 files changed, 2228 insertions(+), 15 deletions(-) create mode 100644 pkg/storage/secret/metadata/data/keeper_create.sql create mode 100644 pkg/storage/secret/metadata/data/keeper_delete.sql create mode 100644 pkg/storage/secret/metadata/data/keeper_list.sql create mode 100644 pkg/storage/secret/metadata/data/keeper_listByName.sql create mode 100644 pkg/storage/secret/metadata/data/keeper_read.sql create mode 100644 pkg/storage/secret/metadata/data/keeper_update.sql create mode 100644 pkg/storage/secret/metadata/keeper_model.go create mode 100644 pkg/storage/secret/metadata/keeper_store_test.go create mode 100644 pkg/storage/secret/metadata/query.go create mode 100644 pkg/storage/secret/metadata/query_test.go create mode 100755 pkg/storage/secret/metadata/testdata/mysql--keeper_create-create.sql create mode 100755 pkg/storage/secret/metadata/testdata/mysql--keeper_delete-delete.sql create mode 100755 pkg/storage/secret/metadata/testdata/mysql--keeper_list-list.sql create mode 100755 pkg/storage/secret/metadata/testdata/mysql--keeper_listByName-list.sql create mode 100755 pkg/storage/secret/metadata/testdata/mysql--keeper_read-read-for-update.sql create mode 100755 pkg/storage/secret/metadata/testdata/mysql--keeper_read-read.sql create mode 100755 pkg/storage/secret/metadata/testdata/mysql--keeper_update-update.sql create mode 100755 pkg/storage/secret/metadata/testdata/postgres--keeper_create-create.sql create mode 100755 pkg/storage/secret/metadata/testdata/postgres--keeper_delete-delete.sql create mode 100755 pkg/storage/secret/metadata/testdata/postgres--keeper_list-list.sql create mode 100755 pkg/storage/secret/metadata/testdata/postgres--keeper_listByName-list.sql create mode 100755 pkg/storage/secret/metadata/testdata/postgres--keeper_read-read-for-update.sql create mode 100755 pkg/storage/secret/metadata/testdata/postgres--keeper_read-read.sql create mode 100755 pkg/storage/secret/metadata/testdata/postgres--keeper_update-update.sql create mode 100755 pkg/storage/secret/metadata/testdata/sqlite--keeper_create-create.sql create mode 100755 pkg/storage/secret/metadata/testdata/sqlite--keeper_delete-delete.sql create mode 100755 pkg/storage/secret/metadata/testdata/sqlite--keeper_list-list.sql create mode 100755 pkg/storage/secret/metadata/testdata/sqlite--keeper_listByName-list.sql create mode 100755 pkg/storage/secret/metadata/testdata/sqlite--keeper_read-read-for-update.sql create mode 100755 pkg/storage/secret/metadata/testdata/sqlite--keeper_read-read.sql create mode 100755 pkg/storage/secret/metadata/testdata/sqlite--keeper_update-update.sql create mode 100644 pkg/tests/apis/secret/keeper_test.go create mode 100644 pkg/tests/apis/secret/main_test.go create mode 100644 pkg/tests/apis/secret/testdata/keeper-aws-generate.yaml create mode 100644 pkg/tests/apis/secret/testdata/keeper-gcp-generate.yaml diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 4d1f7cd679d..93fb04200a3 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -177,6 +177,7 @@ /pkg/tests/apis/query @grafana/grafana-datasources-core-services /pkg/tests/apis/alerting @grafana/grafana-app-platform-squad @grafana/alerting-backend /pkg/tests/api/correlations/ @grafana/datapro +/pkg/tests/apis/secret/ @grafana/grafana-operator-experience-squad /pkg/tsdb/grafanads/ @grafana/grafana-backend-group /pkg/tsdb/opentsdb/ @grafana/partner-datasources /pkg/util/ @grafana/grafana-backend-group diff --git a/pkg/storage/secret/metadata/data/keeper_create.sql b/pkg/storage/secret/metadata/data/keeper_create.sql new file mode 100644 index 00000000000..48319df8249 --- /dev/null +++ b/pkg/storage/secret/metadata/data/keeper_create.sql @@ -0,0 +1,27 @@ +INSERT INTO {{ .Ident "secret_keeper" }} ( + {{ .Ident "guid" }}, + {{ .Ident "name" }}, + {{ .Ident "namespace" }}, + {{ .Ident "annotations" }}, + {{ .Ident "labels" }}, + {{ .Ident "created" }}, + {{ .Ident "created_by" }}, + {{ .Ident "updated" }}, + {{ .Ident "updated_by" }}, + {{ .Ident "description" }}, + {{ .Ident "type" }}, + {{ .Ident "payload" }} +) VALUES ( + {{ .Arg .Row.GUID }}, + {{ .Arg .Row.Name }}, + {{ .Arg .Row.Namespace }}, + {{ .Arg .Row.Annotations }}, + {{ .Arg .Row.Labels }}, + {{ .Arg .Row.Created }}, + {{ .Arg .Row.CreatedBy }}, + {{ .Arg .Row.Updated }}, + {{ .Arg .Row.UpdatedBy }}, + {{ .Arg .Row.Description }}, + {{ .Arg .Row.Type }}, + {{ .Arg .Row.Payload }} +); diff --git a/pkg/storage/secret/metadata/data/keeper_delete.sql b/pkg/storage/secret/metadata/data/keeper_delete.sql new file mode 100644 index 00000000000..d0f3305eadf --- /dev/null +++ b/pkg/storage/secret/metadata/data/keeper_delete.sql @@ -0,0 +1,4 @@ +DELETE FROM {{ .Ident "secret_keeper" }} +WHERE {{ .Ident "namespace" }} = {{ .Arg .Namespace }} AND + {{ .Ident "name" }} = {{ .Arg .Name }} +; diff --git a/pkg/storage/secret/metadata/data/keeper_list.sql b/pkg/storage/secret/metadata/data/keeper_list.sql new file mode 100644 index 00000000000..f21b8228faa --- /dev/null +++ b/pkg/storage/secret/metadata/data/keeper_list.sql @@ -0,0 +1,18 @@ +SELECT + {{ .Ident "guid" }}, + {{ .Ident "name" }}, + {{ .Ident "namespace" }}, + {{ .Ident "annotations" }}, + {{ .Ident "labels" }}, + {{ .Ident "created" }}, + {{ .Ident "created_by" }}, + {{ .Ident "updated" }}, + {{ .Ident "updated_by" }}, + {{ .Ident "description" }}, + {{ .Ident "type" }}, + {{ .Ident "payload" }} +FROM + {{ .Ident "secret_keeper" }} +WHERE {{ .Ident "namespace" }} = {{ .Arg .Namespace }} +ORDER BY {{ .Ident "updated" }} DESC +; diff --git a/pkg/storage/secret/metadata/data/keeper_listByName.sql b/pkg/storage/secret/metadata/data/keeper_listByName.sql new file mode 100644 index 00000000000..d7c2b6cb666 --- /dev/null +++ b/pkg/storage/secret/metadata/data/keeper_listByName.sql @@ -0,0 +1,10 @@ +{{/* this query is used to validate the keeper update or creation */}} + +SELECT + {{ .Ident "name" }} +FROM + {{ .Ident "secret_keeper" }} +WHERE {{ .Ident "namespace" }} = {{ .Arg .Namespace }} AND + {{ .Ident "name" }} IN ({{ .ArgList .KeeperNames }}) +{{ .SelectFor "UPDATE" }} +; diff --git a/pkg/storage/secret/metadata/data/keeper_read.sql b/pkg/storage/secret/metadata/data/keeper_read.sql new file mode 100644 index 00000000000..2a944a0f7e3 --- /dev/null +++ b/pkg/storage/secret/metadata/data/keeper_read.sql @@ -0,0 +1,21 @@ +SELECT + {{ .Ident "guid" }}, + {{ .Ident "name" }}, + {{ .Ident "namespace" }}, + {{ .Ident "annotations" }}, + {{ .Ident "labels" }}, + {{ .Ident "created" }}, + {{ .Ident "created_by" }}, + {{ .Ident "updated" }}, + {{ .Ident "updated_by" }}, + {{ .Ident "description" }}, + {{ .Ident "type" }}, + {{ .Ident "payload" }} +FROM + {{ .Ident "secret_keeper" }} +WHERE {{ .Ident "namespace" }} = {{ .Arg .Namespace }} AND + {{ .Ident "name" }} = {{ .Arg .Name }} +{{ if .IsForUpdate }} +{{ .SelectFor "UPDATE" }} +{{ end }} +; diff --git a/pkg/storage/secret/metadata/data/keeper_update.sql b/pkg/storage/secret/metadata/data/keeper_update.sql new file mode 100644 index 00000000000..6d494c6c2ff --- /dev/null +++ b/pkg/storage/secret/metadata/data/keeper_update.sql @@ -0,0 +1,18 @@ +UPDATE + {{ .Ident "secret_keeper" }} +SET + {{ .Ident "guid" }} = {{ .Arg .Row.GUID }}, + {{ .Ident "name" }} = {{ .Arg .Row.Name }}, + {{ .Ident "namespace" }} = {{ .Arg .Row.Namespace }}, + {{ .Ident "annotations" }} = {{ .Arg .Row.Annotations }}, + {{ .Ident "labels" }} = {{ .Arg .Row.Labels }}, + {{ .Ident "created" }} = {{ .Arg .Row.Created }}, + {{ .Ident "created_by" }} = {{ .Arg .Row.CreatedBy }}, + {{ .Ident "updated" }} = {{ .Arg .Row.Updated }}, + {{ .Ident "updated_by" }} = {{ .Arg .Row.UpdatedBy }}, + {{ .Ident "description" }} = {{ .Arg .Row.Description }}, + {{ .Ident "type" }} = {{ .Arg .Row.Type }}, + {{ .Ident "payload" }} = {{ .Arg .Row.Payload }} +WHERE {{ .Ident "namespace" }} = {{ .Arg .Row.Namespace }} AND + {{ .Ident "name" }} = {{ .Arg .Row.Name }} +; diff --git a/pkg/storage/secret/metadata/keeper_model.go b/pkg/storage/secret/metadata/keeper_model.go new file mode 100644 index 00000000000..53843e7bf4a --- /dev/null +++ b/pkg/storage/secret/metadata/keeper_model.go @@ -0,0 +1,248 @@ +package metadata + +import ( + "encoding/json" + "fmt" + "time" + + "github.com/google/uuid" + "github.com/grafana/grafana/pkg/apimachinery/utils" + secretv0alpha1 "github.com/grafana/grafana/pkg/apis/secret/v0alpha1" + "github.com/grafana/grafana/pkg/registry/apis/secret/xkube" + "github.com/grafana/grafana/pkg/storage/secret/migrator" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" +) + +type keeperDB struct { + // Kubernetes Metadata + GUID string + Name string + Namespace string + Annotations string // map[string]string + Labels string // map[string]string + Created int64 + CreatedBy string + Updated int64 + UpdatedBy string + + // Spec + Description string + Type string + Payload string +} + +func (*keeperDB) TableName() string { + return migrator.TableNameKeeper +} + +// toKubernetes maps a DB row into a Kubernetes resource (metadata + spec). +func (kp *keeperDB) toKubernetes() (*secretv0alpha1.Keeper, error) { + annotations := make(map[string]string, 0) + if kp.Annotations != "" { + if err := json.Unmarshal([]byte(kp.Annotations), &annotations); err != nil { + return nil, fmt.Errorf("failed to unmarshal annotations: %w", err) + } + } + + labels := make(map[string]string, 0) + if kp.Labels != "" { + if err := json.Unmarshal([]byte(kp.Labels), &labels); err != nil { + return nil, fmt.Errorf("failed to unmarshal labels: %w", err) + } + } + + resource := &secretv0alpha1.Keeper{ + Spec: secretv0alpha1.KeeperSpec{ + Description: kp.Description, + }, + } + + // Obtain provider configs + provider := toProvider(secretv0alpha1.KeeperType(kp.Type), kp.Payload) + switch v := provider.(type) { + case *secretv0alpha1.AWSKeeperConfig: + resource.Spec.AWS = v + case *secretv0alpha1.AzureKeeperConfig: + resource.Spec.Azure = v + case *secretv0alpha1.GCPKeeperConfig: + resource.Spec.GCP = v + case *secretv0alpha1.HashiCorpKeeperConfig: + resource.Spec.HashiCorp = v + } + + // Set all meta fields here for consistency. + meta, err := utils.MetaAccessor(resource) + if err != nil { + return nil, fmt.Errorf("failed to get meta accessor: %w", err) + } + + updated := time.Unix(kp.Updated, 0).UTC() + + meta.SetUID(types.UID(kp.GUID)) + meta.SetName(kp.Name) + meta.SetNamespace(kp.Namespace) + meta.SetAnnotations(annotations) + meta.SetLabels(labels) + meta.SetCreatedBy(kp.CreatedBy) + meta.SetCreationTimestamp(metav1.NewTime(time.Unix(kp.Created, 0).UTC())) + meta.SetUpdatedBy(kp.UpdatedBy) + meta.SetUpdatedTimestamp(&updated) + meta.SetResourceVersionInt64(kp.Updated) + + return resource, nil +} + +// toKeeperCreateRow maps a Kubernetes resource into a DB row for new resources being created/inserted. +func toKeeperCreateRow(kp *secretv0alpha1.Keeper, actorUID string) (*keeperDB, error) { + row, err := toKeeperRow(kp) + if err != nil { + return nil, fmt.Errorf("failed to map to row: %w", err) + } + + now := time.Now().UTC().Unix() + + row.GUID = uuid.New().String() + row.Created = now + row.CreatedBy = actorUID + row.Updated = now + row.UpdatedBy = actorUID + + return row, nil +} + +// toKeeperUpdateRow maps a Kubernetes resource into a DB row for existing resources being updated. +func toKeeperUpdateRow(currentRow *keeperDB, newKeeper *secretv0alpha1.Keeper, actorUID string) (*keeperDB, error) { + row, err := toKeeperRow(newKeeper) + if err != nil { + return nil, fmt.Errorf("failed to map to row: %w", err) + } + + now := time.Now().UTC().Unix() + + row.GUID = currentRow.GUID + row.Created = currentRow.Created + row.CreatedBy = currentRow.CreatedBy + row.Updated = now + row.UpdatedBy = actorUID + + return row, nil +} + +// toKeeperRow maps a Kubernetes Keeper resource into a Keeper DB row. +func toKeeperRow(kp *secretv0alpha1.Keeper) (*keeperDB, error) { + var annotations string + if len(kp.Annotations) > 0 { + cleanedAnnotations := xkube.CleanAnnotations(kp.Annotations) + if len(cleanedAnnotations) > 0 { + kp.Annotations = make(map[string]string) // Safety: reset to prohibit use of kp.Annotations further. + + encodedAnnotations, err := json.Marshal(cleanedAnnotations) + if err != nil { + return nil, fmt.Errorf("failed to encode annotations: %w", err) + } + + annotations = string(encodedAnnotations) + } + } + + var labels string + if len(kp.Labels) > 0 { + encodedLabels, err := json.Marshal(kp.Labels) + if err != nil { + return nil, fmt.Errorf("failed to encode labels: %w", err) + } + + labels = string(encodedLabels) + } + + meta, err := utils.MetaAccessor(kp) + if err != nil { + return nil, fmt.Errorf("failed to get meta accessor: %w", err) + } + + if meta.GetFolder() != "" { + return nil, fmt.Errorf("folders are not supported") + } + + updatedTimestamp, err := meta.GetResourceVersionInt64() + if err != nil { + return nil, fmt.Errorf("failed to get resource version: %w", err) + } + + keeperType, keeperPayload, err := toTypeAndPayload(kp) + if err != nil { + return nil, fmt.Errorf("failed to obtain type and payload: %w", err) + } + + return &keeperDB{ + // Kubernetes Metadata + GUID: string(kp.UID), + Name: kp.Name, + Namespace: kp.Namespace, + Annotations: annotations, + Labels: labels, + Created: meta.GetCreationTimestamp().Unix(), + CreatedBy: meta.GetCreatedBy(), + Updated: updatedTimestamp, + UpdatedBy: meta.GetUpdatedBy(), + + // Spec + Description: kp.Spec.Description, + Type: keeperType.String(), + Payload: keeperPayload, + }, nil +} + +// toTypeAndPayload obtain keeper type and payload from a Kubernetes Keeper resource. +// TODO: Move as method of KeeperSpec +func toTypeAndPayload(kp *secretv0alpha1.Keeper) (secretv0alpha1.KeeperType, string, error) { + if kp.Spec.AWS != nil { + payload, err := json.Marshal(kp.Spec.AWS.AWSCredentials) + return secretv0alpha1.AWSKeeperType, string(payload), err + } else if kp.Spec.Azure != nil { + payload, err := json.Marshal(kp.Spec.Azure) + return secretv0alpha1.AzureKeeperType, string(payload), err + } else if kp.Spec.GCP != nil { + payload, err := json.Marshal(kp.Spec.GCP) + return secretv0alpha1.GCPKeeperType, string(payload), err + } else if kp.Spec.HashiCorp != nil { + payload, err := json.Marshal(kp.Spec.HashiCorp) + return secretv0alpha1.HashiCorpKeeperType, string(payload), err + } + + return "", "", fmt.Errorf("no keeper type found") +} + +// toProvider maps a KeeperType and payload into a provider config struct. +// TODO: Move as method of KeeperType +func toProvider(keeperType secretv0alpha1.KeeperType, payload string) secretv0alpha1.KeeperConfig { + switch keeperType { + case secretv0alpha1.AWSKeeperType: + aws := &secretv0alpha1.AWSKeeperConfig{} + if err := json.Unmarshal([]byte(payload), aws); err != nil { + return nil + } + return aws + case secretv0alpha1.AzureKeeperType: + azure := &secretv0alpha1.AzureKeeperConfig{} + if err := json.Unmarshal([]byte(payload), azure); err != nil { + return nil + } + return azure + case secretv0alpha1.GCPKeeperType: + gcp := &secretv0alpha1.GCPKeeperConfig{} + if err := json.Unmarshal([]byte(payload), gcp); err != nil { + return nil + } + return gcp + case secretv0alpha1.HashiCorpKeeperType: + hashicorp := &secretv0alpha1.HashiCorpKeeperConfig{} + if err := json.Unmarshal([]byte(payload), hashicorp); err != nil { + return nil + } + return hashicorp + default: + return nil + } +} diff --git a/pkg/storage/secret/metadata/keeper_store.go b/pkg/storage/secret/metadata/keeper_store.go index ed7f68c6fc8..4fa1572a594 100644 --- a/pkg/storage/secret/metadata/keeper_store.go +++ b/pkg/storage/secret/metadata/keeper_store.go @@ -2,50 +2,276 @@ package metadata import ( "context" + "fmt" - claims "github.com/grafana/authlib/types" secretv0alpha1 "github.com/grafana/grafana/pkg/apis/secret/v0alpha1" - "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/registry/apis/secret/contracts" "github.com/grafana/grafana/pkg/registry/apis/secret/xkube" "github.com/grafana/grafana/pkg/services/featuremgmt" + "github.com/grafana/grafana/pkg/storage/unified/sql/sqltemplate" ) -func ProvideKeeperMetadataStorage(db db.DB, features featuremgmt.FeatureToggles, accessClient claims.AccessClient) (contracts.KeeperMetadataStorage, error) { +// keeperMetadataStorage is the actual implementation of the keeper metadata storage. +type keeperMetadataStorage struct { + db contracts.Database + dialect sqltemplate.Dialect +} + +var _ contracts.KeeperMetadataStorage = (*keeperMetadataStorage)(nil) + +func ProvideKeeperMetadataStorage(db contracts.Database, features featuremgmt.FeatureToggles) (contracts.KeeperMetadataStorage, error) { if !features.IsEnabledGlobally(featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs) || !features.IsEnabledGlobally(featuremgmt.FlagSecretsManagementAppPlatform) { return &keeperMetadataStorage{}, nil } - return &keeperMetadataStorage{db: db, accessClient: accessClient}, nil -} - -// keeperMetadataStorage is the actual implementation of the keeper metadata storage. -type keeperMetadataStorage struct { - db db.DB - accessClient claims.AccessClient + return &keeperMetadataStorage{ + db: db, + dialect: sqltemplate.DialectForDriver(db.DriverName()), + }, nil } func (s *keeperMetadataStorage) Create(ctx context.Context, keeper *secretv0alpha1.Keeper, actorUID string) (*secretv0alpha1.Keeper, error) { - return nil, nil + row, err := toKeeperCreateRow(keeper, actorUID) + if err != nil { + return nil, fmt.Errorf("failed to create row: %w", err) + } + + req := createKeeper{ + SQLTemplate: sqltemplate.New(s.dialect), + Row: row, + } + + query, err := sqltemplate.Execute(sqlKeeperCreate, req) + if err != nil { + return nil, fmt.Errorf("execute template %q: %w", sqlKeeperCreate.Name(), err) + } + + err = s.db.Transaction(ctx, func(ctx context.Context) error { + result, err := s.db.ExecContext(ctx, query, req.GetArgs()...) + if err != nil { + return fmt.Errorf("inserting row: %w", err) + } + + rowsAffected, err := result.RowsAffected() + if err != nil { + return fmt.Errorf("getting rows affected: %w", err) + } + + if rowsAffected != 1 { + return fmt.Errorf("expected 1 row affected, got %d for %s on %s", rowsAffected, keeper.Name, keeper.Namespace) + } + + return nil + }) + if err != nil { + return nil, fmt.Errorf("db failure: %w", err) + } + + createdKeeper, err := row.toKubernetes() + if err != nil { + return nil, fmt.Errorf("failed to convert to kubernetes object: %w", err) + } + + return createdKeeper, nil } func (s *keeperMetadataStorage) Read(ctx context.Context, namespace xkube.Namespace, name string, opts contracts.ReadOpts) (*secretv0alpha1.Keeper, error) { - return nil, nil + keeperDB, err := s.read(ctx, namespace.String(), name, opts) + if err != nil { + return nil, err + } + + keeper, err := keeperDB.toKubernetes() + if err != nil { + return nil, fmt.Errorf("failed to convert to kubernetes object: %w", err) + } + + return keeper, nil +} + +func (s *keeperMetadataStorage) read(ctx context.Context, namespace, name string, opts contracts.ReadOpts) (*keeperDB, error) { + req := &readKeeper{ + SQLTemplate: sqltemplate.New(s.dialect), + Namespace: namespace, + Name: name, + IsForUpdate: opts.ForUpdate, + } + + query, err := sqltemplate.Execute(sqlKeeperRead, req) + if err != nil { + return nil, fmt.Errorf("execute template %q: %w", sqlKeeperRead.Name(), err) + } + + res, err := s.db.QueryContext(ctx, query, req.GetArgs()...) + if err != nil { + return nil, fmt.Errorf("getting row for %s in namespace %s: %w", name, namespace, err) + } + defer func() { _ = res.Close() }() + + if !res.Next() { + return nil, contracts.ErrKeeperNotFound + } + + var keeper keeperDB + err = res.Scan( + &keeper.GUID, &keeper.Name, &keeper.Namespace, &keeper.Annotations, &keeper.Labels, &keeper.Created, + &keeper.CreatedBy, &keeper.Updated, &keeper.UpdatedBy, &keeper.Description, &keeper.Type, &keeper.Payload, + ) + if err != nil { + return nil, fmt.Errorf("failed to scan keeper row: %w", err) + } + if err := res.Err(); err != nil { + return nil, fmt.Errorf("read rows error: %w", err) + } + + return &keeper, nil } func (s *keeperMetadataStorage) Update(ctx context.Context, newKeeper *secretv0alpha1.Keeper, actorUID string) (*secretv0alpha1.Keeper, error) { - return nil, nil + var newRow *keeperDB + + err := s.db.Transaction(ctx, func(ctx context.Context) error { + // Read old value first. + oldKeeperRow, err := s.read(ctx, newKeeper.Namespace, newKeeper.Name, contracts.ReadOpts{ForUpdate: true}) + if err != nil { + return err + } + + // Generate an update row model. + var updateErr error + newRow, updateErr = toKeeperUpdateRow(oldKeeperRow, newKeeper, actorUID) + if updateErr != nil { + return fmt.Errorf("failed to map into update row: %w", updateErr) + } + + // Update query with new model. + req := &updateKeeper{ + SQLTemplate: sqltemplate.New(s.dialect), + Row: newRow, + } + + query, err := sqltemplate.Execute(sqlKeeperUpdate, req) + if err != nil { + return fmt.Errorf("execute template %q: %w", sqlKeeperUpdate.Name(), err) + } + + result, err := s.db.ExecContext(ctx, query, req.GetArgs()...) + if err != nil { + return fmt.Errorf("updating row: %w", err) + } + + rowsAffected, err := result.RowsAffected() + if err != nil { + return fmt.Errorf("getting rows affected: %w", err) + } + + if rowsAffected != 1 { + return fmt.Errorf("expected 1 row affected, got %d for %s on %s", rowsAffected, newKeeper.Name, newKeeper.Namespace) + } + + return nil + }) + if err != nil { + return nil, fmt.Errorf("db failure: %w", err) + } + + keeper, err := newRow.toKubernetes() + if err != nil { + return nil, fmt.Errorf("failed to convert to kubernetes object: %w", err) + } + + return keeper, nil } func (s *keeperMetadataStorage) Delete(ctx context.Context, namespace xkube.Namespace, name string) error { + req := deleteKeeper{ + SQLTemplate: sqltemplate.New(s.dialect), + Namespace: namespace.String(), + Name: name, + } + + query, err := sqltemplate.Execute(sqlKeeperDelete, req) + if err != nil { + return fmt.Errorf("execute template %q: %w", sqlKeeperDelete.Name(), err) + } + + result, err := s.db.ExecContext(ctx, query, req.GetArgs()...) + if err != nil { + return fmt.Errorf("deleting row: %w", err) + } + + rowsAffected, err := result.RowsAffected() + if err != nil { + return fmt.Errorf("getting rows affected: %w", err) + } + if rowsAffected == 0 { + return contracts.ErrKeeperNotFound + } else if rowsAffected != 1 { + return fmt.Errorf("expected 1 row affected, got %d for %s on %s", rowsAffected, name, namespace) + } + return nil } func (s *keeperMetadataStorage) List(ctx context.Context, namespace xkube.Namespace) ([]secretv0alpha1.Keeper, error) { - return nil, nil + req := listKeeper{ + SQLTemplate: sqltemplate.New(s.dialect), + Namespace: namespace.String(), + } + + query, err := sqltemplate.Execute(sqlKeeperList, req) + if err != nil { + return nil, fmt.Errorf("execute template %q: %w", sqlKeeperList.Name(), err) + } + + rows, err := s.db.QueryContext(ctx, query, req.GetArgs()...) + if err != nil { + return nil, fmt.Errorf("listing keepers %q: %w", sqlKeeperList.Name(), err) + } + defer func() { _ = rows.Close() }() + + keepers := make([]secretv0alpha1.Keeper, 0) + + for rows.Next() { + var row keeperDB + err = rows.Scan( + &row.GUID, &row.Name, &row.Namespace, &row.Annotations, &row.Labels, &row.Created, + &row.CreatedBy, &row.Updated, &row.UpdatedBy, &row.Description, &row.Type, &row.Payload, + ) + if err != nil { + return nil, fmt.Errorf("error reading keeper row: %w", err) + } + + keeper, err := row.toKubernetes() + if err != nil { + return nil, fmt.Errorf("failed to convert to kubernetes object: %w", err) + } + + keepers = append(keepers, *keeper) + } + + if err := rows.Err(); err != nil { + return nil, fmt.Errorf("read rows error: %w", err) + } + + return keepers, nil } func (s *keeperMetadataStorage) GetKeeperConfig(ctx context.Context, namespace string, name *string, opts contracts.ReadOpts) (secretv0alpha1.KeeperConfig, error) { - return nil, nil + // Check if keeper is the systemwide one. + if name == nil { + return nil, nil + } + + // Load keeper config from metadata store, or TODO: keeper cache. + kp, err := s.read(ctx, namespace, *name, opts) + if err != nil { + return nil, err + } + + keeperConfig := toProvider(secretv0alpha1.KeeperType(kp.Type), kp.Payload) + + // TODO: this would be a good place to check if credentials are secure values and load them. + return keeperConfig, nil } diff --git a/pkg/storage/secret/metadata/keeper_store_test.go b/pkg/storage/secret/metadata/keeper_store_test.go new file mode 100644 index 00000000000..698697cdbe9 --- /dev/null +++ b/pkg/storage/secret/metadata/keeper_store_test.go @@ -0,0 +1,344 @@ +package metadata + +import ( + "context" + "testing" + + secretv0alpha1 "github.com/grafana/grafana/pkg/apis/secret/v0alpha1" + "github.com/grafana/grafana/pkg/registry/apis/secret/contracts" + "github.com/grafana/grafana/pkg/registry/apis/secret/xkube" + "github.com/grafana/grafana/pkg/services/featuremgmt" + "github.com/grafana/grafana/pkg/services/sqlstore" + "github.com/grafana/grafana/pkg/storage/secret/database" + "github.com/grafana/grafana/pkg/storage/secret/migrator" + "github.com/stretchr/testify/require" +) + +func Test_KeeperMetadataStorage_GetKeeperConfig(t *testing.T) { + t.Parallel() + + defaultKeeperName := "kp-test" + defaultKeeperNS := "default" + + testKeeper := &secretv0alpha1.Keeper{ + Spec: secretv0alpha1.KeeperSpec{ + Description: "description", + AWS: &secretv0alpha1.AWSKeeperConfig{}, + }, + } + + testKeeper.Name = defaultKeeperName + testKeeper.Namespace = defaultKeeperNS + + t.Run("get the system keeper config", func(t *testing.T) { + t.Parallel() + + ctx := context.Background() + keeperMetadataStorage := initStorage(t) + + // get system keeper config + keeperConfig, err := keeperMetadataStorage.GetKeeperConfig(ctx, defaultKeeperNS, nil, contracts.ReadOpts{}) + require.NoError(t, err) + require.Nil(t, keeperConfig) + }) + + t.Run("get test keeper config", func(t *testing.T) { + t.Parallel() + + ctx := context.Background() + keeperMetadataStorage := initStorage(t) + + // + _, err := keeperMetadataStorage.Create(ctx, testKeeper, "testuser") + require.NoError(t, err) + + keeperConfig, err := keeperMetadataStorage.GetKeeperConfig(ctx, defaultKeeperNS, &defaultKeeperName, contracts.ReadOpts{}) + require.NoError(t, err) + require.NotNil(t, keeperConfig) + require.NotEmpty(t, keeperConfig.Type()) + }) + + t.Run("get test keeper config when listing", func(t *testing.T) { + t.Parallel() + + ctx := context.Background() + keeperMetadataStorage := initStorage(t) + + // + _, err := keeperMetadataStorage.Create(ctx, testKeeper, "testuser") + require.NoError(t, err) + + keeperList, err := keeperMetadataStorage.List(ctx, xkube.Namespace(defaultKeeperNS)) + require.NoError(t, err) + require.NotEmpty(t, keeperList) + + require.Len(t, keeperList, 1) + keeper := keeperList[0] + require.Equal(t, "kp-test", keeper.Name) + require.Equal(t, "default", keeper.Namespace) + require.Equal(t, "description", keeper.Spec.Description) + }) + + t.Run("create a keeper and then delete it", func(t *testing.T) { + t.Parallel() + + ctx := context.Background() + keeperMetadataStorage := initStorage(t) + + keeperTest := "kp-test2" + keeperNamespaceTest := "ns" + + testKeeper := &secretv0alpha1.Keeper{ + Spec: secretv0alpha1.KeeperSpec{ + Description: "another description", + AWS: &secretv0alpha1.AWSKeeperConfig{}, + }, + } + testKeeper.Name = keeperTest + testKeeper.Namespace = keeperNamespaceTest + + // create the keeper + _, err := keeperMetadataStorage.Create(ctx, testKeeper, "testuser") + require.NoError(t, err) + + // we are able to get it + keeperConfig, err := keeperMetadataStorage.GetKeeperConfig(ctx, keeperNamespaceTest, &keeperTest, contracts.ReadOpts{}) + require.NoError(t, err) + require.NotNil(t, keeperConfig) + require.NotEmpty(t, keeperConfig.Type()) + + // now we delete it + delErr := keeperMetadataStorage.Delete(ctx, xkube.Namespace(keeperNamespaceTest), keeperTest) + require.NoError(t, delErr) + + // and we shouldn't be able to get it again + _, getErr := keeperMetadataStorage.GetKeeperConfig(ctx, keeperNamespaceTest, &keeperTest, contracts.ReadOpts{}) + require.Errorf(t, getErr, "keeper not found") + }) + + t.Run("create, update and validate keeper", func(t *testing.T) { + t.Parallel() + + ctx := context.Background() + keeperMetadataStorage := initStorage(t) + + keeperTest := "kp-test3" + keeperNamespaceTest := "ns" + + // Create initial keeper + initialKeeper := &secretv0alpha1.Keeper{ + Spec: secretv0alpha1.KeeperSpec{ + Description: "initial description", + AWS: &secretv0alpha1.AWSKeeperConfig{}, + }, + } + initialKeeper.Name = keeperTest + initialKeeper.Namespace = keeperNamespaceTest + + // Create the keeper + _, err := keeperMetadataStorage.Create(ctx, initialKeeper, "testuser") + require.NoError(t, err) + + // Validate that the description was set + keeper, err := keeperMetadataStorage.Read(ctx, xkube.Namespace(keeperNamespaceTest), keeperTest, contracts.ReadOpts{}) + require.NoError(t, err) + require.Equal(t, "initial description", keeper.Spec.Description) + + // Update the keeper with new values + updatedKeeper := &secretv0alpha1.Keeper{ + Spec: secretv0alpha1.KeeperSpec{ + Description: "updated description", + AWS: &secretv0alpha1.AWSKeeperConfig{}, + }, + } + updatedKeeper.Name = keeperTest + updatedKeeper.Namespace = keeperNamespaceTest + + // Perform the update + _, err = keeperMetadataStorage.Update(ctx, updatedKeeper, "testuser") + require.NoError(t, err) + + // Validate updated values + updatedConfig, err := keeperMetadataStorage.GetKeeperConfig(ctx, keeperNamespaceTest, &keeperTest, contracts.ReadOpts{}) + require.NoError(t, err) + require.NotNil(t, updatedConfig) + require.NotEmpty(t, updatedConfig.Type()) + + // Validate that the description was updated + updatedKeeper, err = keeperMetadataStorage.Read(ctx, xkube.Namespace(keeperNamespaceTest), keeperTest, contracts.ReadOpts{}) + require.NoError(t, err) + require.Equal(t, "updated description", updatedKeeper.Spec.Description) + }) + + t.Run("update keeper with different AWS configuration", func(t *testing.T) { + t.Parallel() + + ctx := context.Background() + keeperMetadataStorage := initStorage(t) + + keeperTest := "kp-test4" + keeperNamespaceTest := "ns" + + // Create initial keeper with first AWS config + initialKeeper := &secretv0alpha1.Keeper{ + Spec: secretv0alpha1.KeeperSpec{ + Description: "initial description", + AWS: &secretv0alpha1.AWSKeeperConfig{ + AWSCredentials: secretv0alpha1.AWSCredentials{ + AccessKeyID: secretv0alpha1.CredentialValue{ + ValueFromEnv: "AWS_ACCESS_KEY_ID_1", + }, + SecretAccessKey: secretv0alpha1.CredentialValue{ + ValueFromEnv: "AWS_SECRET_ACCESS_KEY_1", + }, + KMSKeyID: "kms-key-id-1", + }, + }, + }, + } + initialKeeper.Name = keeperTest + initialKeeper.Namespace = keeperNamespaceTest + + // Create the keeper + _, err := keeperMetadataStorage.Create(ctx, initialKeeper, "testuser") + require.NoError(t, err) + + // Verify initial AWS config + keeper, err := keeperMetadataStorage.Read(ctx, xkube.Namespace(keeperNamespaceTest), keeperTest, contracts.ReadOpts{}) + require.NoError(t, err) + require.Equal(t, "AWS_ACCESS_KEY_ID_1", keeper.Spec.AWS.AccessKeyID.ValueFromEnv) + require.Equal(t, "AWS_SECRET_ACCESS_KEY_1", keeper.Spec.AWS.SecretAccessKey.ValueFromEnv) + require.Equal(t, "kms-key-id-1", keeper.Spec.AWS.KMSKeyID) + + // Update with new AWS config + updatedKeeper := &secretv0alpha1.Keeper{ + Spec: secretv0alpha1.KeeperSpec{ + Description: "updated description", + AWS: &secretv0alpha1.AWSKeeperConfig{ + AWSCredentials: secretv0alpha1.AWSCredentials{ + AccessKeyID: secretv0alpha1.CredentialValue{ + ValueFromEnv: "AWS_ACCESS_KEY_ID_2", + }, + SecretAccessKey: secretv0alpha1.CredentialValue{ + ValueFromEnv: "AWS_SECRET_ACCESS_KEY_2", + }, + KMSKeyID: "kms-key-id-2", + }, + }, + }, + } + updatedKeeper.Name = keeperTest + updatedKeeper.Namespace = keeperNamespaceTest + + // Perform the update + _, err = keeperMetadataStorage.Update(ctx, updatedKeeper, "testuser") + require.NoError(t, err) + + // Verify updated AWS config + updatedKeeper, err = keeperMetadataStorage.Read(ctx, xkube.Namespace(keeperNamespaceTest), keeperTest, contracts.ReadOpts{}) + require.NoError(t, err) + require.Equal(t, "AWS_ACCESS_KEY_ID_2", updatedKeeper.Spec.AWS.AccessKeyID.ValueFromEnv) + require.Equal(t, "AWS_SECRET_ACCESS_KEY_2", updatedKeeper.Spec.AWS.SecretAccessKey.ValueFromEnv) + require.Equal(t, "kms-key-id-2", updatedKeeper.Spec.AWS.KMSKeyID) + }) + + t.Run("list keepers in empty namespace", func(t *testing.T) { + t.Parallel() + + ctx := context.Background() + keeperMetadataStorage := initStorage(t) + + keeperList, err := keeperMetadataStorage.List(ctx, "") + require.NoError(t, err) + require.Empty(t, keeperList) + }) + + t.Run("read non-existent keeper", func(t *testing.T) { + t.Parallel() + + ctx := context.Background() + keeperMetadataStorage := initStorage(t) + + _, err := keeperMetadataStorage.Read(ctx, "ns", "non-existent", contracts.ReadOpts{}) + require.Error(t, err) + require.Equal(t, contracts.ErrKeeperNotFound, err) + }) + + t.Run("update keeper with different namespace", func(t *testing.T) { + t.Parallel() + + ctx := context.Background() + keeperMetadataStorage := initStorage(t) + + keeperTest := "kp-test5" + keeperNamespaceTest := "ns1" + + // Create initial keeper + initialKeeper := &secretv0alpha1.Keeper{ + Spec: secretv0alpha1.KeeperSpec{ + Description: "initial description", + AWS: &secretv0alpha1.AWSKeeperConfig{ + AWSCredentials: secretv0alpha1.AWSCredentials{ + AccessKeyID: secretv0alpha1.CredentialValue{ + ValueFromEnv: "AWS_ACCESS_KEY_ID", + }, + SecretAccessKey: secretv0alpha1.CredentialValue{ + ValueFromEnv: "AWS_SECRET_ACCESS_KEY", + }, + }, + }, + }, + } + initialKeeper.Name = keeperTest + initialKeeper.Namespace = keeperNamespaceTest + + // Create the keeper + _, err := keeperMetadataStorage.Create(ctx, initialKeeper, "testuser") + require.NoError(t, err) + + // Try to update with different namespace + updatedKeeper := initialKeeper.DeepCopy() + updatedKeeper.Namespace = "ns2" + updatedKeeper.Spec.Description = "updated description" + + _, err = keeperMetadataStorage.Update(ctx, updatedKeeper, "testuser") + // should this return contracts.ErrKeeperNotFound directly? + // require.Equal(t, contracts.ErrKeeperNotFound, err) + require.Error(t, err, "db failure: keeper not found") + + // Verify original keeper is unchanged + keeper, err := keeperMetadataStorage.Read(ctx, xkube.Namespace(keeperNamespaceTest), keeperTest, contracts.ReadOpts{}) + require.NoError(t, err) + require.Equal(t, "initial description", keeper.Spec.Description) + }) + + t.Run("update non-existent keeper", func(t *testing.T) { + t.Parallel() + + ctx := context.Background() + keeperMetadataStorage := initStorage(t) + + nonExistentKeeper := &secretv0alpha1.Keeper{ + Spec: secretv0alpha1.KeeperSpec{ + Description: "some description", + AWS: &secretv0alpha1.AWSKeeperConfig{}, + }, + } + nonExistentKeeper.Name = "non-existent" + nonExistentKeeper.Namespace = "ns" + + _, err := keeperMetadataStorage.Update(ctx, nonExistentKeeper, "testuser") + require.Error(t, err, "db failure: keeper not found") + }) +} + +func initStorage(t *testing.T) contracts.KeeperMetadataStorage { + testDB := sqlstore.NewTestStore(t, sqlstore.WithMigrator(migrator.New())) + db := database.ProvideDatabase(testDB) + features := featuremgmt.WithFeatures(featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs, featuremgmt.FlagSecretsManagementAppPlatform) + + // Initialize the keeper storage + keeperMetadataStorage, err := ProvideKeeperMetadataStorage(db, features) + require.NoError(t, err) + return keeperMetadataStorage +} diff --git a/pkg/storage/secret/metadata/query.go b/pkg/storage/secret/metadata/query.go new file mode 100644 index 00000000000..2a27bdad7ad --- /dev/null +++ b/pkg/storage/secret/metadata/query.go @@ -0,0 +1,106 @@ +package metadata + +import ( + "embed" + "fmt" + "text/template" + + "github.com/grafana/grafana/pkg/storage/unified/sql/sqltemplate" +) + +var ( + //go:embed data/*.sql + sqlTemplatesFS embed.FS + + sqlTemplates = template.Must(template.New("sql").ParseFS(sqlTemplatesFS, `data/*.sql`)) + + // The SQL Commands + sqlKeeperCreate = mustTemplate("keeper_create.sql") + sqlKeeperRead = mustTemplate("keeper_read.sql") + sqlKeeperUpdate = mustTemplate("keeper_update.sql") + sqlKeeperList = mustTemplate("keeper_list.sql") + sqlKeeperDelete = mustTemplate("keeper_delete.sql") + + sqlKeeperListByName = mustTemplate("keeper_listByName.sql") +) + +func mustTemplate(filename string) *template.Template { + if t := sqlTemplates.Lookup(filename); t != nil { + return t + } + panic(fmt.Sprintf("template file not found: %s", filename)) +} + +/************************/ +/**-- Keeper Queries --**/ +/************************/ + +// Create +type createKeeper struct { + sqltemplate.SQLTemplate + Row *keeperDB +} + +// Validate is only used if we use `dbutil` from `unifiedstorage` +func (r createKeeper) Validate() error { + return nil // TODO +} + +// Read +type readKeeper struct { + sqltemplate.SQLTemplate + Namespace string + Name string + IsForUpdate bool +} + +// Validate is only used if we use `dbutil` from `unifiedstorage` +func (r readKeeper) Validate() error { + return nil // TODO +} + +// Update +type updateKeeper struct { + sqltemplate.SQLTemplate + Row *keeperDB +} + +// Validate is only used if we use `dbutil` from `unifiedstorage` +func (r updateKeeper) Validate() error { + return nil // TODO +} + +// List +type listKeeper struct { + sqltemplate.SQLTemplate + Namespace string +} + +// Validate is only used if we use `dbutil` from `unifiedstorage` +func (r listKeeper) Validate() error { + return nil // TODO +} + +// Delete +type deleteKeeper struct { + sqltemplate.SQLTemplate + Namespace string + Name string +} + +// Validate is only used if we use `dbutil` from `unifiedstorage` +func (r deleteKeeper) Validate() error { + return nil // TODO +} + +// This is used at keeper store to validate create & update operations +type listByNameKeeper struct { + sqltemplate.SQLTemplate + Namespace string + KeeperNames []string +} + +// Validate is only used if we use `dbutil` from `unifiedstorage` +func (r listByNameKeeper) Validate() error { + return nil // TODO +} diff --git a/pkg/storage/secret/metadata/query_test.go b/pkg/storage/secret/metadata/query_test.go new file mode 100644 index 00000000000..7d0c347db02 --- /dev/null +++ b/pkg/storage/secret/metadata/query_test.go @@ -0,0 +1,108 @@ +package metadata + +import ( + "testing" + "text/template" + + "github.com/grafana/grafana/pkg/storage/unified/sql/sqltemplate/mocks" +) + +func TestKeeperQueries(t *testing.T) { + mocks.CheckQuerySnapshots(t, mocks.TemplateTestSetup{ + RootDir: "testdata", + Templates: map[*template.Template][]mocks.TemplateTestCase{ + sqlKeeperCreate: { + { + Name: "create", + Data: &createKeeper{ + SQLTemplate: mocks.NewTestingSQLTemplate(), + Row: &keeperDB{ + GUID: "abc", + Name: "name", + Namespace: "ns", + Annotations: `{"x":"XXXX"}`, + Labels: `{"a":"AAA", "b", "BBBB"}`, + Created: 1234, + CreatedBy: "user:ryan", + Updated: 5678, + UpdatedBy: "user:cameron", + Description: "description", + Type: "sql", + Payload: "", + }, + }, + }, + }, + sqlKeeperDelete: { + { + Name: "delete", + Data: &deleteKeeper{ + SQLTemplate: mocks.NewTestingSQLTemplate(), + Name: "name", + Namespace: "ns", + }, + }, + }, + sqlKeeperList: { + { + Name: "list", + Data: &listKeeper{ + SQLTemplate: mocks.NewTestingSQLTemplate(), + Namespace: "ns", + }, + }, + }, + sqlKeeperRead: { + { + Name: "read", + Data: &readKeeper{ + SQLTemplate: mocks.NewTestingSQLTemplate(), + Name: "name", + Namespace: "ns", + }, + }, + { + Name: "read-for-update", + Data: &readKeeper{ + SQLTemplate: mocks.NewTestingSQLTemplate(), + Name: "name", + Namespace: "ns", + IsForUpdate: true, + }, + }, + }, + sqlKeeperUpdate: { + { + Name: "update", + Data: &updateKeeper{ + SQLTemplate: mocks.NewTestingSQLTemplate(), + Row: &keeperDB{ + GUID: "abc", + Name: "name", + Namespace: "ns", + Annotations: `{"x":"XXXX"}`, + Labels: `{"a":"AAA", "b", "BBBB"}`, + Created: 1234, + CreatedBy: "user:ryan", + Updated: 5678, + UpdatedBy: "user:cameron", + Description: "description", + Type: "sql", + Payload: "", + }, + }, + }, + }, + sqlKeeperListByName: { + { + Name: "list", + Data: listByNameKeeper{ + SQLTemplate: mocks.NewTestingSQLTemplate(), + Namespace: "ns", + KeeperNames: []string{"a", "b"}, + }, + }, + }, + }, + }) +} diff --git a/pkg/storage/secret/metadata/testdata/mysql--keeper_create-create.sql b/pkg/storage/secret/metadata/testdata/mysql--keeper_create-create.sql new file mode 100755 index 00000000000..f27c227a432 --- /dev/null +++ b/pkg/storage/secret/metadata/testdata/mysql--keeper_create-create.sql @@ -0,0 +1,27 @@ +INSERT INTO `secret_keeper` ( + `guid`, + `name`, + `namespace`, + `annotations`, + `labels`, + `created`, + `created_by`, + `updated`, + `updated_by`, + `description`, + `type`, + `payload` +) VALUES ( + 'abc', + 'name', + 'ns', + '{"x":"XXXX"}', + '{"a":"AAA", "b", "BBBB"}', + 1234, + 'user:ryan', + 5678, + 'user:cameron', + 'description', + 'sql', + '' +); diff --git a/pkg/storage/secret/metadata/testdata/mysql--keeper_delete-delete.sql b/pkg/storage/secret/metadata/testdata/mysql--keeper_delete-delete.sql new file mode 100755 index 00000000000..cf8b0b55bda --- /dev/null +++ b/pkg/storage/secret/metadata/testdata/mysql--keeper_delete-delete.sql @@ -0,0 +1,4 @@ +DELETE FROM `secret_keeper` +WHERE `namespace` = 'ns' AND + `name` = 'name' +; diff --git a/pkg/storage/secret/metadata/testdata/mysql--keeper_list-list.sql b/pkg/storage/secret/metadata/testdata/mysql--keeper_list-list.sql new file mode 100755 index 00000000000..40ea5b6c5db --- /dev/null +++ b/pkg/storage/secret/metadata/testdata/mysql--keeper_list-list.sql @@ -0,0 +1,18 @@ +SELECT + `guid`, + `name`, + `namespace`, + `annotations`, + `labels`, + `created`, + `created_by`, + `updated`, + `updated_by`, + `description`, + `type`, + `payload` +FROM + `secret_keeper` +WHERE `namespace` = 'ns' +ORDER BY `updated` DESC +; diff --git a/pkg/storage/secret/metadata/testdata/mysql--keeper_listByName-list.sql b/pkg/storage/secret/metadata/testdata/mysql--keeper_listByName-list.sql new file mode 100755 index 00000000000..2deb85b96bb --- /dev/null +++ b/pkg/storage/secret/metadata/testdata/mysql--keeper_listByName-list.sql @@ -0,0 +1,8 @@ +SELECT + `name` +FROM + `secret_keeper` +WHERE `namespace` = 'ns' AND + `name` IN ('a', 'b') +FOR UPDATE +; diff --git a/pkg/storage/secret/metadata/testdata/mysql--keeper_read-read-for-update.sql b/pkg/storage/secret/metadata/testdata/mysql--keeper_read-read-for-update.sql new file mode 100755 index 00000000000..d94dc616382 --- /dev/null +++ b/pkg/storage/secret/metadata/testdata/mysql--keeper_read-read-for-update.sql @@ -0,0 +1,19 @@ +SELECT + `guid`, + `name`, + `namespace`, + `annotations`, + `labels`, + `created`, + `created_by`, + `updated`, + `updated_by`, + `description`, + `type`, + `payload` +FROM + `secret_keeper` +WHERE `namespace` = 'ns' AND + `name` = 'name' +FOR UPDATE +; diff --git a/pkg/storage/secret/metadata/testdata/mysql--keeper_read-read.sql b/pkg/storage/secret/metadata/testdata/mysql--keeper_read-read.sql new file mode 100755 index 00000000000..423c4dab3b1 --- /dev/null +++ b/pkg/storage/secret/metadata/testdata/mysql--keeper_read-read.sql @@ -0,0 +1,18 @@ +SELECT + `guid`, + `name`, + `namespace`, + `annotations`, + `labels`, + `created`, + `created_by`, + `updated`, + `updated_by`, + `description`, + `type`, + `payload` +FROM + `secret_keeper` +WHERE `namespace` = 'ns' AND + `name` = 'name' +; diff --git a/pkg/storage/secret/metadata/testdata/mysql--keeper_update-update.sql b/pkg/storage/secret/metadata/testdata/mysql--keeper_update-update.sql new file mode 100755 index 00000000000..0267bf1c6ea --- /dev/null +++ b/pkg/storage/secret/metadata/testdata/mysql--keeper_update-update.sql @@ -0,0 +1,18 @@ +UPDATE + `secret_keeper` +SET + `guid` = 'abc', + `name` = 'name', + `namespace` = 'ns', + `annotations` = '{"x":"XXXX"}', + `labels` = '{"a":"AAA", "b", "BBBB"}', + `created` = 1234, + `created_by` = 'user:ryan', + `updated` = 5678, + `updated_by` = 'user:cameron', + `description` = 'description', + `type` = 'sql', + `payload` = '' +WHERE `namespace` = 'ns' AND + `name` = 'name' +; diff --git a/pkg/storage/secret/metadata/testdata/postgres--keeper_create-create.sql b/pkg/storage/secret/metadata/testdata/postgres--keeper_create-create.sql new file mode 100755 index 00000000000..845aa9c7926 --- /dev/null +++ b/pkg/storage/secret/metadata/testdata/postgres--keeper_create-create.sql @@ -0,0 +1,27 @@ +INSERT INTO "secret_keeper" ( + "guid", + "name", + "namespace", + "annotations", + "labels", + "created", + "created_by", + "updated", + "updated_by", + "description", + "type", + "payload" +) VALUES ( + 'abc', + 'name', + 'ns', + '{"x":"XXXX"}', + '{"a":"AAA", "b", "BBBB"}', + 1234, + 'user:ryan', + 5678, + 'user:cameron', + 'description', + 'sql', + '' +); diff --git a/pkg/storage/secret/metadata/testdata/postgres--keeper_delete-delete.sql b/pkg/storage/secret/metadata/testdata/postgres--keeper_delete-delete.sql new file mode 100755 index 00000000000..81f6c2c15af --- /dev/null +++ b/pkg/storage/secret/metadata/testdata/postgres--keeper_delete-delete.sql @@ -0,0 +1,4 @@ +DELETE FROM "secret_keeper" +WHERE "namespace" = 'ns' AND + "name" = 'name' +; diff --git a/pkg/storage/secret/metadata/testdata/postgres--keeper_list-list.sql b/pkg/storage/secret/metadata/testdata/postgres--keeper_list-list.sql new file mode 100755 index 00000000000..1eb5e059b72 --- /dev/null +++ b/pkg/storage/secret/metadata/testdata/postgres--keeper_list-list.sql @@ -0,0 +1,18 @@ +SELECT + "guid", + "name", + "namespace", + "annotations", + "labels", + "created", + "created_by", + "updated", + "updated_by", + "description", + "type", + "payload" +FROM + "secret_keeper" +WHERE "namespace" = 'ns' +ORDER BY "updated" DESC +; diff --git a/pkg/storage/secret/metadata/testdata/postgres--keeper_listByName-list.sql b/pkg/storage/secret/metadata/testdata/postgres--keeper_listByName-list.sql new file mode 100755 index 00000000000..e65dcbc4db2 --- /dev/null +++ b/pkg/storage/secret/metadata/testdata/postgres--keeper_listByName-list.sql @@ -0,0 +1,8 @@ +SELECT + "name" +FROM + "secret_keeper" +WHERE "namespace" = 'ns' AND + "name" IN ('a', 'b') +FOR UPDATE +; diff --git a/pkg/storage/secret/metadata/testdata/postgres--keeper_read-read-for-update.sql b/pkg/storage/secret/metadata/testdata/postgres--keeper_read-read-for-update.sql new file mode 100755 index 00000000000..4154c4ee2c3 --- /dev/null +++ b/pkg/storage/secret/metadata/testdata/postgres--keeper_read-read-for-update.sql @@ -0,0 +1,19 @@ +SELECT + "guid", + "name", + "namespace", + "annotations", + "labels", + "created", + "created_by", + "updated", + "updated_by", + "description", + "type", + "payload" +FROM + "secret_keeper" +WHERE "namespace" = 'ns' AND + "name" = 'name' +FOR UPDATE +; diff --git a/pkg/storage/secret/metadata/testdata/postgres--keeper_read-read.sql b/pkg/storage/secret/metadata/testdata/postgres--keeper_read-read.sql new file mode 100755 index 00000000000..6587d5970d8 --- /dev/null +++ b/pkg/storage/secret/metadata/testdata/postgres--keeper_read-read.sql @@ -0,0 +1,18 @@ +SELECT + "guid", + "name", + "namespace", + "annotations", + "labels", + "created", + "created_by", + "updated", + "updated_by", + "description", + "type", + "payload" +FROM + "secret_keeper" +WHERE "namespace" = 'ns' AND + "name" = 'name' +; diff --git a/pkg/storage/secret/metadata/testdata/postgres--keeper_update-update.sql b/pkg/storage/secret/metadata/testdata/postgres--keeper_update-update.sql new file mode 100755 index 00000000000..c137c5ca14c --- /dev/null +++ b/pkg/storage/secret/metadata/testdata/postgres--keeper_update-update.sql @@ -0,0 +1,18 @@ +UPDATE + "secret_keeper" +SET + "guid" = 'abc', + "name" = 'name', + "namespace" = 'ns', + "annotations" = '{"x":"XXXX"}', + "labels" = '{"a":"AAA", "b", "BBBB"}', + "created" = 1234, + "created_by" = 'user:ryan', + "updated" = 5678, + "updated_by" = 'user:cameron', + "description" = 'description', + "type" = 'sql', + "payload" = '' +WHERE "namespace" = 'ns' AND + "name" = 'name' +; diff --git a/pkg/storage/secret/metadata/testdata/sqlite--keeper_create-create.sql b/pkg/storage/secret/metadata/testdata/sqlite--keeper_create-create.sql new file mode 100755 index 00000000000..845aa9c7926 --- /dev/null +++ b/pkg/storage/secret/metadata/testdata/sqlite--keeper_create-create.sql @@ -0,0 +1,27 @@ +INSERT INTO "secret_keeper" ( + "guid", + "name", + "namespace", + "annotations", + "labels", + "created", + "created_by", + "updated", + "updated_by", + "description", + "type", + "payload" +) VALUES ( + 'abc', + 'name', + 'ns', + '{"x":"XXXX"}', + '{"a":"AAA", "b", "BBBB"}', + 1234, + 'user:ryan', + 5678, + 'user:cameron', + 'description', + 'sql', + '' +); diff --git a/pkg/storage/secret/metadata/testdata/sqlite--keeper_delete-delete.sql b/pkg/storage/secret/metadata/testdata/sqlite--keeper_delete-delete.sql new file mode 100755 index 00000000000..81f6c2c15af --- /dev/null +++ b/pkg/storage/secret/metadata/testdata/sqlite--keeper_delete-delete.sql @@ -0,0 +1,4 @@ +DELETE FROM "secret_keeper" +WHERE "namespace" = 'ns' AND + "name" = 'name' +; diff --git a/pkg/storage/secret/metadata/testdata/sqlite--keeper_list-list.sql b/pkg/storage/secret/metadata/testdata/sqlite--keeper_list-list.sql new file mode 100755 index 00000000000..1eb5e059b72 --- /dev/null +++ b/pkg/storage/secret/metadata/testdata/sqlite--keeper_list-list.sql @@ -0,0 +1,18 @@ +SELECT + "guid", + "name", + "namespace", + "annotations", + "labels", + "created", + "created_by", + "updated", + "updated_by", + "description", + "type", + "payload" +FROM + "secret_keeper" +WHERE "namespace" = 'ns' +ORDER BY "updated" DESC +; diff --git a/pkg/storage/secret/metadata/testdata/sqlite--keeper_listByName-list.sql b/pkg/storage/secret/metadata/testdata/sqlite--keeper_listByName-list.sql new file mode 100755 index 00000000000..8cd91224f6c --- /dev/null +++ b/pkg/storage/secret/metadata/testdata/sqlite--keeper_listByName-list.sql @@ -0,0 +1,7 @@ +SELECT + "name" +FROM + "secret_keeper" +WHERE "namespace" = 'ns' AND + "name" IN ('a', 'b') +; diff --git a/pkg/storage/secret/metadata/testdata/sqlite--keeper_read-read-for-update.sql b/pkg/storage/secret/metadata/testdata/sqlite--keeper_read-read-for-update.sql new file mode 100755 index 00000000000..6587d5970d8 --- /dev/null +++ b/pkg/storage/secret/metadata/testdata/sqlite--keeper_read-read-for-update.sql @@ -0,0 +1,18 @@ +SELECT + "guid", + "name", + "namespace", + "annotations", + "labels", + "created", + "created_by", + "updated", + "updated_by", + "description", + "type", + "payload" +FROM + "secret_keeper" +WHERE "namespace" = 'ns' AND + "name" = 'name' +; diff --git a/pkg/storage/secret/metadata/testdata/sqlite--keeper_read-read.sql b/pkg/storage/secret/metadata/testdata/sqlite--keeper_read-read.sql new file mode 100755 index 00000000000..6587d5970d8 --- /dev/null +++ b/pkg/storage/secret/metadata/testdata/sqlite--keeper_read-read.sql @@ -0,0 +1,18 @@ +SELECT + "guid", + "name", + "namespace", + "annotations", + "labels", + "created", + "created_by", + "updated", + "updated_by", + "description", + "type", + "payload" +FROM + "secret_keeper" +WHERE "namespace" = 'ns' AND + "name" = 'name' +; diff --git a/pkg/storage/secret/metadata/testdata/sqlite--keeper_update-update.sql b/pkg/storage/secret/metadata/testdata/sqlite--keeper_update-update.sql new file mode 100755 index 00000000000..c137c5ca14c --- /dev/null +++ b/pkg/storage/secret/metadata/testdata/sqlite--keeper_update-update.sql @@ -0,0 +1,18 @@ +UPDATE + "secret_keeper" +SET + "guid" = 'abc', + "name" = 'name', + "namespace" = 'ns', + "annotations" = '{"x":"XXXX"}', + "labels" = '{"a":"AAA", "b", "BBBB"}', + "created" = 1234, + "created_by" = 'user:ryan', + "updated" = 5678, + "updated_by" = 'user:cameron', + "description" = 'description', + "type" = 'sql', + "payload" = '' +WHERE "namespace" = 'ns' AND + "name" = 'name' +; diff --git a/pkg/tests/apis/secret/keeper_test.go b/pkg/tests/apis/secret/keeper_test.go new file mode 100644 index 00000000000..971295746ce --- /dev/null +++ b/pkg/tests/apis/secret/keeper_test.go @@ -0,0 +1,569 @@ +package secret + +import ( + "context" + "errors" + "math/rand/v2" + "net/http" + "strconv" + "strings" + "testing" + + secretv0alpha1 "github.com/grafana/grafana/pkg/apis/secret/v0alpha1" + "github.com/grafana/grafana/pkg/services/featuremgmt" + "github.com/grafana/grafana/pkg/tests/apis" + "github.com/grafana/grafana/pkg/tests/testinfra" + "github.com/stretchr/testify/require" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" +) + +var gvrKeepers = schema.GroupVersionResource{ + Group: secretv0alpha1.GROUP, + Version: secretv0alpha1.VERSION, + Resource: secretv0alpha1.KeeperResourceInfo.GetName(), +} + +func TestIntegrationKeeper(t *testing.T) { + if testing.Short() { + t.Skip("skipping integration test") + } + + ctx, cancel := context.WithCancel(context.Background()) + t.Cleanup(cancel) + + helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{ + AppModeProduction: false, // required for experimental APIs + EnableFeatureToggles: []string{ + // Required to start the example service + featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs, + featuremgmt.FlagSecretsManagementAppPlatform, + }, + }) + + permissions := map[string]ResourcePermission{ResourceKeepers: {Actions: ActionsAllKeepers}} + + genericUserEditor := mustCreateUsers(t, helper, permissions).Editor + + client := helper.GetResourceClient(apis.ResourceClientArgs{ + User: genericUserEditor, + GVR: gvrKeepers, + }) + + t.Run("reading a keeper that does not exist returns a 404", func(t *testing.T) { + raw, err := client.Resource.Get(ctx, "some-keeper-that-does-not-exist", metav1.GetOptions{}) + require.Error(t, err) + require.Nil(t, raw) + + var statusErr *apierrors.StatusError + require.True(t, errors.As(err, &statusErr)) + require.Equal(t, "keeper.secret.grafana.app \"some-keeper-that-does-not-exist\" not found", err.Error()) + require.Equal(t, http.StatusNotFound, int(statusErr.Status().Code)) + }) + + t.Run("deleting a keeper that does not exist returns an error", func(t *testing.T) { + err := client.Resource.Delete(ctx, "some-keeper-that-does-not-exist", metav1.DeleteOptions{}) + require.Error(t, err) + + var statusErr *apierrors.StatusError + require.True(t, errors.As(err, &statusErr)) + require.Equal(t, "keeper.secret.grafana.app \"some-keeper-that-does-not-exist\" not found", err.Error()) + require.Equal(t, http.StatusNotFound, int(statusErr.Status().Code)) + }) + + t.Run("creating a keeper returns it", func(t *testing.T) { + raw := mustGenerateKeeper(t, helper, genericUserEditor, nil, "testdata/keeper-aws-generate.yaml") + + keeper := new(secretv0alpha1.Keeper) + err := runtime.DefaultUnstructuredConverter.FromUnstructured(raw.Object, keeper) + require.NoError(t, err) + require.NotNil(t, keeper) + + require.NotEmpty(t, keeper.Spec.Description) + require.NotEmpty(t, keeper.Spec.AWS) + require.Empty(t, keeper.Spec.Azure) + + t.Run("and creating another keeper with the same name in the same namespace returns an error", func(t *testing.T) { + testKeeper := helper.LoadYAMLOrJSONFile("testdata/keeper-gcp-generate.yaml") + testKeeper.SetName(raw.GetName()) + + raw, err := client.Resource.Create(ctx, testKeeper, metav1.CreateOptions{}) + require.Error(t, err) + require.Nil(t, raw) + }) + + t.Run("and reading th keeper returns it same as if when it was created", func(t *testing.T) { + raw, err := client.Resource.Get(ctx, keeper.Name, metav1.GetOptions{}) + require.NoError(t, err) + require.NotNil(t, raw) + + anotherKeeper := new(secretv0alpha1.Keeper) + err = runtime.DefaultUnstructuredConverter.FromUnstructured(raw.Object, anotherKeeper) + require.NoError(t, err) + require.NotNil(t, anotherKeeper) + + require.EqualValues(t, keeper, anotherKeeper) + }) + + t.Run("and listing keepers returns the created keeper", func(t *testing.T) { + rawList, err := client.Resource.List(ctx, metav1.ListOptions{}) + require.NoError(t, err) + require.NotNil(t, rawList) + require.GreaterOrEqual(t, len(rawList.Items), 1) + require.Equal(t, keeper.Name, rawList.Items[0].GetName()) + }) + + t.Run("and updating the keeper replaces the spec fields and returns them", func(t *testing.T) { + newRaw := helper.LoadYAMLOrJSONFile("testdata/keeper-gcp-generate.yaml") + newRaw.SetName(raw.GetName()) + newRaw.Object["spec"].(map[string]any)["description"] = "New description" + newRaw.Object["metadata"].(map[string]any)["annotations"] = map[string]any{"newAnnotation": "newValue"} + + updatedRaw, err := client.Resource.Update(ctx, newRaw, metav1.UpdateOptions{}) + require.NoError(t, err) + require.NotNil(t, updatedRaw) + + updatedKeeper := new(secretv0alpha1.Keeper) + err = runtime.DefaultUnstructuredConverter.FromUnstructured(updatedRaw.Object, updatedKeeper) + require.NoError(t, err) + require.NotNil(t, updatedKeeper) + + require.NotEqualValues(t, updatedKeeper.Spec, keeper.Spec) + }) + + t.Run("and updating the keeper to reference securevalues that does not exist returns an error", func(t *testing.T) { + t.Skip("skipping because storing credentials as securevalues is not implemented yet") + newRaw := helper.LoadYAMLOrJSONFile("testdata/keeper-aws-generate.yaml") + newRaw.SetName(raw.GetName()) + newRaw.Object["spec"].(map[string]any)["aws"] = map[string]any{ + "accessKeyId": map[string]any{ + "secureValueName": "securevalue-does-not-exist-1", + }, + "secretAccessKey": map[string]any{ + "secureValueName": "securevalue-does-not-exist-2", + }, + } + + updatedRaw, err := client.Resource.Update(ctx, newRaw, metav1.UpdateOptions{}) + require.Error(t, err) + require.Nil(t, updatedRaw) + require.Contains(t, err.Error(), "securevalue-does-not-exist-1") + require.Contains(t, err.Error(), "securevalue-does-not-exist-2") + }) + }) + + t.Run("creating an invalid keeper fails validation and returns an error", func(t *testing.T) { + testData := helper.LoadYAMLOrJSONFile("testdata/keeper-aws-generate.yaml") + testData.Object["spec"].(map[string]any)["description"] = "" + + raw, err := client.Resource.Create(ctx, testData, metav1.CreateOptions{}) + require.Error(t, err) + require.Nil(t, raw) + + var statusErr *apierrors.StatusError + require.True(t, errors.As(err, &statusErr)) + }) + + t.Run("creating a keeper with a provider then changing the provider does not return an error", func(t *testing.T) { + rawAWS := mustGenerateKeeper(t, helper, genericUserEditor, nil, "testdata/keeper-aws-generate.yaml") + + testDataKeeperGCP := rawAWS.DeepCopy() + testDataKeeperGCP.Object["spec"].(map[string]any)["aws"] = nil + testDataKeeperGCP.Object["spec"].(map[string]any)["gcp"] = map[string]any{ + "projectId": "project-id", + "credentialsFile": "/path/to/file.json", + } + + rawGCP, err := client.Resource.Update(ctx, testDataKeeperGCP, metav1.UpdateOptions{}) + require.NoError(t, err) + require.NotNil(t, rawGCP) + + require.NotEqualValues(t, rawAWS.Object["spec"], rawGCP.Object["spec"]) + }) + + t.Run("creating a keeper that references securevalues that does not exist returns an error", func(t *testing.T) { + t.Skip("skipping because storing credentials as securevalues is not implemented yet") + testDataKeeper := helper.LoadYAMLOrJSONFile("testdata/keeper-aws-generate.yaml") + testDataKeeper.Object["spec"].(map[string]any)["aws"] = map[string]any{ + "accessKeyId": map[string]any{ + "secureValueName": "securevalue-does-not-exist-1", + }, + "secretAccessKey": map[string]any{ + "secureValueName": "securevalue-does-not-exist-2", + }, + } + + raw, err := client.Resource.Create(ctx, testDataKeeper, metav1.CreateOptions{}) + require.Error(t, err) + require.Nil(t, raw) + require.Contains(t, err.Error(), "securevalue-does-not-exist-1") + require.Contains(t, err.Error(), "securevalue-does-not-exist-2") + }) + + t.Run("deleting a keeper that exists does not return an error", func(t *testing.T) { + generatePrefix := "generated-" + + testData := helper.LoadYAMLOrJSONFile("testdata/keeper-aws-generate.yaml") + testData.SetGenerateName(generatePrefix) + + raw, err := client.Resource.Create(ctx, testData, metav1.CreateOptions{}) + require.NoError(t, err) + require.NotNil(t, raw) + + name := raw.GetName() + require.True(t, strings.HasPrefix(name, generatePrefix)) + + err = client.Resource.Delete(ctx, name, metav1.DeleteOptions{}) + require.NoError(t, err) + + t.Run("and then trying to read it returns a 404 error", func(t *testing.T) { + raw, err := client.Resource.Get(ctx, name, metav1.GetOptions{}) + require.Error(t, err) + require.Nil(t, raw) + + var statusErr *apierrors.StatusError + require.True(t, errors.As(err, &statusErr)) + require.Equal(t, http.StatusNotFound, int(statusErr.Status().Code)) + }) + + t.Run("and listing keepers returns an empty list", func(t *testing.T) { + rawList, err := client.Resource.List(ctx, metav1.ListOptions{}) + require.NoError(t, err) + require.NotNil(t, rawList) + require.Empty(t, rawList.Items) + }) + }) + + t.Run("creating keepers in multiple namespaces", func(t *testing.T) { + permissions := map[string]ResourcePermission{ + ResourceKeepers: {Actions: ActionsAllKeepers}, + } + + editorOrgA := mustCreateUsers(t, helper, permissions).Editor + editorOrgB := mustCreateUsers(t, helper, permissions).Editor + + keeperOrgA := mustGenerateKeeper(t, helper, editorOrgA, nil, "testdata/keeper-aws-generate.yaml") + keeperOrgB := mustGenerateKeeper(t, helper, editorOrgB, nil, "testdata/keeper-aws-generate.yaml") + + clientOrgA := helper.GetResourceClient(apis.ResourceClientArgs{User: editorOrgA, GVR: gvrKeepers}) + clientOrgB := helper.GetResourceClient(apis.ResourceClientArgs{User: editorOrgB, GVR: gvrKeepers}) + + // Create + t.Run("creating a keeper with the same name as one from another namespace does not return an error", func(t *testing.T) { + // OrgA creating a keeper with the same name from OrgB. + testData := helper.LoadYAMLOrJSONFile("testdata/keeper-aws-generate.yaml") + testData.SetName(keeperOrgB.GetName()) + + raw, err := clientOrgA.Resource.Create(ctx, testData, metav1.CreateOptions{}) + require.NoError(t, err) + require.NotNil(t, raw) + + // OrgA creating a keeper with the same name from OrgB. + testData = helper.LoadYAMLOrJSONFile("testdata/keeper-aws-generate.yaml") + testData.SetName(keeperOrgA.GetName()) + + raw, err = clientOrgB.Resource.Create(ctx, testData, metav1.CreateOptions{}) + require.NoError(t, err) + require.NotNil(t, raw) + + require.NoError(t, clientOrgA.Resource.Delete(ctx, keeperOrgB.GetName(), metav1.DeleteOptions{})) + require.NoError(t, clientOrgB.Resource.Delete(ctx, keeperOrgA.GetName(), metav1.DeleteOptions{})) + }) + + // Read + t.Run("fetching a keeper from another namespace returns not found", func(t *testing.T) { + var statusErr *apierrors.StatusError + + // OrgA trying to fetch keeper from OrgB. + raw, err := clientOrgA.Resource.Get(ctx, keeperOrgB.GetName(), metav1.GetOptions{}) + require.Error(t, err) + require.Nil(t, raw) + require.True(t, errors.As(err, &statusErr)) + require.Equal(t, http.StatusNotFound, int(statusErr.Status().Code)) + + // OrgB trying to fetch keeper from OrgA. + raw, err = clientOrgB.Resource.Get(ctx, keeperOrgA.GetName(), metav1.GetOptions{}) + require.Error(t, err) + require.Nil(t, raw) + require.True(t, errors.As(err, &statusErr)) + require.Equal(t, http.StatusNotFound, int(statusErr.Status().Code)) + }) + + // Update + t.Run("updating a keeper from another namespace returns not found", func(t *testing.T) { + var statusErr *apierrors.StatusError + + // OrgA trying to update securevalue from OrgB. + testData := helper.LoadYAMLOrJSONFile("testdata/keeper-aws-generate.yaml") + testData.SetName(keeperOrgB.GetName()) + testData.Object["spec"].(map[string]any)["description"] = "New description" + + raw, err := clientOrgA.Resource.Update(ctx, testData, metav1.UpdateOptions{}) + require.Error(t, err) + require.Nil(t, raw) + require.True(t, errors.As(err, &statusErr)) + require.Equal(t, http.StatusNotFound, int(statusErr.Status().Code)) + + // OrgB trying to update keeper from OrgA. + testData = helper.LoadYAMLOrJSONFile("testdata/keeper-aws-generate.yaml") + testData.SetName(keeperOrgA.GetName()) + testData.Object["spec"].(map[string]any)["description"] = "New description" + + raw, err = clientOrgB.Resource.Update(ctx, testData, metav1.UpdateOptions{}) + require.Error(t, err) + require.Nil(t, raw) + require.True(t, errors.As(err, &statusErr)) + require.Equal(t, http.StatusNotFound, int(statusErr.Status().Code)) + }) + + // Delete + t.Run("deleting a keeper from another namespace returns an error and does not delete it", func(t *testing.T) { + var statusErr *apierrors.StatusError + + // OrgA trying to delete keeper from OrgB. + err := clientOrgA.Resource.Delete(ctx, keeperOrgB.GetName(), metav1.DeleteOptions{}) + require.Error(t, err) + require.True(t, errors.As(err, &statusErr)) + require.Equal(t, http.StatusNotFound, int(statusErr.Status().Code)) + + // Check that it still exists from the perspective of OrgB. + raw, err := clientOrgB.Resource.Get(ctx, keeperOrgB.GetName(), metav1.GetOptions{}) + require.NoError(t, err) + require.NotNil(t, raw) + + // OrgB trying to delete keeper from OrgA. + err = clientOrgB.Resource.Delete(ctx, keeperOrgA.GetName(), metav1.DeleteOptions{}) + require.Error(t, err) + require.Equal(t, http.StatusNotFound, int(statusErr.Status().Code)) + + // Check that it still exists from the perspective of OrgA. + raw, err = clientOrgA.Resource.Get(ctx, keeperOrgA.GetName(), metav1.GetOptions{}) + require.NoError(t, err) + require.NotNil(t, raw) + }) + + // List + t.Run("listing keeper from a namespace does not return the ones from another namespace", func(t *testing.T) { + // OrgA listing keeper. + listOrgA, err := clientOrgA.Resource.List(ctx, metav1.ListOptions{}) + require.NoError(t, err) + require.NotNil(t, listOrgA) + require.Len(t, listOrgA.Items, 1) + require.Equal(t, *keeperOrgA, listOrgA.Items[0]) + + // OrgB listing keeper. + listOrgB, err := clientOrgB.Resource.List(ctx, metav1.ListOptions{}) + require.NoError(t, err) + require.NotNil(t, listOrgB) + require.Len(t, listOrgB.Items, 1) + require.Equal(t, *keeperOrgB, listOrgB.Items[0]) + }) + }) + + t.Run("keeper actions without having required permissions", func(t *testing.T) { + // Create users on a random org without specifying secrets-related permissions. + editorP := mustCreateUsers(t, helper, nil).Editor + + clientP := helper.GetResourceClient(apis.ResourceClientArgs{ + User: editorP, + GVR: gvrKeepers, + }) + + // GET + rawGet, err := clientP.Resource.Get(ctx, "some-keeper", metav1.GetOptions{}) + require.Error(t, err) + require.Nil(t, rawGet) + + var statusGetErr *apierrors.StatusError + require.True(t, errors.As(err, &statusGetErr)) + require.EqualValues(t, http.StatusForbidden, statusGetErr.Status().Code) + + // LIST + rawList, err := clientP.Resource.List(ctx, metav1.ListOptions{}) + require.Error(t, err) + require.Nil(t, rawList) + + var statusListErr *apierrors.StatusError + require.True(t, errors.As(err, &statusListErr)) + require.EqualValues(t, http.StatusForbidden, statusListErr.Status().Code) + + // CREATE + testKeeper := helper.LoadYAMLOrJSONFile("testdata/keeper-gcp-generate.yaml") // to pass validation before authz. + rawCreate, err := clientP.Resource.Create(ctx, testKeeper, metav1.CreateOptions{}) + require.Error(t, err) + require.Nil(t, rawCreate) + + var statusCreateErr *apierrors.StatusError + require.True(t, errors.As(err, &statusCreateErr)) + require.EqualValues(t, http.StatusForbidden, statusCreateErr.Status().Code) + + // UPDATE + testKeeper.SetName("test") // to pass validation before authz. + rawUpdate, err := clientP.Resource.Update(ctx, testKeeper, metav1.UpdateOptions{}) + require.Error(t, err) + require.Nil(t, rawUpdate) + + var statusUpdateErr *apierrors.StatusError + require.True(t, errors.As(err, &statusUpdateErr)) + require.EqualValues(t, http.StatusForbidden, statusUpdateErr.Status().Code) + + // DELETE + err = clientP.Resource.Delete(ctx, "some-keeper", metav1.DeleteOptions{}) + require.Error(t, err) + + var statusDeleteErr *apierrors.StatusError + require.True(t, errors.As(err, &statusDeleteErr)) + require.EqualValues(t, http.StatusForbidden, statusDeleteErr.Status().Code) + }) + + t.Run("keeper actions with permissions but with limited scope", func(t *testing.T) { + suffix := strconv.FormatInt(rand.Int64(), 10) + + // Fix the Keeper names. + keeperName := "kp-" + suffix + testKeeper := helper.LoadYAMLOrJSONFile("testdata/keeper-gcp-generate.yaml") + testKeeper.SetName(keeperName) + + keeperNameAnother := "kp-another-" + suffix + testKeeperAnother := helper.LoadYAMLOrJSONFile("testdata/keeper-gcp-generate.yaml") + testKeeperAnother.SetName(keeperNameAnother) + + // Fix the org ID because we will create another user with scope "all" permissions on the same org, to compare. + orgID := rand.Int64() + 2 + + // Permissions which allow any action, but scoped actions (get, update, delete) only on `keeperName` and NO OTHER Keeper. + scopedLimitedPermissions := map[string]ResourcePermission{ + ResourceKeepers: { + Actions: ActionsAllKeepers, + Name: keeperName, + }, + } + + // Create users (+ client) with permission to manage ONLY the Keeper `keeperName`. + editorLimited := mustCreateUsersWithOrg(t, helper, orgID, scopedLimitedPermissions).Editor + + clientScopedLimited := helper.GetResourceClient(apis.ResourceClientArgs{ + User: editorLimited, + GVR: gvrKeepers, + }) + + // Create users (+ client) with permission to manage ANY Keepers. + scopedAllPermissions := map[string]ResourcePermission{ + ResourceKeepers: { + Actions: ActionsAllKeepers, + Name: "*", // this or not sending a `Name` have the same effect. + }, + } + + editorAll := mustCreateUsersWithOrg(t, helper, orgID, scopedAllPermissions).Editor + + clientScopedAll := helper.GetResourceClient(apis.ResourceClientArgs{ + User: editorAll, + GVR: gvrKeepers, + }) + + // For create, we don't have actual granular permissions, so we can use any client that has unscoped create permissions. + // This is because when we don't know yet what the name of the resource will be, the request comes with an empty value. + // And thus the authorizer can't do granular checks. + t.Run("CREATE", func(t *testing.T) { + rawCreateLimited, err := clientScopedAll.Resource.Create(ctx, testKeeper, metav1.CreateOptions{}) + require.NoError(t, err) + require.NotNil(t, rawCreateLimited) + + rawCreateLimited, err = clientScopedAll.Resource.Create(ctx, testKeeperAnother, metav1.CreateOptions{}) + require.NoError(t, err) + require.NotNil(t, rawCreateLimited) + }) + + t.Run("READ", func(t *testing.T) { + // Retrieve `keeperName` from the limited client. + rawGetLimited, err := clientScopedLimited.Resource.Get(ctx, keeperName, metav1.GetOptions{}) + require.NoError(t, err) + require.NotNil(t, rawGetLimited) + require.Equal(t, rawGetLimited.GetUID(), rawGetLimited.GetUID()) + + // Retrieve `keeperName` from the scope-all client. + rawGetAll, err := clientScopedAll.Resource.Get(ctx, keeperName, metav1.GetOptions{}) + require.NoError(t, err) + require.NotNil(t, rawGetAll) + require.Equal(t, rawGetAll.GetUID(), rawGetLimited.GetUID()) + + // Even though we can create it, we cannot retrieve `keeperNameAnother` from the limited client. + rawGetLimited, err = clientScopedLimited.Resource.Get(ctx, keeperNameAnother, metav1.GetOptions{}) + require.Error(t, err) + require.Nil(t, rawGetLimited) + + var statusGetErr *apierrors.StatusError + require.True(t, errors.As(err, &statusGetErr)) + require.EqualValues(t, http.StatusForbidden, statusGetErr.Status().Code) + + // Retrieve `keeperNameAnother` from the scope-all client. + rawGetAll, err = clientScopedAll.Resource.Get(ctx, keeperNameAnother, metav1.GetOptions{}) + require.NoError(t, err) + require.NotNil(t, rawGetAll) + }) + + t.Run("LIST", func(t *testing.T) { + // List Keepers from the limited client should return only 1. + rawList, err := clientScopedLimited.Resource.List(ctx, metav1.ListOptions{}) + require.NoError(t, err) + require.NotNil(t, rawList) + require.Len(t, rawList.Items, 1) + require.Equal(t, keeperName, rawList.Items[0].GetName()) + + // List Keepers from the scope-all client should return all of them. + rawList, err = clientScopedAll.Resource.List(ctx, metav1.ListOptions{}) + require.NoError(t, err) + require.NotNil(t, rawList) + require.Len(t, rawList.Items, 2) + }) + + t.Run("UPDATE", func(t *testing.T) { + // Update `keeperName` from the limited client. + testKeeperUpdate := testKeeper.DeepCopy() + testKeeperUpdate.Object["spec"].(map[string]any)["description"] = "keeper-description-1234" + + rawUpdate, err := clientScopedLimited.Resource.Update(ctx, testKeeperUpdate, metav1.UpdateOptions{}) + require.NoError(t, err) + require.NotNil(t, rawUpdate) + + // Try to update `keeperNameAnother` from the limited client. + testKeeperAnotherUpdate := testKeeperAnother.DeepCopy() + testKeeperAnotherUpdate.Object["spec"].(map[string]any)["description"] = "keeper-description-5678" + + rawUpdate, err = clientScopedLimited.Resource.Update(ctx, testKeeperAnotherUpdate, metav1.UpdateOptions{}) + require.Error(t, err) + require.Nil(t, rawUpdate) + + var statusUpdateErr *apierrors.StatusError + require.True(t, errors.As(err, &statusUpdateErr)) + require.EqualValues(t, http.StatusForbidden, statusUpdateErr.Status().Code) + + // Update `keeperNameAnother` from the scope-all client. + rawUpdate, err = clientScopedAll.Resource.Update(ctx, testKeeperAnotherUpdate, metav1.UpdateOptions{}) + require.NoError(t, err) + require.NotNil(t, rawUpdate) + }) + + // Keep this last for cleaning up the resources. + t.Run("DELETE", func(t *testing.T) { + // Try to delete `keeperNameAnother` from the limited client. + err := clientScopedLimited.Resource.Delete(ctx, keeperNameAnother, metav1.DeleteOptions{}) + require.Error(t, err) + + var statusDeleteErr *apierrors.StatusError + require.True(t, errors.As(err, &statusDeleteErr)) + require.EqualValues(t, http.StatusForbidden, statusDeleteErr.Status().Code) + + // Delete `keeperNameAnother` from the scope-all client. + err = clientScopedAll.Resource.Delete(ctx, keeperNameAnother, metav1.DeleteOptions{}) + require.NoError(t, err) + + // Delete `keeperName` from the limited client. + err = clientScopedLimited.Resource.Delete(ctx, keeperName, metav1.DeleteOptions{}) + require.NoError(t, err) + }) + }) +} diff --git a/pkg/tests/apis/secret/main_test.go b/pkg/tests/apis/secret/main_test.go new file mode 100644 index 00000000000..20169fdbaa2 --- /dev/null +++ b/pkg/tests/apis/secret/main_test.go @@ -0,0 +1,150 @@ +package secret + +import ( + "cmp" + "context" + "encoding/json" + "math/rand/v2" + "strconv" + "testing" + + "github.com/grafana/grafana/pkg/registry/apis/secret" + "github.com/grafana/grafana/pkg/services/accesscontrol/resourcepermissions" + "github.com/grafana/grafana/pkg/services/featuremgmt" + "github.com/grafana/grafana/pkg/services/org" + "github.com/grafana/grafana/pkg/services/team" + "github.com/grafana/grafana/pkg/tests/apis" + "github.com/grafana/grafana/pkg/tests/testinfra" + "github.com/grafana/grafana/pkg/tests/testsuite" + "github.com/stretchr/testify/require" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" +) + +var ( + ResourceSecureValues = "secret.securevalues" + ResourceKeepers = "secret.keepers" + + ActionsAllKeepers = []string{ + secret.ActionSecretKeepersCreate, + secret.ActionSecretKeepersWrite, + secret.ActionSecretKeepersRead, + secret.ActionSecretKeepersDelete, + } + ActionsAllSecureValues = []string{ + secret.ActionSecretSecureValuesCreate, + secret.ActionSecretSecureValuesWrite, + secret.ActionSecretSecureValuesRead, + secret.ActionSecretSecureValuesDelete, + } +) + +type ResourcePermission struct { + Actions []string + Name string // empty or "*" for all +} + +func TestMain(m *testing.M) { + testsuite.Run(m) +} + +func TestIntegrationDiscoveryClient(t *testing.T) { + if testing.Short() { + t.Skip("skipping integration test") + } + + helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{ + AppModeProduction: false, // required for experimental APIs + EnableFeatureToggles: []string{ + // Required to start the example service + featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs, + featuremgmt.FlagSecretsManagementAppPlatform, + }, + }) + + t.Run("check discovery client", func(t *testing.T) { + disco := helper.NewDiscoveryClient() + + resources, err := disco.ServerResourcesForGroupVersion("secret.grafana.app/v0alpha1") + require.NoError(t, err) + + v1Disco, err := json.MarshalIndent(resources, "", " ") + require.NoError(t, err) + + var apiResourceList map[string]any + require.NoError(t, json.Unmarshal(v1Disco, &apiResourceList)) + + groupVersion, ok := apiResourceList["groupVersion"].(string) + require.True(t, ok) + require.Equal(t, "secret.grafana.app/v0alpha1", groupVersion) + + apiResources, ok := apiResourceList["resources"].([]any) + require.True(t, ok) + require.Len(t, apiResources, 2) // securevalue + keeper + (subresources...) + }) +} + +func mustCreateUsersWithOrg(t *testing.T, helper *apis.K8sTestHelper, orgID int64, permissionMap map[string]ResourcePermission) apis.OrgUsers { + t.Helper() + + permissions := make([]resourcepermissions.SetResourcePermissionCommand, 0, len(permissionMap)) + for resource, permission := range permissionMap { + permissions = append(permissions, resourcepermissions.SetResourcePermissionCommand{ + Actions: permission.Actions, + Resource: resource, + ResourceAttribute: "uid", + ResourceID: cmp.Or(permission.Name, "*"), + }) + } + + orgName := "org-" + strconv.FormatInt(orgID, 10) + + userSuffix := strconv.FormatInt(rand.Int64(), 10) + + // Add here admin or viewer if necessary. + editor := helper.CreateUser("editor-"+userSuffix, orgName, org.RoleEditor, permissions) + + staff := helper.CreateTeam("staff-"+userSuffix, "staff-"+userSuffix+"@"+orgName, editor.Identity.GetOrgID()) + + // Also call this method for each new user. + helper.AddOrUpdateTeamMember(editor, staff.ID, team.PermissionTypeMember) + + return apis.OrgUsers{ + Editor: editor, + Staff: staff, + } +} + +func mustCreateUsers(t *testing.T, helper *apis.K8sTestHelper, permissionMap map[string]ResourcePermission) apis.OrgUsers { + orgID := rand.Int64() + 2 // if it is 0, becomes 2 and not 1. + return mustCreateUsersWithOrg(t, helper, orgID, permissionMap) +} + +func mustGenerateKeeper(t *testing.T, helper *apis.K8sTestHelper, user apis.User, specType map[string]any, testFile string) *unstructured.Unstructured { + t.Helper() + + require.NotEmpty(t, testFile, "testFile must not be empty") + + ctx, cancel := context.WithCancel(context.Background()) + t.Cleanup(cancel) + + keeperClient := helper.GetResourceClient(apis.ResourceClientArgs{ + User: user, + GVR: gvrKeepers, + }) + + testKeeper := helper.LoadYAMLOrJSONFile(testFile) + if specType != nil { + testKeeper.Object["spec"] = specType + } + + raw, err := keeperClient.Resource.Create(ctx, testKeeper, metav1.CreateOptions{}) + require.NoError(t, err) + require.NotNil(t, raw) + + t.Cleanup(func() { + require.NoError(t, keeperClient.Resource.Delete(ctx, raw.GetName(), metav1.DeleteOptions{})) + }) + + return raw +} diff --git a/pkg/tests/apis/secret/testdata/keeper-aws-generate.yaml b/pkg/tests/apis/secret/testdata/keeper-aws-generate.yaml new file mode 100644 index 00000000000..c210ab66838 --- /dev/null +++ b/pkg/tests/apis/secret/testdata/keeper-aws-generate.yaml @@ -0,0 +1,15 @@ +apiVersion: secret.grafana.app/v0alpha1 +kind: Keeper +metadata: + annotations: + xx: XXX + labels: + aa: AAA +spec: + description: AWS XYZ value + aws: + accessKeyId: + valueFromEnv: ACCESS_KEY_ID_XYZ + secretAccessKey: + valueFromEnv: SECRET_ACCESS_KEY_XYZ + kmsKeyId: kmsKeyId-xyz diff --git a/pkg/tests/apis/secret/testdata/keeper-gcp-generate.yaml b/pkg/tests/apis/secret/testdata/keeper-gcp-generate.yaml new file mode 100644 index 00000000000..485a4cb2ea5 --- /dev/null +++ b/pkg/tests/apis/secret/testdata/keeper-gcp-generate.yaml @@ -0,0 +1,14 @@ +apiVersion: secret.grafana.app/v0alpha1 +kind: Keeper +metadata: + annotations: + xx: XXX + yy: YYY + labels: + aa: AAA + bb: BBB +spec: + description: GCP XYZ value + gcp: + projectId: project-id + credentialsFile: /path/to/file.json From 844c854d75b0a74478ecce295285b8255c204d3f Mon Sep 17 00:00:00 2001 From: Andreas Christou Date: Thu, 22 May 2025 14:35:41 +0100 Subject: [PATCH 14/73] Azure: Fix Application Insights metadata requests (#105614) * Fix App insights metadata requests - Correctly handle error responses * Fix linting and always log the error message * More lint --- .../azure-log-analytics-datasource.go | 90 +++++++++++++++++-- 1 file changed, 83 insertions(+), 7 deletions(-) diff --git a/pkg/tsdb/azuremonitor/loganalytics/azure-log-analytics-datasource.go b/pkg/tsdb/azuremonitor/loganalytics/azure-log-analytics-datasource.go index e46af17f0b4..4c0ef150c1b 100644 --- a/pkg/tsdb/azuremonitor/loganalytics/azure-log-analytics-datasource.go +++ b/pkg/tsdb/azuremonitor/loganalytics/azure-log-analytics-datasource.go @@ -40,6 +40,71 @@ func filterTablesWithData(tables []types.MetadataTable) []types.MetadataTable { return filtered } +func writeErrorResponse(rw http.ResponseWriter, statusCode int, message string) error { + rw.Header().Set("Content-Type", "application/json") + rw.WriteHeader(statusCode) + + // Log the raw error message + backend.Logger.Error(message) + + // Set error response to initial error message + errorBody := map[string]string{"error": message} + + // Attempt to locate JSON portion in error message + re := regexp.MustCompile(`\{.*\}`) + jsonPart := re.FindString(message) + if jsonPart != "" { + var jsonData map[string]interface{} + if unmarshalErr := json.Unmarshal([]byte(jsonPart), &jsonData); unmarshalErr != nil { + errorBody["error"] = fmt.Sprintf("Invalid JSON format in error message. Raw error: %s", message) + backend.Logger.Error("failed to unmarshal JSON error message", "error", unmarshalErr) + } else { + // Extract relevant fields for a formatted error message + errorType, ok := jsonData["error"].(string) + if ok { + errorDescription, ok := jsonData["error_description"].(string) + if !ok { + backend.Logger.Error("unable to convert error_description to string", "rawError", jsonData["error_description"]) + // Attempt to just format the error as a string + errorDescription = fmt.Sprintf("%v", jsonData["error_description"]) + } + if errorType == "" { + errorType = "UnknownError" + } + + errorBody["error"] = fmt.Sprintf("%s: %s", errorType, errorDescription) + } else { + nestedError, ok := jsonData["error"].(map[string]interface{}) + + if !ok { + errorBody["error"] = fmt.Sprintf("Invalid JSON format in error message. Raw error: %s", message) + backend.Logger.Error("failed to unmarshal JSON error message", "error", unmarshalErr) + } + + errorType := nestedError["code"].(string) + errorDescription, ok := nestedError["message"].(string) + if !ok { + backend.Logger.Error("unable to convert error_description to string", "rawError", jsonData["error_description"]) + // Attempt to just format the error as a string + errorDescription = fmt.Sprintf("%v", nestedError["message"]) + } + + if errorType == "" { + errorType = "UnknownError" + } + errorBody["error"] = fmt.Sprintf("%s: %s", errorType, errorDescription) + } + } + } + + jsonRes, _ := json.Marshal(errorBody) + _, err := rw.Write(jsonRes) + if err != nil { + return fmt.Errorf("unable to write HTTP response: %v", err) + } + return nil +} + func (e *AzureLogAnalyticsDatasource) ResourceRequest(rw http.ResponseWriter, req *http.Request, cli *http.Client) (http.ResponseWriter, error) { if req.URL.Path == "/usage/basiclogs" { newUrl := &url.URL{ @@ -49,15 +114,21 @@ func (e *AzureLogAnalyticsDatasource) ResourceRequest(rw http.ResponseWriter, re } return e.GetBasicLogsUsage(req.Context(), newUrl.String(), cli, rw, req.Body) } else if strings.Contains(req.URL.Path, "/metadata") { + isAppInsights := strings.Contains(req.URL.Path, "Microsoft.Insights/components") // Add necessary headers - req.Header.Set("Prefer", "metadata-format-v4,exclude-resourcetypes,exclude-customfunctions") + if isAppInsights { + // metadata-format-v4 is not supported for AppInsights resources + req.Header.Set("Prefer", "metadata-format-v3,exclude-resourcetypes,exclude-customfunctions") + } else { + req.Header.Set("Prefer", "metadata-format-v4,exclude-resourcetypes,exclude-customfunctions") + } queryParams := req.URL.Query() // Add necessary query params queryParams.Add("select", "categories,solutions,tables,workspaces") req.URL.RawQuery = queryParams.Encode() resp, err := cli.Do(req) if err != nil { - return nil, fmt.Errorf("failed to fetch metadata: %w", err) + return nil, writeErrorResponse(rw, resp.StatusCode, fmt.Sprintf("failed to fetch metadata: %s", err)) } defer func() { @@ -69,23 +140,28 @@ func (e *AzureLogAnalyticsDatasource) ResourceRequest(rw http.ResponseWriter, re encoding := resp.Header.Get("Content-Encoding") body, err := decode(encoding, resp.Body) if err != nil { - return nil, fmt.Errorf("failed to read metadata response: %w", err) + return nil, writeErrorResponse(rw, resp.StatusCode, fmt.Sprintf("failed to read metadata response: %s", err)) } if resp.StatusCode != http.StatusOK { - return nil, fmt.Errorf("metadata API error: %s", string(body)) + return nil, writeErrorResponse(rw, resp.StatusCode, fmt.Sprintf("metadata API error: %s", string(body))) } var metadata types.AzureLogAnalyticsMetadata err = json.Unmarshal(body, &metadata) if err != nil { - return nil, fmt.Errorf("failed to unmarshal metadata response: %w", err) + return nil, writeErrorResponse(rw, http.StatusInternalServerError, fmt.Sprintf("failed to unmarshal metadata response: %s", err)) + } + + // AppInsights metadata requests do not return the HasData field + // So we return all tables + if !isAppInsights { + metadata.Tables = filterTablesWithData(metadata.Tables) } - metadata.Tables = filterTablesWithData(metadata.Tables) responseBody, err := json.Marshal(metadata) if err != nil { - return nil, fmt.Errorf("failed to marshal metadata response: %w", err) + return nil, writeErrorResponse(rw, http.StatusInternalServerError, fmt.Sprintf("failed to marshal metadata response: %s", err)) } rw.Header().Set("Content-Type", "application/json") From 0166b6bcc612f5ffadcc247e3c3eccf0b5112b9a Mon Sep 17 00:00:00 2001 From: mohammad-hamid Date: Thu, 22 May 2025 09:58:58 -0400 Subject: [PATCH 15/73] Update API docs for team groups search (#105512) * Update API docs for team groups search * update team sync doc * remove param block --------- Co-authored-by: Gabriel Mabille --- docs/sources/developers/http_api/team_sync.md | 51 +++++++++ public/api-enterprise-spec.json | 92 +++++++++++++++ public/api-merged.json | 92 +++++++++++++++ public/openapi3.json | 106 ++++++++++++++++++ 4 files changed, 341 insertions(+) diff --git a/docs/sources/developers/http_api/team_sync.md b/docs/sources/developers/http_api/team_sync.md index c4c0e607a2d..e3061524684 100644 --- a/docs/sources/developers/http_api/team_sync.md +++ b/docs/sources/developers/http_api/team_sync.md @@ -147,3 +147,54 @@ Status Codes: - **401** - Unauthorized - **403** - Permission denied - **404** - Team not found/Group not found + +## Search Team Groups + +`GET /api/teams/:teamId/groups/search` + +Search for team groups with pagination support. + +**Required permissions** + +| Action | Scope | +| ---------------------- | -------- | +| teams.permissions:read | teams:\* | + +**Example Request**: + +```http +GET /api/teams/1/groups/search?name=editors&query=group&page=1&perpage=10 HTTP/1.1 +Accept: application/json +Content-Type: application/json +Authorization: Bearer glsa_kcVxDhZtu5ISOZIEt +``` + +**Example Response**: + +```http +HTTP/1.1 200 +Content-Type: application/json + +[ + { + "totalCount": 1, + "teamGroups": [ + { + "orgId": 1, + "teamId": 1, + "groupId": "cn=editors,ou=groups,dc=grafana,dc=org" + } + ], + "page": 1, + "perPage": 10 + } +] +``` + +Status Codes: + +- **200** - Ok +- **400** - Bad Request (invalid team ID format or missing query parameter) +- **401** - Unauthorized +- **403** - Permission denied +- **500** - Internal Server Error diff --git a/public/api-enterprise-spec.json b/public/api-enterprise-spec.json index 532f039a68f..08eb4aebd31 100644 --- a/public/api-enterprise-spec.json +++ b/public/api-enterprise-spec.json @@ -2369,6 +2369,69 @@ } } } + }, + "/teams/{teamId}/groups/search": { + "get": { + "tags": [ + "sync_team_groups", + "enterprise" + ], + "summary": "Search for team groups with optional filtering and pagination.", + "operationId": "searchTeamGroups", + "parameters": [ + { + "type": "integer", + "format": "int64", + "name": "teamId", + "in": "path", + "required": true + }, + { + "type": "integer", + "format": "int64", + "default": 1, + "name": "page", + "in": "query" + }, + { + "type": "integer", + "format": "int64", + "default": 1000, + "description": "Number of items per page", + "name": "perpage", + "in": "query" + }, + { + "type": "string", + "description": "If set it will return results where the query value is contained in the name field. Query values with spaces need to be URL encoded.", + "name": "query", + "in": "query" + }, + { + "type": "string", + "description": "Filter by exact name match", + "name": "name", + "in": "query" + } + ], + "responses": { + "200": { + "$ref": "#/responses/searchTeamGroupsResponse" + }, + "400": { + "$ref": "#/responses/badRequestError" + }, + "401": { + "$ref": "#/responses/unauthorisedError" + }, + "403": { + "$ref": "#/responses/forbiddenError" + }, + "500": { + "$ref": "#/responses/internalServerError" + } + } + } } }, "definitions": { @@ -7370,6 +7433,29 @@ } } }, + "SearchTeamGroupsQueryResult": { + "type": "object", + "properties": { + "page": { + "type": "integer", + "format": "int64" + }, + "perPage": { + "type": "integer", + "format": "int64" + }, + "teamGroups": { + "type": "array", + "items": { + "$ref": "#/definitions/TeamGroupDTO" + } + }, + "totalCount": { + "type": "integer", + "format": "int64" + } + } + }, "SearchTeamQueryResult": { "type": "object", "properties": { @@ -10352,6 +10438,12 @@ "$ref": "#/definitions/SearchResult" } }, + "searchTeamGroupsResponse": { + "description": "", + "schema": { + "$ref": "#/definitions/SearchTeamGroupsQueryResult" + } + }, "searchTeamsResponse": { "description": "", "schema": { diff --git a/public/api-merged.json b/public/api-merged.json index 8da6fc47894..b7e77910163 100644 --- a/public/api-merged.json +++ b/public/api-merged.json @@ -9550,6 +9550,69 @@ } } }, + "/teams/{teamId}/groups/search": { + "get": { + "tags": [ + "sync_team_groups", + "enterprise" + ], + "summary": "Search for team groups with optional filtering and pagination.", + "operationId": "searchTeamGroups", + "parameters": [ + { + "type": "integer", + "format": "int64", + "name": "teamId", + "in": "path", + "required": true + }, + { + "type": "integer", + "format": "int64", + "default": 1, + "name": "page", + "in": "query" + }, + { + "type": "integer", + "format": "int64", + "default": 1000, + "description": "Number of items per page", + "name": "perpage", + "in": "query" + }, + { + "type": "string", + "description": "If set it will return results where the query value is contained in the name field. Query values with spaces need to be URL encoded.", + "name": "query", + "in": "query" + }, + { + "type": "string", + "description": "Filter by exact name match", + "name": "name", + "in": "query" + } + ], + "responses": { + "200": { + "$ref": "#/responses/searchTeamGroupsResponse" + }, + "400": { + "$ref": "#/responses/badRequestError" + }, + "401": { + "$ref": "#/responses/unauthorisedError" + }, + "403": { + "$ref": "#/responses/forbiddenError" + }, + "500": { + "$ref": "#/responses/internalServerError" + } + } + } + }, "/teams/{team_id}": { "get": { "tags": [ @@ -20612,6 +20675,29 @@ } } }, + "SearchTeamGroupsQueryResult": { + "type": "object", + "properties": { + "page": { + "type": "integer", + "format": "int64" + }, + "perPage": { + "type": "integer", + "format": "int64" + }, + "teamGroups": { + "type": "array", + "items": { + "$ref": "#/definitions/TeamGroupDTO" + } + }, + "totalCount": { + "type": "integer", + "format": "int64" + } + } + }, "SearchTeamQueryResult": { "type": "object", "properties": { @@ -24949,6 +25035,12 @@ "$ref": "#/definitions/SearchResult" } }, + "searchTeamGroupsResponse": { + "description": "(empty)", + "schema": { + "$ref": "#/definitions/SearchTeamGroupsQueryResult" + } + }, "searchTeamsResponse": { "description": "(empty)", "schema": { diff --git a/public/openapi3.json b/public/openapi3.json index c757125dde7..ca7f23f4414 100644 --- a/public/openapi3.json +++ b/public/openapi3.json @@ -1909,6 +1909,16 @@ }, "description": "(empty)" }, + "searchTeamGroupsResponse": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SearchTeamGroupsQueryResult" + } + } + }, + "description": "(empty)" + }, "searchTeamsResponse": { "content": { "application/json": { @@ -10641,6 +10651,29 @@ }, "type": "object" }, + "SearchTeamGroupsQueryResult": { + "properties": { + "page": { + "format": "int64", + "type": "integer" + }, + "perPage": { + "format": "int64", + "type": "integer" + }, + "teamGroups": { + "items": { + "$ref": "#/components/schemas/TeamGroupDTO" + }, + "type": "array" + }, + "totalCount": { + "format": "int64", + "type": "integer" + } + }, + "type": "object" + }, "SearchTeamQueryResult": { "properties": { "page": { @@ -23945,6 +23978,79 @@ ] } }, + "/teams/{teamId}/groups/search": { + "get": { + "operationId": "searchTeamGroups", + "parameters": [ + { + "in": "path", + "name": "teamId", + "required": true, + "schema": { + "format": "int64", + "type": "integer" + } + }, + { + "in": "query", + "name": "page", + "schema": { + "default": 1, + "format": "int64", + "type": "integer" + } + }, + { + "description": "Number of items per page", + "in": "query", + "name": "perpage", + "schema": { + "default": 1000, + "format": "int64", + "type": "integer" + } + }, + { + "description": "If set it will return results where the query value is contained in the name field. Query values with spaces need to be URL encoded.", + "in": "query", + "name": "query", + "schema": { + "type": "string" + } + }, + { + "description": "Filter by exact name match", + "in": "query", + "name": "name", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "$ref": "#/components/responses/searchTeamGroupsResponse" + }, + "400": { + "$ref": "#/components/responses/badRequestError" + }, + "401": { + "$ref": "#/components/responses/unauthorisedError" + }, + "403": { + "$ref": "#/components/responses/forbiddenError" + }, + "500": { + "$ref": "#/components/responses/internalServerError" + } + }, + "summary": "Search for team groups with optional filtering and pagination.", + "tags": [ + "sync_team_groups", + "enterprise" + ] + } + }, "/teams/{team_id}": { "delete": { "operationId": "deleteTeamByID", From c6ada816c25879e3de1f66b1f9d312e89a3b31f1 Mon Sep 17 00:00:00 2001 From: Haris Rozajac <58232930+harisrozajac@users.noreply.github.com> Date: Thu, 22 May 2025 08:18:48 -0600 Subject: [PATCH 16/73] Dashboards: Disable saving in the UI for provisioned k8s dashboards (#105429) * disable editing in UI for k8s dashboards * lint * use AnnoKeyManagerAllowsEdits; clean up * fix * add tests; update the logic * clean up --- .../saving/SaveDashboardDrawer.test.tsx | 70 +++++++++++++++- .../saving/SaveDashboardDrawer.tsx | 5 +- .../scene/DashboardScene.test.tsx | 79 ++++++++++++++++++- .../dashboard-scene/scene/DashboardScene.tsx | 16 +++- 4 files changed, 164 insertions(+), 6 deletions(-) diff --git a/public/app/features/dashboard-scene/saving/SaveDashboardDrawer.test.tsx b/public/app/features/dashboard-scene/saving/SaveDashboardDrawer.test.tsx index 7d99dea4247..d78b4971bfe 100644 --- a/public/app/features/dashboard-scene/saving/SaveDashboardDrawer.test.tsx +++ b/public/app/features/dashboard-scene/saving/SaveDashboardDrawer.test.tsx @@ -3,9 +3,12 @@ import userEvent from '@testing-library/user-event'; import { TestProvider } from 'test/helpers/TestProvider'; import { selectors } from '@grafana/e2e-selectors'; +import { config } from '@grafana/runtime'; import { sceneGraph, SceneRefreshPicker } from '@grafana/scenes'; +import { AnnoKeyManagerKind, ManagerKind } from 'app/features/apiserver/types'; import { SaveDashboardResponseDTO } from 'app/types'; +import { DashboardSceneState } from '../scene/DashboardScene'; import { transformSaveModelToScene } from '../serialization/transformSaveModelToScene'; import { transformSceneToSaveModel } from '../serialization/transformSceneToSaveModel'; @@ -152,6 +155,70 @@ describe('SaveDashboardDrawer', () => { }); }); + describe('When a dashboard is managed by an external system', () => { + beforeEach(() => { + config.featureToggles.provisioning = true; + }); + + afterEach(() => { + config.featureToggles.provisioning = false; + }); + + it('It should show the changes tab if the resource can be edited', async () => { + const { dashboard, openAndRender } = setup({ + meta: { + k8s: { + annotations: { + [AnnoKeyManagerKind]: ManagerKind.Repo, + }, + }, + }, + }); + + // just changing the title here, in real case scenario changes are reflected through migrations + // eg. panel version - same for other manager tests below + dashboard.setState({ title: 'updated title' }); + openAndRender(); + + expect(screen.queryByRole('tab', { name: /Changes/ })).toBeInTheDocument(); + }); + + it('It should not show the changes tab if the resource cannot be edited; kubectl', async () => { + const { dashboard, openAndRender } = setup({ + meta: { k8s: { annotations: { [AnnoKeyManagerKind]: ManagerKind.Kubectl } } }, + }); + + dashboard.setState({ title: 'updated title' }); + openAndRender(); + + expect(screen.queryByRole('tab', { name: /Changes/ })).not.toBeInTheDocument(); + }); + + it('It should not show the changes tab if the resource cannot be edited; terraform', async () => { + const { dashboard, openAndRender } = setup({ + meta: { k8s: { annotations: { [AnnoKeyManagerKind]: ManagerKind.Terraform } } }, + }); + + dashboard.setState({ title: 'updated title' }); + openAndRender(); + + expect(screen.queryByRole('tab', { name: /Changes/ })).not.toBeInTheDocument(); + }); + + it('It should not show the changes tab if the resource cannot be edited; plugin', async () => { + const { dashboard, openAndRender } = setup({ + meta: { + k8s: { annotations: { [AnnoKeyManagerKind]: ManagerKind.Plugin } }, + }, + }); + + dashboard.setState({ title: 'updated title' }); + openAndRender(); + + expect(screen.queryByRole('tab', { name: /Changes/ })).not.toBeInTheDocument(); + }); + }); + describe('Save as copy', () => { it('Should show save as form', async () => { const { openAndRender } = setup(); @@ -199,7 +266,7 @@ function mockSaveDashboard(options: Partial = {}) { let cleanUp = () => {}; -function setup() { +function setup(overrides?: Partial) { const dashboard = transformSaveModelToScene({ dashboard: { title: 'hello', @@ -209,6 +276,7 @@ function setup() { version: 10, }, meta: {}, + ...overrides, }); // Clear any data layers diff --git a/public/app/features/dashboard-scene/saving/SaveDashboardDrawer.tsx b/public/app/features/dashboard-scene/saving/SaveDashboardDrawer.tsx index 9bb2f70c3e7..dbc01acf19c 100644 --- a/public/app/features/dashboard-scene/saving/SaveDashboardDrawer.tsx +++ b/public/app/features/dashboard-scene/saving/SaveDashboardDrawer.tsx @@ -56,6 +56,7 @@ export class SaveDashboardDrawer extends SceneObjectBase model.setState({ showDiff: false })} /> - {changesCount > 0 && ( + {changesCount > 0 && !managedResourceCannotBeEdited && ( ; } - if (isProvisioned) { + if (isProvisioned || managedResourceCannotBeEdited) { return ; } diff --git a/public/app/features/dashboard-scene/scene/DashboardScene.test.tsx b/public/app/features/dashboard-scene/scene/DashboardScene.test.tsx index 217816dd01d..ec90b73ab67 100644 --- a/public/app/features/dashboard-scene/scene/DashboardScene.test.tsx +++ b/public/app/features/dashboard-scene/scene/DashboardScene.test.tsx @@ -1,5 +1,5 @@ import { CoreApp, GrafanaConfig, LoadingState, getDefaultTimeRange, locationUtil, store } from '@grafana/data'; -import { locationService, RefreshEvent } from '@grafana/runtime'; +import { config, locationService, RefreshEvent } from '@grafana/runtime'; import { sceneGraph, SceneGridLayout, @@ -15,6 +15,7 @@ import { import { Dashboard, DashboardCursorSync, LibraryPanel } from '@grafana/schema'; import appEvents from 'app/core/app_events'; import { LS_PANEL_COPY_KEY } from 'app/core/constants'; +import { AnnoKeyManagerKind, ManagerKind } from 'app/features/apiserver/types'; import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv'; import { VariablesChanged } from 'app/features/variables/types'; @@ -797,6 +798,82 @@ describe('DashboardScene', () => { } }); }); + + describe('When checking dashboard managed by an external system', () => { + beforeEach(() => { + config.featureToggles.provisioning = true; + }); + + afterEach(() => { + config.featureToggles.provisioning = false; + }); + + it('should return true if the dashboard is managed', () => { + const scene = buildTestScene({ + meta: { + k8s: { + annotations: { + [AnnoKeyManagerKind]: ManagerKind.Repo, + }, + }, + }, + }); + expect(scene.isManaged()).toBe(true); + }); + + it('dashboard should be editable if managed by repo', () => { + const scene = buildTestScene({ + meta: { + k8s: { + annotations: { + [AnnoKeyManagerKind]: ManagerKind.Repo, + }, + }, + }, + }); + expect(scene.managedResourceCannotBeEdited()).toBe(false); + }); + + it('dashboard should not be editable if managed by systems that do not allow edits: kubectl', () => { + const scene = buildTestScene({ + meta: { + k8s: { + annotations: { + [AnnoKeyManagerKind]: ManagerKind.Kubectl, + }, + }, + }, + }); + expect(scene.managedResourceCannotBeEdited()).toBe(true); + }); + + it('dashboard should not be editable if managed by systems that do not allow edits: terraform', () => { + const scene = buildTestScene({ + meta: { + k8s: { + annotations: { + [AnnoKeyManagerKind]: ManagerKind.Terraform, + }, + }, + }, + }); + expect(scene.managedResourceCannotBeEdited()).toBe(true); + }); + + it('dashboard should not be editable if managed by systems that do not allow edits: plugin', () => { + const scene = buildTestScene({ + meta: { + k8s: { annotations: { [AnnoKeyManagerKind]: ManagerKind.Plugin } }, + }, + }); + expect(scene.managedResourceCannotBeEdited()).toBe(true); + }); + + it('dashboard should be editable if not managed', () => { + const scene = buildTestScene(); + expect(scene.managedResourceCannotBeEdited()).toBe(false); + }); + }); }); function buildTestScene(overrides?: Partial) { diff --git a/public/app/features/dashboard-scene/scene/DashboardScene.tsx b/public/app/features/dashboard-scene/scene/DashboardScene.tsx index 1206b787fdd..718e9b87b3e 100644 --- a/public/app/features/dashboard-scene/scene/DashboardScene.tsx +++ b/public/app/features/dashboard-scene/scene/DashboardScene.tsx @@ -33,7 +33,13 @@ import { VariablesChanged } from 'app/features/variables/types'; import { DashboardDTO, DashboardMeta, KioskMode, SaveDashboardResponseDTO } from 'app/types'; import { ShowConfirmModalEvent } from 'app/types/events'; -import { AnnoKeyManagerKind, AnnoKeySourcePath, ManagerKind, ResourceForCreate } from '../../apiserver/types'; +import { + AnnoKeyManagerAllowsEdits, + AnnoKeyManagerKind, + AnnoKeySourcePath, + ManagerKind, + ResourceForCreate, +} from '../../apiserver/types'; import { DashboardEditPane } from '../edit-pane/DashboardEditPane'; import { PanelEditor } from '../panel-edit/PanelEditor'; import { DashboardSceneChangeTracker } from '../saving/DashboardSceneChangeTracker'; @@ -311,7 +317,7 @@ export class DashboardScene extends SceneObjectBase impleme return; } - if (!this.state.isDirty || skipConfirm) { + if (!this.state.isDirty || skipConfirm || this.managedResourceCannotBeEdited()) { this.exitEditModeConfirmed(restoreInitialState || this.state.isDirty); return; } @@ -804,6 +810,12 @@ export class DashboardScene extends SceneObjectBase impleme return Boolean(this.getManagerKind() === ManagerKind.Repo); } + managedResourceCannotBeEdited() { + return ( + this.isManaged() && !this.isManagedRepository() && !this.state.meta.k8s?.annotations?.[AnnoKeyManagerAllowsEdits] + ); + } + getPath() { return this.state.meta.k8s?.annotations?.[AnnoKeySourcePath]; } From f984c4cbe7c0cdc388afccb932428b07ca04eb70 Mon Sep 17 00:00:00 2001 From: Isabel Matwawana <76437239+imatwawana@users.noreply.github.com> Date: Thu, 22 May 2025 10:34:58 -0400 Subject: [PATCH 17/73] Docs: Fixed broken links (#105796) --- .../observability-as-code/schema-v2/_index.md | 22 +++++++++---------- .../schema-v2/layout-schema.md | 18 +++++++-------- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/docs/sources/observability-as-code/schema-v2/_index.md b/docs/sources/observability-as-code/schema-v2/_index.md index 207b85dc891..c86fd130818 100644 --- a/docs/sources/observability-as-code/schema-v2/_index.md +++ b/docs/sources/observability-as-code/schema-v2/_index.md @@ -148,31 +148,31 @@ The table includes default and other fields: ### `annotations` The configuration for the list of annotations that are associated with the dashboard. -For the JSON and field usage notes, refer to the [annotations schema documentation](https://grafana.com/docs/grafana//observability-as-code/json-models/schema-v2/annotations-schema/). +For the JSON and field usage notes, refer to the [annotations schema documentation](https://grafana.com/docs/grafana//observability-as-code/schema-v2/annotations-schema/). ### `elements` Dashboards can contain the following elements: -- [PanelKind](https://grafana.com/docs/grafana//observability-as-code/json-models/schema-v2/panel-schema/) -- [LibraryPanelKind](https://grafana.com/docs/grafana//observability-as-code/json-models/schema-v2/librarypanel-schema/) +- [PanelKind](https://grafana.com/docs/grafana//observability-as-code/schema-v2/panel-schema/) +- [LibraryPanelKind](https://grafana.com/docs/grafana//observability-as-code/schema-v2/librarypanel-schema/) ### `layout` Dashboards can have four layout options: -- [GridLayoutKind](https://grafana.com/docs/grafana//observability-as-code/json-models/schema-v2/layout-schema/#gridlayoutkind) -- [AutoGridLayoutKind](https://grafana.com/docs/grafana//observability-as-code/json-models/schema-v2/layout-schema/#autogridlayoutkind) -- [RowsLayoutKind](https://grafana.com/docs/grafana//observability-as-code/json-models/schema-v2/layout-schema/#rowslayoutkind) -- [TabsLayoutKind](https://grafana.com/docs/grafana//observability-as-code/json-models/schema-v2/layout-schema/#tabslayoutkind) +- [GridLayoutKind](https://grafana.com/docs/grafana//observability-as-code/schema-v2/layout-schema/#gridlayoutkind) +- [AutoGridLayoutKind](https://grafana.com/docs/grafana//observability-as-code/schema-v2/layout-schema/#autogridlayoutkind) +- [RowsLayoutKind](https://grafana.com/docs/grafana//observability-as-code/schema-v2/layout-schema/#rowslayoutkind) +- [TabsLayoutKind](https://grafana.com/docs/grafana//observability-as-code/schema-v2/layout-schema/#tabslayoutkind) -For the JSON and field usage notes about each of these, refer to the [layout schema documentation](https://grafana.com/docs/grafana//observability-as-code/json-models/schema-v2/layout-schema/). +For the JSON and field usage notes about each of these, refer to the [layout schema documentation](https://grafana.com/docs/grafana//observability-as-code/schema-v2/layout-schema/). ### `links` The configuration for links with references to other dashboards or external websites. -For the JSON and field usage notes, refer to the [links schema documentation](https://grafana.com/docs/grafana//observability-as-code/json-models/schema-v2/links-schema/). +For the JSON and field usage notes, refer to the [links schema documentation](https://grafana.com/docs/grafana//observability-as-code/schema-v2/links-schema/). ### `tags` @@ -183,7 +183,7 @@ The tags associated with the dashboard: ### `timesettings` The `TimeSettingsSpec` defines the default time configuration for the time picker and the refresh picker for the specific dashboard. -For the JSON and field usage notes about the `TimeSettingsSpec`, refer to the [timesettings schema documentation](https://grafana.com/docs/grafana//observability-as-code/json-models/schema-v2/timesettings-schema/). +For the JSON and field usage notes about the `TimeSettingsSpec`, refer to the [timesettings schema documentation](https://grafana.com/docs/grafana//observability-as-code/schema-v2/timesettings-schema/). ### `variables` @@ -200,7 +200,7 @@ There are eight variables types: - GroupByVariableKind - AdhocVariableKind -For the JSON and field usage notes about the `variables` spec, refer to the [variables schema documentation](https://grafana.com/docs/grafana//observability-as-code/json-models/schema-v2/variables-schema/). +For the JSON and field usage notes about the `variables` spec, refer to the [variables schema documentation](https://grafana.com/docs/grafana//observability-as-code/schema-v2/variables-schema/). ## Notes and limitations diff --git a/docs/sources/observability-as-code/schema-v2/layout-schema.md b/docs/sources/observability-as-code/schema-v2/layout-schema.md index f9654286929..71bd7e30399 100644 --- a/docs/sources/observability-as-code/schema-v2/layout-schema.md +++ b/docs/sources/observability-as-code/schema-v2/layout-schema.md @@ -87,14 +87,14 @@ Following is the JSON for a default grid layout, a grid layout item, and a grid The following table explains the usage of the grid layout item JSON fields: -| Name | Usage | -| ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| x | integer. Position of the item x-axis. | -| y | integer. Position of the item y-axis. | -| width | Width of the item in pixels. | -| height | Height of the item in pixels. | -| element | `ElementReference`. Reference to a [`PanelKind`](https://grafana.com/docs/grafana//observability-as-code/json-models/schema-v2/panel-schema/) from `dashboard.spec.elements` expressed as JSON Schema reference. | -| repeat? | [RepeatOptions](#repeatoptions). Configured repeat options, if any | +| Name | Usage | +| ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| x | integer. Position of the item x-axis. | +| y | integer. Position of the item y-axis. | +| width | Width of the item in pixels. | +| height | Height of the item in pixels. | +| element | `ElementReference`. Reference to a [`PanelKind`](https://grafana.com/docs/grafana//observability-as-code/schema-v2/panel-schema/) from `dashboard.spec.elements` expressed as JSON Schema reference. | +| repeat? | [RepeatOptions](#repeatoptions). Configured repeat options, if any | #### `RepeatOptions` @@ -188,7 +188,7 @@ The following table explains the usage of the auto grid layout item JSON fields: | Name | Usage | | ---- | ----- | -| element | `ElementReference`. Reference to a [`PanelKind`](https://grafana.com/docs/grafana//observability-as-code/json-models/schema-v2/panel-schema/) from `dashboard.spec.elements` expressed as JSON Schema reference. | +| element | `ElementReference`. Reference to a [`PanelKind`](https://grafana.com/docs/grafana//observability-as-code/schema-v2/panel-schema/) from `dashboard.spec.elements` expressed as JSON Schema reference. | | repeat? | [AutoGridRepeatOptions](#autogridrepeatoptions). Configured repeat options, if any. | | conditionalRendering? | `ConditionalRenderingGroupKind`. Rules for hiding or showing panels, if any. Consists of:
  • kind: "ConditionalRenderingGroup"
  • spec: [ConditionalRenderingGroupSpec](#conditionalrenderinggroupspec)
| From b3fac7a2db2094d1482718f1e32bef022068f42a Mon Sep 17 00:00:00 2001 From: Kevin Minehart <5140827+kminehart@users.noreply.github.com> Date: Thu, 22 May 2025 09:35:52 -0500 Subject: [PATCH 18/73] Chore: Update changelog (#105794) * Update changelog --------- Co-authored-by: github-actions[bot] --- CHANGELOG.md | 63 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index aed72ee265b..831bc77a751 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,66 @@ + + +# 12.0.0+security-01 (2025-05-21) + +### Bug fixes + +- **Security:** Fix CVE-2025-4123 + + + + +# 11.6.1+security-01 (2025-05-21) + +### Bug fixes + +- **Security:** Fix CVE-2025-4123 + + + + +# 11.5.4+security-01 (2025-05-21) + +### Bug fixes + +- **Security:** Fix CVE-2025-4123 + + + + +# 11.4.4+security-01 (2025-05-21) + +### Bug fixes + +- **Security:** Fix CVE-2025-4123 + + + + +# 11.3.6+security-01 (2025-05-21) + +### Bug fixes + +- **Security:** Fix CVE-2025-4123 + + + + +# 11.2.9+security-01 (2025-05-21) + +### Bug fixes + +- **Security:** Fix CVE-2025-4123 + + + + +# 10.4.18+security-01 (2025-05-21) + +### Bug fixes + +- **Security:** Fix CVE-2025-4123 + + # 12.0.0 (2025-05-05) From c98792d92436deb225afbd326c93c09ef9d7d110 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Irene=20Rodr=C3=ADguez?= Date: Thu, 22 May 2025 16:37:50 +0200 Subject: [PATCH 19/73] add new sentence about GH enterprise (#105816) --- .../sources/observability-as-code/provision-resources/_index.md | 2 +- .../provision-resources/file-path-setup.md | 2 +- .../observability-as-code/provision-resources/git-sync-setup.md | 2 +- .../observability-as-code/provision-resources/intro-git-sync.md | 2 +- .../provision-resources/provisioned-dashboards.md | 2 +- .../observability-as-code/provision-resources/use-git-sync.md | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/sources/observability-as-code/provision-resources/_index.md b/docs/sources/observability-as-code/provision-resources/_index.md index d8b06048917..f6eb97af7be 100644 --- a/docs/sources/observability-as-code/provision-resources/_index.md +++ b/docs/sources/observability-as-code/provision-resources/_index.md @@ -18,7 +18,7 @@ weight: 300 # Provision resources and sync dashboards {{< admonition type="caution" >}} -Provisioning is an [experimental feature](https://grafana.com/docs/release-life-cycle/) introduced in Grafana v12 for open source and Enterprise editions. Engineering and on-call support is not available. Documentation is either limited or not provided outside of code comments. No SLA is provided. This feature is not publicly available in Grafana Cloud yet. +Provisioning is an [experimental feature](https://grafana.com/docs/release-life-cycle/) introduced in Grafana v12 for open source and Enterprise editions. Engineering and on-call support is not available. Documentation is either limited or not provided outside of code comments. No SLA is provided. This feature is not publicly available in Grafana Cloud yet. Only the cloud-hosted version of GitHub (GitHub.com) is supported at this time. GitHub Enterprise is not yet compatible. Sign up for Grafana Cloud Git Sync early access using [this form](https://forms.gle/WKkR3EVMcbqsNnkD9). {{< /admonition >}} diff --git a/docs/sources/observability-as-code/provision-resources/file-path-setup.md b/docs/sources/observability-as-code/provision-resources/file-path-setup.md index 830728270d3..b5e922456b6 100644 --- a/docs/sources/observability-as-code/provision-resources/file-path-setup.md +++ b/docs/sources/observability-as-code/provision-resources/file-path-setup.md @@ -16,7 +16,7 @@ weight: 200 # Set up file provisioning {{< admonition type="caution" >}} -Local file provisioning is an [experimental feature](https://grafana.com/docs/release-life-cycle/) introduced in Grafana v12 for open source and Enterprise editions. 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 `provisioning` and `kubernetesDashboards` feature toggles in Grafana to use this feature. This feature is not publicly available in Grafana Cloud yet. +Local file provisioning is an [experimental feature](https://grafana.com/docs/release-life-cycle/) introduced in Grafana v12 for open source and Enterprise editions. 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 `provisioning` and `kubernetesDashboards` feature toggles in Grafana to use this feature. This feature is not publicly available in Grafana Cloud yet. Only the cloud-hosted version of GitHub (GitHub.com) is supported at this time. GitHub Enterprise is not yet compatible. Sign up for Grafana Cloud Git Sync early access using [this form](https://forms.gle/WKkR3EVMcbqsNnkD9). diff --git a/docs/sources/observability-as-code/provision-resources/git-sync-setup.md b/docs/sources/observability-as-code/provision-resources/git-sync-setup.md index 213d8da2961..4a81b71a197 100644 --- a/docs/sources/observability-as-code/provision-resources/git-sync-setup.md +++ b/docs/sources/observability-as-code/provision-resources/git-sync-setup.md @@ -16,7 +16,7 @@ weight: 100 # Set up Git Sync {{< admonition type="caution" >}} -Git Sync is an [experimental feature](https://grafana.com/docs/release-life-cycle/) introduced in Grafana v12 for open source and Enterprise editions. 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 `provisioning` and `kubernetesDashboards` feature toggles in Grafana to use this feature. This feature is not publicly available in Grafana Cloud yet. +Git Sync is an [experimental feature](https://grafana.com/docs/release-life-cycle/) introduced in Grafana v12 for open source and Enterprise editions. 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 `provisioning` and `kubernetesDashboards` feature toggles in Grafana to use this feature. This feature is not publicly available in Grafana Cloud yet. Only the cloud-hosted version of GitHub (GitHub.com) is supported at this time. GitHub Enterprise is not yet compatible. Sign up for Grafana Cloud Git Sync early access using [this form](https://forms.gle/WKkR3EVMcbqsNnkD9). diff --git a/docs/sources/observability-as-code/provision-resources/intro-git-sync.md b/docs/sources/observability-as-code/provision-resources/intro-git-sync.md index 0a953c9c5d9..3ebcd0bd2e8 100644 --- a/docs/sources/observability-as-code/provision-resources/intro-git-sync.md +++ b/docs/sources/observability-as-code/provision-resources/intro-git-sync.md @@ -16,7 +16,7 @@ weight: 100 # Git Sync {{< admonition type="caution" >}} -Git Sync is an [experimental feature](https://grafana.com/docs/release-life-cycle/) introduced in Grafana v12 for open source and Enterprise editions. 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 `provisioning` and `kubernetesDashboards` feature toggles in Grafana to use this feature. This feature is not publicly available in Grafana Cloud yet. +Git Sync is an [experimental feature](https://grafana.com/docs/release-life-cycle/) introduced in Grafana v12 for open source and Enterprise editions. 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 `provisioning` and `kubernetesDashboards` feature toggles in Grafana to use this feature. This feature is not publicly available in Grafana Cloud yet. Only the cloud-hosted version of GitHub (GitHub.com) is supported at this time. GitHub Enterprise is not yet compatible. Sign up for Grafana Cloud Git Sync early access using [this form](https://forms.gle/WKkR3EVMcbqsNnkD9). diff --git a/docs/sources/observability-as-code/provision-resources/provisioned-dashboards.md b/docs/sources/observability-as-code/provision-resources/provisioned-dashboards.md index 6ae40c456a4..8ab3afa9c03 100644 --- a/docs/sources/observability-as-code/provision-resources/provisioned-dashboards.md +++ b/docs/sources/observability-as-code/provision-resources/provisioned-dashboards.md @@ -16,7 +16,7 @@ weight: 300 # Work with provisioned dashboards {{< admonition type="caution" >}} -Git Sync and File path provisioning an [experimental feature](https://grafana.com/docs/release-life-cycle/) introduced in Grafana v12 for open source and Enterprise editions. 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 `provisioning` and `kubernetesDashboards` feature toggles in Grafana. These features aren't available publicly in Grafana Cloud yet. +Git Sync and File path provisioning an [experimental feature](https://grafana.com/docs/release-life-cycle/) introduced in Grafana v12 for open source and Enterprise editions. 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 `provisioning` and `kubernetesDashboards` feature toggles in Grafana. These features aren't available publicly in Grafana Cloud yet. Only the cloud-hosted version of GitHub (GitHub.com) is supported at this time. GitHub Enterprise is not yet compatible. Sign up for Grafana Cloud Git Sync early access using [this form](https://forms.gle/WKkR3EVMcbqsNnkD9). diff --git a/docs/sources/observability-as-code/provision-resources/use-git-sync.md b/docs/sources/observability-as-code/provision-resources/use-git-sync.md index acb9b77dad6..f54d43347dd 100644 --- a/docs/sources/observability-as-code/provision-resources/use-git-sync.md +++ b/docs/sources/observability-as-code/provision-resources/use-git-sync.md @@ -19,7 +19,7 @@ weight: 400 # Manage provisioned repositories with Git Sync {{< admonition type="caution" >}} -Git Sync is an [experimental feature](https://grafana.com/docs/release-life-cycle/) introduced in Grafana v12 for open source and Enterprise editions. 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 `provisioning` and `kubernetesDashboards` feature toggles in Grafana to use this feature. This feature is not publicly available in Grafana Cloud yet. +Git Sync is an [experimental feature](https://grafana.com/docs/release-life-cycle/) introduced in Grafana v12 for open source and Enterprise editions. 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 `provisioning` and `kubernetesDashboards` feature toggles in Grafana to use this feature. This feature is not publicly available in Grafana Cloud yet. Only the cloud-hosted version of GitHub (GitHub.com) is supported at this time. GitHub Enterprise is not yet compatible. Sign up for Grafana Cloud Git Sync early access using [this form](https://forms.gle/WKkR3EVMcbqsNnkD9). From bc3679957bb1ee40aa67e3cf6bc273dd1867d70a Mon Sep 17 00:00:00 2001 From: Kristina Date: Thu, 22 May 2025 09:43:35 -0500 Subject: [PATCH 20/73] Transformations: Add documentation around new behavior of Calculate field (#104429) * Docs changes * remove generated placeholder for binary ops with variables * Update public/app/features/transformers/docs/content.ts Co-authored-by: Isabel Matwawana <76437239+imatwawana@users.noreply.github.com> * Update public/app/features/transformers/docs/content.ts Co-authored-by: Isabel Matwawana <76437239+imatwawana@users.noreply.github.com> --------- Co-authored-by: Isabel Matwawana <76437239+imatwawana@users.noreply.github.com> --- .../query-transform-data/transform-data/index.md | 1 + .../src/transformations/transformers/calculateField.ts | 6 +++--- public/app/features/transformers/docs/content.ts | 1 + 3 files changed, 5 insertions(+), 3 deletions(-) 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 2748f83d97d..ad67cc099c8 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 @@ -201,6 +201,7 @@ Use this transformation to add a new field calculated from two other fields. Eac - **All number fields** - Set the left side of a **Binary operation** to apply the calculation to all number fields. - **As percentile** - If you select **Row index** mode, then the **As percentile** switch appears. This switch allows you to transform the row index as a percentage of the total number of rows. - **Alias** - (Optional) Enter the name of your new field. If you leave this blank, then the field will be named to match the calculation. + > **Note:** If a variable will be used in this transformation, the default alias will be interpolated with the value of the variable. Please explicitly define an alias if you would like the alias to not be affected by variable changes. - **Replace all fields** - (Optional) Select this option if you want to hide all other fields and display only your calculated field in the visualization. In the example below, we added two fields together and named them Sum. diff --git a/packages/grafana-data/src/transformations/transformers/calculateField.ts b/packages/grafana-data/src/transformations/transformers/calculateField.ts index 8b290f3cbc9..1e9baf3f872 100644 --- a/packages/grafana-data/src/transformations/transformers/calculateField.ts +++ b/packages/grafana-data/src/transformations/transformers/calculateField.ts @@ -695,9 +695,9 @@ export function getNameFromOptions(options: CalculateFieldTransformerOptions) { const { binary } = options; const alias = `${binary?.left?.matcher?.options ?? binary?.left?.fixed ?? ''} ${binary?.operator ?? ''} ${binary?.right?.matcher?.options ?? binary?.right?.fixed ?? ''}`; - //Remove $ signs as they will be interpolated and cause issues. Variables can still be used - //in alias but shouldn't in the autogenerated name - return alias.replace(/\$/g, ''); + // binary calculations with variables will be interpolated on the visualization but we don't want to do that here, so just give a blank placeholder + const variableFound = /\$/g.test(alias); + return variableFound ? '' : alias; } case CalculateFieldMode.ReduceRow: { diff --git a/public/app/features/transformers/docs/content.ts b/public/app/features/transformers/docs/content.ts index 25d0a99ce0b..191d1b9af31 100644 --- a/public/app/features/transformers/docs/content.ts +++ b/public/app/features/transformers/docs/content.ts @@ -67,6 +67,7 @@ Use this transformation to add a new field calculated from two other fields. Eac - **All number fields** - Set the left side of a **Binary operation** to apply the calculation to all number fields. - **As percentile** - If you select **Row index** mode, then the **As percentile** switch appears. This switch allows you to transform the row index as a percentage of the total number of rows. - **Alias** - (Optional) Enter the name of your new field. If you leave this blank, then the field will be named to match the calculation. +> **Note:** If a variable is used in this transformation, the default alias will be interpolated with the value of the variable. If you want an alias to be unaffected by variable changes, explicitly define the alias. - **Replace all fields** - (Optional) Select this option if you want to hide all other fields and display only your calculated field in the visualization. In the example below, we added two fields together and named them Sum. From a37953bb6fcd56d59a076c202eb5c9b1e978c6d0 Mon Sep 17 00:00:00 2001 From: Kevin Minehart <5140827+kminehart@users.noreply.github.com> Date: Thu, 22 May 2025 09:49:38 -0500 Subject: [PATCH 21/73] CI: remove go-version flag (#105844) remove go-version flag --- pkg/build/daggerbuild/scripts/drone_build_main_enterprise.sh | 1 - pkg/build/daggerbuild/scripts/drone_build_main_pro.sh | 1 - .../daggerbuild/scripts/drone_build_nightly_enterprise.sh | 3 +-- pkg/build/daggerbuild/scripts/drone_build_nightly_grafana.sh | 1 - pkg/build/daggerbuild/scripts/drone_build_tag_enterprise.sh | 1 - pkg/build/daggerbuild/scripts/drone_build_tag_grafana.sh | 1 - pkg/build/daggerbuild/scripts/drone_build_tag_pro.sh | 1 - 7 files changed, 1 insertion(+), 8 deletions(-) diff --git a/pkg/build/daggerbuild/scripts/drone_build_main_enterprise.sh b/pkg/build/daggerbuild/scripts/drone_build_main_enterprise.sh index ee87d4842c0..a7031aa29c8 100755 --- a/pkg/build/daggerbuild/scripts/drone_build_main_enterprise.sh +++ b/pkg/build/daggerbuild/scripts/drone_build_main_enterprise.sh @@ -25,7 +25,6 @@ dagger run --silent go run ./pkg/build/cmd \ --grafana-repo="https://github.com/grafana/grafana.git" \ --enterprise-ref=${DRONE_COMMIT} \ --github-token=${GITHUB_TOKEN} \ - --go-version=${GO_VERSION} \ --ubuntu-base=${UBUNTU_BASE} \ --alpine-base=${ALPINE_BASE} \ --patches-repo=${PATCHES_REPO} \ diff --git a/pkg/build/daggerbuild/scripts/drone_build_main_pro.sh b/pkg/build/daggerbuild/scripts/drone_build_main_pro.sh index fe276c98872..1a3345a39b5 100755 --- a/pkg/build/daggerbuild/scripts/drone_build_main_pro.sh +++ b/pkg/build/daggerbuild/scripts/drone_build_main_pro.sh @@ -21,7 +21,6 @@ dagger run --silent go run ./pkg/build/cmd \ --grafana-repo="https://github.com/grafana/grafana.git" \ --enterprise-ref=${DRONE_COMMIT} \ --github-token=${GITHUB_TOKEN} \ - --go-version=${GO_VERSION} \ --ubuntu-base=${UBUNTU_BASE} \ --alpine-base=${ALPINE_BASE} \ --patches-repo=${PATCHES_REPO} \ diff --git a/pkg/build/daggerbuild/scripts/drone_build_nightly_enterprise.sh b/pkg/build/daggerbuild/scripts/drone_build_nightly_enterprise.sh index dd1ce54153e..f93eb94d604 100755 --- a/pkg/build/daggerbuild/scripts/drone_build_nightly_enterprise.sh +++ b/pkg/build/daggerbuild/scripts/drone_build_nightly_enterprise.sh @@ -12,7 +12,7 @@ docker run --privileged --rm tonistiigi/binfmt:qemu-v7.0.0-28 --install all # -a deb:enterprise:linux/arm/v7:nightly \ # -a docker:enterprise:linux/arm/v7 \ # -a docker:enterprise:linux/arm/v7:ubuntu \ - + dagger run --silent go run ./pkg/build/cmd \ artifacts \ -a targz:enterprise:linux/amd64 \ @@ -46,7 +46,6 @@ dagger run --silent go run ./pkg/build/cmd \ --github-token=${GITHUB_TOKEN} \ --destination=${local_dst} \ --yarn-cache=${YARN_CACHE_FOLDER} \ - --go-version=${GO_VERSION} \ --ubuntu-base="${UBUNTU_BASE}" \ --alpine-base="${ALPINE_BASE}" > assets.txt diff --git a/pkg/build/daggerbuild/scripts/drone_build_nightly_grafana.sh b/pkg/build/daggerbuild/scripts/drone_build_nightly_grafana.sh index 4b4c4d3a1a4..11c071e6ed8 100755 --- a/pkg/build/daggerbuild/scripts/drone_build_nightly_grafana.sh +++ b/pkg/build/daggerbuild/scripts/drone_build_nightly_grafana.sh @@ -37,7 +37,6 @@ dagger run --silent go run ./pkg/build/cmd \ --github-token=${GITHUB_TOKEN} \ --destination=${local_dst} \ --yarn-cache=${YARN_CACHE_FOLDER} \ - --go-version=${GO_VERSION} \ --ubuntu-base="${UBUNTU_BASE}" \ --alpine-base="${ALPINE_BASE}" > assets.txt diff --git a/pkg/build/daggerbuild/scripts/drone_build_tag_enterprise.sh b/pkg/build/daggerbuild/scripts/drone_build_tag_enterprise.sh index c1598149934..a30870fed6a 100755 --- a/pkg/build/daggerbuild/scripts/drone_build_tag_enterprise.sh +++ b/pkg/build/daggerbuild/scripts/drone_build_tag_enterprise.sh @@ -42,7 +42,6 @@ dagger run --silent go run ./pkg/build/cmd \ --grafana-ref=${DRONE_TAG} \ --grafana-repo=https://github.com/grafana/grafana-security-mirror.git \ --github-token=${GITHUB_TOKEN} \ - --go-version=${GO_VERSION} \ --ubuntu-base="${UBUNTU_BASE}" \ --alpine-base="${ALPINE_BASE}" \ --version=${DRONE_TAG} \ diff --git a/pkg/build/daggerbuild/scripts/drone_build_tag_grafana.sh b/pkg/build/daggerbuild/scripts/drone_build_tag_grafana.sh index 766b3e47e7a..d28e703ef93 100755 --- a/pkg/build/daggerbuild/scripts/drone_build_tag_grafana.sh +++ b/pkg/build/daggerbuild/scripts/drone_build_tag_grafana.sh @@ -39,7 +39,6 @@ dagger run --silent go run ./pkg/build/cmd \ --build-id=${DRONE_BUILD_NUMBER} \ --grafana-dir=${GRAFANA_DIR} \ --github-token=${GITHUB_TOKEN} \ - --go-version=${GO_VERSION} \ --ubuntu-base="${UBUNTU_BASE}" \ --alpine-base="${ALPINE_BASE}" \ --version=${DRONE_TAG} \ diff --git a/pkg/build/daggerbuild/scripts/drone_build_tag_pro.sh b/pkg/build/daggerbuild/scripts/drone_build_tag_pro.sh index 0578f2e96c4..f7dd689efad 100755 --- a/pkg/build/daggerbuild/scripts/drone_build_tag_pro.sh +++ b/pkg/build/daggerbuild/scripts/drone_build_tag_pro.sh @@ -33,7 +33,6 @@ dagger run --silent go run ./pkg/build/cmd \ --grafana-repo=https://github.com/grafana/grafana-security-mirror.git \ --github-token=${GITHUB_TOKEN} \ --version=${DRONE_TAG} \ - --go-version=${GO_VERSION} \ --ubuntu-base="${UBUNTU_BASE}" \ --alpine-base="${ALPINE_BASE}" \ --destination=${local_dst} > assets.txt From 95eabcf03d57a161a5c350b19647f24144f871fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-Philippe=20Qu=C3=A9m=C3=A9ner?= Date: Thu, 22 May 2025 17:12:31 +0200 Subject: [PATCH 22/73] refactor(unified-storage): rename cache to avoid import conflicts (#105831) --- pkg/storage/unified/resource/broadcaster.go | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/pkg/storage/unified/resource/broadcaster.go b/pkg/storage/unified/resource/broadcaster.go index 547bfe576e8..febf13af583 100644 --- a/pkg/storage/unified/resource/broadcaster.go +++ b/pkg/storage/unified/resource/broadcaster.go @@ -250,7 +250,7 @@ type channelCache[T any] interface { ReadInto(dst chan T) error } -type cache[T any] struct { +type localCache[T any] struct { cache []T size int cacheZero int @@ -261,7 +261,7 @@ type cache[T any] struct { } func newChannelCache[T any](ctx context.Context, size int) channelCache[T] { - c := &cache[T]{} + c := &localCache[T]{} c.ctx = ctx if size <= 0 { @@ -278,15 +278,15 @@ func newChannelCache[T any](ctx context.Context, size int) channelCache[T] { return c } -func (c *cache[T]) Len() int { +func (c *localCache[T]) Len() int { return c.cacheLen } -func (c *cache[T]) Add(item T) { +func (c *localCache[T]) Add(item T) { c.add <- item } -func (c *cache[T]) run() { +func (c *localCache[T]) run() { for { select { case <-c.ctx.Done(): @@ -314,7 +314,7 @@ func (c *cache[T]) run() { } } -func (c *cache[T]) Get(i int) T { +func (c *localCache[T]) Get(i int) T { r := make(chan T, c.size) c.read <- r idx := 0 @@ -328,7 +328,7 @@ func (c *cache[T]) Get(i int) T { return zero } -func (c *cache[T]) Range(f func(T) error) error { +func (c *localCache[T]) Range(f func(T) error) error { r := make(chan T, c.size) c.read <- r for item := range r { @@ -340,7 +340,7 @@ func (c *cache[T]) Range(f func(T) error) error { return nil } -func (c *cache[T]) Slice() []T { +func (c *localCache[T]) Slice() []T { s := make([]T, 0, c.size) r := make(chan T, c.size) c.read <- r @@ -350,7 +350,7 @@ func (c *cache[T]) Slice() []T { return s } -func (c *cache[T]) ReadInto(dst chan T) error { +func (c *localCache[T]) ReadInto(dst chan T) error { r := make(chan T, c.size) c.read <- r for item := range r { From 08fdc956355e220b777777527bfb1772837873bd Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 22 May 2025 08:21:57 -0700 Subject: [PATCH 23/73] Release: update changelog for 10.4.19 (#105840) * Update changelog * empty commit --------- Co-authored-by: github-actions[bot] Co-authored-by: idastambuk --- CHANGELOG.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 831bc77a751..d57ef13c2d7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,13 @@ + + +# 10.4.19 (2025-05-22) + +### Features and enhancements + +- **Chore:** Bump Go version to 1.24.3 [#105115](https://github.com/grafana/grafana/pull/105115), [@macabu](https://github.com/macabu) +- **Dependencies:** Bump golang.org/x/net from v0.37.0 to v0.40.0 [#105449](https://github.com/grafana/grafana/pull/105449), [@macabu](https://github.com/macabu) + + # 12.0.0+security-01 (2025-05-21) From e433af003f64ef8a98d3ae993775be2b261a02a7 Mon Sep 17 00:00:00 2001 From: Kevin Minehart <5140827+kminehart@users.noreply.github.com> Date: Thu, 22 May 2025 11:01:39 -0500 Subject: [PATCH 24/73] CI: dagger run missing go run (#105778) * dagger run missing go run * Add missing build ID arg From e389407907880bf39e6367fe7009bfa2fb294d80 Mon Sep 17 00:00:00 2001 From: Smit Patel <11639260+sm17p@users.noreply.github.com> Date: Thu, 22 May 2025 12:08:46 -0400 Subject: [PATCH 25/73] Grafana-[data,sql,ui]: Use named imports (#105734) grafana-[data,sql,ui]: use named imports --- packages/grafana-data/src/field/fieldOverrides.ts | 2 +- .../src/components/query-editor-raw/QueryValidator.tsx | 3 +-- .../src/components/visual-query-builder/SQLWhereRow.tsx | 2 +- packages/grafana-ui/src/components/DataLinks/DataLinkInput.tsx | 2 +- .../src/components/DataLinks/DataLinkSuggestions.tsx | 2 +- packages/grafana-ui/src/components/Input/Input.tsx | 2 +- .../grafana-ui/src/components/Segment/SegmentAsync.story.tsx | 2 +- packages/grafana-ui/src/components/Segment/SegmentAsync.tsx | 2 +- packages/grafana-ui/src/components/Segment/SegmentInput.tsx | 2 +- 9 files changed, 9 insertions(+), 10 deletions(-) diff --git a/packages/grafana-data/src/field/fieldOverrides.ts b/packages/grafana-data/src/field/fieldOverrides.ts index 97362791486..6b29551e1c0 100644 --- a/packages/grafana-data/src/field/fieldOverrides.ts +++ b/packages/grafana-data/src/field/fieldOverrides.ts @@ -1,6 +1,6 @@ import { isNumber, set, unset, get, cloneDeep } from 'lodash'; import { useMemo, useRef } from 'react'; -import usePrevious from 'react-use/lib/usePrevious'; +import { usePrevious } from 'react-use'; import { ThresholdsMode, VariableFormatID } from '@grafana/schema'; diff --git a/packages/grafana-sql/src/components/query-editor-raw/QueryValidator.tsx b/packages/grafana-sql/src/components/query-editor-raw/QueryValidator.tsx index 5ababb2f236..0f86f8aeb50 100644 --- a/packages/grafana-sql/src/components/query-editor-raw/QueryValidator.tsx +++ b/packages/grafana-sql/src/components/query-editor-raw/QueryValidator.tsx @@ -1,7 +1,6 @@ import { css } from '@emotion/css'; import { useState, useMemo, useEffect } from 'react'; -import { useAsyncFn } from 'react-use'; -import useDebounce from 'react-use/lib/useDebounce'; +import { useAsyncFn, useDebounce } from 'react-use'; import { formattedValueToString, getValueFormat, TimeRange } from '@grafana/data'; import { Icon, Spinner, useTheme2 } from '@grafana/ui'; diff --git a/packages/grafana-sql/src/components/visual-query-builder/SQLWhereRow.tsx b/packages/grafana-sql/src/components/visual-query-builder/SQLWhereRow.tsx index 95ac8676511..8a77663398f 100644 --- a/packages/grafana-sql/src/components/visual-query-builder/SQLWhereRow.tsx +++ b/packages/grafana-sql/src/components/visual-query-builder/SQLWhereRow.tsx @@ -1,4 +1,4 @@ -import useAsync from 'react-use/lib/useAsync'; +import { useAsync } from 'react-use'; import { SelectableValue, TypedVariableModel } from '@grafana/data'; import { getTemplateSrv } from '@grafana/runtime'; diff --git a/packages/grafana-ui/src/components/DataLinks/DataLinkInput.tsx b/packages/grafana-ui/src/components/DataLinks/DataLinkInput.tsx index 5f91741bfd4..6b808b44542 100644 --- a/packages/grafana-ui/src/components/DataLinks/DataLinkInput.tsx +++ b/packages/grafana-ui/src/components/DataLinks/DataLinkInput.tsx @@ -3,7 +3,7 @@ import { autoUpdate, flip, offset, shift, useFloating } from '@floating-ui/react import Prism, { Grammar, LanguageMap } from 'prismjs'; import { memo, useEffect, useRef, useState } from 'react'; import * as React from 'react'; -import usePrevious from 'react-use/lib/usePrevious'; +import { usePrevious } from 'react-use'; import { Value } from 'slate'; import Plain from 'slate-plain-serializer'; import { Editor } from 'slate-react'; diff --git a/packages/grafana-ui/src/components/DataLinks/DataLinkSuggestions.tsx b/packages/grafana-ui/src/components/DataLinks/DataLinkSuggestions.tsx index 9d943c4e0c3..1308657df35 100644 --- a/packages/grafana-ui/src/components/DataLinks/DataLinkSuggestions.tsx +++ b/packages/grafana-ui/src/components/DataLinks/DataLinkSuggestions.tsx @@ -2,7 +2,7 @@ import { css, cx } from '@emotion/css'; import { groupBy, capitalize } from 'lodash'; import { useRef, useMemo } from 'react'; import * as React from 'react'; -import useClickAway from 'react-use/lib/useClickAway'; +import { useClickAway } from 'react-use'; import { VariableSuggestion, GrafanaTheme2 } from '@grafana/data'; diff --git a/packages/grafana-ui/src/components/Input/Input.tsx b/packages/grafana-ui/src/components/Input/Input.tsx index 563adfa9772..f812b863840 100644 --- a/packages/grafana-ui/src/components/Input/Input.tsx +++ b/packages/grafana-ui/src/components/Input/Input.tsx @@ -1,6 +1,6 @@ import { css, cx } from '@emotion/css'; import { forwardRef, HTMLProps, ReactNode, useContext } from 'react'; -import useMeasure from 'react-use/lib/useMeasure'; +import { useMeasure } from 'react-use'; import { GrafanaTheme2 } from '@grafana/data'; diff --git a/packages/grafana-ui/src/components/Segment/SegmentAsync.story.tsx b/packages/grafana-ui/src/components/Segment/SegmentAsync.story.tsx index df06086c430..26460bf62c6 100644 --- a/packages/grafana-ui/src/components/Segment/SegmentAsync.story.tsx +++ b/packages/grafana-ui/src/components/Segment/SegmentAsync.story.tsx @@ -2,7 +2,7 @@ import { action } from '@storybook/addon-actions'; import { Meta, StoryFn } from '@storybook/react'; import { useState } from 'react'; import * as React from 'react'; -import { AsyncState } from 'react-use/lib/useAsync'; +import { type AsyncState } from 'react-use/lib/useAsync'; import { SelectableValue } from '@grafana/data'; diff --git a/packages/grafana-ui/src/components/Segment/SegmentAsync.tsx b/packages/grafana-ui/src/components/Segment/SegmentAsync.tsx index b0f9907554b..9819507db01 100644 --- a/packages/grafana-ui/src/components/Segment/SegmentAsync.tsx +++ b/packages/grafana-ui/src/components/Segment/SegmentAsync.tsx @@ -3,7 +3,7 @@ import { isObject } from 'lodash'; import { HTMLProps } from 'react'; import * as React from 'react'; import { useAsyncFn } from 'react-use'; -import { AsyncState } from 'react-use/lib/useAsync'; +import { type AsyncState } from 'react-use/lib/useAsync'; import { SelectableValue } from '@grafana/data'; diff --git a/packages/grafana-ui/src/components/Segment/SegmentInput.tsx b/packages/grafana-ui/src/components/Segment/SegmentInput.tsx index 24ab7e8af1e..45ac598203b 100644 --- a/packages/grafana-ui/src/components/Segment/SegmentInput.tsx +++ b/packages/grafana-ui/src/components/Segment/SegmentInput.tsx @@ -1,7 +1,7 @@ import { cx, css } from '@emotion/css'; import { HTMLProps, useRef, useState } from 'react'; import * as React from 'react'; -import useClickAway from 'react-use/lib/useClickAway'; +import { useClickAway } from 'react-use'; import { useStyles2 } from '../../themes'; import { measureText } from '../../utils/measureText'; From 71a60e273432f870e400ff28ef556861c73caa35 Mon Sep 17 00:00:00 2001 From: Tom Ratcliffe Date: Thu, 22 May 2025 17:12:17 +0100 Subject: [PATCH 26/73] i18n: Add shortcut/command palette item to toggle pseudo locale (#105826) --- .github/CODEOWNERS | 2 +- public/app/core/lifecycle-hooks.ts | 2 +- public/app/core/services/keybindingSrv.ts | 5 ++++- .../app/{mock-api-utils.ts => dev-utils.ts} | 21 +++++++++++++++++++ public/app/dev.ts | 2 +- .../commandPalette/actions/staticActions.ts | 14 ++++++++++++- 6 files changed, 41 insertions(+), 5 deletions(-) rename public/app/{mock-api-utils.ts => dev-utils.ts} (68%) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 93fb04200a3..55285548803 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -486,7 +486,7 @@ playwright.config.ts @grafana/plugins-platform-frontend /public/app/core/components/TimelineChart/ @grafana/dataviz-squad /public/app/core/components/Form/ @grafana/grafana-frontend-platform /public/app/core/components/OptionsUI/ @grafana/dashboards-squad @grafana/dataviz-squad -/public/app/mock-api-utils.ts @grafana/grafana-frontend-platform +/public/app/dev-utils.ts @grafana/grafana-frontend-platform /public/app/core/history/ @grafana/observability-traces-and-profiling /public/app/features/admin/ @grafana/identity-access-team diff --git a/public/app/core/lifecycle-hooks.ts b/public/app/core/lifecycle-hooks.ts index 1cf2cce1fb8..07cfe490beb 100644 --- a/public/app/core/lifecycle-hooks.ts +++ b/public/app/core/lifecycle-hooks.ts @@ -1,5 +1,5 @@ import { initDevFeatures } from 'app/dev'; -import { notifyIfMockApiEnabled } from 'app/mock-api-utils'; +import { notifyIfMockApiEnabled } from 'app/dev-utils'; /** * Lifecycle tasks that need to be run prior to app initialization, diff --git a/public/app/core/services/keybindingSrv.ts b/public/app/core/services/keybindingSrv.ts index b7a1ea64501..3ff866f92c9 100644 --- a/public/app/core/services/keybindingSrv.ts +++ b/public/app/core/services/keybindingSrv.ts @@ -2,10 +2,10 @@ import { LegacyGraphHoverClearEvent, SetPanelAttentionEvent, locationUtil } from import { LocationService } from '@grafana/runtime'; import appEvents from 'app/core/app_events'; import { getExploreUrl } from 'app/core/utils/explore'; +import { toggleMockApiAndReload, togglePseudoLocale } from 'app/dev-utils'; import { SaveDashboardDrawer } from 'app/features/dashboard/components/SaveDashboard/SaveDashboardDrawer'; import { ShareModal } from 'app/features/dashboard/components/ShareModal/ShareModal'; import { DashboardModel } from 'app/features/dashboard/state/DashboardModel'; -import { toggleMockApiAndReload } from 'app/mock-api-utils'; import { getTimeSrv } from '../../features/dashboard/services/TimeSrv'; import { @@ -58,7 +58,10 @@ export class KeybindingSrv { this.bind('c r', () => toggleTheme(true)); if (process.env.NODE_ENV === 'development') { + // 'change mock' this.bind('c m', () => toggleMockApiAndReload()); + // 'change pseudo locale' + this.bind('c p l', () => togglePseudoLocale()); } } diff --git a/public/app/mock-api-utils.ts b/public/app/dev-utils.ts similarity index 68% rename from public/app/mock-api-utils.ts rename to public/app/dev-utils.ts index b530978d013..478af4f8f64 100644 --- a/public/app/mock-api-utils.ts +++ b/public/app/dev-utils.ts @@ -1,6 +1,8 @@ +import { DEFAULT_LANGUAGE, PSEUDO_LOCALE } from '@grafana/i18n'; import store from 'app/core/store'; import { sendAppNotification } from './core/copy/appNotification'; +import { PreferencesService } from './core/services/PreferencesService'; import { AppNotificationSeverity } from './types'; export const STORAGE_MOCK_API_KEY = 'grafana.dev.mockApi'; @@ -41,3 +43,22 @@ export const notifyIfMockApiEnabled = () => { ); } }; + +export const togglePseudoLocale = async () => { + const prefsService = new PreferencesService('user'); + const prefs = await prefsService.load(); + + const isPseudoEnabled = prefs.language === PSEUDO_LOCALE; + + const action = isPseudoEnabled ? 'Disabling' : 'Enabling'; + sendAppNotification(`${action} pseudo locale`, 'Reloading...', AppNotificationSeverity.Info); + + await prefsService.update({ + ...prefs, + language: isPseudoEnabled ? DEFAULT_LANGUAGE : PSEUDO_LOCALE, + }); + + setTimeout(() => { + window.location.reload(); + }, 200); +}; diff --git a/public/app/dev.ts b/public/app/dev.ts index 19c46dbca8d..7ce4811e39a 100644 --- a/public/app/dev.ts +++ b/public/app/dev.ts @@ -1,6 +1,6 @@ import * as React from 'react'; -import { potentiallySetupMockApi } from './mock-api-utils'; +import { potentiallySetupMockApi } from './dev-utils'; export async function initDevFeatures() { // if why-render is in url enable why did you render react extension diff --git a/public/app/features/commandPalette/actions/staticActions.ts b/public/app/features/commandPalette/actions/staticActions.ts index 9e0ebcec665..7f4f7d0adb3 100644 --- a/public/app/features/commandPalette/actions/staticActions.ts +++ b/public/app/features/commandPalette/actions/staticActions.ts @@ -5,7 +5,7 @@ import { t } from '@grafana/i18n/internal'; import { enrichHelpItem } from 'app/core/components/AppChrome/MegaMenu/utils'; import { performInviteUserClick, shouldRenderInviteUserButton } from 'app/core/components/InviteUserButton/utils'; import { changeTheme } from 'app/core/services/theme'; -import { currentMockApiState, toggleMockApiAndReload } from 'app/mock-api-utils'; +import { currentMockApiState, toggleMockApiAndReload, togglePseudoLocale } from 'app/dev-utils'; import { useSelector } from '../../../types'; import { CommandPaletteAction } from '../types'; @@ -116,6 +116,18 @@ function getGlobalActions(): CommandPaletteAction[] { priority: PREFERENCES_PRIORITY, perform: toggleMockApiAndReload, }); + + actions.push({ + id: 'preferences/dev/pseudo-locale', + section, + name: 'Toggle pseudo locale', + subtitle: 'Toggles between default language and pseudo locale', + keywords: 'pseudo locale', + priority: PREFERENCES_PRIORITY, + perform: () => { + togglePseudoLocale(); + }, + }); // eslint-enable @grafana/no-untranslated-strings } From dff2473410c51ab84e74ade7db2ae596c57af6ac Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 22 May 2025 12:04:26 -0600 Subject: [PATCH 27/73] Release: update changelog for 11.2.10 (#105845) * Update changelog * baldm0mma/ add CVE fixes to changelog --------- Co-authored-by: github-actions[bot] Co-authored-by: Kevin Yu Co-authored-by: jev forsberg --- CHANGELOG.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d57ef13c2d7..5a54951e44a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,16 @@ + + +# 11.2.10 (2025-05-22) + +### Features and enhancements + +- **Chore:** Bump Go version to 1.24.3 [#105113](https://github.com/grafana/grafana/pull/105113), [@macabu](https://github.com/macabu) +- **Dependencies:** Bump github.com/openfga/openfga from v1.8.5 to v1.8.12 [#105378](https://github.com/grafana/grafana/pull/105378), [@macabu](https://github.com/macabu) +- **Dependencies:** Unpin and bump github.com/getkin/kin-openapi from v0.125.0 to v0.132.0 [#105255](https://github.com/grafana/grafana/pull/105255), [@macabu](https://github.com/macabu) +- **Security:** Fix CVE-2025-4123 +- **Security:** Fix CVE-2025-3580 + + # 10.4.19 (2025-05-22) @@ -6,6 +19,8 @@ - **Chore:** Bump Go version to 1.24.3 [#105115](https://github.com/grafana/grafana/pull/105115), [@macabu](https://github.com/macabu) - **Dependencies:** Bump golang.org/x/net from v0.37.0 to v0.40.0 [#105449](https://github.com/grafana/grafana/pull/105449), [@macabu](https://github.com/macabu) +- **Security:** Fix CVE-2025-4123 +- **Security:** Fix CVE-2025-3580 From 67d28465dec0539a6869a1d4133179ccd0ccaba0 Mon Sep 17 00:00:00 2001 From: Fayzal Ghantiwala <114010985+fayzal-g@users.noreply.github.com> Date: Thu, 22 May 2025 19:07:12 +0100 Subject: [PATCH 28/73] Alerting: Fix debug log line in External Alertmanager sender (#105855) Fix log line --- pkg/services/ngalert/sender/sender.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pkg/services/ngalert/sender/sender.go b/pkg/services/ngalert/sender/sender.go index 1df7315ffd2..e4bb96146ff 100644 --- a/pkg/services/ngalert/sender/sender.go +++ b/pkg/services/ngalert/sender/sender.go @@ -182,14 +182,14 @@ func (s *ExternalAlertmanager) SendAlerts(alerts apimodels.PostableAlerts) { na := s.alertToNotifierAlert(a) as = append(as, na) - s.logger.Debug("msg", + s.logger.Debug( "Sending alert", "alert", - a, + na.String(), "starts_at", - a.StartsAt, + na.StartsAt, "ends_at", - a.EndsAt) + na.EndsAt) } s.manager.Send(as...) From a676fd620a861df3f53fe73f000390812208de05 Mon Sep 17 00:00:00 2001 From: Matias Chomicki Date: Thu, 22 May 2025 20:12:06 +0200 Subject: [PATCH 29/73] New Logs Panel: follow up improvements and fixes (#105767) * LogList: add version with no scroll * LogList: fix support for permalinked and pinned logs * LogLineDetails: improve resizable * LogList: improve height adjustment when details are open * LogList: let the people select text * Revert "LogList: add version with no scroll" This reverts commit f26cdce696cd297d0fc1a295330a631ba29bc9b9. * LogList: update test * New Logs Panel: rename permalinkedRowId to permalinkedLogId * LogListContext: update mock * ControlledLogRows: implement custom scrollIntoView * Logs Panel: fix re-render regression * LogLine: tweak hover and expanded colors --- public/app/features/explore/Logs/Logs.tsx | 2 +- .../logs/components/ControlledLogRows.tsx | 31 +++++++++++++++++-- .../logs/components/panel/LogLine.tsx | 12 ++++--- .../logs/components/panel/LogLineDetails.tsx | 11 +++++-- .../logs/components/panel/LogList.test.tsx | 20 ++++++++++++ .../logs/components/panel/LogList.tsx | 30 +++++++++++++++--- .../components/panel/LogListContext.test.tsx | 26 ++++++++++++++-- .../logs/components/panel/LogListContext.tsx | 19 +++++++++--- .../panel/__mocks__/LogListContext.tsx | 12 +++++-- 9 files changed, 141 insertions(+), 22 deletions(-) diff --git a/public/app/features/explore/Logs/Logs.tsx b/public/app/features/explore/Logs/Logs.tsx index fb61d584ee6..6025d4f8c9e 100644 --- a/public/app/features/explore/Logs/Logs.tsx +++ b/public/app/features/explore/Logs/Logs.tsx @@ -1063,7 +1063,6 @@ const UnthemedLogs: React.FunctionComponent = (props: Props) => { onOpenContext={onOpenContext} onPermalinkClick={onPermalinkClick} permalinkedRowId={panelState?.logs?.id} - scrollIntoView={scrollIntoView} isFilterLabelActive={props.isFilterLabelActive} onClickFilterString={props.onClickFilterString} onClickFilterOutString={props.onClickFilterOutString} @@ -1179,6 +1178,7 @@ const UnthemedLogs: React.FunctionComponent = (props: Props) => { onPermalinkClick={onPermalinkClick} onPinLine={onPinToContentOutlineClick} onUnpinLine={onPinToContentOutlineClick} + permalinkedLogId={panelState?.logs?.id} pinLineButtonTooltipTitle={pinLineButtonTooltipTitle} pinnedLogs={pinnedLogs} showControls diff --git a/public/app/features/logs/components/ControlledLogRows.tsx b/public/app/features/logs/components/ControlledLogRows.tsx index f48558c1c86..240e96e6b17 100644 --- a/public/app/features/logs/components/ControlledLogRows.tsx +++ b/public/app/features/logs/components/ControlledLogRows.tsx @@ -1,5 +1,5 @@ import { css } from '@emotion/css'; -import { useEffect, useMemo, useRef, forwardRef, useImperativeHandle } from 'react'; +import { useEffect, useMemo, useRef, forwardRef, useImperativeHandle, useCallback } from 'react'; import { AbsoluteTimeRange, @@ -96,7 +96,17 @@ export const ControlledLogRows = forwardRef( - ({ loading, loadMoreLogs, deduplicatedRows = [], range, ...rest }: LogRowsComponentProps, ref) => { + ( + { + loading, + loadMoreLogs, + deduplicatedRows = [], + range, + scrollIntoView: scrollIntoViewProp, + ...rest + }: LogRowsComponentProps, + ref + ) => { const { app, dedupStrategy, @@ -135,6 +145,22 @@ const LogRowsComponent = forwardRef { + if (scrollIntoViewProp) { + scrollIntoViewProp(element); + return; + } + if (scrollElementRef.current) { + scrollElementRef.current.scroll({ + behavior: 'smooth', + top: scrollElementRef.current.scrollTop + element.getBoundingClientRect().top - window.innerHeight / 2, + }); + } + }, + [scrollIntoViewProp] + ); + return (
@@ -158,6 +184,7 @@ const LogRowsComponent = forwardRef(null); const pinned = useLogIsPinned(log); + const permalinked = useLogIsPermalinked(log); useEffect(() => { if (!onOverflow || !logLineRef.current) { @@ -92,7 +93,7 @@ export const LogLine = ({ return (
{ fontSize: theme.typography.fontSize, wordBreak: 'break-all', '&:hover': { - background: `hsla(0, 0%, 0%, 0.2)`, + background: theme.isDark ? `hsla(0, 0%, 0%, 0.3)` : `hsla(0, 0%, 0%, 0.1)`, }, '&.infinite-scroll': { '&::before': { @@ -311,11 +312,14 @@ export const getStyles = (theme: GrafanaTheme2) => { }, }), detailsDisplayed: css({ - background: `hsla(0, 0%, 0%, 0.2)`, + background: theme.isDark ? `hsla(0, 0%, 0%, 0.5)` : `hsla(0, 0%, 0%, 0.1)`, }), pinnedLogLine: css({ backgroundColor: tinycolor(theme.colors.info.transparent).setAlpha(0.25).toString(), }), + permalinkedLogLine: css({ + backgroundColor: tinycolor(theme.colors.info.transparent).setAlpha(0.25).toString(), + }), menuIcon: css({ height: getLineHeight(), margin: 0, diff --git a/public/app/features/logs/components/panel/LogLineDetails.tsx b/public/app/features/logs/components/panel/LogLineDetails.tsx index d290e43be29..df55103225a 100644 --- a/public/app/features/logs/components/panel/LogLineDetails.tsx +++ b/public/app/features/logs/components/panel/LogLineDetails.tsx @@ -4,7 +4,7 @@ import { useCallback, useRef } from 'react'; import { GrafanaTheme2 } from '@grafana/data'; import { useTranslate } from '@grafana/i18n'; -import { IconButton, useStyles2, useTheme2 } from '@grafana/ui'; +import { getDragStyles, IconButton, useStyles2, useTheme2 } from '@grafana/ui'; import { GetFieldLinksFn } from 'app/plugins/panel/logs/types'; import { LogDetails } from '../LogDetails'; @@ -40,6 +40,7 @@ export const LogLineDetails = ({ containerElement, getFieldLinks, logs, onResize const getRows = useCallback(() => logs, [logs]); const logRowsStyles = getLogRowStyles(useTheme2()); const styles = useStyles2(getStyles); + const dragStyles = useStyles2(getDragStyles); const containerRef = useRef(null); const { t } = useTranslate(); @@ -51,7 +52,13 @@ export const LogLineDetails = ({ containerElement, getFieldLinks, logs, onResize }, [onResize, setDetailsWidth]); return ( - +
{ expect(screen.queryByText('Fields')).not.toBeInTheDocument(); expect(screen.queryByText('Close log details')).not.toBeInTheDocument(); }); + + test('Allows people to select text without opening log details', async () => { + const spy = jest.spyOn(document, 'getSelection'); + spy.mockReturnValue({ + toString: () => 'selected log line', + removeAllRanges: () => {}, + addRange: (range: Range) => {}, + } as Selection); + + render(); + + await userEvent.click(screen.getByText('log message 1')); + + expect(screen.queryByText('name_of_the_label')).not.toBeInTheDocument(); + expect(screen.queryByText('value of the label')).not.toBeInTheDocument(); + expect(screen.queryByText('Fields')).not.toBeInTheDocument(); + expect(screen.queryByText('Close log details')).not.toBeInTheDocument(); + + spy.mockRestore(); + }); }); diff --git a/public/app/features/logs/components/panel/LogList.tsx b/public/app/features/logs/components/panel/LogList.tsx index f55a670b526..1dc80967a90 100644 --- a/public/app/features/logs/components/panel/LogList.tsx +++ b/public/app/features/logs/components/panel/LogList.tsx @@ -69,6 +69,7 @@ export interface Props { onPinLine?: (row: LogRowModel) => void; onOpenContext?: (row: LogRowModel, onClose: () => void) => void; onUnpinLine?: (row: LogRowModel) => void; + permalinkedLogId?: string; pinLineButtonTooltipTitle?: PopoverContent; pinnedLogs?: string[]; showControls: boolean; @@ -88,6 +89,7 @@ type LogListComponentProps = Omit< | 'dedupStrategy' | 'displayedFields' | 'enableLogDetails' + | 'permalinkedLogId' | 'showTime' | 'sortOrder' | 'syntaxHighlighting' @@ -125,6 +127,7 @@ export const LogList = ({ onPinLine, onOpenContext, onUnpinLine, + permalinkedLogId, pinLineButtonTooltipTitle, pinnedLogs, showControls, @@ -161,6 +164,7 @@ export const LogList = ({ onPinLine={onPinLine} onOpenContext={onOpenContext} onUnpinLine={onUnpinLine} + permalinkedLogId={permalinkedLogId} pinLineButtonTooltipTitle={pinLineButtonTooltipTitle} pinnedLogs={pinnedLogs} showControls={showControls} @@ -205,6 +209,7 @@ const LogListComponent = ({ dedupStrategy, filterLevels, forceEscape, + permalinkedLogId, showDetails, showTime, sortOrder, @@ -213,7 +218,9 @@ const LogListComponent = ({ } = useLogListContext(); const [processedLogs, setProcessedLogs] = useState([]); const [listHeight, setListHeight] = useState( - app === CoreApp.Explore ? window.innerHeight * 0.75 : containerElement.clientHeight + app === CoreApp.Explore + ? Math.max(window.innerHeight * 0.8, containerElement.clientHeight) + : containerElement.clientHeight ); const theme = useTheme2(); const listRef = useRef(null); @@ -262,7 +269,11 @@ const LogListComponent = ({ useEffect(() => { const handleResize = debounce(() => { - setListHeight(app === CoreApp.Explore ? window.innerHeight * 0.75 : containerElement.clientHeight); + setListHeight( + app === CoreApp.Explore + ? Math.max(window.innerHeight * 0.8, containerElement.clientHeight) + : containerElement.clientHeight + ); }, 50); window.addEventListener('resize', handleResize); handleResize(); @@ -292,8 +303,15 @@ const LogListComponent = ({ ); const handleScrollPosition = useCallback(() => { - listRef.current?.scrollToItem(initialScrollPosition === 'top' ? 0 : logs.length - 1); - }, [initialScrollPosition, logs.length]); + if (permalinkedLogId) { + const index = processedLogs.findIndex((log) => log.uid === permalinkedLogId); + if (index >= 0) { + listRef.current?.scrollToItem(index, 'start'); + return; + } + } + listRef.current?.scrollToItem(initialScrollPosition === 'top' ? 0 : processedLogs.length - 1); + }, [initialScrollPosition, permalinkedLogId, processedLogs]); if (!containerElement || listHeight == null) { // Wait for container to be rendered @@ -302,6 +320,10 @@ const LogListComponent = ({ const handleLogLineClick = useCallback( (log: LogListModel) => { + // Let people select text + if (document.getSelection()?.toString()) { + return; + } toggleDetails(log); }, [toggleDetails] diff --git a/public/app/features/logs/components/panel/LogListContext.test.tsx b/public/app/features/logs/components/panel/LogListContext.test.tsx index 8010efcdf74..efe2281d8b9 100644 --- a/public/app/features/logs/components/panel/LogListContext.test.tsx +++ b/public/app/features/logs/components/panel/LogListContext.test.tsx @@ -3,13 +3,20 @@ import { ReactNode } from 'react'; import { createLogLine } from '../__mocks__/logRow'; -import { useLogListContextData, useLogListContext, useLogIsPinned, LogListContext } from './LogListContext'; +import { + useLogListContextData, + useLogListContext, + useLogIsPinned, + LogListContext, + useLogIsPermalinked, +} from './LogListContext'; import { defaultValue } from './__mocks__/LogListContext'; -const log = createLogLine({ rowId: 'yep' }); +const log = createLogLine({ rowId: 'yep', uid: 'uid' }); const value = { ...defaultValue, pinnedLogs: ['yep'], + permalinkedLogId: log.uid, }; const wrapper = ({ children }: { children: ReactNode }) => ( {children} @@ -33,9 +40,22 @@ test('Allows to tell if a log is pinned', () => { expect(result.current).toBe(true); }); -test('Allows to tell if a log is pinned', () => { +test('Allows to tell if a log is not pinned', () => { const otherLog = createLogLine({ rowId: 'nope' }); const { result } = renderHook(() => useLogIsPinned(otherLog), { wrapper }); expect(result.current).toBe(false); }); + +test('Allows to tell if a log is permalinked', () => { + const { result } = renderHook(() => useLogIsPermalinked(log), { wrapper }); + + expect(result.current).toBe(true); +}); + +test('Allows to tell if a log is not permalinked', () => { + const otherLog = createLogLine({ rowId: 'nope' }); + const { result } = renderHook(() => useLogIsPermalinked(otherLog), { wrapper }); + + expect(result.current).toBe(false); +}); diff --git a/public/app/features/logs/components/panel/LogListContext.tsx b/public/app/features/logs/components/panel/LogListContext.tsx index 2e9b35ee165..546643fc813 100644 --- a/public/app/features/logs/components/panel/LogListContext.tsx +++ b/public/app/features/logs/components/panel/LogListContext.tsx @@ -91,11 +91,16 @@ export const useLogListContext = (): LogListContextData => { return useContext(LogListContext); }; -export const useLogIsPinned = (log: LogRowModel) => { +export const useLogIsPinned = (log: LogListModel) => { const { pinnedLogs } = useContext(LogListContext); return pinnedLogs?.some((logId) => logId === log.rowId); }; +export const useLogIsPermalinked = (log: LogListModel) => { + const { permalinkedLogId } = useContext(LogListContext); + return permalinkedLogId && permalinkedLogId === log.uid; +}; + export type LogListState = Pick< LogListContextData, | 'dedupStrategy' @@ -139,6 +144,7 @@ export interface Props { onPinLine?: (row: LogRowModel) => void; onOpenContext?: (row: LogRowModel, onClose: () => void) => void; onUnpinLine?: (row: LogRowModel) => void; + permalinkedLogId?: string; pinLineButtonTooltipTitle?: PopoverContent; pinnedLogs?: string[]; prettifyJSON?: boolean; @@ -178,6 +184,7 @@ export const LogListContextProvider = ({ onPinLine, onOpenContext, onUnpinLine, + permalinkedLogId, pinLineButtonTooltipTitle, pinnedLogs, prettifyJSON, @@ -217,9 +224,6 @@ export const LogListContextProvider = ({ syntaxHighlighting, wrapLogMessage, }; - if (!shallowCompare(logListState.pinnedLogs ?? [], pinnedLogs ?? [])) { - newState.pinnedLogs = pinnedLogs; - } if (!shallowCompare(logListState, newState)) { setLogListState(newState); } @@ -250,6 +254,12 @@ export const LogListContextProvider = ({ } }, [hasUnescapedContent, logListState]); + useEffect(() => { + if (!shallowCompare(logListState.pinnedLogs ?? [], pinnedLogs ?? [])) { + setLogListState({ ...logListState, pinnedLogs }); + } + }, [logListState, pinnedLogs]); + const detailsDisplayed = useCallback( (log: LogListModel) => !!showDetails.find((shownLog) => shownLog.uid === log.uid), [showDetails] @@ -426,6 +436,7 @@ export const LogListContextProvider = ({ onPinLine, onOpenContext, onUnpinLine, + permalinkedLogId, pinLineButtonTooltipTitle, pinnedLogs: logListState.pinnedLogs, prettifyJSON: logListState.prettifyJSON, diff --git a/public/app/features/logs/components/panel/__mocks__/LogListContext.tsx b/public/app/features/logs/components/panel/__mocks__/LogListContext.tsx index 53f002a2a2d..e9fcaabdeca 100644 --- a/public/app/features/logs/components/panel/__mocks__/LogListContext.tsx +++ b/public/app/features/logs/components/panel/__mocks__/LogListContext.tsx @@ -1,8 +1,9 @@ import { createContext, useContext } from 'react'; -import { CoreApp, LogRowModel, LogsDedupStrategy, LogsSortOrder } from '@grafana/data'; +import { CoreApp, LogsDedupStrategy, LogsSortOrder } from '@grafana/data'; import { LogListContextData, Props } from '../LogListContext'; +import { LogListModel } from '../processing'; export const LogListContext = createContext({ app: CoreApp.Unknown, @@ -44,11 +45,16 @@ export const useLogListContext = (): LogListContextData => { return useContext(LogListContext); }; -export const useLogIsPinned = (log: LogRowModel) => { +export const useLogIsPinned = (log: LogListModel) => { const { pinnedLogs } = useContext(LogListContext); return pinnedLogs?.some((logId) => logId === log.rowId); }; +export const useLogIsPermalinked = (log: LogListModel) => { + const { permalinkedLogId } = useContext(LogListContext); + return permalinkedLogId && permalinkedLogId === log.uid; +}; + export const defaultValue: LogListContextData = { setDedupStrategy: jest.fn(), setFilterLevels: jest.fn(), @@ -114,6 +120,7 @@ export const LogListContextProvider = ({ onPinLine = jest.fn(), onOpenContext = jest.fn(), onUnpinLine = jest.fn(), + permalinkedLogId, pinnedLogs = [], showTime = true, sortOrder = LogsSortOrder.Descending, @@ -137,6 +144,7 @@ export const LogListContextProvider = ({ onPinLine, onOpenContext, onUnpinLine, + permalinkedLogId, pinnedLogs, setDedupStrategy: jest.fn(), setFilterLevels: jest.fn(), From 4b03edda44f9315a75fe54974fd8809e33cd0444 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 22 May 2025 18:36:37 +0000 Subject: [PATCH 30/73] Release: update changelog for 11.3.7 (#105882) * Update changelog * update changelog --------- Co-authored-by: github-actions[bot] Co-authored-by: Kevin Yu --- CHANGELOG.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5a54951e44a..14fcbf8a670 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,17 @@ + + +# 11.3.7 (2025-05-22) + +### Features and enhancements + +- **Chore:** Bump Go version to 1.24.3 [#105112](https://github.com/grafana/grafana/pull/105112), [@macabu](https://github.com/macabu) +- **Dependencies:** Bump github.com/blevesearch/bleve/v2 from v2.4.2 to v2.5.0 [#105447](https://github.com/grafana/grafana/pull/105447), [@macabu](https://github.com/macabu) +- **Dependencies:** Bump github.com/openfga/openfga from v1.8.5 to v1.8.12 [#105376](https://github.com/grafana/grafana/pull/105376), [@macabu](https://github.com/macabu) +- **Dependencies:** Unpin and bump github.com/getkin/kin-openapi from v0.125.0 to v0.132.0 [#105254](https://github.com/grafana/grafana/pull/105254), [@macabu](https://github.com/macabu) +- **Security:** Fix CVE-2025-4123 +- **Security:** Fix CVE-2025-3580 + + # 11.2.10 (2025-05-22) From 2e64bd8dbf264feaa84a92d5d2f7198a66ecaa65 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 22 May 2025 18:57:10 +0000 Subject: [PATCH 31/73] Release: update changelog for 11.4.5 (#105886) * Update changelog * update changelog --------- Co-authored-by: github-actions[bot] Co-authored-by: Kevin Yu --- CHANGELOG.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 14fcbf8a670..e288170914d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,17 @@ + + +# 11.4.5 (2025-05-22) + +### Features and enhancements + +- **Chore:** Bump Go version to 1.24.3 [#105110](https://github.com/grafana/grafana/pull/105110), [@macabu](https://github.com/macabu) +- **Dependencies:** Bump github.com/blevesearch/bleve/v2 from v2.4.2 to v2.5.0 [#105445](https://github.com/grafana/grafana/pull/105445), [@macabu](https://github.com/macabu) +- **Dependencies:** Bump github.com/openfga/openfga from v1.8.5 to v1.8.12 [#105375](https://github.com/grafana/grafana/pull/105375), [@macabu](https://github.com/macabu) +- **Dependencies:** Unpin and bump github.com/getkin/kin-openapi from v0.125.0 to v0.132.0 [#105253](https://github.com/grafana/grafana/pull/105253), [@macabu](https://github.com/macabu) +- **Security:** Fix CVE-2025-4123 +- **Security:** Fix CVE-2025-3580 + + # 11.3.7 (2025-05-22) From 249e2f3d34f5175e62a17629646994d3116e64f6 Mon Sep 17 00:00:00 2001 From: Cory Forseth Date: Thu, 22 May 2025 14:13:31 -0500 Subject: [PATCH 32/73] Authorization: add toggle for LBAC filtering for Tempo datasources (#105688) add toggle for LBAC filtering for Tempo datasources --- .../grafana-data/src/types/featureToggles.gen.ts | 4 ++++ pkg/services/featuremgmt/registry.go | 7 +++++++ pkg/services/featuremgmt/toggles_gen.csv | 1 + pkg/services/featuremgmt/toggles_gen.go | 4 ++++ pkg/services/featuremgmt/toggles_gen.json | 15 +++++++++++++++ 5 files changed, 31 insertions(+) diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index a3544cf6938..64d213a904b 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -841,6 +841,10 @@ export interface FeatureToggles { */ teamHttpHeadersMimir?: boolean; /** + * Enables LBAC for datasources for Tempo to apply LBAC filtering of traces to the client requests for users in teams + */ + teamHttpHeadersTempo?: boolean; + /** * Test feature toggle to see how cohorts could be set up AB testing * @default false */ diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index a72ae57b7f6..94a41812b98 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -1430,6 +1430,13 @@ var ( AllowSelfServe: true, Owner: identityAccessTeam, }, + { + Name: "teamHttpHeadersTempo", + Description: "Enables LBAC for datasources for Tempo to apply LBAC filtering of traces to the client requests for users in teams", + Stage: FeatureStageExperimental, + FrontendOnly: false, + Owner: identityAccessTeam, + }, { Name: "ABTestFeatureToggleA", Description: "Test feature toggle to see how cohorts could be set up AB testing", diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv index 9e359fef7f9..fb9a6dddad9 100644 --- a/pkg/services/featuremgmt/toggles_gen.csv +++ b/pkg/services/featuremgmt/toggles_gen.csv @@ -188,6 +188,7 @@ k8SFolderCounts,experimental,@grafana/search-and-storage,false,false,false k8SFolderMove,experimental,@grafana/search-and-storage,false,false,false improvedExternalSessionHandlingSAML,preview,@grafana/identity-access-team,false,false,false teamHttpHeadersMimir,preview,@grafana/identity-access-team,false,false,false +teamHttpHeadersTempo,experimental,@grafana/identity-access-team,false,false,false ABTestFeatureToggleA,experimental,@grafana/sharing-squad,false,false,false templateVariablesUsesCombobox,experimental,@grafana/grafana-frontend-platform,false,false,true ABTestFeatureToggleB,experimental,@grafana/sharing-squad,false,false,false diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go index 7b30101fb72..ea0db108a09 100644 --- a/pkg/services/featuremgmt/toggles_gen.go +++ b/pkg/services/featuremgmt/toggles_gen.go @@ -763,6 +763,10 @@ const ( // Enables LBAC for datasources for Mimir to apply LBAC filtering of metrics to the client requests for users in teams FlagTeamHttpHeadersMimir = "teamHttpHeadersMimir" + // FlagTeamHttpHeadersTempo + // Enables LBAC for datasources for Tempo to apply LBAC filtering of traces to the client requests for users in teams + FlagTeamHttpHeadersTempo = "teamHttpHeadersTempo" + // FlagABTestFeatureToggleA // Test feature toggle to see how cohorts could be set up AB testing FlagABTestFeatureToggleA = "ABTestFeatureToggleA" diff --git a/pkg/services/featuremgmt/toggles_gen.json b/pkg/services/featuremgmt/toggles_gen.json index bcbe952df23..fba14fd6db1 100644 --- a/pkg/services/featuremgmt/toggles_gen.json +++ b/pkg/services/featuremgmt/toggles_gen.json @@ -3109,6 +3109,21 @@ "allowSelfServe": true } }, + { + "metadata": { + "name": "teamHttpHeadersTempo", + "resourceVersion": "1747930192816", + "creationTimestamp": "2025-05-20T14:50:32Z", + "annotations": { + "grafana.app/updatedTimestamp": "2025-05-22 16:09:52.816517 +0000 UTC" + } + }, + "spec": { + "description": "Enables LBAC for datasources for Tempo to apply LBAC filtering of traces to the client requests for users in teams", + "stage": "experimental", + "codeowner": "@grafana/identity-access-team" + } + }, { "metadata": { "name": "templateVariablesUsesCombobox", From a54b0e384bd3f62f61b6cec93c4f64645fe16739 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 22 May 2025 12:20:53 -0700 Subject: [PATCH 33/73] Release: update changelog for 11.5.5 (#105888) * Update changelog * update changelog --------- Co-authored-by: github-actions[bot] Co-authored-by: Kevin Yu --- CHANGELOG.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e288170914d..952c2a3468c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,17 @@ + + +# 11.5.5 (2025-05-22) + +### Features and enhancements + +- **Chore:** Bump Go version to 1.24.3 [#105109](https://github.com/grafana/grafana/pull/105109), [@macabu](https://github.com/macabu) +- **Dependencies:** Bump github.com/blevesearch/bleve/v2 from v2.4.3 to v2.5.0 [#105441](https://github.com/grafana/grafana/pull/105441), [@macabu](https://github.com/macabu) +- **Dependencies:** Bump github.com/openfga/openfga from v1.8.5 to v1.8.12 [#105373](https://github.com/grafana/grafana/pull/105373), [@macabu](https://github.com/macabu) +- **Dependencies:** Unpin and bump github.com/getkin/kin-openapi from v0.126.0 to v0.132.0 [#105252](https://github.com/grafana/grafana/pull/105252), [@macabu](https://github.com/macabu) +- **Security:** Fix CVE-2025-4123 +- **Security:** Fix CVE-2025-3580 + + # 11.4.5 (2025-05-22) From bc10c4ee7b263cd8f43ca6cf03f99bc944a73b47 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 22 May 2025 19:39:57 +0000 Subject: [PATCH 34/73] Release: update changelog for 11.6.2 (#105889) * Update changelog * update changelog --------- Co-authored-by: github-actions[bot] Co-authored-by: Kevin Yu --- CHANGELOG.md | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 952c2a3468c..3fe70a7b7c8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,24 @@ + + +# 11.6.2 (2025-05-22) + +### Features and enhancements + +- **Chore:** Bump Go version to 1.24.3 [#105103](https://github.com/grafana/grafana/pull/105103), [@macabu](https://github.com/macabu) +- **Dependencies:** Bump github.com/blevesearch/bleve/v2 from v2.4.4-git to v2.5.0 [#105443](https://github.com/grafana/grafana/pull/105443), [@macabu](https://github.com/macabu) +- **Dependencies:** Bump github.com/openfga/openfga from v1.8.6 to v1.8.12 [#105369](https://github.com/grafana/grafana/pull/105369), [@macabu](https://github.com/macabu) +- **Dependencies:** Unpin and bump github.com/getkin/kin-openapi from v0.126.0 to v0.132.0 [#105251](https://github.com/grafana/grafana/pull/105251), [@macabu](https://github.com/macabu) + +### Bug fixes + +- **Dashboard:** Fixes issue with row repeats and first row [#104467](https://github.com/grafana/grafana/pull/104467), [@torkelo](https://github.com/torkelo) +- **Graphite:** Ensure template variables are interpolated correctly [#105388](https://github.com/grafana/grafana/pull/105388), [@aangelisc](https://github.com/aangelisc) +- **Graphite:** Fix Graphite series interpolation [#104568](https://github.com/grafana/grafana/pull/104568), [@aangelisc](https://github.com/aangelisc) +- **Prometheus:** Fix semver import path [#104943](https://github.com/grafana/grafana/pull/104943), [@jackw](https://github.com/jackw) +- **Security:** Fix CVE-2025-4123 +- **Security:** Fix CVE-2025-3580 + + # 11.5.5 (2025-05-22) From 98ee83bf3f40bd7dd6b771a80ad7d70e6b511b5a Mon Sep 17 00:00:00 2001 From: Matheus Macabu Date: Thu, 22 May 2025 22:17:00 +0200 Subject: [PATCH 35/73] Extensions: Add direct dependencies used by Enterprise build (#105832) --- go.mod | 4 ++-- pkg/extensions/enterprise_imports.go | 9 +++++---- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index a4dc55e3fe0..535d3382128 100644 --- a/go.mod +++ b/go.mod @@ -193,7 +193,7 @@ require ( gopkg.in/mail.v2 v2.3.1 // @grafana/grafana-backend-group gopkg.in/yaml.v2 v2.4.0 // @grafana/alerting-backend gopkg.in/yaml.v3 v3.0.1 // @grafana/alerting-backend - k8s.io/api v0.32.3 // indirect; @grafana/grafana-app-platform-squad + k8s.io/api v0.32.3 // @grafana/grafana-app-platform-squad k8s.io/apimachinery v0.32.3 // @grafana/grafana-app-platform-squad k8s.io/apiserver v0.32.3 // @grafana/grafana-app-platform-squad k8s.io/client-go v0.32.3 // @grafana/grafana-app-platform-squad @@ -202,6 +202,7 @@ require ( k8s.io/kube-aggregator v0.32.0 // @grafana/grafana-app-platform-squad k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff // @grafana/grafana-app-platform-squad k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 // @grafana/partner-datasources + sigs.k8s.io/randfill v1.0.0 // @grafana/grafana-app-platform-squad sigs.k8s.io/structured-merge-diff/v4 v4.6.0 // @grafana-app-platform-squad xorm.io/builder v0.3.6 // @grafana/grafana-backend-group ) @@ -569,7 +570,6 @@ require ( modernc.org/sqlite v1.37.0 // indirect sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.0 // indirect sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect - sigs.k8s.io/randfill v1.0.0 // indirect sigs.k8s.io/yaml v1.4.0 // indirect ) diff --git a/pkg/extensions/enterprise_imports.go b/pkg/extensions/enterprise_imports.go index f54f5d134c5..44fe4e5cfc0 100644 --- a/pkg/extensions/enterprise_imports.go +++ b/pkg/extensions/enterprise_imports.go @@ -30,6 +30,11 @@ import ( _ "github.com/spf13/cobra" // used by the standalone apiserver cli _ "github.com/stretchr/testify/require" _ "golang.org/x/time/rate" + _ "k8s.io/api" + _ "k8s.io/kube-aggregator/pkg/apiserver/scheme" + _ "k8s.io/kube-aggregator/pkg/generated/openapi" + _ "k8s.io/kube-aggregator/pkg/registry/apiservice/rest" + _ "sigs.k8s.io/randfill" _ "xorm.io/builder" _ "github.com/grafana/authlib/authn" @@ -42,8 +47,4 @@ import ( _ "github.com/grafana/e2e" _ "github.com/grafana/gofpdf" _ "github.com/grafana/gomemcache/memcache" - - _ "k8s.io/kube-aggregator/pkg/apiserver/scheme" - _ "k8s.io/kube-aggregator/pkg/generated/openapi" - _ "k8s.io/kube-aggregator/pkg/registry/apiservice/rest" ) From 73e971eedcf22cced19fb62b23cedcbd62fb10cf Mon Sep 17 00:00:00 2001 From: Jacob Valdez Date: Thu, 22 May 2025 15:46:18 -0500 Subject: [PATCH 36/73] Docs: Clarifying information about using the API for license renewal (#105879) Docs: Clarifying information about using the API for licnese renewal --- docs/sources/developers/http_api/licensing.md | 38 +++++++++++++------ 1 file changed, 26 insertions(+), 12 deletions(-) diff --git a/docs/sources/developers/http_api/licensing.md b/docs/sources/developers/http_api/licensing.md index 0d09183750c..c17eef55e1d 100644 --- a/docs/sources/developers/http_api/licensing.md +++ b/docs/sources/developers/http_api/licensing.md @@ -112,22 +112,36 @@ Content-Type: application/json Content-Length: 357 { - "jti":"2", - "iss":"https://grafana.com", - "sub":"https://play.grafana.org/" - "lid":"1", - "included_users":15, - "lic_exp_warn_days":30, - "tok_exp_warn_days":2, - "update_days":1, - "prod":["grafana-enterprise"], - "company":"Grafana Labs" +"status":0, +"jti":"", +"iss":"", +"sub":"", +"iat":0, +"exp":0, +"nbf":0, +"lexp":0, +"lid":"", +"limit_by":"", +"included_users":0, +"lic_exp_warn_days":0, +"tok_exp_warn_days":0, +"update_days":0, +"prod":null, +"company":"", +"account":"", +"slug":"", +"usage_billing":false, +"max_concurrent_user_sessions":0, +"details_url":"", +"trial":false, +"trial_exp":0, +"anonymousRatio":0 } ``` -The response is a JSON blob available for debugging purposes. The -available fields may change at any time without any prior notice. +The response is a JSON blob with specific values intentionally not shown. The +available fields may change at any time without any prior notice. Refer to [Check license availability](#check-license-availability) for information on using the API to check the status of your license. Status Codes: From 8124980e1b36d1f35727e7d255e5f4313037c0d1 Mon Sep 17 00:00:00 2001 From: Kevin Minehart <5140827+kminehart@users.noreply.github.com> Date: Thu, 22 May 2025 15:58:30 -0500 Subject: [PATCH 37/73] CI: use -C when go-mod-dir is provided (#105851) * use -C when go-mod-dir is provided * remove unused target variable * use my branch for testing * fix syntax? * use condition in bash instead * dagger-for-github doesn't support multiline command * just skip using the composite action * update setup-go * add missing space * grafana-main -> .grafana-main * use 'patch' as version * use generated token for cloning / pushing * colons in wrong place * update generate-token action * reduce permissions needed * add org prefix to repositories list * ok the repo names did not work like that * pull-request -> pull_request * add workflows: write permission * push branch on dry_run * Add missing DRY_RUN flag * initialize LATEST_FLAG as array * use RELEASE_BRANCH as the base branch * use workflow in main * done testing; use main --- .../workflows/create-next-release-branch.yml | 4 +- .github/workflows/release-comms.yml | 19 +++----- .github/workflows/release-pr.yml | 47 ++++++++++--------- pkg/build/actions/bump-version/action.yml | 5 +- 4 files changed, 38 insertions(+), 37 deletions(-) diff --git a/.github/workflows/create-next-release-branch.yml b/.github/workflows/create-next-release-branch.yml index 6a48327dc04..5f537a5418a 100644 --- a/.github/workflows/create-next-release-branch.yml +++ b/.github/workflows/create-next-release-branch.yml @@ -42,10 +42,12 @@ jobs: GRAFANA_DELIVERY_BOT_APP_PEM=delivery-bot-app:PRIVATE_KEY - name: "Generate token" id: generate_token - uses: tibdex/github-app-token@b62528385c34dbc9f38e5f4225ac829252d1ea92 + uses: tibdex/github-app-token@3beb63f4bd073e61482598c45c71c1019b59b73a with: app_id: ${{ vars.DELIVERY_BOT_APP_ID }} private_key: ${{ env.GRAFANA_DELIVERY_BOT_APP_PEM }} + repositories: "[\"grafana\", \"grafana-enterprise\"]" + permissions: "{\"contents\": \"write\", \"pull_requests\": \"write\", \"workflows\":\"write\"}" - name: Create release branch id: branch uses: grafana/grafana-github-actions-go/bump-release@main # zizmor: ignore[unpinned-uses] diff --git a/.github/workflows/release-comms.yml b/.github/workflows/release-comms.yml index e0bd93d3b7d..5208b3a4606 100644 --- a/.github/workflows/release-comms.yml +++ b/.github/workflows/release-comms.yml @@ -44,13 +44,6 @@ jobs: VERSION: ${{ inputs.version }} runs-on: ubuntu-latest steps: - - name: "Get vault secrets" - id: vault-secrets - uses: grafana/shared-workflows/actions/get-vault-secrets@main - with: - # Secrets placed in the ci/data/repo/grafana/grafana/delivery-bot-app path in Vault - repo_secrets: | - GRAFANA_DELIVERY_BOT_APP_PEM=delivery-bot-app:PRIVATE_KEY - if: ${{ github.event.pull_request.merged == true && startsWith(github.head_ref, 'release/') }} run: | { @@ -73,14 +66,14 @@ jobs: create_next_release_branch_grafana: name: Create next release branch (Grafana) needs: setup - uses: ./.github/workflows/create-next-release-branch.yml + uses: grafana/grafana/.github/workflows/create-next-release-branch.yml@main with: ownerRepo: 'grafana/grafana' source: ${{ needs.setup.outputs.release_branch }} create_next_release_branch_enterprise: name: Create next release branch (Grafana Enterprise) needs: setup - uses: ./.github/workflows/create-next-release-branch.yml + uses: grafana/grafana/.github/workflows/create-next-release-branch.yml@main with: ownerRepo: 'grafana/grafana-enterprise' source: ${{ needs.setup.outputs.release_branch }} @@ -88,7 +81,7 @@ jobs: needs: - setup - create_next_release_branch_grafana - uses: ./.github/workflows/migrate-prs.yml + uses: grafana/grafana/.github/workflows/migrate-prs.yml@main with: ownerRepo: 'grafana/grafana' from: ${{ needs.setup.outputs.release_branch }} @@ -97,14 +90,14 @@ jobs: needs: - setup - create_next_release_branch_enterprise - uses: ./.github/workflows/migrate-prs.yml + uses: grafana/grafana/.github/workflows/migrate-prs.yml@main with: ownerRepo: 'grafana/grafana-enterprise' from: ${{ needs.setup.outputs.release_branch }} to: ${{ needs.create_next_release_branch_enterprise.outputs.branch }} post_changelog_on_forum: needs: setup - uses: ./.github/workflows/community-release.yml + uses: grafana/grafana/.github/workflows/community-release.yml@main with: version: ${{ needs.setup.outputs.version }} dry_run: ${{ needs.setup.outputs.dry_run == 'true' }} @@ -113,7 +106,7 @@ jobs: # The github-release action retrieves the changelog using the /repos/grafana/grafana/contents/CHANGELOG.md API # endpoint. needs: setup - uses: ./.github/workflows/github-release.yml + uses: grafana/grafana/.github/workflows/github-release.yml@main with: version: ${{ needs.setup.outputs.version }} dry_run: ${{ needs.setup.outputs.dry_run == 'true' }} diff --git a/.github/workflows/release-pr.yml b/.github/workflows/release-pr.yml index 02f283e5d0b..fb6a9f2058a 100644 --- a/.github/workflows/release-pr.yml +++ b/.github/workflows/release-pr.yml @@ -16,10 +16,6 @@ on: required: true type: string description: The version of Grafana that is being released (without the `v` prefix)` - target: - required: true - type: string - description: The release branch pattern (eg v9.5.x) that these changes are being merged into changelog: required: false type: boolean @@ -73,19 +69,28 @@ jobs: id: vault-secrets uses: grafana/shared-workflows/actions/get-vault-secrets@main with: - # Secrets placed in the ci/data/repo/grafana/grafana/delivery-bot-app path in Vault repo_secrets: | GRAFANA_DELIVERY_BOT_APP_PEM=delivery-bot-app:PRIVATE_KEY + - name: Generate token + id: generate_changelog_token + uses: tibdex/github-app-token@3beb63f4bd073e61482598c45c71c1019b59b73a + with: + app_id: ${{ vars.DELIVERY_BOT_APP_ID }} + private_key: ${{ env.GRAFANA_DELIVERY_BOT_APP_PEM }} + repositories: "[\"grafana\", \"grafana-enterprise\"]" + permissions: "{\"contents\": \"write\", \"pull_requests\": \"write\", \"workflows\":\"write\"}" - run: echo "RELEASE_BRANCH=release-${VERSION}" >> "$GITHUB_ENV" - name: Checkout Grafana uses: actions/checkout@v4 with: + token: ${{ steps.generate_changelog_token.outputs.token }} ref: ${{ env.RELEASE_BRANCH }} fetch-tags: true fetch-depth: 0 - name: Checkout Grafana (main) uses: actions/checkout@v4 with: + token: ${{ steps.generate_changelog_token.outputs.token }} ref: main fetch-depth: '0' path: .grafana-main @@ -94,21 +99,17 @@ jobs: uses: actions/setup-node@v4 with: node-version-file: .nvmrc + - uses: actions/setup-go@v5 + with: + go-version-file: go.mod - name: Configure git user run: | - git config --local user.name "github-actions[bot]" - git config --local user.email "github-actions[bot]@users.noreply.github.com" + git config --local user.name "grafana-delivery-bot[bot]" + git config --local user.email "grafana-delivery-bot[bot]@users.noreply.github.com" git config --local --add --bool push.autoSetupRemote true - name: Create branch run: git checkout -b "release/${{ github.run_number }}/$VERSION" - - name: Generate changelog token - if: ${{ inputs.changelog == true || inputs.changelog == 'true' }} - id: generate_changelog_token - uses: tibdex/github-app-token@b62528385c34dbc9f38e5f4225ac829252d1ea92 - with: - app_id: ${{ vars.DELIVERY_BOT_APP_ID }} - private_key: ${{ env.GRAFANA_DELIVERY_BOT_APP_PEM }} - name: Generate changelog id: changelog if: ${{ inputs.changelog == true || inputs.changelog == 'true' }} @@ -156,13 +157,16 @@ jobs: - name: Commit CHANGELOG.md changes if: ${{ inputs.changelog == true || inputs.changelog == 'true' }} run: git add CHANGELOG.md && git commit --allow-empty -m "Update changelog" CHANGELOG.md - - - name: Update package.json versions + - name: Bump versions if: ${{ inputs.bump == true || inputs.bump == 'true' }} - uses: ./.grafana-main/pkg/build/actions/bump-version + uses: dagger/dagger-for-github@e47aba410ef9bb9ed81a4d2a97df31061e5e842e with: - version: 'patch' + verb: run + args: go run -C .grafana-main ./pkg/build/actions/bump-version -version="patch" + - name: make gen-cue + shell: bash + run: make gen-cue - name: Add package.json changes if: ${{ inputs.bump == true || inputs.bump == 'true' }} run: | @@ -171,14 +175,13 @@ jobs: git commit -m "Update version to $VERSION" - name: Git push - if: inputs.dry_run != true run: git push - name: Create PR env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - BRANCH: ${{ steps.branch.outputs.branch }} + DRY_RUN: ${{ inputs.dry_run }} run: | - LATEST_FLAG="" + LATEST_FLAG=() if [ "$LATEST" = "true" ]; then LATEST_FLAG=(-l "release/latest") fi @@ -186,6 +189,6 @@ jobs: "${LATEST_FLAG[@]}" \ -l "no-changelog" \ --dry-run="$DRY_RUN" \ - -B "$BRANCH" \ + -B "${RELEASE_BRANCH}" \ --title "Release: $VERSION" \ --body "These code changes must be merged after a release is complete" diff --git a/pkg/build/actions/bump-version/action.yml b/pkg/build/actions/bump-version/action.yml index 24ae182daf2..d7d6fdf5c50 100644 --- a/pkg/build/actions/bump-version/action.yml +++ b/pkg/build/actions/bump-version/action.yml @@ -12,9 +12,12 @@ runs: go-version-file: go.mod - name: Bump versions uses: dagger/dagger-for-github@e47aba410ef9bb9ed81a4d2a97df31061e5e842e + env: + GO_MOD_DIR: ${{ inputs.go-mod-dir }} + VERSION: ${{ inputs.version }} with: verb: run - args: go run ./pkg/build/actions/bump-version -version=${{ inputs.version }} + args: go run ./pkg/build/actions/bump-version -version=${VERSION} - name: make gen-cue shell: bash run: make gen-cue From 43275fb6f2bd8e50e6226b898591e368246f6b37 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 22 May 2025 14:10:51 -0700 Subject: [PATCH 38/73] Release: update changelog for 12.0.1 (#105891) * Update changelog * update changelog --------- Co-authored-by: github-actions[bot] Co-authored-by: Kevin Yu --- CHANGELOG.md | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3fe70a7b7c8..e0a269eb40b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,31 @@ + + +# 12.0.1 (2025-05-22) + +### Features and enhancements + +- **Chore:** Bump Go version to 1.24.3 [#105101](https://github.com/grafana/grafana/pull/105101), [@macabu](https://github.com/macabu) +- **Dependencies:** Bump github.com/openfga/openfga from v1.8.6 to v1.8.12 [#105368](https://github.com/grafana/grafana/pull/105368), [@macabu](https://github.com/macabu) +- **Dependencies:** Unpin and bump github.com/getkin/kin-openapi from v0.126.0 to v0.132.0 [#105249](https://github.com/grafana/grafana/pull/105249), [@macabu](https://github.com/macabu) +- **K8s:** Dashboards: Add fine grained access control checks to /apis [#104419](https://github.com/grafana/grafana/pull/104419), [@stephaniehingtgen](https://github.com/stephaniehingtgen) + +### Bug fixes + +- **Dashboard:** Fix Panel Explore link subpath duplication (#104952) [#105056](https://github.com/grafana/grafana/pull/105056), [@axelavargas](https://github.com/axelavargas) +- **Dashboard:** Fixes issue with row repeats and first row [#104469](https://github.com/grafana/grafana/pull/104469), [@torkelo](https://github.com/torkelo) +- **Graphite:** Ensure template variables are interpolated correctly [#105389](https://github.com/grafana/grafana/pull/105389), [@aangelisc](https://github.com/aangelisc) +- **Graphite:** Fix Graphite series interpolation [#104516](https://github.com/grafana/grafana/pull/104516), [@aangelisc](https://github.com/aangelisc) +- **InfluxDB:** Fix nested variable interpolation [#104176](https://github.com/grafana/grafana/pull/104176), [@aangelisc](https://github.com/aangelisc) +- **MetricsDrilldown:** Restore link to Metrics Drilldown from Explore [#104073](https://github.com/grafana/grafana/pull/104073), [@NWRichmond](https://github.com/NWRichmond) +- **NestedFolderPicker:** Fix scroll jumps back to top [#105769](https://github.com/grafana/grafana/pull/105769), [@samsch](https://github.com/samsch) +- **Preferences:** Disable the save button whilst saving preferences [#105612](https://github.com/grafana/grafana/pull/105612), [@ashharrison90](https://github.com/ashharrison90) +- **Prometheus:** Fix semver import path [#104945](https://github.com/grafana/grafana/pull/104945), [@jackw](https://github.com/jackw) +- **Themes:** Prevent duplicated API call in drawer [#105611](https://github.com/grafana/grafana/pull/105611), [@ashharrison90](https://github.com/ashharrison90) +- **XYChart:** Coerce threshold steps to numbers [#104492](https://github.com/grafana/grafana/pull/104492), [@leeoniya](https://github.com/leeoniya) +- **Security:** Fix CVE-2025-4123 +- **Security:** Fix CVE-2025-3580 + + # 11.6.2 (2025-05-22) From 7e1c5a37e59ddd68baa1c4fc475c766c670f7f7e Mon Sep 17 00:00:00 2001 From: Kevin Minehart <5140827+kminehart@users.noreply.github.com> Date: Thu, 22 May 2025 17:13:16 -0500 Subject: [PATCH 39/73] CI: temporarily remove verify pipeline (#105910) temporarily remove verify pipeline --- pkg/build/daggerbuild/scripts/drone_build_main.sh | 1 - pkg/build/daggerbuild/scripts/drone_build_main_enterprise.sh | 1 - pkg/build/daggerbuild/scripts/drone_build_main_pro.sh | 1 - 3 files changed, 3 deletions(-) diff --git a/pkg/build/daggerbuild/scripts/drone_build_main.sh b/pkg/build/daggerbuild/scripts/drone_build_main.sh index ae1d356152f..35de14d8df1 100755 --- a/pkg/build/daggerbuild/scripts/drone_build_main.sh +++ b/pkg/build/daggerbuild/scripts/drone_build_main.sh @@ -24,7 +24,6 @@ dagger run --silent go run ./pkg/build/cmd \ -a docker:grafana:linux/arm/v7 \ --yarn-cache=${YARN_CACHE_FOLDER} \ --checksum \ - --verify \ --build-id=${DRONE_BUILD_NUMBER} \ --grafana-dir=${GRAFANA_DIR} \ --github-token=${GITHUB_TOKEN} \ diff --git a/pkg/build/daggerbuild/scripts/drone_build_main_enterprise.sh b/pkg/build/daggerbuild/scripts/drone_build_main_enterprise.sh index a7031aa29c8..4b1273f3743 100755 --- a/pkg/build/daggerbuild/scripts/drone_build_main_enterprise.sh +++ b/pkg/build/daggerbuild/scripts/drone_build_main_enterprise.sh @@ -19,7 +19,6 @@ dagger run --silent go run ./pkg/build/cmd \ -a docker:enterprise:linux/arm64 \ --yarn-cache=${YARN_CACHE_FOLDER} \ --checksum \ - --verify \ --build-id=${DRONE_BUILD_NUMBER} \ --grafana-ref=${SOURCE_COMMIT} \ --grafana-repo="https://github.com/grafana/grafana.git" \ diff --git a/pkg/build/daggerbuild/scripts/drone_build_main_pro.sh b/pkg/build/daggerbuild/scripts/drone_build_main_pro.sh index 1a3345a39b5..0f8b09c9b8b 100755 --- a/pkg/build/daggerbuild/scripts/drone_build_main_pro.sh +++ b/pkg/build/daggerbuild/scripts/drone_build_main_pro.sh @@ -15,7 +15,6 @@ dagger run --silent go run ./pkg/build/cmd \ -a frontend:enterprise \ --yarn-cache=${YARN_CACHE_FOLDER} \ --checksum \ - --verify \ --build-id=${DRONE_BUILD_NUMBER} \ --grafana-ref=${SOURCE_COMMIT} \ --grafana-repo="https://github.com/grafana/grafana.git" \ From 5cee301e752cc8de95c2a5f511706e367a28d9bf Mon Sep 17 00:00:00 2001 From: Victor Marin <36818606+mdvictor@users.noreply.github.com> Date: Fri, 23 May 2025 01:13:52 +0300 Subject: [PATCH 40/73] GroupByVariable: Add support for default values in schema (#105518) * update schema * update schema * refactor property to singular naming * bump scenes * leave scenes to caret version * add defaultValue to groupBy model * typo --- .../kinds/v2alpha1/dashboard_spec.cue | 1 + .../dashboard/v2alpha1/dashboard_spec.cue | 1 + .../dashboard/v2alpha1/dashboard_spec_gen.go | 19 ++++++++++--------- .../grafana-data/src/types/templateVars.ts | 1 + .../dashboard/v2alpha1/types.spec.gen.ts | 1 + .../dashboard-scene/utils/variables.ts | 1 + yarn.lock | 16 ++++++++-------- 7 files changed, 23 insertions(+), 17 deletions(-) diff --git a/apps/dashboard/kinds/v2alpha1/dashboard_spec.cue b/apps/dashboard/kinds/v2alpha1/dashboard_spec.cue index 6bedd25edb7..04273371f01 100644 --- a/apps/dashboard/kinds/v2alpha1/dashboard_spec.cue +++ b/apps/dashboard/kinds/v2alpha1/dashboard_spec.cue @@ -874,6 +874,7 @@ CustomVariableKind: { GroupByVariableSpec: { name: string | *"" datasource?: DataSourceRef + defaultValue?: VariableOption current: VariableOption | *{ text: "" value: "" diff --git a/apps/dashboard/pkg/apis/dashboard/v2alpha1/dashboard_spec.cue b/apps/dashboard/pkg/apis/dashboard/v2alpha1/dashboard_spec.cue index c6eb849f6bc..38ec4bf46ce 100644 --- a/apps/dashboard/pkg/apis/dashboard/v2alpha1/dashboard_spec.cue +++ b/apps/dashboard/pkg/apis/dashboard/v2alpha1/dashboard_spec.cue @@ -878,6 +878,7 @@ CustomVariableKind: { GroupByVariableSpec: { name: string | *"" datasource?: DataSourceRef + defaultValue?: VariableOption current: VariableOption | *{ text: "" value: "" diff --git a/apps/dashboard/pkg/apis/dashboard/v2alpha1/dashboard_spec_gen.go b/apps/dashboard/pkg/apis/dashboard/v2alpha1/dashboard_spec_gen.go index 19ccd04527d..45db16e7490 100644 --- a/apps/dashboard/pkg/apis/dashboard/v2alpha1/dashboard_spec_gen.go +++ b/apps/dashboard/pkg/apis/dashboard/v2alpha1/dashboard_spec_gen.go @@ -1601,15 +1601,16 @@ func NewDashboardGroupByVariableKind() *DashboardGroupByVariableKind { // GroupBy variable specification // +k8s:openapi-gen=true type DashboardGroupByVariableSpec struct { - Name string `json:"name"` - Datasource *DashboardDataSourceRef `json:"datasource,omitempty"` - Current DashboardVariableOption `json:"current"` - Options []DashboardVariableOption `json:"options"` - Multi bool `json:"multi"` - Label *string `json:"label,omitempty"` - Hide DashboardVariableHide `json:"hide"` - SkipUrlSync bool `json:"skipUrlSync"` - Description *string `json:"description,omitempty"` + Name string `json:"name"` + Datasource *DashboardDataSourceRef `json:"datasource,omitempty"` + DefaultValue *DashboardVariableOption `json:"defaultValue,omitempty"` + Current DashboardVariableOption `json:"current"` + Options []DashboardVariableOption `json:"options"` + Multi bool `json:"multi"` + Label *string `json:"label,omitempty"` + Hide DashboardVariableHide `json:"hide"` + SkipUrlSync bool `json:"skipUrlSync"` + Description *string `json:"description,omitempty"` } // NewDashboardGroupByVariableSpec creates a new DashboardGroupByVariableSpec object. diff --git a/packages/grafana-data/src/types/templateVars.ts b/packages/grafana-data/src/types/templateVars.ts index 56b391a92b8..60a5c6a6026 100644 --- a/packages/grafana-data/src/types/templateVars.ts +++ b/packages/grafana-data/src/types/templateVars.ts @@ -78,6 +78,7 @@ export interface GroupByVariableModel extends VariableWithOptions { datasource: DataSourceRef | null; multi: true; allowCustomValue?: boolean; + defaultValue?: VariableOption; } export interface VariableOption { diff --git a/packages/grafana-schema/src/schema/dashboard/v2alpha1/types.spec.gen.ts b/packages/grafana-schema/src/schema/dashboard/v2alpha1/types.spec.gen.ts index 009026fa970..33e7f2ed0b8 100644 --- a/packages/grafana-schema/src/schema/dashboard/v2alpha1/types.spec.gen.ts +++ b/packages/grafana-schema/src/schema/dashboard/v2alpha1/types.spec.gen.ts @@ -1290,6 +1290,7 @@ export const defaultGroupByVariableKind = (): GroupByVariableKind => ({ export interface GroupByVariableSpec { name: string; datasource?: DataSourceRef; + defaultValue?: VariableOption; current: VariableOption; options: VariableOption[]; multi: boolean; diff --git a/public/app/features/dashboard-scene/utils/variables.ts b/public/app/features/dashboard-scene/utils/variables.ts index d5c385d267b..b1975139550 100644 --- a/public/app/features/dashboard-scene/utils/variables.ts +++ b/public/app/features/dashboard-scene/utils/variables.ts @@ -251,6 +251,7 @@ export function createSceneVariableFromVariableModel(variable: TypedVariableMode hide: variable.hide, // @ts-expect-error defaultOptions: variable.options, + defaultValue: variable.defaultValue, allowCustomValue: variable.allowCustomValue, }); } else { diff --git a/yarn.lock b/yarn.lock index fb79a552b96..4d2b8c699f4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3490,10 +3490,10 @@ __metadata: linkType: soft "@grafana/scenes-react@npm:^6.12.0": - version: 6.12.0 - resolution: "@grafana/scenes-react@npm:6.12.0" + version: 6.13.0 + resolution: "@grafana/scenes-react@npm:6.13.0" dependencies: - "@grafana/scenes": "npm:6.12.0" + "@grafana/scenes": "npm:6.13.0" lru-cache: "npm:^10.2.2" react-use: "npm:^17.4.0" peerDependencies: @@ -3505,13 +3505,13 @@ __metadata: react: ^18.0.0 react-dom: ^18.0.0 react-router-dom: ^6.28.0 - checksum: 10/56113394d169c96537a46ca5dc4c62eda54c6addd348c9e3321a122a6d01c19a14c57b04945ee3d836638f3dc1c1941f7647fa649495226432664e0ff8bd0af0 + checksum: 10/8bbb098770d3b24fd60f4caa88579cd2e2327131061c7f54c25aa86688b4f4b7326d112515fe8ff3d62134af37af757d12d023c76dfaa68836f034e38f02da6d languageName: node linkType: hard -"@grafana/scenes@npm:6.12.0, @grafana/scenes@npm:^6.12.0": - version: 6.12.0 - resolution: "@grafana/scenes@npm:6.12.0" +"@grafana/scenes@npm:6.13.0, @grafana/scenes@npm:^6.12.0": + version: 6.13.0 + resolution: "@grafana/scenes@npm:6.13.0" dependencies: "@floating-ui/react": "npm:^0.26.16" "@leeoniya/ufuzzy": "npm:^1.0.16" @@ -3529,7 +3529,7 @@ __metadata: react: ^18.0.0 react-dom: ^18.0.0 react-router-dom: ^6.28.0 - checksum: 10/1bcddada8bf626d552aaa29ae4f916a50968bb36761285b0517500f50d8f52314148a8ed73419f381f8e5b901308cdf2450f0b82754691af512a2cff9f52b6bb + checksum: 10/62247045c095ac951c83059816214407e2fcf3736a61b536323616cdce08cd77a99b6a3532a1dbbe4f50ca26c94094a82e1713df84c37b0a7c0bebcf164b0b26 languageName: node linkType: hard From 0a49849cb392de79b049ae458bd364b732d46b3c Mon Sep 17 00:00:00 2001 From: Kevin Minehart <5140827+kminehart@users.noreply.github.com> Date: Thu, 22 May 2025 18:52:03 -0500 Subject: [PATCH 41/73] CI: Fix calling 'move_packages' in wrong directory (#105912) move_packages is in pkg now --- pkg/build/daggerbuild/scripts/drone_build_main.sh | 2 +- pkg/build/daggerbuild/scripts/drone_build_main_enterprise.sh | 2 +- pkg/build/daggerbuild/scripts/drone_build_main_pro.sh | 4 ++-- pkg/build/daggerbuild/scripts/drone_build_tag_enterprise.sh | 2 +- pkg/build/daggerbuild/scripts/drone_build_tag_grafana.sh | 2 +- pkg/build/daggerbuild/scripts/drone_build_tag_pro.sh | 2 +- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/pkg/build/daggerbuild/scripts/drone_build_main.sh b/pkg/build/daggerbuild/scripts/drone_build_main.sh index 35de14d8df1..4c584cdc3a1 100755 --- a/pkg/build/daggerbuild/scripts/drone_build_main.sh +++ b/pkg/build/daggerbuild/scripts/drone_build_main.sh @@ -35,4 +35,4 @@ echo "Final list of artifacts:" cat assets.txt # Move the tar.gz packages to their expected locations -cat assets.txt | IS_MAIN=true go run ./scripts/move_packages.go ./dist/main +cat assets.txt | IS_MAIN=true go run ./pkg/build/daggerbuild/scripts/move_packages.go ./dist/main diff --git a/pkg/build/daggerbuild/scripts/drone_build_main_enterprise.sh b/pkg/build/daggerbuild/scripts/drone_build_main_enterprise.sh index 4b1273f3743..e224ce9443b 100755 --- a/pkg/build/daggerbuild/scripts/drone_build_main_enterprise.sh +++ b/pkg/build/daggerbuild/scripts/drone_build_main_enterprise.sh @@ -33,4 +33,4 @@ dagger run --silent go run ./pkg/build/cmd \ cat assets.txt # Move the tar.gz packages to their expected locations -cat assets.txt | DESTINATION=gs://grafana-downloads IS_MAIN=true go run ./scripts/move_packages.go ./dist/main +cat assets.txt | DESTINATION=gs://grafana-downloads IS_MAIN=true go run ./pkg/build/daggerbuild/scripts/move_packages.go ./dist/main diff --git a/pkg/build/daggerbuild/scripts/drone_build_main_pro.sh b/pkg/build/daggerbuild/scripts/drone_build_main_pro.sh index 0f8b09c9b8b..932c6420d08 100755 --- a/pkg/build/daggerbuild/scripts/drone_build_main_pro.sh +++ b/pkg/build/daggerbuild/scripts/drone_build_main_pro.sh @@ -28,5 +28,5 @@ dagger run --silent go run ./pkg/build/cmd \ echo "Final list of artifacts:" # Move the tar.gz packages to their expected locations -cat assets.txt | grep -v "public" | DESTINATION=gs://grafana-downloads-enterprise2 IS_MAIN=true go run ./scripts/move_packages.go ./dist/main -cat assets.txt | grep "public" | DESTINATION=gs://grafana-static-assets IS_MAIN=true go run ./scripts/move_packages.go ./dist/cdn +cat assets.txt | grep -v "public" | DESTINATION=gs://grafana-downloads-enterprise2 IS_MAIN=true go run ./pkg/build/daggerbuild/scripts/move_packages.go ./dist/main +cat assets.txt | grep "public" | DESTINATION=gs://grafana-static-assets IS_MAIN=true go run ./pkg/build/daggerbuild/scripts/move_packages.go ./dist/cdn diff --git a/pkg/build/daggerbuild/scripts/drone_build_tag_enterprise.sh b/pkg/build/daggerbuild/scripts/drone_build_tag_enterprise.sh index a30870fed6a..ac31860b56a 100755 --- a/pkg/build/daggerbuild/scripts/drone_build_tag_enterprise.sh +++ b/pkg/build/daggerbuild/scripts/drone_build_tag_enterprise.sh @@ -48,4 +48,4 @@ dagger run --silent go run ./pkg/build/cmd \ --destination=${local_dst} > assets.txt # Move the tar.gz packages to their expected locations -cat assets.txt | go run ./scripts/move_packages.go ./dist/prerelease +cat assets.txt | go run ./pkg/build/daggerbuild/scripts/move_packages.go ./dist/prerelease diff --git a/pkg/build/daggerbuild/scripts/drone_build_tag_grafana.sh b/pkg/build/daggerbuild/scripts/drone_build_tag_grafana.sh index d28e703ef93..e21a705b380 100755 --- a/pkg/build/daggerbuild/scripts/drone_build_tag_grafana.sh +++ b/pkg/build/daggerbuild/scripts/drone_build_tag_grafana.sh @@ -44,4 +44,4 @@ dagger run --silent go run ./pkg/build/cmd \ --version=${DRONE_TAG} \ --destination=${local_dst} > assets.txt -cat assets.txt | go run ./scripts/move_packages.go ./dist/prerelease +cat assets.txt | go run ./pkg/build/daggerbuild/scripts/move_packages.go ./dist/prerelease diff --git a/pkg/build/daggerbuild/scripts/drone_build_tag_pro.sh b/pkg/build/daggerbuild/scripts/drone_build_tag_pro.sh index f7dd689efad..1bd7c4e7cc8 100755 --- a/pkg/build/daggerbuild/scripts/drone_build_tag_pro.sh +++ b/pkg/build/daggerbuild/scripts/drone_build_tag_pro.sh @@ -38,4 +38,4 @@ dagger run --silent go run ./pkg/build/cmd \ --destination=${local_dst} > assets.txt # Move the tar.gz packages to their expected locations -cat assets.txt | go run ./scripts/move_packages.go ./dist/prerelease +cat assets.txt | go run ./pkg/build/daggerbuild/scripts/move_packages.go ./dist/prerelease From b6c1f846859bf9a9105257970bb08970c59fd5bf Mon Sep 17 00:00:00 2001 From: "grafana-pr-automation[bot]" <140550294+grafana-pr-automation[bot]@users.noreply.github.com> Date: Fri, 23 May 2025 03:33:06 +0300 Subject: [PATCH 42/73] I18n: Download translations from Crowdin (#105913) New Crowdin translations by GitHub Action Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- public/locales/cs-CZ/grafana.json | 13 +++++++++++-- public/locales/de-DE/grafana.json | 13 +++++++++++-- public/locales/es-ES/grafana.json | 13 +++++++++++-- public/locales/fr-FR/grafana.json | 13 +++++++++++-- public/locales/hu-HU/grafana.json | 13 +++++++++++-- public/locales/id-ID/grafana.json | 13 +++++++++++-- public/locales/it-IT/grafana.json | 13 +++++++++++-- public/locales/ja-JP/grafana.json | 13 +++++++++++-- public/locales/ko-KR/grafana.json | 13 +++++++++++-- public/locales/nl-NL/grafana.json | 13 +++++++++++-- public/locales/pl-PL/grafana.json | 13 +++++++++++-- public/locales/pt-BR/grafana.json | 13 +++++++++++-- public/locales/pt-PT/grafana.json | 13 +++++++++++-- public/locales/ru-RU/grafana.json | 13 +++++++++++-- public/locales/sv-SE/grafana.json | 13 +++++++++++-- public/locales/tr-TR/grafana.json | 13 +++++++++++-- public/locales/zh-Hans/grafana.json | 13 +++++++++++-- public/locales/zh-Hant/grafana.json | 13 +++++++++++-- 18 files changed, 198 insertions(+), 36 deletions(-) diff --git a/public/locales/cs-CZ/grafana.json b/public/locales/cs-CZ/grafana.json index 17f34de29fd..50a97ceb3f1 100644 --- a/public/locales/cs-CZ/grafana.json +++ b/public/locales/cs-CZ/grafana.json @@ -57,7 +57,11 @@ "aria-label-add": "Přidat", "aria-label-delete": "Odstranit", "placeholder-key": "Klíč", + "placeholder-name": "Název", "placeholder-value": "Hodnota" + }, + "variables-editor": { + "placeholder-type": "Typ" } }, "admin": { @@ -5446,10 +5450,14 @@ "grafana-ui": { "action-editor": { "button": { + "action-variables-title": "", "confirm": "Potvrdit", "confirm-action": "Potvrdit akci", - "style": "" + "style": "", + "variable-value-placeholder": "Hodnota" }, + "close": "Zavřít", + "continue": "", "inline": { "add-action": "Přidat akci", "edit-action": "Upravit akci" @@ -5460,6 +5468,7 @@ "action-query-params": "Parametry dotazu", "action-title": "Název", "action-title-placeholder": "Název akce", + "action-variables": "Proměnné", "one-click-description": "Současně může být povoleno pouze jedno kliknutí nebo akce na jeden odkaz" } }, @@ -6918,7 +6927,7 @@ "title": "Účty služeb" }, "setup-guide": { - "title": "Průvodce nastavením" + "title": "" }, "shared-dashboard": { "subtitle": "Spravujte externě sdílené nástěnky vaší organizace", diff --git a/public/locales/de-DE/grafana.json b/public/locales/de-DE/grafana.json index b06159c9af5..9e2c394928b 100644 --- a/public/locales/de-DE/grafana.json +++ b/public/locales/de-DE/grafana.json @@ -57,7 +57,11 @@ "aria-label-add": "Hinzufügen", "aria-label-delete": "Löschen", "placeholder-key": "Key", + "placeholder-name": "Name", "placeholder-value": "Wert" + }, + "variables-editor": { + "placeholder-type": "Typ" } }, "admin": { @@ -5404,10 +5408,14 @@ "grafana-ui": { "action-editor": { "button": { + "action-variables-title": "", "confirm": "Bestätigen", "confirm-action": "Aktion bestätigen", - "style": "" + "style": "", + "variable-value-placeholder": "Wert" }, + "close": "Schließen", + "continue": "", "inline": { "add-action": "Aktion hinzufügen", "edit-action": "Aktion bearbeiten" @@ -5418,6 +5426,7 @@ "action-query-params": "Abfrageparameter", "action-title": "Titel", "action-title-placeholder": "Titel der Aktion", + "action-variables": "Variable", "one-click-description": "Der Ein-Klick-Modus kann nur für einen Link oder eine Aktion gleichzeitig aktiviert werden" } }, @@ -6866,7 +6875,7 @@ "title": "Servicekonten" }, "setup-guide": { - "title": "Setup-Anleitung" + "title": "" }, "shared-dashboard": { "subtitle": "Verwalten Sie die extern freigegebenen Dashboards Ihrer Organisation", diff --git a/public/locales/es-ES/grafana.json b/public/locales/es-ES/grafana.json index cf123004792..4a412853b45 100644 --- a/public/locales/es-ES/grafana.json +++ b/public/locales/es-ES/grafana.json @@ -57,7 +57,11 @@ "aria-label-add": "Añadir", "aria-label-delete": "Eliminar", "placeholder-key": "Clave", + "placeholder-name": "Nombre", "placeholder-value": "Valor" + }, + "variables-editor": { + "placeholder-type": "Tipo" } }, "admin": { @@ -5404,10 +5408,14 @@ "grafana-ui": { "action-editor": { "button": { + "action-variables-title": "", "confirm": "Confirmar", "confirm-action": "Confirmar acción", - "style": "" + "style": "", + "variable-value-placeholder": "Valor" }, + "close": "Cerrar", + "continue": "", "inline": { "add-action": "Añadir acción", "edit-action": "Editar acción" @@ -5418,6 +5426,7 @@ "action-query-params": "Parámetros de la consulta", "action-title": "Título", "action-title-placeholder": "Título de la acción", + "action-variables": "Variables", "one-click-description": "Solo un enlace o acción puede tener un clic habilitado a la vez" } }, @@ -6866,7 +6875,7 @@ "title": "Cuentas de servicios" }, "setup-guide": { - "title": "Guía de configuración" + "title": "" }, "shared-dashboard": { "subtitle": "Gestiona los paneles de control compartidos externamente de tu organización", diff --git a/public/locales/fr-FR/grafana.json b/public/locales/fr-FR/grafana.json index c06ab2dd449..e81145a6b0a 100644 --- a/public/locales/fr-FR/grafana.json +++ b/public/locales/fr-FR/grafana.json @@ -57,7 +57,11 @@ "aria-label-add": "Ajouter", "aria-label-delete": "Supprimer", "placeholder-key": "Clé", + "placeholder-name": "Nom", "placeholder-value": "Valeur" + }, + "variables-editor": { + "placeholder-type": "Type" } }, "admin": { @@ -5404,10 +5408,14 @@ "grafana-ui": { "action-editor": { "button": { + "action-variables-title": "", "confirm": "Confirmer", "confirm-action": "Confirmer l'action", - "style": "" + "style": "", + "variable-value-placeholder": "Valeur" }, + "close": "Fermer", + "continue": "", "inline": { "add-action": "Ajouter l'action", "edit-action": "Modifier l'action" @@ -5418,6 +5426,7 @@ "action-query-params": "Paramètres de requête", "action-title": "Titre", "action-title-placeholder": "Titre de l'action", + "action-variables": "Variables", "one-click-description": "Un seul lien ou une seule action peut avoir un clic activé à la fois" } }, @@ -6866,7 +6875,7 @@ "title": "Comptes de service" }, "setup-guide": { - "title": "Guide de configuration" + "title": "" }, "shared-dashboard": { "subtitle": "Gérez les tableaux de bord partagés en externe de votre organisation", diff --git a/public/locales/hu-HU/grafana.json b/public/locales/hu-HU/grafana.json index a880119b5e8..56edca8730e 100644 --- a/public/locales/hu-HU/grafana.json +++ b/public/locales/hu-HU/grafana.json @@ -57,7 +57,11 @@ "aria-label-add": "Hozzáadás", "aria-label-delete": "Törlés", "placeholder-key": "Kulcs", + "placeholder-name": "Név", "placeholder-value": "Érték" + }, + "variables-editor": { + "placeholder-type": "Típus" } }, "admin": { @@ -5404,10 +5408,14 @@ "grafana-ui": { "action-editor": { "button": { + "action-variables-title": "", "confirm": "Megerősítés", "confirm-action": "Művelet megerősítése", - "style": "" + "style": "", + "variable-value-placeholder": "Érték" }, + "close": "Bezárás", + "continue": "", "inline": { "add-action": "Művelet hozzáadása", "edit-action": "Művelet szerkesztése" @@ -5418,6 +5426,7 @@ "action-query-params": "Lekérdezési paraméterek", "action-title": "Cím", "action-title-placeholder": "Művelet címe", + "action-variables": "Változók", "one-click-description": "Egyszerre csak egy hivatkozáshoz vagy művelethez lehet egy kattintás engedélyezve" } }, @@ -6866,7 +6875,7 @@ "title": "Szolgáltatási fiókok" }, "setup-guide": { - "title": "Beállítási útmutató" + "title": "" }, "shared-dashboard": { "subtitle": "Szervezete külső megosztott irányítópultjainak kezelése", diff --git a/public/locales/id-ID/grafana.json b/public/locales/id-ID/grafana.json index 99e65171bcc..82a3115339d 100644 --- a/public/locales/id-ID/grafana.json +++ b/public/locales/id-ID/grafana.json @@ -57,7 +57,11 @@ "aria-label-add": "Tambahkan", "aria-label-delete": "Hapus", "placeholder-key": "Kunci", + "placeholder-name": "Nama", "placeholder-value": "Nilai" + }, + "variables-editor": { + "placeholder-type": "Jenis" } }, "admin": { @@ -5383,10 +5387,14 @@ "grafana-ui": { "action-editor": { "button": { + "action-variables-title": "", "confirm": "Konfirmasi", "confirm-action": "Konfirmasi tindakan", - "style": "" + "style": "", + "variable-value-placeholder": "Nilai" }, + "close": "Tutup", + "continue": "", "inline": { "add-action": "Tambah tindakan", "edit-action": "Edit tindakan" @@ -5397,6 +5405,7 @@ "action-query-params": "Parameter kueri", "action-title": "Judul", "action-title-placeholder": "Judul tindakan", + "action-variables": "Variabel", "one-click-description": "Hanya satu tautan atau tindakan yang dapat diaktifkan satu klik pada satu waktu" } }, @@ -6840,7 +6849,7 @@ "title": "Akun layanan" }, "setup-guide": { - "title": "Panduan penyiapan" + "title": "" }, "shared-dashboard": { "subtitle": "Kelola dasbor bersama eksternal organisasi Anda", diff --git a/public/locales/it-IT/grafana.json b/public/locales/it-IT/grafana.json index 22649f67c18..1908975ea3e 100644 --- a/public/locales/it-IT/grafana.json +++ b/public/locales/it-IT/grafana.json @@ -57,7 +57,11 @@ "aria-label-add": "Aggiungi", "aria-label-delete": "Elimina", "placeholder-key": "Codice", + "placeholder-name": "Nome", "placeholder-value": "Valore" + }, + "variables-editor": { + "placeholder-type": "Tipo" } }, "admin": { @@ -5404,10 +5408,14 @@ "grafana-ui": { "action-editor": { "button": { + "action-variables-title": "", "confirm": "Conferma", "confirm-action": "Conferma azione", - "style": "" + "style": "", + "variable-value-placeholder": "Valore" }, + "close": "Chiudi", + "continue": "", "inline": { "add-action": "Aggiungi azione", "edit-action": "Modifica azione" @@ -5418,6 +5426,7 @@ "action-query-params": "Parametri delle query", "action-title": "Titolo", "action-title-placeholder": "Titolo azione", + "action-variables": "Variabili", "one-click-description": "Solo un link o un'azione possono avere un clic abilitato alla volta" } }, @@ -6866,7 +6875,7 @@ "title": "Account di servizio" }, "setup-guide": { - "title": "Guida alle impostazioni" + "title": "" }, "shared-dashboard": { "subtitle": "Gestisci i dashboard condivisi esternamente della tua organizzazione", diff --git a/public/locales/ja-JP/grafana.json b/public/locales/ja-JP/grafana.json index 437778f7424..7901e383592 100644 --- a/public/locales/ja-JP/grafana.json +++ b/public/locales/ja-JP/grafana.json @@ -57,7 +57,11 @@ "aria-label-add": "追加", "aria-label-delete": "削除", "placeholder-key": "キー", + "placeholder-name": "名前", "placeholder-value": "値" + }, + "variables-editor": { + "placeholder-type": "種類" } }, "admin": { @@ -5383,10 +5387,14 @@ "grafana-ui": { "action-editor": { "button": { + "action-variables-title": "", "confirm": "確定", "confirm-action": "操作を確認", - "style": "" + "style": "", + "variable-value-placeholder": "値" }, + "close": "閉じる", + "continue": "", "inline": { "add-action": "操作を追加", "edit-action": "操作を編集" @@ -5397,6 +5405,7 @@ "action-query-params": "クエリのパラメータ", "action-title": "タイトル", "action-title-placeholder": "操作のタイトル", + "action-variables": "変数", "one-click-description": "一度に1つのクリックを有効にできるリンクまたは操作は1つだけです" } }, @@ -6840,7 +6849,7 @@ "title": "サービスアカウント" }, "setup-guide": { - "title": "セットアップガイド" + "title": "" }, "shared-dashboard": { "subtitle": "組織の外部共有ダッシュボードを管理する", diff --git a/public/locales/ko-KR/grafana.json b/public/locales/ko-KR/grafana.json index 42fb2842f3f..6368b25a701 100644 --- a/public/locales/ko-KR/grafana.json +++ b/public/locales/ko-KR/grafana.json @@ -57,7 +57,11 @@ "aria-label-add": "추가", "aria-label-delete": "삭제", "placeholder-key": "키", + "placeholder-name": "이름", "placeholder-value": "값" + }, + "variables-editor": { + "placeholder-type": "유형" } }, "admin": { @@ -5383,10 +5387,14 @@ "grafana-ui": { "action-editor": { "button": { + "action-variables-title": "", "confirm": "확인", "confirm-action": "작업 확인", - "style": "" + "style": "", + "variable-value-placeholder": "값" }, + "close": "닫기", + "continue": "", "inline": { "add-action": "작업 추가", "edit-action": "작업 편집" @@ -5397,6 +5405,7 @@ "action-query-params": "쿼리 파라미터", "action-title": "제목", "action-title-placeholder": "작업 제목", + "action-variables": "변수", "one-click-description": "한 번에 하나의 링크나 작업만 원 클릭을 활성화할 수 있습니다." } }, @@ -6840,7 +6849,7 @@ "title": "서비스 계정" }, "setup-guide": { - "title": "초기 설정 가이드" + "title": "" }, "shared-dashboard": { "subtitle": "조직의 외부 공유 대시보드 관리", diff --git a/public/locales/nl-NL/grafana.json b/public/locales/nl-NL/grafana.json index 3e9cc58cb20..94fe64e885e 100644 --- a/public/locales/nl-NL/grafana.json +++ b/public/locales/nl-NL/grafana.json @@ -57,7 +57,11 @@ "aria-label-add": "Toevoegen", "aria-label-delete": "Verwijderen", "placeholder-key": "Sleutel", + "placeholder-name": "Naam", "placeholder-value": "Waarde" + }, + "variables-editor": { + "placeholder-type": "Type" } }, "admin": { @@ -5404,10 +5408,14 @@ "grafana-ui": { "action-editor": { "button": { + "action-variables-title": "", "confirm": "Bevestigen", "confirm-action": "Actie bevestigen", - "style": "" + "style": "", + "variable-value-placeholder": "Waarde" }, + "close": "Sluiten", + "continue": "", "inline": { "add-action": "Actie toevoegen", "edit-action": "Actie bewerken" @@ -5418,6 +5426,7 @@ "action-query-params": "Query-parameters", "action-title": "Titel", "action-title-placeholder": "Actietitel", + "action-variables": "Variabelen", "one-click-description": "Slechts één link of actie kan tegelijk de één-klik-functionaliteit hebben ingeschakeld" } }, @@ -6866,7 +6875,7 @@ "title": "Serviceaccounts" }, "setup-guide": { - "title": "Installatiehandleiding" + "title": "" }, "shared-dashboard": { "subtitle": "De extern gedeelde dashboards van je organisatie beheren", diff --git a/public/locales/pl-PL/grafana.json b/public/locales/pl-PL/grafana.json index 00e2645c330..7cf9b850e86 100644 --- a/public/locales/pl-PL/grafana.json +++ b/public/locales/pl-PL/grafana.json @@ -57,7 +57,11 @@ "aria-label-add": "Dodaj", "aria-label-delete": "Usuń", "placeholder-key": "Klucz", + "placeholder-name": "Imię", "placeholder-value": "Wartość" + }, + "variables-editor": { + "placeholder-type": "Typ" } }, "admin": { @@ -5446,10 +5450,14 @@ "grafana-ui": { "action-editor": { "button": { + "action-variables-title": "", "confirm": "Potwierdź", "confirm-action": "Potwierdź działanie", - "style": "" + "style": "", + "variable-value-placeholder": "Wartość" }, + "close": "Zamknij", + "continue": "", "inline": { "add-action": "Dodaj działanie", "edit-action": "Edytuj działanie" @@ -5460,6 +5468,7 @@ "action-query-params": "Parametry zapytania", "action-title": "Tytuł", "action-title-placeholder": "Tytuł działania", + "action-variables": "Zmienne", "one-click-description": "Tylko jeden link lub działanie może mieć włączoną opcję jednego kliknięcia" } }, @@ -6918,7 +6927,7 @@ "title": "Konta usługi" }, "setup-guide": { - "title": "Instrukcja konfiguracji" + "title": "" }, "shared-dashboard": { "subtitle": "Zarządzaj udostępnionymi zewnętrznie pulpitami swojej organizacji", diff --git a/public/locales/pt-BR/grafana.json b/public/locales/pt-BR/grafana.json index 352003b33cc..332808faddc 100644 --- a/public/locales/pt-BR/grafana.json +++ b/public/locales/pt-BR/grafana.json @@ -57,7 +57,11 @@ "aria-label-add": "Adicionar", "aria-label-delete": "Excluir", "placeholder-key": "Chave", + "placeholder-name": "Nome", "placeholder-value": "Valor" + }, + "variables-editor": { + "placeholder-type": "Tipo" } }, "admin": { @@ -5404,10 +5408,14 @@ "grafana-ui": { "action-editor": { "button": { + "action-variables-title": "", "confirm": "Confirmar", "confirm-action": "Confirmar ação", - "style": "" + "style": "", + "variable-value-placeholder": "Valor" }, + "close": "Fechar", + "continue": "", "inline": { "add-action": "Adicionar ação", "edit-action": "Editar ação" @@ -5418,6 +5426,7 @@ "action-query-params": "Parâmetros de consulta", "action-title": "Título", "action-title-placeholder": "Título da ação", + "action-variables": "Variáveis", "one-click-description": "Apenas um link ou ação pode ter um clique habilitado de cada vez" } }, @@ -6866,7 +6875,7 @@ "title": "Contas de serviço" }, "setup-guide": { - "title": "Guia de configuração" + "title": "" }, "shared-dashboard": { "subtitle": "Gerencie os painéis de controle da sua organização compartilhados externamente", diff --git a/public/locales/pt-PT/grafana.json b/public/locales/pt-PT/grafana.json index 0d0af77a3be..fd970ec2ce6 100644 --- a/public/locales/pt-PT/grafana.json +++ b/public/locales/pt-PT/grafana.json @@ -57,7 +57,11 @@ "aria-label-add": "Adicionar", "aria-label-delete": "Eliminar", "placeholder-key": "Chave", + "placeholder-name": "Nome", "placeholder-value": "Valor" + }, + "variables-editor": { + "placeholder-type": "Tipo" } }, "admin": { @@ -5404,10 +5408,14 @@ "grafana-ui": { "action-editor": { "button": { + "action-variables-title": "", "confirm": "Confirmar", "confirm-action": "Confirmar ação", - "style": "" + "style": "", + "variable-value-placeholder": "Valor" }, + "close": "Fechar", + "continue": "", "inline": { "add-action": "Adicionar ação", "edit-action": "Editar ação" @@ -5418,6 +5426,7 @@ "action-query-params": "Parâmetros da consulta", "action-title": "Título", "action-title-placeholder": "Título da ação", + "action-variables": "Variáveis", "one-click-description": "Apenas um link ou ação pode ter um clique ativado de cada vez" } }, @@ -6866,7 +6875,7 @@ "title": "Contas de serviço" }, "setup-guide": { - "title": "Guia de configuração" + "title": "" }, "shared-dashboard": { "subtitle": "Faça a gestão dos painéis de controlo partilhados externamente da sua organização", diff --git a/public/locales/ru-RU/grafana.json b/public/locales/ru-RU/grafana.json index 9e60fd9b936..ed36a9bf7db 100644 --- a/public/locales/ru-RU/grafana.json +++ b/public/locales/ru-RU/grafana.json @@ -57,7 +57,11 @@ "aria-label-add": "Добавить", "aria-label-delete": "Удалить", "placeholder-key": "Ключ", + "placeholder-name": "Имя", "placeholder-value": "Значение" + }, + "variables-editor": { + "placeholder-type": "Тип" } }, "admin": { @@ -5446,10 +5450,14 @@ "grafana-ui": { "action-editor": { "button": { + "action-variables-title": "", "confirm": "Подтвердить", "confirm-action": "Подтвердить действие", - "style": "" + "style": "", + "variable-value-placeholder": "Значение" }, + "close": "Закрыть", + "continue": "", "inline": { "add-action": "Добавить действие", "edit-action": "Редактировать действие" @@ -5460,6 +5468,7 @@ "action-query-params": "Параметры запроса", "action-title": "Название", "action-title-placeholder": "Название действия", + "action-variables": "Переменные", "one-click-description": "Только одна ссылка или действие могут быть активированы одним нажатием за раз" } }, @@ -6918,7 +6927,7 @@ "title": "Служебные учетные записи" }, "setup-guide": { - "title": "Руководство по настройке" + "title": "" }, "shared-dashboard": { "subtitle": "Управление дашбордами организации во внешнем совместном использовании", diff --git a/public/locales/sv-SE/grafana.json b/public/locales/sv-SE/grafana.json index 8664cb37c85..50619343eb7 100644 --- a/public/locales/sv-SE/grafana.json +++ b/public/locales/sv-SE/grafana.json @@ -57,7 +57,11 @@ "aria-label-add": "Lägg till", "aria-label-delete": "Ta bort", "placeholder-key": "Nyckel", + "placeholder-name": "Namn", "placeholder-value": "Värde" + }, + "variables-editor": { + "placeholder-type": "Typ" } }, "admin": { @@ -5404,10 +5408,14 @@ "grafana-ui": { "action-editor": { "button": { + "action-variables-title": "", "confirm": "Bekräfta", "confirm-action": "Bekräfta åtgärd", - "style": "" + "style": "", + "variable-value-placeholder": "Värde" }, + "close": "Stäng", + "continue": "", "inline": { "add-action": "Lägg till åtgärd", "edit-action": "Redigera åtgärd" @@ -5418,6 +5426,7 @@ "action-query-params": "Frågeparametrar", "action-title": "Titel", "action-title-placeholder": "Åtgärdstitel", + "action-variables": "Variabler", "one-click-description": "Endast en länk eller en åtgärd kan ha en aktiverad klickning åt gången" } }, @@ -6866,7 +6875,7 @@ "title": "Servicekonton" }, "setup-guide": { - "title": "Installationsguide" + "title": "" }, "shared-dashboard": { "subtitle": "Hantera din organisations externt delade instrumentpaneler", diff --git a/public/locales/tr-TR/grafana.json b/public/locales/tr-TR/grafana.json index d1a37980860..91dda42d0bf 100644 --- a/public/locales/tr-TR/grafana.json +++ b/public/locales/tr-TR/grafana.json @@ -57,7 +57,11 @@ "aria-label-add": "Ekle", "aria-label-delete": "Sil", "placeholder-key": "Anahtar", + "placeholder-name": "Ad", "placeholder-value": "Değer" + }, + "variables-editor": { + "placeholder-type": "Tür" } }, "admin": { @@ -5404,10 +5408,14 @@ "grafana-ui": { "action-editor": { "button": { + "action-variables-title": "", "confirm": "Onayla", "confirm-action": "İşlemi onayla", - "style": "" + "style": "", + "variable-value-placeholder": "Değer" }, + "close": "Kapat", + "continue": "", "inline": { "add-action": "İşlem ekle", "edit-action": "İşlemi düzenle" @@ -5418,6 +5426,7 @@ "action-query-params": "Sorgu parametreleri", "action-title": "Başlık", "action-title-placeholder": "İşlem başlığı", + "action-variables": "Değişkenler", "one-click-description": "Aynı anda yalnızca bir bağlantı veya işlem için tek tıklama etkinleştirilebilir" } }, @@ -6866,7 +6875,7 @@ "title": "Servis hesapları" }, "setup-guide": { - "title": "Kurulum kılavuzu" + "title": "" }, "shared-dashboard": { "subtitle": "Kuruluşunuzun haricî olarak paylaşılan panolarını yönetin", diff --git a/public/locales/zh-Hans/grafana.json b/public/locales/zh-Hans/grafana.json index cf59f01a4aa..29fb0734057 100644 --- a/public/locales/zh-Hans/grafana.json +++ b/public/locales/zh-Hans/grafana.json @@ -57,7 +57,11 @@ "aria-label-add": "添加", "aria-label-delete": "删除", "placeholder-key": "密钥", + "placeholder-name": "名称", "placeholder-value": "值" + }, + "variables-editor": { + "placeholder-type": "类型" } }, "admin": { @@ -5383,10 +5387,14 @@ "grafana-ui": { "action-editor": { "button": { + "action-variables-title": "", "confirm": "确认", "confirm-action": "确认操作", - "style": "" + "style": "", + "variable-value-placeholder": "值" }, + "close": "关闭", + "continue": "", "inline": { "add-action": "添加操作", "edit-action": "编辑操作" @@ -5397,6 +5405,7 @@ "action-query-params": "查询参数", "action-title": "标题", "action-title-placeholder": "操作标题", + "action-variables": "变量", "one-click-description": "一次只能为一个链接或操作启用一次点击" } }, @@ -6840,7 +6849,7 @@ "title": "服务帐户" }, "setup-guide": { - "title": "安装指南" + "title": "" }, "shared-dashboard": { "subtitle": "管理您组织的外部共享数据面板", diff --git a/public/locales/zh-Hant/grafana.json b/public/locales/zh-Hant/grafana.json index 4e458d18710..9a4045cebb1 100644 --- a/public/locales/zh-Hant/grafana.json +++ b/public/locales/zh-Hant/grafana.json @@ -57,7 +57,11 @@ "aria-label-add": "新增", "aria-label-delete": "刪除", "placeholder-key": "鍵", + "placeholder-name": "名稱(名字)", "placeholder-value": "數值" + }, + "variables-editor": { + "placeholder-type": "類型" } }, "admin": { @@ -5383,10 +5387,14 @@ "grafana-ui": { "action-editor": { "button": { + "action-variables-title": "", "confirm": "確認", "confirm-action": "確認動作", - "style": "" + "style": "", + "variable-value-placeholder": "數值" }, + "close": "關閉", + "continue": "", "inline": { "add-action": "新增動作", "edit-action": "編輯動作" @@ -5397,6 +5405,7 @@ "action-query-params": "查詢參數", "action-title": "標題", "action-title-placeholder": "動作標題", + "action-variables": "變數", "one-click-description": "一次只能啟用一個連結或動作的點擊" } }, @@ -6840,7 +6849,7 @@ "title": "服務帳戶" }, "setup-guide": { - "title": "快速安裝指南" + "title": "" }, "shared-dashboard": { "subtitle": "管理組織的外部共用儀表板", From 86a366126a09379365acb3d1cadf362a236e24da Mon Sep 17 00:00:00 2001 From: Kristina Date: Thu, 22 May 2025 21:38:32 -0500 Subject: [PATCH 43/73] Transformations: Transpose should respect renamed field names (#105893) Co-authored-by: Leon Sorokin --- .../transformations/transformers/transpose.test.ts | 10 +++++++++- .../src/transformations/transformers/transpose.ts | 11 +++++++++-- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/packages/grafana-data/src/transformations/transformers/transpose.test.ts b/packages/grafana-data/src/transformations/transformers/transpose.test.ts index e522b93e885..c14026a8920 100644 --- a/packages/grafana-data/src/transformations/transformers/transpose.test.ts +++ b/packages/grafana-data/src/transformations/transformers/transpose.test.ts @@ -86,9 +86,17 @@ describe('Transpose transformer', () => { { name: 'env', type: FieldType.string, values: ['dev', 'prod', 'staging', 'release', 'beta'] }, { name: 'january', type: FieldType.number, values: [11, 12, 13, 14, 15] }, { name: 'february', type: FieldType.number, values: [6, 7, 8, 9, 10] }, - { name: 'type', type: FieldType.string, values: ['metricA', 'metricB', 'metricC', 'metricD', 'metricE'] }, + { + name: 'metricName', + type: FieldType.string, + values: ['metricA', 'metricB', 'metricC', 'metricD', 'metricE'], + config: { + displayName: 'type', + }, + }, ], }); + await expect(transformDataFrame([cfgB], [seriesB])).toEmitValuesWith((received) => { const result = received[0]; expect(result[0].fields).toEqual([ diff --git a/packages/grafana-data/src/transformations/transformers/transpose.ts b/packages/grafana-data/src/transformations/transformers/transpose.ts index ae97e8f4ceb..651254a736d 100644 --- a/packages/grafana-data/src/transformations/transformers/transpose.ts +++ b/packages/grafana-data/src/transformations/transformers/transpose.ts @@ -1,5 +1,6 @@ import { map } from 'rxjs/operators'; +import { cacheFieldDisplayNames } from '../../field/fieldState'; import { DataFrame, Field, FieldType } from '../../types/dataFrame'; import { DataTransformerInfo } from '../../types/transformations'; @@ -28,6 +29,8 @@ export const transposeTransformer: DataTransformerInfo { const firstField = frame.fields[0]; const firstName = !options.firstFieldName ? 'Field' : options.firstFieldName; @@ -38,8 +41,12 @@ function transposeDataFrame(options: TransposeTransformerOptions, data: DataFram ? [firstName, ...fieldValuesAsStrings(firstField, firstField.values)] : [firstName, ...firstField.values.map((_, i) => restName)]; const rows = useFirstFieldAsHeaders - ? frame.fields.map((field) => field.name).slice(1) - : frame.fields.map((field) => field.name); + ? frame.fields + .map((field) => { + return field.state?.displayName ?? field.name; + }) + .slice(1) + : frame.fields.map((field) => field.state?.displayName ?? field.name); const fieldType = determineFieldType( useFirstFieldAsHeaders ? frame.fields.map((field) => field.type).slice(1) From 04de9c2740019f624e51ca9a76341ec41598c07c Mon Sep 17 00:00:00 2001 From: Sven Grossmann Date: Fri, 23 May 2025 10:16:13 +0300 Subject: [PATCH 44/73] Plugins: Support suffix in `grafanaDependency` (#105916) Plugins: Support appendix in `grafanaDependency` --- docs/sources/developers/plugins/plugin.schema.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/sources/developers/plugins/plugin.schema.json b/docs/sources/developers/plugins/plugin.schema.json index a2c7d1af36f..d87e542bd05 100644 --- a/docs/sources/developers/plugins/plugin.schema.json +++ b/docs/sources/developers/plugins/plugin.schema.json @@ -172,7 +172,7 @@ "grafanaDependency": { "type": "string", "description": "Required Grafana version for this plugin. Validated using https://github.com/npm/node-semver.", - "pattern": "^(<=|>=|<|>|=|~|\\^)?([0-9]+)(\\.[0-9x\\*]+)?(\\.[0-9x\\*]+)?(\\s(<=|>=|<|=>)?([0-9]+)(\\.[0-9x\\*]+)?(\\.[0-9x\\*]+)?)?$" + "pattern": "^(<=|>=|<|>|=|~|\\^)?([0-9]+)(\\.[0-9x\\*]+)?(\\.[0-9x\\*]+)?(-[0-9A-Za-z-.]+)?(\\s(<=|>=|<|=>)?([0-9]+)(\\.[0-9x\\*]+)?(\\.[0-9x\\*]+)?(-[0-9A-Za-z-.]+)?)?$" }, "plugins": { "type": "array", From e57be369365c673446dd0d3a3536397c6dd6265e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-Philippe=20Qu=C3=A9m=C3=A9ner?= Date: Fri, 23 May 2025 09:19:20 +0200 Subject: [PATCH 45/73] fix(unified-storage): race in resource server watch (#105786) --- pkg/storage/unified/resource/server.go | 45 ++++++++++++++++++++++---- 1 file changed, 39 insertions(+), 6 deletions(-) diff --git a/pkg/storage/unified/resource/server.go b/pkg/storage/unified/resource/server.go index c340c7a6e39..42474e706a3 100644 --- a/pkg/storage/unified/resource/server.go +++ b/pkg/storage/unified/resource/server.go @@ -918,15 +918,48 @@ func (s *server) Watch(req *resourcepb.WatchRequest, srv resourcepb.ResourceStor } defer s.broadcaster.Unsubscribe(stream) + // Determine a safe starting resource-version for the watch. + // When the client requests SendInitialEvents we will use the resource-version + // of the last object returned from the initial list (handled below). + // When the client supplies an explicit `since` we honour that. + // In the remaining case (SendInitialEvents == false && since == 0) we need + // a high-water-mark representing the current state of storage so that we + // donʼt replay events that happened before the watch was established. Using + // `mostRecentRV` – which is updated asynchronously by the broadcaster – is + // subject to races because the broadcaster may not yet have observed the + // latest committed writes. Instead we ask the backend directly for the + // current resource-version. + var mostRecentRV int64 if !req.SendInitialEvents && req.Since == 0 { - // This is a temporary hack only relevant for tests to ensure that the first events are sent. - // This is required because the SQL backend polls the database every 100ms. - // TODO: Implement a getLatestResourceVersion method in the backend. - time.Sleep(10 * time.Millisecond) + // We only need the current RV. A cheap way to obtain it is to issue a + // List with a very small limit and read the listRV returned by the + // iterator. The callback is a no-op so we avoid materialising any + // items. + listReq := &resourcepb.ListRequest{ + Options: req.Options, + // This has right now no effect, as the list request only uses the limit if it lists from history or trash. + // It might be worth adding it in a subsequent PR. We only list once during setup of the watch, so it's + // fine for now. + Limit: 1, + } + + rv, err := s.backend.ListIterator(ctx, listReq, func(ListIterator) error { return nil }) + if err != nil { + // Fallback to the broadcasterʼs view if the backend lookup fails. + // This preserves previous behaviour while still eliminating the + // common race in the majority of cases. + s.log.Warn("watch: failed to fetch current RV from backend, falling back to broadcaster", "err", err) + mostRecentRV = s.mostRecentRV.Load() + } else { + mostRecentRV = rv + } + } else { + // For all other code-paths we either already have an explicit RV or we + // will derive it from the initial list below. + mostRecentRV = s.mostRecentRV.Load() } - mostRecentRV := s.mostRecentRV.Load() // get the latest resource version - var initialEventsRV int64 // resource version coming from the initial events + var initialEventsRV int64 // resource version coming from the initial events if req.SendInitialEvents { // Backfill the stream by adding every existing entities. initialEventsRV, err = s.backend.ListIterator(ctx, &resourcepb.ListRequest{Options: req.Options}, func(iter ListIterator) error { From c35803d992b1c375b498c4d3688fc5c0299ba07b Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 23 May 2025 09:22:22 +0100 Subject: [PATCH 46/73] Update dependency @bsull/augurs to ^0.10.0 (#105810) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package.json | 2 +- yarn.lock | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index d08b3f4c16e..b87ee24795a 100644 --- a/package.json +++ b/package.json @@ -259,7 +259,7 @@ "yargs": "^17.5.1" }, "dependencies": { - "@bsull/augurs": "^0.9.0", + "@bsull/augurs": "^0.10.0", "@emotion/css": "11.13.5", "@emotion/react": "11.14.0", "@fingerprintjs/fingerprintjs": "^3.4.2", diff --git a/yarn.lock b/yarn.lock index 4d2b8c699f4..25584e7ed68 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1594,10 +1594,10 @@ __metadata: languageName: node linkType: hard -"@bsull/augurs@npm:^0.9.0": - version: 0.9.0 - resolution: "@bsull/augurs@npm:0.9.0" - checksum: 10/284299c0af22ca9f91efc981b4e6471d9a5791de2ffa6b987ed95309c9d0fce1d6bfa6b3f58e8a935859b04b5b3714d3617be1d569f7164c68d4748ba12b6722 +"@bsull/augurs@npm:^0.10.0": + version: 0.10.0 + resolution: "@bsull/augurs@npm:0.10.0" + checksum: 10/ba981263ed270d5c552ca41b0a8a22065dc67a34bb4834a30dda587694f5fa8a42dc7ffdd9d4f59695d3a5ac401a3ca513d54eba0de6b7eecbd04226b3afd6c4 languageName: node linkType: hard @@ -17693,7 +17693,7 @@ __metadata: "@babel/runtime": "npm:7.27.0" "@betterer/betterer": "npm:5.4.0" "@betterer/cli": "npm:5.4.0" - "@bsull/augurs": "npm:^0.9.0" + "@bsull/augurs": "npm:^0.10.0" "@crowdin/crowdin-api-client": "npm:^1.42.0" "@cypress/webpack-preprocessor": "npm:6.0.4" "@emotion/css": "npm:11.13.5" From c7cd06d5f5a5094a38dd73e5a5258ac11b6c1f10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=A4ggmark?= Date: Fri, 23 May 2025 10:34:35 +0200 Subject: [PATCH 47/73] i18n: exports TFunction from @grafana/i18n package (#105921) --- packages/grafana-i18n/src/i18n.tsx | 10 +++++----- packages/grafana-i18n/src/index.ts | 2 +- packages/grafana-i18n/src/internal/index.ts | 7 +------ packages/grafana-i18n/src/types.ts | 7 ++++++- public/app/core/components/ThemeSelector/ThemeCard.tsx | 3 +-- public/app/core/components/help/HelpModal.tsx | 3 +-- .../components/rule-editor/labels/LabelsField.tsx | 3 +-- .../Forms/ConfigureCorrelationSourceForm.tsx | 3 +-- .../components/picker/BuiltInDataSourceList.tsx | 3 +-- public/app/features/inspector/InspectJSONTab.tsx | 3 +-- .../features/logs/components/panel/InfiniteScroll.tsx | 3 +-- .../onprem/EmptyState/CallToAction/ConnectModal.tsx | 3 +-- public/app/features/migrate-to-cloud/onprem/Page.tsx | 3 +-- .../migrate-to-cloud/onprem/ResourceDetailsModal.tsx | 3 +-- .../app/features/migrate-to-cloud/onprem/TypeCell.tsx | 3 +-- .../migrate-to-cloud/onprem/useNotifyOnSuccess.tsx | 3 +-- .../plugins/admin/components/UpdateAllModal.tsx | 3 +-- public/app/features/provisioning/Config/ConfigForm.tsx | 3 +-- .../provisioning/GettingStarted/GettingStarted.tsx | 3 +-- public/app/features/provisioning/Job/RecentJobs.tsx | 3 +-- .../provisioning/Wizard/ProvisioningWizard.tsx | 3 +-- .../app/features/search/page/components/ActionRow.tsx | 3 +-- 22 files changed, 31 insertions(+), 49 deletions(-) diff --git a/packages/grafana-i18n/src/i18n.tsx b/packages/grafana-i18n/src/i18n.tsx index 4afa54bb0f5..5435dc3168a 100644 --- a/packages/grafana-i18n/src/i18n.tsx +++ b/packages/grafana-i18n/src/i18n.tsx @@ -1,4 +1,4 @@ -import i18n, { InitOptions, ReactOptions, TFunction } from 'i18next'; +import i18n, { InitOptions, ReactOptions, TFunction as I18NextTFunction } from 'i18next'; import LanguageDetector, { DetectorOptions } from 'i18next-browser-languagedetector'; // eslint-disable-next-line no-restricted-imports import { initReactI18next, setDefaults, setI18n, Trans as I18NextTrans, getI18n } from 'react-i18next'; @@ -6,9 +6,9 @@ import { initReactI18next, setDefaults, setI18n, Trans as I18NextTrans, getI18n import { DEFAULT_LANGUAGE, PSEUDO_LOCALE } from './constants'; import { initRegionalFormat } from './dates'; import { LANGUAGES } from './languages'; -import { TransProps, TransType } from './types'; +import { TFunction, TransProps, TransType } from './types'; -let tFunc: TFunction | undefined; +let tFunc: I18NextTFunction | undefined; let transComponent: TransType; export async function initPluginTranslations(id: string) { @@ -140,7 +140,7 @@ export function addResourceBundle(language: string, namespace: string, resource: getI18nInstance().addResourceBundle(language, namespace, resource, undefined, true); } -export function t(id: string, defaultMessage: string, values?: Record) { +export const t: TFunction = (id: string, defaultMessage: string, values?: Record) => { if (!tFunc) { if (process.env.NODE_ENV !== 'test') { console.warn( @@ -156,7 +156,7 @@ export function t(id: string, defaultMessage: string, values?: Record) => string; + +export type { UseTranslateHook, TransProps, TransType, TFunction }; diff --git a/public/app/core/components/ThemeSelector/ThemeCard.tsx b/public/app/core/components/ThemeSelector/ThemeCard.tsx index 38892d8fa30..24aed29a467 100644 --- a/public/app/core/components/ThemeSelector/ThemeCard.tsx +++ b/public/app/core/components/ThemeSelector/ThemeCard.tsx @@ -1,8 +1,7 @@ import { css } from '@emotion/css'; import { FeatureState, GrafanaTheme2, ThemeRegistryItem } from '@grafana/data'; -import { useTranslate } from '@grafana/i18n'; -import { TFunction } from '@grafana/i18n/internal'; +import { TFunction, useTranslate } from '@grafana/i18n'; import { FeatureBadge, RadioButtonDot, useStyles2 } from '@grafana/ui'; import { ThemePreview } from '../Theme/ThemePreview'; diff --git a/public/app/core/components/help/HelpModal.tsx b/public/app/core/components/help/HelpModal.tsx index 7d1c9d208a2..c413663c9bd 100644 --- a/public/app/core/components/help/HelpModal.tsx +++ b/public/app/core/components/help/HelpModal.tsx @@ -2,8 +2,7 @@ import { css } from '@emotion/css'; import { useMemo } from 'react'; import { GrafanaTheme2 } from '@grafana/data'; -import { Trans, useTranslate } from '@grafana/i18n'; -import { TFunction } from '@grafana/i18n/internal'; +import { TFunction, Trans, useTranslate } from '@grafana/i18n'; import { Grid, Modal, useStyles2, Text } from '@grafana/ui'; import { getModKey } from 'app/core/utils/browser'; diff --git a/public/app/features/alerting/unified/components/rule-editor/labels/LabelsField.tsx b/public/app/features/alerting/unified/components/rule-editor/labels/LabelsField.tsx index 10f82b1e051..a690fd34ea3 100644 --- a/public/app/features/alerting/unified/components/rule-editor/labels/LabelsField.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/labels/LabelsField.tsx @@ -3,8 +3,7 @@ import { FC, useCallback, useMemo, useState } from 'react'; import { Controller, FormProvider, useFieldArray, useForm, useFormContext } from 'react-hook-form'; import { GrafanaTheme2, SelectableValue } from '@grafana/data'; -import { Trans, useTranslate } from '@grafana/i18n'; -import { TFunction } from '@grafana/i18n/internal'; +import { TFunction, Trans, useTranslate } from '@grafana/i18n'; import { Button, Field, InlineLabel, Input, LoadingPlaceholder, Space, Stack, Text, useStyles2 } from '@grafana/ui'; import { labelsApi } from '../../../api/labelsApi'; diff --git a/public/app/features/correlations/Forms/ConfigureCorrelationSourceForm.tsx b/public/app/features/correlations/Forms/ConfigureCorrelationSourceForm.tsx index e03d5b8aa34..a0cc03080a7 100644 --- a/public/app/features/correlations/Forms/ConfigureCorrelationSourceForm.tsx +++ b/public/app/features/correlations/Forms/ConfigureCorrelationSourceForm.tsx @@ -2,8 +2,7 @@ import { css } from '@emotion/css'; import { Controller, useFormContext } from 'react-hook-form'; import { DataSourceInstanceSettings, GrafanaTheme2 } from '@grafana/data'; -import { Trans, useTranslate } from '@grafana/i18n'; -import { TFunction } from '@grafana/i18n/internal'; +import { TFunction, Trans, useTranslate } from '@grafana/i18n'; import { Card, Field, FieldSet, Input, useStyles2 } from '@grafana/ui'; import { DataSourcePicker } from 'app/features/datasources/components/picker/DataSourcePicker'; import { getDatasourceSrv } from 'app/features/plugins/datasource_srv'; diff --git a/public/app/features/datasources/components/picker/BuiltInDataSourceList.tsx b/public/app/features/datasources/components/picker/BuiltInDataSourceList.tsx index 33a26b3abd1..c58003f8529 100644 --- a/public/app/features/datasources/components/picker/BuiltInDataSourceList.tsx +++ b/public/app/features/datasources/components/picker/BuiltInDataSourceList.tsx @@ -1,7 +1,6 @@ import { DataSourceInstanceSettings } from '@grafana/data'; import { selectors } from '@grafana/e2e-selectors'; -import { useTranslate } from '@grafana/i18n'; -import { TFunction } from '@grafana/i18n/internal'; +import { TFunction, useTranslate } from '@grafana/i18n'; import { DataSourceRef } from '@grafana/schema'; import { useDatasources } from '../../hooks'; diff --git a/public/app/features/inspector/InspectJSONTab.tsx b/public/app/features/inspector/InspectJSONTab.tsx index 255bfe5e870..c9348dca512 100644 --- a/public/app/features/inspector/InspectJSONTab.tsx +++ b/public/app/features/inspector/InspectJSONTab.tsx @@ -6,8 +6,7 @@ import { firstValueFrom } from 'rxjs'; import { AppEvents, PanelData, SelectableValue, LoadingState } from '@grafana/data'; import { selectors } from '@grafana/e2e-selectors'; -import { Trans, useTranslate } from '@grafana/i18n'; -import { TFunction } from '@grafana/i18n/internal'; +import { TFunction, Trans, useTranslate } from '@grafana/i18n'; import { locationService } from '@grafana/runtime'; import { Button, CodeEditor, Field, Select, useStyles2 } from '@grafana/ui'; import { appEvents } from 'app/core/core'; diff --git a/public/app/features/logs/components/panel/InfiniteScroll.tsx b/public/app/features/logs/components/panel/InfiniteScroll.tsx index aec46e6ede5..05385f02a04 100644 --- a/public/app/features/logs/components/panel/InfiniteScroll.tsx +++ b/public/app/features/logs/components/panel/InfiniteScroll.tsx @@ -3,8 +3,7 @@ import { usePrevious } from 'react-use'; import { ListChildComponentProps, ListOnItemsRenderedProps } from 'react-window'; import { AbsoluteTimeRange, LogsSortOrder, TimeRange } from '@grafana/data'; -import { useTranslate } from '@grafana/i18n'; -import { TFunction } from '@grafana/i18n/internal'; +import { TFunction, useTranslate } from '@grafana/i18n'; import { config, reportInteraction } from '@grafana/runtime'; import { Spinner, useStyles2 } from '@grafana/ui'; diff --git a/public/app/features/migrate-to-cloud/onprem/EmptyState/CallToAction/ConnectModal.tsx b/public/app/features/migrate-to-cloud/onprem/EmptyState/CallToAction/ConnectModal.tsx index 86333f0efbc..3711bfecf16 100644 --- a/public/app/features/migrate-to-cloud/onprem/EmptyState/CallToAction/ConnectModal.tsx +++ b/public/app/features/migrate-to-cloud/onprem/EmptyState/CallToAction/ConnectModal.tsx @@ -3,8 +3,7 @@ import { useId } from 'react'; import { SubmitHandler, useForm } from 'react-hook-form'; import { GrafanaTheme2 } from '@grafana/data'; -import { Trans, useTranslate } from '@grafana/i18n'; -import { TFunction } from '@grafana/i18n/internal'; +import { TFunction, Trans, useTranslate } from '@grafana/i18n'; import { Modal, Button, Stack, TextLink, Field, Input, Text, useStyles2 } from '@grafana/ui'; import { AlertWithTraceID } from 'app/features/migrate-to-cloud/shared/AlertWithTraceID'; diff --git a/public/app/features/migrate-to-cloud/onprem/Page.tsx b/public/app/features/migrate-to-cloud/onprem/Page.tsx index 9a6ea846572..4be9fb10548 100644 --- a/public/app/features/migrate-to-cloud/onprem/Page.tsx +++ b/public/app/features/migrate-to-cloud/onprem/Page.tsx @@ -1,8 +1,7 @@ import { skipToken } from '@reduxjs/toolkit/query/react'; import { useCallback, useEffect, useState } from 'react'; -import { Trans, useTranslate } from '@grafana/i18n'; -import { TFunction } from '@grafana/i18n/internal'; +import { TFunction, Trans, useTranslate } from '@grafana/i18n'; import { config } from '@grafana/runtime'; import { AlertVariant, Box, Stack, Text } from '@grafana/ui'; diff --git a/public/app/features/migrate-to-cloud/onprem/ResourceDetailsModal.tsx b/public/app/features/migrate-to-cloud/onprem/ResourceDetailsModal.tsx index c7756cd038d..ecc96e5f31b 100644 --- a/public/app/features/migrate-to-cloud/onprem/ResourceDetailsModal.tsx +++ b/public/app/features/migrate-to-cloud/onprem/ResourceDetailsModal.tsx @@ -1,5 +1,4 @@ -import { Trans, useTranslate } from '@grafana/i18n'; -import { TFunction } from '@grafana/i18n/internal'; +import { TFunction, Trans, useTranslate } from '@grafana/i18n'; import { Button, Modal, Stack, Text } from '@grafana/ui'; import { MigrateDataResponseItemDto } from '../api'; diff --git a/public/app/features/migrate-to-cloud/onprem/TypeCell.tsx b/public/app/features/migrate-to-cloud/onprem/TypeCell.tsx index abd9e414e0f..ef42de2a6fd 100644 --- a/public/app/features/migrate-to-cloud/onprem/TypeCell.tsx +++ b/public/app/features/migrate-to-cloud/onprem/TypeCell.tsx @@ -1,5 +1,4 @@ -import { useTranslate } from '@grafana/i18n'; -import { TFunction } from '@grafana/i18n/internal'; +import { TFunction, useTranslate } from '@grafana/i18n'; import { CellProps } from '@grafana/ui'; import { ResourceTableItem } from './types'; diff --git a/public/app/features/migrate-to-cloud/onprem/useNotifyOnSuccess.tsx b/public/app/features/migrate-to-cloud/onprem/useNotifyOnSuccess.tsx index effaaddc1ee..f2e04d146cd 100644 --- a/public/app/features/migrate-to-cloud/onprem/useNotifyOnSuccess.tsx +++ b/public/app/features/migrate-to-cloud/onprem/useNotifyOnSuccess.tsx @@ -1,7 +1,6 @@ import { useRef, useEffect } from 'react'; -import { useTranslate } from '@grafana/i18n'; -import { TFunction } from '@grafana/i18n/internal'; +import { TFunction, useTranslate } from '@grafana/i18n'; import { useAppNotification } from 'app/core/copy/appNotification'; import { GetSnapshotResponseDto, SnapshotDto } from '../api'; diff --git a/public/app/features/plugins/admin/components/UpdateAllModal.tsx b/public/app/features/plugins/admin/components/UpdateAllModal.tsx index 1a09db39fc4..fff4cd18331 100644 --- a/public/app/features/plugins/admin/components/UpdateAllModal.tsx +++ b/public/app/features/plugins/admin/components/UpdateAllModal.tsx @@ -1,7 +1,6 @@ import { useEffect, useMemo, useRef, useState } from 'react'; -import { useTranslate } from '@grafana/i18n'; -import { TFunction } from '@grafana/i18n/internal'; +import { TFunction, useTranslate } from '@grafana/i18n'; import { config, reportInteraction } from '@grafana/runtime'; import { ConfirmModal } from '@grafana/ui'; diff --git a/public/app/features/provisioning/Config/ConfigForm.tsx b/public/app/features/provisioning/Config/ConfigForm.tsx index 27522303f48..2e3b0dd7cab 100644 --- a/public/app/features/provisioning/Config/ConfigForm.tsx +++ b/public/app/features/provisioning/Config/ConfigForm.tsx @@ -2,8 +2,7 @@ import { useEffect, useMemo, useState } from 'react'; import { Controller, useForm } from 'react-hook-form'; import { useNavigate } from 'react-router-dom-v5-compat'; -import { Trans, useTranslate } from '@grafana/i18n'; -import { TFunction } from '@grafana/i18n/internal'; +import { TFunction, Trans, useTranslate } from '@grafana/i18n'; import { Button, Checkbox, diff --git a/public/app/features/provisioning/GettingStarted/GettingStarted.tsx b/public/app/features/provisioning/GettingStarted/GettingStarted.tsx index ddecc146123..cadf84a2cbe 100644 --- a/public/app/features/provisioning/GettingStarted/GettingStarted.tsx +++ b/public/app/features/provisioning/GettingStarted/GettingStarted.tsx @@ -2,8 +2,7 @@ import { css } from '@emotion/css'; import { useState } from 'react'; import { GrafanaTheme2 } from '@grafana/data'; -import { Trans, useTranslate } from '@grafana/i18n'; -import { TFunction } from '@grafana/i18n/internal'; +import { TFunction, Trans, useTranslate } from '@grafana/i18n'; import { Alert, Stack, useStyles2 } from '@grafana/ui'; import { useGetFrontendSettingsQuery, Repository } from 'app/api/clients/provisioning'; import provisioningSvg from 'img/provisioning/provisioning.svg'; diff --git a/public/app/features/provisioning/Job/RecentJobs.tsx b/public/app/features/provisioning/Job/RecentJobs.tsx index de5cbb8f59d..46270c6785b 100644 --- a/public/app/features/provisioning/Job/RecentJobs.tsx +++ b/public/app/features/provisioning/Job/RecentJobs.tsx @@ -1,8 +1,7 @@ import { useMemo } from 'react'; import { intervalToAbbreviatedDurationString, TraceKeyValuePair } from '@grafana/data'; -import { Trans, useTranslate } from '@grafana/i18n'; -import { TFunction } from '@grafana/i18n/internal'; +import { TFunction, Trans, useTranslate } from '@grafana/i18n'; import { Alert, Badge, Box, Card, Icon, InteractiveTable, Spinner, Stack, Text } from '@grafana/ui'; import { Job, Repository, SyncStatus } from 'app/api/clients/provisioning'; import KeyValuesTable from 'app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/KeyValuesTable'; diff --git a/public/app/features/provisioning/Wizard/ProvisioningWizard.tsx b/public/app/features/provisioning/Wizard/ProvisioningWizard.tsx index 18713ac2562..b241089815d 100644 --- a/public/app/features/provisioning/Wizard/ProvisioningWizard.tsx +++ b/public/app/features/provisioning/Wizard/ProvisioningWizard.tsx @@ -4,8 +4,7 @@ import { FormProvider, useForm } from 'react-hook-form'; import { useNavigate } from 'react-router-dom-v5-compat'; import { AppEvents, GrafanaTheme2 } from '@grafana/data'; -import { useTranslate } from '@grafana/i18n'; -import { TFunction } from '@grafana/i18n/internal'; +import { TFunction, useTranslate } from '@grafana/i18n'; import { getAppEvents, isFetchError } from '@grafana/runtime'; import { Alert, Box, Button, Stack, Text, useStyles2 } from '@grafana/ui'; import { useDeleteRepositoryMutation, useGetFrontendSettingsQuery } from 'app/api/clients/provisioning'; diff --git a/public/app/features/search/page/components/ActionRow.tsx b/public/app/features/search/page/components/ActionRow.tsx index 23de16ab8ce..dce8e0256dd 100644 --- a/public/app/features/search/page/components/ActionRow.tsx +++ b/public/app/features/search/page/components/ActionRow.tsx @@ -2,8 +2,7 @@ import { css } from '@emotion/css'; import { FormEvent } from 'react'; import { GrafanaTheme2, SelectableValue } from '@grafana/data'; -import { Trans, useTranslate } from '@grafana/i18n'; -import { TFunction } from '@grafana/i18n/internal'; +import { TFunction, Trans, useTranslate } from '@grafana/i18n'; import { config } from '@grafana/runtime'; import { Button, Checkbox, Stack, RadioButtonGroup, useStyles2 } from '@grafana/ui'; import { SortPicker } from 'app/core/components/Select/SortPicker'; From 9bda7b92938f5b4e6d2ded2c1a4fe59c60e645ff Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 23 May 2025 09:50:26 +0100 Subject: [PATCH 48/73] Update dependency @grafana/aws-sdk to v0.7.0 (#105811) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package.json | 2 +- yarn.lock | 308 +++++---------------------------------------------- 2 files changed, 31 insertions(+), 279 deletions(-) diff --git a/package.json b/package.json index b87ee24795a..6076d3c46bc 100644 --- a/package.json +++ b/package.json @@ -267,7 +267,7 @@ "@formatjs/intl-durationformat": "^0.7.0", "@glideapps/glide-data-grid": "^6.0.0", "@grafana/alerting": "workspace:*", - "@grafana/aws-sdk": "0.6.0", + "@grafana/aws-sdk": "0.7.0", "@grafana/azure-sdk": "0.0.7", "@grafana/data": "workspace:*", "@grafana/e2e-selectors": "workspace:*", diff --git a/yarn.lock b/yarn.lock index 25584e7ed68..5c79a2842dc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1425,7 +1425,7 @@ __metadata: languageName: node linkType: hard -"@babel/runtime@npm:^7.0.0, @babel/runtime@npm:^7.1.2, @babel/runtime@npm:^7.10.1, @babel/runtime@npm:^7.11.1, @babel/runtime@npm:^7.11.2, @babel/runtime@npm:^7.12.0, @babel/runtime@npm:^7.12.1, @babel/runtime@npm:^7.12.13, @babel/runtime@npm:^7.12.5, @babel/runtime@npm:^7.13.10, @babel/runtime@npm:^7.15.4, @babel/runtime@npm:^7.16.3, @babel/runtime@npm:^7.17.8, @babel/runtime@npm:^7.18.0, @babel/runtime@npm:^7.18.3, @babel/runtime@npm:^7.20.0, @babel/runtime@npm:^7.20.7, @babel/runtime@npm:^7.23.2, @babel/runtime@npm:^7.23.9, @babel/runtime@npm:^7.24.5, @babel/runtime@npm:^7.24.7, @babel/runtime@npm:^7.25.0, @babel/runtime@npm:^7.25.6, @babel/runtime@npm:^7.25.7, @babel/runtime@npm:^7.27.0, @babel/runtime@npm:^7.3.1, @babel/runtime@npm:^7.5.5, @babel/runtime@npm:^7.7.6, @babel/runtime@npm:^7.8.4, @babel/runtime@npm:^7.8.7": +"@babel/runtime@npm:^7.0.0, @babel/runtime@npm:^7.1.2, @babel/runtime@npm:^7.10.1, @babel/runtime@npm:^7.11.1, @babel/runtime@npm:^7.11.2, @babel/runtime@npm:^7.12.0, @babel/runtime@npm:^7.12.1, @babel/runtime@npm:^7.12.13, @babel/runtime@npm:^7.12.5, @babel/runtime@npm:^7.13.10, @babel/runtime@npm:^7.16.3, @babel/runtime@npm:^7.17.8, @babel/runtime@npm:^7.18.0, @babel/runtime@npm:^7.18.3, @babel/runtime@npm:^7.20.0, @babel/runtime@npm:^7.20.7, @babel/runtime@npm:^7.23.2, @babel/runtime@npm:^7.23.9, @babel/runtime@npm:^7.24.5, @babel/runtime@npm:^7.24.7, @babel/runtime@npm:^7.25.0, @babel/runtime@npm:^7.25.6, @babel/runtime@npm:^7.25.7, @babel/runtime@npm:^7.27.0, @babel/runtime@npm:^7.3.1, @babel/runtime@npm:^7.5.5, @babel/runtime@npm:^7.7.6, @babel/runtime@npm:^7.8.4, @babel/runtime@npm:^7.8.7": version: 7.27.1 resolution: "@babel/runtime@npm:7.27.1" checksum: 10/34cefcbf781ea5a4f1b93f8563327b9ac82694bebdae91e8bd9d7f58d084cbe5b9a6e7f94d77076e15b0bcdaa0040a36cb30737584028df6c4673b4c67b2a31d @@ -1752,24 +1752,6 @@ __metadata: languageName: node linkType: hard -"@date-io/core@npm:^1.3.13": - version: 1.3.13 - resolution: "@date-io/core@npm:1.3.13" - checksum: 10/5a9e9d1de20f0346a3c7d2d5946190caef4bfb0b64d82ba1f4c566657a9192667c94ebe7f438d11d4286d9c190974daad4fb2159294225cd8af4d9a140239879 - languageName: node - linkType: hard - -"@date-io/moment@npm:^1.3.13": - version: 1.3.13 - resolution: "@date-io/moment@npm:1.3.13" - dependencies: - "@date-io/core": "npm:^1.3.13" - peerDependencies: - moment: ^2.24.0 - checksum: 10/ed62d4fd5629935110dbf75dc2e5cfa376c9da4338726dcca967d04d082006a3cddcc18eacd3b8332e930a6d2edb6ebe8eb5a2e8b11427ac534c50a2acd0b3e1 - languageName: node - linkType: hard - "@daybrush/utils@npm:^1.1.1, @daybrush/utils@npm:^1.13.0, @daybrush/utils@npm:^1.4.0, @daybrush/utils@npm:^1.6.0, @daybrush/utils@npm:^1.7.1": version: 1.13.0 resolution: "@daybrush/utils@npm:1.13.0" @@ -2950,22 +2932,23 @@ __metadata: languageName: unknown linkType: soft -"@grafana/async-query-data@npm:0.3.0": - version: 0.3.0 - resolution: "@grafana/async-query-data@npm:0.3.0" +"@grafana/async-query-data@npm:0.4.0": + version: 0.4.0 + resolution: "@grafana/async-query-data@npm:0.4.0" dependencies: + semver: "npm:^7.6.3" tslib: "npm:^2.8.0" - checksum: 10/257331c0333abce16f6b4c994abfd60ca27076a4f8897392118beddc2f24686ecaad8dfbcbbf573f91f842e1c236124f99febb01a0e4f671d7759cdc1c9481af + checksum: 10/58232cc085b1bc813b002189bdf80f6e12a2dc418c86b43e42dc50349b2ab327fcaec1310d5878a6ba974eab9194c20cef6540912656bbb4d18ddac4dae08d7c languageName: node linkType: hard -"@grafana/aws-sdk@npm:0.6.0": - version: 0.6.0 - resolution: "@grafana/aws-sdk@npm:0.6.0" +"@grafana/aws-sdk@npm:0.7.0": + version: 0.7.0 + resolution: "@grafana/aws-sdk@npm:0.7.0" dependencies: - "@grafana/async-query-data": "npm:0.3.0" - "@grafana/plugin-ui": "npm:^0.9.3" - checksum: 10/a9ec4b54fbc7397c6ade0b7698baf5f296c121ba1c2f913ce9f12e03b3179628acaff157c04c1ab5c71f7fdeb4509e35e31c3e7b2eaf83dc7674a860d77698d6 + "@grafana/async-query-data": "npm:0.4.0" + "@grafana/plugin-ui": "npm:^0.10.1" + checksum: 10/7f29b5234081c1a13e6e8ad099d0e5e26b4b220c9343382105554e95251df3171f17b93280f1e3265912359c496ac5fe0182b93211ed16f907271cbb27884a7c languageName: node linkType: hard @@ -3320,7 +3303,7 @@ __metadata: languageName: node linkType: hard -"@grafana/plugin-ui@npm:0.10.6": +"@grafana/plugin-ui@npm:0.10.6, @grafana/plugin-ui@npm:^0.10.1": version: 0.10.6 resolution: "@grafana/plugin-ui@npm:0.10.6" dependencies: @@ -3348,36 +3331,6 @@ __metadata: languageName: node linkType: hard -"@grafana/plugin-ui@npm:^0.9.3": - version: 0.9.6 - resolution: "@grafana/plugin-ui@npm:0.9.6" - dependencies: - "@hello-pangea/dnd": "npm:^17.0.0" - lodash: "npm:^4.17.21" - prismjs: "npm:^1.29.0" - prompts: "npm:^2.4.2" - rc-cascader: "npm:1.0.1" - react-awesome-query-builder: "npm:^5.3.1" - react-popper-tooltip: "npm:^4.4.2" - react-use: "npm:17.3.1" - react-virtualized-auto-sizer: "npm:^1.0.6" - sql-formatter-plus: "npm:^1.3.6" - uuid: "npm:^8.3.2" - peerDependencies: - "@changesets/cli": ">=2.x" - "@grafana/data": ^10.4.0 || ^11.0.0 - "@grafana/e2e-selectors": ^10.4.0 || ^11.0.0 - "@grafana/runtime": ^10.4.0 || ^11.0.0 - "@grafana/ui": ^10.4.0 || ^11.0.0 - react: ^18.2.0 - react-dom: ^18.2.0 - rxjs: ^7.8.1 - bin: - changeset-improved: dist/utils/changeset/index.js - checksum: 10/260317fd20becd3bed4b7beb758e855075c707952622169e1cd1c8624ff40e2d297b8b92584abb55a012cecbf81f1712372bc5420c3b93cfb0079504e649f858 - languageName: node - linkType: hard - "@grafana/prometheus@workspace:*, @grafana/prometheus@workspace:packages/grafana-prometheus": version: 0.0.0-use.local resolution: "@grafana/prometheus@workspace:packages/grafana-prometheus" @@ -9322,7 +9275,7 @@ __metadata: languageName: node linkType: hard -"@types/hoist-non-react-statics@npm:3.3.6, @types/hoist-non-react-statics@npm:^3.3.0, @types/hoist-non-react-statics@npm:^3.3.1": +"@types/hoist-non-react-statics@npm:3.3.6, @types/hoist-non-react-statics@npm:^3.3.1": version: 3.3.6 resolution: "@types/hoist-non-react-statics@npm:3.3.6" dependencies: @@ -9757,18 +9710,6 @@ __metadata: languageName: node linkType: hard -"@types/react-redux@npm:^7.1.20": - version: 7.1.34 - resolution: "@types/react-redux@npm:7.1.34" - dependencies: - "@types/hoist-non-react-statics": "npm:^3.3.0" - "@types/react": "npm:*" - hoist-non-react-statics: "npm:^3.3.0" - redux: "npm:^4.0.0" - checksum: 10/febcd1db0c83c5002c6bee0fdda9e70da0653454ffbb72d6c37cbf2f5c005e06fb5271cff344d7164c385c944526565282de9a95ff379e040476b71d27fc2512 - languageName: node - linkType: hard - "@types/react-resizable@npm:3.0.8": version: 3.0.8 resolution: "@types/react-resizable@npm:3.0.8" @@ -10905,15 +10846,6 @@ __metadata: languageName: node linkType: hard -"add-dom-event-listener@npm:^1.1.0": - version: 1.1.0 - resolution: "add-dom-event-listener@npm:1.1.0" - dependencies: - object-assign: "npm:4.x" - checksum: 10/dcab339517668e46887b4d5ddf3e299d5b3a1797e027cafe0480da6fb32591bfe04b567dd002d3b2c4d43f3c8addf208f7ae45db847b79daa1d7a7391dc3c076 - languageName: node - linkType: hard - "add-px-to-style@npm:1.0.0": version: 1.0.0 resolution: "add-px-to-style@npm:1.0.0" @@ -11315,13 +11247,6 @@ __metadata: languageName: node linkType: hard -"array-tree-filter@npm:^2.1.0": - version: 2.1.0 - resolution: "array-tree-filter@npm:2.1.0" - checksum: 10/ca65dbeb80777eadadfcd4dbd2658d9eb0be66e426a6b6d64f1a71dff5351f2e6f370e0cbcc418e9e6e01d06b337b128441a71c7143abe4d925d027d5aa0100f - languageName: node - linkType: hard - "array-union@npm:^2.1.0": version: 2.1.0 resolution: "array-union@npm:2.1.0" @@ -14861,13 +14786,6 @@ __metadata: languageName: node linkType: hard -"dom-align@npm:^1.7.0": - version: 1.12.2 - resolution: "dom-align@npm:1.12.2" - checksum: 10/4659909b9ab48de2976f0668220289e3835490537b1ce834b7f2dd3189b21e0996011ec8826a2789c9597729bbb457f6e54f07787a00774e3ce04b15fcbfa636 - languageName: node - linkType: hard - "dom-converter@npm:^0.2.0": version: 0.2.0 resolution: "dom-converter@npm:0.2.0" @@ -17704,7 +17622,7 @@ __metadata: "@formatjs/intl-durationformat": "npm:^0.7.0" "@glideapps/glide-data-grid": "npm:^6.0.0" "@grafana/alerting": "workspace:*" - "@grafana/aws-sdk": "npm:0.6.0" + "@grafana/aws-sdk": "npm:0.7.0" "@grafana/azure-sdk": "npm:0.0.7" "@grafana/data": "workspace:*" "@grafana/e2e-selectors": "workspace:*" @@ -22712,7 +22630,7 @@ __metadata: languageName: node linkType: hard -"nano-css@npm:^5.3.1, nano-css@npm:^5.6.1, nano-css@npm:^5.6.2": +"nano-css@npm:^5.6.1, nano-css@npm:^5.6.2": version: 5.6.2 resolution: "nano-css@npm:5.6.2" dependencies: @@ -23361,13 +23279,6 @@ __metadata: languageName: node linkType: hard -"object-assign@npm:4.x, object-assign@npm:^4, object-assign@npm:^4.1.0, object-assign@npm:^4.1.1": - version: 4.1.1 - resolution: "object-assign@npm:4.1.1" - checksum: 10/fcc6e4ea8c7fe48abfbb552578b1c53e0d194086e2e6bbbf59e0a536381a292f39943c6e9628af05b5528aa5e3318bb30d6b2e53cadaf5b8fe9e12c4b69af23f - languageName: node - linkType: hard - "object-assign@npm:^3.0.0": version: 3.0.0 resolution: "object-assign@npm:3.0.0" @@ -23375,6 +23286,13 @@ __metadata: languageName: node linkType: hard +"object-assign@npm:^4, object-assign@npm:^4.1.0, object-assign@npm:^4.1.1": + version: 4.1.1 + resolution: "object-assign@npm:4.1.1" + checksum: 10/fcc6e4ea8c7fe48abfbb552578b1c53e0d194086e2e6bbbf59e0a536381a292f39943c6e9628af05b5528aa5e3318bb30d6b2e53cadaf5b8fe9e12c4b69af23f + languageName: node + linkType: hard + "object-inspect@npm:^1.13.3": version: 1.13.3 resolution: "object-inspect@npm:1.13.3" @@ -25118,7 +25036,7 @@ __metadata: languageName: node linkType: hard -"prismjs@npm:1.30.0, prismjs@npm:^1.27.0, prismjs@npm:^1.29.0, prismjs@npm:^1.30.0": +"prismjs@npm:1.30.0, prismjs@npm:^1.27.0, prismjs@npm:^1.30.0": version: 1.30.0 resolution: "prismjs@npm:1.30.0" checksum: 10/6b48a2439a82e5c6882f48ebc1564c3890e16463ba17ac10c3ad4f62d98dea5b5c915b172b63b83023a70ad4f5d7be3e8a60304420db34a161fae69dd4e3e2da @@ -25198,7 +25116,7 @@ __metadata: languageName: node linkType: hard -"prompts@npm:^2.0.1, prompts@npm:^2.4.2": +"prompts@npm:^2.0.1": version: 2.4.2 resolution: "prompts@npm:2.4.2" dependencies: @@ -25436,7 +25354,7 @@ __metadata: languageName: node linkType: hard -"raf@npm:^3.1.0, raf@npm:^3.4.1": +"raf@npm:^3.1.0": version: 3.4.1 resolution: "raf@npm:3.4.1" dependencies: @@ -25518,37 +25436,6 @@ __metadata: languageName: node linkType: hard -"rc-align@npm:^4.0.0": - version: 4.0.15 - resolution: "rc-align@npm:4.0.15" - dependencies: - "@babel/runtime": "npm:^7.10.1" - classnames: "npm:2.x" - dom-align: "npm:^1.7.0" - rc-util: "npm:^5.26.0" - resize-observer-polyfill: "npm:^1.5.1" - peerDependencies: - react: ">=16.9.0" - react-dom: ">=16.9.0" - checksum: 10/de1af4a58a371bc01cf5f327ce82b5e5631960c3e2dbae03532081e63bf86a809cebf9b8a231495db1dc659c28b6dbb35b0b33f2c29579b8b068388c1288c1b1 - languageName: node - linkType: hard - -"rc-cascader@npm:1.0.1": - version: 1.0.1 - resolution: "rc-cascader@npm:1.0.1" - dependencies: - array-tree-filter: "npm:^2.1.0" - rc-trigger: "npm:^4.0.0" - rc-util: "npm:^4.0.4" - warning: "npm:^4.0.1" - peerDependencies: - react: ^15.0.0 || ^16.0.0 - react-dom: ^15.0.0 || ^16.0.0 - checksum: 10/e4c7b156527d048f28bcfb116c06df9522476ecfbee23ec9cec2e46a34a7f3f55abb8639f0c3589cc9a17e028404c8fda902f51c1a7b6b9af33706f96c6c3e96 - languageName: node - linkType: hard - "rc-cascader@npm:3.33.0": version: 3.33.0 resolution: "rc-cascader@npm:3.33.0" @@ -25581,21 +25468,6 @@ __metadata: languageName: node linkType: hard -"rc-motion@npm:^1.0.0": - version: 1.1.2 - resolution: "rc-motion@npm:1.1.2" - dependencies: - "@babel/runtime": "npm:^7.11.1" - classnames: "npm:^2.2.1" - raf: "npm:^3.4.1" - rc-util: "npm:^5.0.6" - peerDependencies: - react: ^16.0.0 - react-dom: ^16.0.0 - checksum: 10/61ab6b1f22de26247c7dbe28f13ddfa2fc53e00d389552ce7319ad1b4f8fca68b089c8766c61a6a899b0a76f47361353b1cde105c175653d5e06287fb5412d20 - languageName: node - linkType: hard - "rc-motion@npm:^2.0.0, rc-motion@npm:^2.0.1, rc-motion@npm:^2.6.1": version: 2.6.2 resolution: "rc-motion@npm:2.6.2" @@ -25733,34 +25605,7 @@ __metadata: languageName: node linkType: hard -"rc-trigger@npm:^4.0.0": - version: 4.4.3 - resolution: "rc-trigger@npm:4.4.3" - dependencies: - "@babel/runtime": "npm:^7.11.2" - classnames: "npm:^2.2.6" - raf: "npm:^3.4.1" - rc-align: "npm:^4.0.0" - rc-motion: "npm:^1.0.0" - rc-util: "npm:^5.0.1" - checksum: 10/4af3ee112dd6daf71b4190b16276bbdbae9e608ae441ae2aeadd0eea9d5aac755a6344c69b6273e26d84d5646b82efe82ad55c8dd91934915ba84b82156229e8 - languageName: node - linkType: hard - -"rc-util@npm:^4.0.4": - version: 4.21.1 - resolution: "rc-util@npm:4.21.1" - dependencies: - add-dom-event-listener: "npm:^1.1.0" - prop-types: "npm:^15.5.10" - react-is: "npm:^16.12.0" - react-lifecycles-compat: "npm:^3.0.4" - shallowequal: "npm:^1.1.0" - checksum: 10/aa68990486c6e45d4a37446ef6ec0ad4d6829ec01975a009494ba36b2d3c2d4eda8a64c75ee8d6984cdc30450b83e975dcefb6781bff9297f4b5c0fa56567629 - languageName: node - linkType: hard - -"rc-util@npm:^5.0.1, rc-util@npm:^5.0.6, rc-util@npm:^5.15.0, rc-util@npm:^5.16.1, rc-util@npm:^5.21.0, rc-util@npm:^5.24.4, rc-util@npm:^5.26.0, rc-util@npm:^5.36.0, rc-util@npm:^5.37.0, rc-util@npm:^5.38.0, rc-util@npm:^5.38.1, rc-util@npm:^5.43.0, rc-util@npm:^5.44.1, rc-util@npm:^5.44.3": +"rc-util@npm:^5.15.0, rc-util@npm:^5.16.1, rc-util@npm:^5.21.0, rc-util@npm:^5.24.4, rc-util@npm:^5.36.0, rc-util@npm:^5.37.0, rc-util@npm:^5.38.0, rc-util@npm:^5.38.1, rc-util@npm:^5.43.0, rc-util@npm:^5.44.1, rc-util@npm:^5.44.3": version: 5.44.3 resolution: "rc-util@npm:5.44.3" dependencies: @@ -25798,46 +25643,6 @@ __metadata: languageName: node linkType: hard -"react-awesome-query-builder@npm:^5.3.1": - version: 5.4.0 - resolution: "react-awesome-query-builder@npm:5.4.0" - dependencies: - "@date-io/moment": "npm:^1.3.13" - classnames: "npm:^2.3.1" - clone: "npm:^2.1.2" - immutable: "npm:^3.8.2" - lodash: "npm:^4.17.21" - moment: "npm:^2.29.4" - prop-types: "npm:^15.7.2" - react-redux: "npm:^7.2.2" - redux: "npm:^4.2.0" - spel2js: "npm:^0.2.8" - sqlstring: "npm:^2.3.3" - peerDependencies: - "@ant-design/icons": ^4.0.0 - "@emotion/react": ^11.7.1 - "@emotion/styled": ^11.6.0 - "@fortawesome/fontawesome-svg-core": ^1.2.0 || ^6.1.0 - "@fortawesome/free-solid-svg-icons": ^5.15.4 || ^6.0.0 - "@fortawesome/react-fontawesome": ^0.1.16 || ^0.2.0 - "@material-ui/core": ^4.12.3 - "@material-ui/icons": ^4.0.0 - "@material-ui/lab": ^4.0.0-alpha.57 - "@material-ui/pickers": ^3.2.10 - "@mui/icons-material": ^5.2.4 - "@mui/lab": ^5.0.0-alpha.60 - "@mui/material": ^5.2.4 - "@mui/x-date-pickers": ^5.0.0-beta.2 - antd: ^4.0.0 - bootstrap: ^5.1.3 - material-ui-confirm: ^2.0.1 || ^3.0.0 - react: ^16.8.4 || ^17.0.1 || ^18.0.0 - react-dom: ^16.8.4 || ^17.0.1 || ^18.0.0 - reactstrap: ^9.0.0 - checksum: 10/2a71349e3b091adc62ae68dbbcac0d0d8ad71a9fe42ef634009313df8303188c8c19d7dce7b21a30826c6f0647fbe493eb57fcb1ad1b3db7c78f0e69328af8c8 - languageName: node - linkType: hard - "react-calendar@npm:^4.8.0": version: 4.8.0 resolution: "react-calendar@npm:4.8.0" @@ -26195,14 +26000,14 @@ __metadata: languageName: node linkType: hard -"react-is@npm:^16.12.0, react-is@npm:^16.13.1, react-is@npm:^16.6.0, react-is@npm:^16.7.0": +"react-is@npm:^16.13.1, react-is@npm:^16.6.0, react-is@npm:^16.7.0": version: 16.13.1 resolution: "react-is@npm:16.13.1" checksum: 10/5aa564a1cde7d391ac980bedee21202fc90bdea3b399952117f54fb71a932af1e5902020144fb354b4690b2414a0c7aafe798eb617b76a3d441d956db7726fdf languageName: node linkType: hard -"react-is@npm:^17.0.1, react-is@npm:^17.0.2": +"react-is@npm:^17.0.1": version: 17.0.2 resolution: "react-is@npm:17.0.2" checksum: 10/73b36281e58eeb27c9cc6031301b6ae19ecdc9f18ae2d518bdb39b0ac564e65c5779405d623f1df9abf378a13858b79442480244bd579968afc1faf9a2ce5e05 @@ -26305,27 +26110,6 @@ __metadata: languageName: node linkType: hard -"react-redux@npm:^7.2.2": - version: 7.2.9 - resolution: "react-redux@npm:7.2.9" - dependencies: - "@babel/runtime": "npm:^7.15.4" - "@types/react-redux": "npm:^7.1.20" - hoist-non-react-statics: "npm:^3.3.2" - loose-envify: "npm:^1.4.0" - prop-types: "npm:^15.7.2" - react-is: "npm:^17.0.2" - peerDependencies: - react: ^16.8.3 || ^17 || ^18 - peerDependenciesMeta: - react-dom: - optional: true - react-native: - optional: true - checksum: 10/1c3018bd2601e6d18339281867910b583dcbb3d8856403086e08c00abf0dfe467a94c0d1356bafa8cdf107bf1e2c9899a28486e4778e85c8bc4dfed2076b116f - languageName: node - linkType: hard - "react-redux@npm:^8.1.3": version: 8.1.3 resolution: "react-redux@npm:8.1.3" @@ -26600,31 +26384,6 @@ __metadata: languageName: node linkType: hard -"react-use@npm:17.3.1": - version: 17.3.1 - resolution: "react-use@npm:17.3.1" - dependencies: - "@types/js-cookie": "npm:^2.2.6" - "@xobotyi/scrollbar-width": "npm:^1.9.5" - copy-to-clipboard: "npm:^3.3.1" - fast-deep-equal: "npm:^3.1.3" - fast-shallow-equal: "npm:^1.0.0" - js-cookie: "npm:^2.2.1" - nano-css: "npm:^5.3.1" - react-universal-interface: "npm:^0.6.2" - resize-observer-polyfill: "npm:^1.5.1" - screenfull: "npm:^5.1.0" - set-harmonic-interval: "npm:^1.0.1" - throttle-debounce: "npm:^3.0.1" - ts-easing: "npm:^0.2.0" - tslib: "npm:^2.1.0" - peerDependencies: - react: ^16.8.0 || ^17.0.0 - react-dom: ^16.8.0 || ^17.0.0 - checksum: 10/9e4fd337ec7c62ddb99b1f75aef488b8f9b8b923586a1fe5176f581ccbe7fb876bf64cf93b6a81925b92a779440b8d0994ed4edf883671a614652915e98a9fe4 - languageName: node - linkType: hard - "react-use@npm:17.5.0": version: 17.5.0 resolution: "react-use@npm:17.5.0" @@ -28118,13 +27877,6 @@ __metadata: languageName: node linkType: hard -"shallowequal@npm:^1.1.0": - version: 1.1.0 - resolution: "shallowequal@npm:1.1.0" - checksum: 10/f4c1de0837f106d2dbbfd5d0720a5d059d1c66b42b580965c8f06bb1db684be8783538b684092648c981294bf817869f743a066538771dbecb293df78f765e00 - languageName: node - linkType: hard - "shebang-command@npm:^2.0.0": version: 2.0.0 resolution: "shebang-command@npm:2.0.0" @@ -31242,7 +30994,7 @@ __metadata: languageName: node linkType: hard -"warning@npm:^4.0.0, warning@npm:^4.0.1, warning@npm:^4.0.2": +"warning@npm:^4.0.0, warning@npm:^4.0.2": version: 4.0.3 resolution: "warning@npm:4.0.3" dependencies: From eb8f3bc0718886ef1e8031d0d58adedaa4ec4a0e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-Philippe=20Qu=C3=A9m=C3=A9ner?= Date: Fri, 23 May 2025 11:03:01 +0200 Subject: [PATCH 49/73] fix(unified-storage): wait for result to be available (#105922) --- pkg/storage/unified/testing/benchmark.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pkg/storage/unified/testing/benchmark.go b/pkg/storage/unified/testing/benchmark.go index dfcacde86c1..14335c5c701 100644 --- a/pkg/storage/unified/testing/benchmark.go +++ b/pkg/storage/unified/testing/benchmark.go @@ -368,7 +368,11 @@ func BenchmarkIndexServer(tb testing.TB, ctx context.Context, backend resource.S // Run the storage backend benchmark write throughput to create events startTime := time.Now() var result *BenchmarkResult + // Channel to signal when the benchmark goroutine completes + benchmarkDone := make(chan struct{}) + go func() { + defer close(benchmarkDone) result, err = runStorageBackendBenchmark(ctx, backend, opts) require.NoError(tb, err) }() @@ -380,6 +384,8 @@ func BenchmarkIndexServer(tb testing.TB, ctx context.Context, backend resource.S latencies = append(latencies, evt.Latency.Seconds()) } totalDuration := time.Since(startTime) + + <-benchmarkDone // Calculate index latency percentiles sort.Float64s(latencies) var p50, p90, p99 float64 From 9769871a88cc48579ac94e005b9b5bd21a826037 Mon Sep 17 00:00:00 2001 From: Ashley Harrison Date: Fri, 23 May 2025 10:34:01 +0100 Subject: [PATCH 50/73] Internationalisation: Disable automatic task creation (for now) (#105926) comment out automatic task creation --- .github/workflows/i18n-crowdin-create-tasks.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/i18n-crowdin-create-tasks.yml b/.github/workflows/i18n-crowdin-create-tasks.yml index ade6fd32028..1a5847d83da 100644 --- a/.github/workflows/i18n-crowdin-create-tasks.yml +++ b/.github/workflows/i18n-crowdin-create-tasks.yml @@ -3,8 +3,9 @@ name: Crowdin automatic task management on: workflow_dispatch: # once a week on Sunday at midnight - schedule: - - cron: "0 0 * * 0" + # TODO enable once we're ready to create tasks automatically + # schedule: + # - cron: "0 0 * * 0" jobs: create-tasks-in-crowdin: From c4d3eb1cd03f30ea8e6aab71b2597bf3c6a6f46f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Peter=20=C5=A0tibran=C3=BD?= Date: Fri, 23 May 2025 11:35:59 +0200 Subject: [PATCH 51/73] Remove support for Google Spanner database. (#105846) * Remove support for Google Spanner database. --- .gitignore | 2 - .ignore | 1 - go.mod | 3 - go.sum | 13 - go.work.sum | 3 +- pkg/extensions/enterprise_imports.go | 1 - pkg/infra/db/db.go | 9 - pkg/infra/serverlock/serverlock.go | 4 - .../annotationsimpl/annotations_test.go | 4 - .../annotations/annotationsimpl/xorm_store.go | 1 - .../anonymous/anonimpl/anonstore/database.go | 29 +- .../database/database_folder_test.go | 4 - pkg/services/folder/folderimpl/folder.go | 4 - pkg/services/folder/folderimpl/sqlstore.go | 2 +- pkg/services/login/authinfoimpl/store_test.go | 2 +- .../ngalert/store/alertmanager_test.go | 2 +- pkg/services/sqlstore/database_config.go | 2 - .../sqlstore/migrations/migrations_test.go | 8 +- pkg/services/sqlstore/migrations/star_mig.go | 9 - pkg/services/sqlstore/migrations/user_mig.go | 3 +- ...ice_account_multiple_org_login_migrator.go | 26 -- pkg/services/sqlstore/migrator/migrations.go | 4 - pkg/services/sqlstore/migrator/migrator.go | 5 - .../sqlstore/migrator/spanner_dialect.go | 355 ---------------- pkg/services/sqlstore/migrator/types.go | 1 - .../sqlstore/permissions/dashboard_test.go | 13 - pkg/services/sqlstore/session.go | 5 - pkg/services/sqlstore/session/session.go | 5 - pkg/services/sqlstore/session_test.go | 6 +- pkg/services/sqlstore/sqlstore.go | 16 - pkg/services/sqlstore/sqlstore_testinfra.go | 128 +----- pkg/services/sqlstore/sqlutil/sqlutil.go | 50 --- pkg/services/sqlstore/transactions.go | 2 +- pkg/services/ssosettings/database/database.go | 2 +- .../unified/sql/sqltemplate/dialect.go | 2 - .../sql/sqltemplate/dialect_spanner.go | 32 -- .../unified/sql/test/benchmark_test.go | 9 +- .../unified/sql/test/integration_test.go | 13 - pkg/tests/api/alerting/api_testing_test.go | 2 +- .../notifications/receivers/receiver_test.go | 2 +- .../routingtree/routing_tree_test.go | 2 +- .../templategroup/templates_group_test.go | 2 +- .../timeinterval/timeinterval_test.go | 2 +- pkg/tests/testinfra/testinfra.go | 13 +- pkg/tests/testsuite/testsuite.go | 5 - pkg/util/spanner/spanner.go | 40 -- pkg/util/xorm/dialect_spanner.go | 399 ------------------ pkg/util/xorm/session_insert.go | 7 +- pkg/util/xorm/xorm.go | 8 - pkg/util/xorm/xorm_spanner_test.go | 29 -- 50 files changed, 26 insertions(+), 1265 deletions(-) delete mode 100644 pkg/services/sqlstore/migrator/spanner_dialect.go delete mode 100644 pkg/storage/unified/sql/sqltemplate/dialect_spanner.go delete mode 100644 pkg/util/spanner/spanner.go delete mode 100644 pkg/util/xorm/dialect_spanner.go delete mode 100644 pkg/util/xorm/xorm_spanner_test.go diff --git a/.gitignore b/.gitignore index 29fe8a788bd..9ffe2cf1e61 100644 --- a/.gitignore +++ b/.gitignore @@ -41,8 +41,6 @@ __debug_bin* # This is the new place of the block, but I leave the previous here for a while /devenv/docker/blocks/auth/saml-enterprise /devenv/docker/blocks/auth/signer -/devenv/docker/blocks/spanner_tests -/devenv/docker/blocks/spanner_tests_multi /devenv/docker/blocks/mt-db /tmp diff --git a/.ignore b/.ignore index 01e1bb8d6e2..e2253d1f69d 100644 --- a/.ignore +++ b/.ignore @@ -19,4 +19,3 @@ # This is the new place of the block, but I leave the previous here for a while !/devenv/docker/blocks/auth/saml-enterprise !/devenv/docker/blocks/auth/signer -!/devenv/docker/blocks/spanner_tests \ No newline at end of file diff --git a/go.mod b/go.mod index 535d3382128..fdb41c952ad 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,6 @@ require ( buf.build/gen/go/parca-dev/parca/connectrpc/go v1.17.0-20240902100956-02fd72488966.1 // @grafana/observability-traces-and-profiling buf.build/gen/go/parca-dev/parca/protocolbuffers/go v1.34.2-20240902100956-02fd72488966.2 // @grafana/observability-traces-and-profiling cloud.google.com/go/kms v1.20.5 // @grafana/grafana-backend-group - cloud.google.com/go/spanner v1.75.0 // @grafana/grafana-search-and-storage cloud.google.com/go/storage v1.50.0 // @grafana/grafana-backend-group connectrpc.com/connect v1.17.0 // @grafana/observability-traces-and-profiling cuelang.org/go v0.11.1 // @grafana/grafana-as-code @@ -75,7 +74,6 @@ require ( github.com/google/uuid v1.6.0 // @grafana/grafana-backend-group github.com/google/wire v0.6.0 // @grafana/grafana-backend-group github.com/googleapis/gax-go/v2 v2.14.1 // @grafana/grafana-backend-group - github.com/googleapis/go-sql-spanner v1.11.1 // @grafana/grafana-search-and-storage github.com/gorilla/mux v1.8.1 // @grafana/grafana-backend-group github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 // @grafana/grafana-app-platform-squad github.com/grafana/alerting v0.0.0-20250521131632-6e476b0b04c3 // @grafana/alerting-backend @@ -253,7 +251,6 @@ require ( github.com/Azure/go-ntlmssp v0.0.0-20220621081337-cb9428e4ac1e // indirect github.com/AzureAD/microsoft-authentication-library-for-go v1.3.2 // indirect github.com/FZambia/eagle v0.2.0 // indirect - github.com/GoogleCloudPlatform/grpc-gcp-go/grpcgcp v1.5.2 // indirect github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.26.0 // indirect github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.49.0 // indirect github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.49.0 // indirect diff --git a/go.sum b/go.sum index b73237ae465..b0fe824a133 100644 --- a/go.sum +++ b/go.sum @@ -187,7 +187,6 @@ cloud.google.com/go/compute v1.14.0/go.mod h1:YfLtxrj9sU4Yxv+sXzZkyPjEyPBZfXHUvj cloud.google.com/go/compute v1.15.1/go.mod h1:bjjoF/NtFUrkD/urWfdHaKuOPDR5nWIs63rR+SXhcpA= cloud.google.com/go/compute v1.18.0/go.mod h1:1X7yHxec2Ga+Ss6jPyjxRxpu2uu7PLgsOVXvgU0yacs= cloud.google.com/go/compute v1.19.0/go.mod h1:rikpw2y+UMidAe9tISo04EHNOIf42RLYF/q8Bs93scU= -cloud.google.com/go/compute v1.19.1/go.mod h1:6ylj3a05WF8leseCdIf77NK0g1ey+nj5IKd5/kvShxE= cloud.google.com/go/compute v1.19.3/go.mod h1:qxvISKp/gYnXkSAD1ppcSOveRAmzxicEv/JlizULFrI= cloud.google.com/go/compute v1.20.1/go.mod h1:4tCnrn48xsqlwSAiLf1HXMQk8CONslYbdiEZc9FEIbM= cloud.google.com/go/compute/metadata v0.1.0/go.mod h1:Z1VN+bulIf6bt4P/C37K4DyZYZEXYonfTBHHFPO/4UU= @@ -541,8 +540,6 @@ cloud.google.com/go/shell v1.6.0/go.mod h1:oHO8QACS90luWgxP3N9iZVuEiSF84zNyLytb+ cloud.google.com/go/spanner v1.41.0/go.mod h1:MLYDBJR/dY4Wt7ZaMIQ7rXOTLjYrmxLE/5ve9vFfWos= cloud.google.com/go/spanner v1.44.0/go.mod h1:G8XIgYdOK+Fbcpbs7p2fiprDw4CaZX63whnSMLVBxjk= cloud.google.com/go/spanner v1.45.0/go.mod h1:FIws5LowYz8YAE1J8fOS7DJup8ff7xJeetWEo5REA2M= -cloud.google.com/go/spanner v1.75.0 h1:2zrltTJv/4P3pCgpYgde4Eb1vN8Cgy1fNy7pbTnOovg= -cloud.google.com/go/spanner v1.75.0/go.mod h1:TLFZBvPQmx3We7sGh12eTk9lLsRLczzZaiweqfMpR80= cloud.google.com/go/speech v1.6.0/go.mod h1:79tcr4FHCimOp56lwC01xnt/WPJZc4v3gzyT7FoBkCM= cloud.google.com/go/speech v1.7.0/go.mod h1:KptqL+BAQIhMsj1kOP2la5DSEEerPDuOP/2mmkhHhZQ= cloud.google.com/go/speech v1.8.0/go.mod h1:9bYIl1/tjsAnMgKGHKmBZzXKEkGgtU+MpdDPTE9f7y0= @@ -718,8 +715,6 @@ github.com/DataDog/datadog-go v2.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3 github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= github.com/FZambia/eagle v0.2.0 h1:1kQaZpJvbkvAXFRE/9K2ucBMuVqo+E29EMLYB74hIis= github.com/FZambia/eagle v0.2.0/go.mod h1:LKMYBwGYhao5sJI0TppvQ4SvvldFj9gITxrl8NvGwG0= -github.com/GoogleCloudPlatform/grpc-gcp-go/grpcgcp v1.5.2 h1:DBjmt6/otSdULyJdVg2BlG0qGZO5tKL4VzOs0jpvw5Q= -github.com/GoogleCloudPlatform/grpc-gcp-go/grpcgcp v1.5.2/go.mod h1:dppbR7CwXD4pgtV9t3wD1812RaLDcBjtblcDF5f1vI0= github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.26.0 h1:f2Qw/Ehhimh5uO1fayV0QIW7DShEQqhtUfhYc+cBPlw= github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.26.0/go.mod h1:2bIszWvQRlJVmJLiuLhukLImRjKPcYdzzsx6darK02A= github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.49.0 h1:o90wcURuxekmXrtxmYWTyNla0+ZEHhud6DI1ZTxd1vI= @@ -1019,7 +1014,6 @@ github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWH github.com/cncf/xds/go v0.0.0-20220314180256-7f1daf1720fc/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20230105202645-06c439db220b/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20230310173818-32f1caf87195/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cncf/xds/go v0.0.0-20230607035331-e9ce68804cb4/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20250121191232-2f005788dc42 h1:Om6kYQYDUk5wWbT0t0q6pvyM49i9XZAv9dDrkDA7gjk= github.com/cncf/xds/go v0.0.0-20250121191232-2f005788dc42/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8= github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= @@ -1133,7 +1127,6 @@ github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go. github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE= 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-0.20230524094728-9239064ad72f/go.mod h1:sfYdkwUW4BA3PbKjySwjJy+O4Pu0h62rlqCMHNk+K+Q= github.com/envoyproxy/go-control-plane v0.13.4 h1:zEqyPVyku6IvWCFwux4x9RxkLOMUL+1vC9xUFv5l2/M= github.com/envoyproxy/go-control-plane v0.13.4/go.mod h1:kDfuBlDVsSj2MjrLEtRWtHlsWIFcGyB2RMO44Dc5GZA= github.com/envoyproxy/go-control-plane/envoy v1.32.4 h1:jb83lalDRZSpPWW2Z7Mck/8kXZ5CQAFYVjQcdVIr83A= @@ -1144,7 +1137,6 @@ github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7 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 v0.10.0/go.mod h1:DRjgyB0I43LtJapqN6NiRwroiAU2PaFuvk/vjgh61ss= -github.com/envoyproxy/protoc-gen-validate v0.10.1/go.mod h1:DRjgyB0I43LtJapqN6NiRwroiAU2PaFuvk/vjgh61ss= github.com/envoyproxy/protoc-gen-validate v1.2.1 h1:DEo3O99U8j4hBFwbJfrz9VtgcDfUKS7KJ7spH3d86P8= github.com/envoyproxy/protoc-gen-validate v1.2.1/go.mod h1:d/C80l/jxXLdfEIhX1W2TmLfsJ31lvEjwamM4DxlWXU= github.com/evanphx/json-patch v4.2.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= @@ -1549,8 +1541,6 @@ github.com/googleapis/gax-go/v2 v2.14.1/go.mod h1:Hb/NubMaVM88SrNkvl8X/o8XWwDJEP github.com/googleapis/gnostic v0.0.0-20170426233943-68f4ded48ba9/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY= github.com/googleapis/gnostic v0.0.0-20170729233727-0c5108395e2d/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY= github.com/googleapis/gnostic v0.3.0/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY= -github.com/googleapis/go-sql-spanner v1.11.1 h1:z3ThtKV5HFvaNv9UGc26+ggS+lS0dsCAkaFduKL7vws= -github.com/googleapis/go-sql-spanner v1.11.1/go.mod h1:fuA5q4yMS3SZiVfRr5bvksPNk7zUn/irbQW62H/ffZw= github.com/googleapis/go-type-adapters v1.0.0/go.mod h1:zHW75FOG2aur7gAO2B+MLby+cLsWGBF62rFAi7WjWO4= github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= github.com/gophercloud/gophercloud v0.3.0/go.mod h1:vxM41WHh5uqHVBMZHzuwNOHh8XEoIEcSTewFxm1c5g8= @@ -2392,7 +2382,6 @@ github.com/stretchr/testify v1.7.5/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= @@ -3398,11 +3387,9 @@ google.golang.org/grpc v1.50.0/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCD google.golang.org/grpc v1.50.1/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI= google.golang.org/grpc v1.51.0/go.mod h1:wgNDFcnuBGmxLKI/qn4T+m5BtEBYXJPvibbUPsAIPww= google.golang.org/grpc v1.52.0/go.mod h1:pu6fVzoFb+NBYNAvQL08ic+lvB2IojljRYuun5vorUY= -google.golang.org/grpc v1.52.3/go.mod h1:pu6fVzoFb+NBYNAvQL08ic+lvB2IojljRYuun5vorUY= google.golang.org/grpc v1.53.0/go.mod h1:OnIrk0ipVdj4N5d9IUoFUx72/VlD7+jUsHwZgwSMQpw= google.golang.org/grpc v1.54.0/go.mod h1:PUSEXI6iWghWaB6lXM4knEgpJNu2qUcKfDtNci3EC2g= google.golang.org/grpc v1.55.0/go.mod h1:iYEXKGkEBhg1PjZQvoYEVPTDkHo1/bjTnfwTeGONTY8= -google.golang.org/grpc v1.56.3/go.mod h1:I9bI3vqKfayGqPUAwGdOSu7kt6oIJLixfffKrpXqQ9s= google.golang.org/grpc v1.72.1 h1:HR03wO6eyZ7lknl75XlxABNVLLFc2PAb6mHlYh756mA= google.golang.org/grpc v1.72.1/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= diff --git a/go.work.sum b/go.work.sum index 84895460656..99c60fe0c96 100644 --- a/go.work.sum +++ b/go.work.sum @@ -442,6 +442,7 @@ cloud.google.com/go/shell v1.8.1/go.mod h1:jaU7OHeldDhTwgs3+clM0KYEDYnBAPevUI6wN cloud.google.com/go/shell v1.8.3 h1:mjYgUsOtV3jl9xvDmcvlRRmA64deEPf52zOfuc68b/g= cloud.google.com/go/shell v1.8.3/go.mod h1:OYcrgWF6JSp/uk76sNTtYFlMD0ho2+Cdzc7U3P/bF54= cloud.google.com/go/spanner v1.70.0/go.mod h1:X5T0XftydYp0K1adeJQDJtdWpbrOeJ7wHecM4tK6FiE= +cloud.google.com/go/spanner v1.73.0 h1:0bab8QDn6MNj9lNK6XyGAVFhMlhMU2waePPa6GZNoi8= cloud.google.com/go/spanner v1.73.0/go.mod h1:mw98ua5ggQXVWwp83yjwggqEmW9t8rjs9Po1ohcUGW4= cloud.google.com/go/speech v1.25.1 h1:iGZJS3wrdkje/Vqiacx1+r+zVwUZoXVMdklYIVsvfNw= cloud.google.com/go/speech v1.25.1/go.mod h1:WgQghvghkZ1htG6BhYn98mP7Tg0mti8dBFDLMVXH/vM= @@ -1047,8 +1048,6 @@ github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg78 github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= github.com/go-viper/mapstructure/v2 v2.0.0-alpha.1 h1:TQcrn6Wq+sKGkpyPvppOz99zsMBaUOKXq6HSv655U1c= github.com/go-viper/mapstructure/v2 v2.0.0-alpha.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= -github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss= -github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/go-zookeeper/zk v1.0.2/go.mod h1:nOB03cncLtlp4t+UAkGSV+9beXP/akpekBwL+UX1Qcw= github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU= github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM= diff --git a/pkg/extensions/enterprise_imports.go b/pkg/extensions/enterprise_imports.go index 44fe4e5cfc0..faab1f25efc 100644 --- a/pkg/extensions/enterprise_imports.go +++ b/pkg/extensions/enterprise_imports.go @@ -6,7 +6,6 @@ package extensions import ( _ "cloud.google.com/go/kms/apiv1" _ "cloud.google.com/go/kms/apiv1/kmspb" - _ "cloud.google.com/go/spanner" _ "github.com/Azure/azure-sdk-for-go/sdk/azidentity" _ "github.com/Azure/azure-sdk-for-go/sdk/keyvault/azkeys" _ "github.com/Azure/azure-sdk-for-go/services/keyvault/v7.1/keyvault" diff --git a/pkg/infra/db/db.go b/pkg/infra/db/db.go index f3621b0a3ac..ede3d64677c 100644 --- a/pkg/infra/db/db.go +++ b/pkg/infra/db/db.go @@ -54,7 +54,6 @@ type InitTestDBOpt = sqlstore.InitTestDBOpt var SetupTestDB = sqlstore.SetupTestDB var CleanupTestDB = sqlstore.CleanupTestDB var ProvideService = sqlstore.ProvideService -var SkipTestsOnSpanner = sqlstore.SkipTestsOnSpanner func InitTestDB(t sqlutil.ITestDB, opts ...InitTestDBOpt) *sqlstore.SQLStore { db, _ := InitTestDBWithCfg(t, opts...) @@ -96,11 +95,3 @@ func IsTestDBMSSQL() bool { return false } - -func IsTestDBSpanner() bool { - if db, present := os.LookupEnv("GRAFANA_TEST_DB"); present { - return db == migrator.Spanner - } - - return false -} diff --git a/pkg/infra/serverlock/serverlock.go b/pkg/infra/serverlock/serverlock.go index ea66851c34a..768854f9b06 100644 --- a/pkg/infra/serverlock/serverlock.go +++ b/pkg/infra/serverlock/serverlock.go @@ -374,10 +374,6 @@ func (sl *ServerLockService) createLock(ctx context.Context, } lockRow.Id = id } else { - if sl.SQLStore.GetDBType() == migrator.Spanner { - rawSQL += " THEN RETURN id" // Required for successful LastInsertId call. - } - res, err := dbSession.Exec( rawSQL, lockRow.OperationUID, lockRow.LastExecution, 0) diff --git a/pkg/services/annotations/annotationsimpl/annotations_test.go b/pkg/services/annotations/annotationsimpl/annotations_test.go index bcee5ba7412..28ee61f4afb 100644 --- a/pkg/services/annotations/annotationsimpl/annotations_test.go +++ b/pkg/services/annotations/annotationsimpl/annotations_test.go @@ -203,10 +203,6 @@ func TestIntegrationAnnotationListingWithInheritedRBAC(t *testing.T) { t.Skip("skipping integration test") } - if db.IsTestDBSpanner() { - t.Skip("skipping integration test") - } - orgID := int64(1) permissions := []accesscontrol.Permission{ { diff --git a/pkg/services/annotations/annotationsimpl/xorm_store.go b/pkg/services/annotations/annotationsimpl/xorm_store.go index 711be12e68c..317f9915566 100644 --- a/pkg/services/annotations/annotationsimpl/xorm_store.go +++ b/pkg/services/annotations/annotationsimpl/xorm_store.go @@ -334,7 +334,6 @@ func (r *xormRepositoryImpl) Get(ctx context.Context, query annotations.ItemQuer } if len(tags) > 0 { - // "at" is a keyword in Spanner and needs to be quoted. tagsSubQuery := fmt.Sprintf(` SELECT SUM(1) FROM annotation_tag `+r.db.Quote("at")+` INNER JOIN tag on tag.id = `+r.db.Quote("at")+`.tag_id diff --git a/pkg/services/anonymous/anonimpl/anonstore/database.go b/pkg/services/anonymous/anonimpl/anonstore/database.go index fe53ee66dee..6fa8766ba46 100644 --- a/pkg/services/anonymous/anonimpl/anonstore/database.go +++ b/pkg/services/anonymous/anonimpl/anonstore/database.go @@ -146,14 +146,11 @@ func (s *AnonDBStore) CreateOrUpdateDevice(ctx context.Context, device *Device) } // If CreatedAt time is not set (i.e. it's zero), and we end up creating the device, use current time as creation time. - // Spanner converts zero time to NULL, but CreatedAt is not nullable, so this helps to fix that problem too. + // If database converts zero time to NULL, but CreatedAt is not nullable, this helps to fix that problem too. created := device.CreatedAt if created.IsZero() { created = time.Now() } - if s.sqlStore.GetDBType() == migrator.Spanner { - return s.insertIntoSpanner(ctx, device, created) - } args := []any{device.DeviceID, device.ClientIP, device.UserAgent, created.UTC(), device.UpdatedAt.UTC()} switch s.sqlStore.GetDBType() { @@ -192,30 +189,6 @@ func (s *AnonDBStore) CreateOrUpdateDevice(ctx context.Context, device *Device) return err } -// In Spanner INSERT OR UPDATE only works when conflict is on primary key. However here we expect conflict on non-PK -// column "device_id", so we need to use a transaction instead. -func (s *AnonDBStore) insertIntoSpanner(ctx context.Context, dev *Device, created time.Time) error { - return s.sqlStore.WithTransactionalDbSession(ctx, func(dbSession *sqlstore.DBSession) error { - prev := &Device{DeviceID: dev.DeviceID} - ok, err := dbSession.Table(tableName).Get(prev) - if err != nil { - return err - } - if !ok { - // Don't call Insert(dev) directly, because successful Insert modifies dev.DeviceID, - // but we don't do the same for other databases. That's why we create a copy. - devCopy := *dev - devCopy.CreatedAt = created - _, err = dbSession.Table(tableName).Insert(devCopy) - return err - } - - // Include all columns in the update, even when empty (it's what inserts for other databases do too). - _, err = dbSession.Table(tableName).Where("device_id=?", dev.DeviceID).MustCols("client_ip", "user_agent", "created_at", "updated_at").Update(dev) - return err - }) -} - func (s *AnonDBStore) CountDevices(ctx context.Context, from time.Time, to time.Time) (int64, error) { var count int64 err := s.sqlStore.WithDbSession(ctx, func(dbSession *sqlstore.DBSession) error { diff --git a/pkg/services/dashboards/database/database_folder_test.go b/pkg/services/dashboards/database/database_folder_test.go index 4a743b5840b..f41ffe9a11f 100644 --- a/pkg/services/dashboards/database/database_folder_test.go +++ b/pkg/services/dashboards/database/database_folder_test.go @@ -234,10 +234,6 @@ func TestIntegrationDashboardInheritedFolderRBAC(t *testing.T) { if testing.Short() { t.Skip("skipping integration test") } - if db.IsTestDBSpanner() { - t.Skip("skipping integration test") - } - // the maximux nested folder hierarchy starting from parent down to subfolders nestedFolders := make([]*folder.Folder, 0, folder.MaxNestedFolderDepth+1) diff --git a/pkg/services/folder/folderimpl/folder.go b/pkg/services/folder/folderimpl/folder.go index 7dc6ff099ab..68e570cdb6d 100644 --- a/pkg/services/folder/folderimpl/folder.go +++ b/pkg/services/folder/folderimpl/folder.go @@ -168,10 +168,6 @@ func (s *Service) DBMigration(db db.DB) { SELECT uid, org_id, title, created, updated FROM dashboard WHERE is_folder = true ON CONFLICT(uid, org_id) DO UPDATE SET title=excluded.title, updated=excluded.updated `) - } else if db.GetDialect().DriverName() == migrator.Spanner { - // We may eventually make this migration work with Spanner, but for now don't do anything. - // We intend to store dashboards and folders only in unified storage when using spanner. - deleteOldFolders = false } else { // covered by UQE_folder_org_id_uid _, err = sess.Exec(` diff --git a/pkg/services/folder/folderimpl/sqlstore.go b/pkg/services/folder/folderimpl/sqlstore.go index 580ce43755d..65cd5623ec4 100644 --- a/pkg/services/folder/folderimpl/sqlstore.go +++ b/pkg/services/folder/folderimpl/sqlstore.go @@ -620,7 +620,7 @@ func (ss *FolderStoreImpl) GetDescendants(ctx context.Context, orgID int64, ance func getFullpathSQL(dialect migrator.Dialect) string { escaped := `\/` - if dialect.DriverName() == migrator.MySQL || dialect.DriverName() == migrator.Spanner { + if dialect.DriverName() == migrator.MySQL { escaped = `\\/` } concatCols := make([]string, 0, folder.MaxNestedFolderDepth) diff --git a/pkg/services/login/authinfoimpl/store_test.go b/pkg/services/login/authinfoimpl/store_test.go index 6dcfbf7910d..db85443fc4f 100644 --- a/pkg/services/login/authinfoimpl/store_test.go +++ b/pkg/services/login/authinfoimpl/store_test.go @@ -52,7 +52,7 @@ func TestIntegrationAuthInfoStore(t *testing.T) { // There is no guarantee that user with user_id=1 gets "oauth_azuread" or "ldap". // Both are valid results for the query (basically SELECT * FROM `user_auth` WHERE `user_id` IN (1,2) ORDER BY created), - // Spanner emulator will randomize its output, so test cannot rely on the ordering (other than "Created" column, which is equal here). + // Some databases may randomize its output, so test cannot rely on the ordering (other than "Created" column, which is equal here). require.True(t, labels[1] == login.AzureADAuthModule || labels[1] == login.LDAPAuthModule) require.Equal(t, login.GoogleAuthModule, labels[2]) }) diff --git a/pkg/services/ngalert/store/alertmanager_test.go b/pkg/services/ngalert/store/alertmanager_test.go index 4ea0144f967..43932247831 100644 --- a/pkg/services/ngalert/store/alertmanager_test.go +++ b/pkg/services/ngalert/store/alertmanager_test.go @@ -17,7 +17,7 @@ import ( ) func TestMain(m *testing.M) { - testsuite.RunButSkipOnSpanner(m) + testsuite.Run(m) } func TestIntegrationAlertmanagerStore(t *testing.T) { diff --git a/pkg/services/sqlstore/database_config.go b/pkg/services/sqlstore/database_config.go index e3cc626066a..171ff4972b9 100644 --- a/pkg/services/sqlstore/database_config.go +++ b/pkg/services/sqlstore/database_config.go @@ -203,8 +203,6 @@ func (dbCfg *DatabaseConfig) buildConnectionString(cfg *setting.Cfg, features fe } cnnstr += buildExtraConnectionString('&', dbCfg.UrlQueryParams) - case migrator.Spanner: - cnnstr = dbCfg.Name default: return fmt.Errorf("unknown database type: %s", dbCfg.Type) } diff --git a/pkg/services/sqlstore/migrations/migrations_test.go b/pkg/services/sqlstore/migrations/migrations_test.go index 5db6600e96a..a67f989a83d 100644 --- a/pkg/services/sqlstore/migrations/migrations_test.go +++ b/pkg/services/sqlstore/migrations/migrations_test.go @@ -77,8 +77,8 @@ func TestIntegrationMigrationLock(t *testing.T) { } dbType := sqlutil.GetTestDBType() - // skip for SQLite and Spanner since there is no database locking (only migrator locking) - if dbType == SQLite || dbType == Spanner { + // skip for SQLite since there is no database locking (only migrator locking) + if dbType == SQLite { t.Skip() } @@ -235,8 +235,8 @@ func TestMigratorLocking(t *testing.T) { func TestDatabaseLocking(t *testing.T) { dbType := sqlutil.GetTestDBType() - // skip for SQLite and Spanner since there is no database locking (only migrator locking) - if dbType == SQLite || dbType == Spanner { + // skip for SQLite since there is no database locking (only migrator locking) + if dbType == SQLite { t.Skip() } diff --git a/pkg/services/sqlstore/migrations/star_mig.go b/pkg/services/sqlstore/migrations/star_mig.go index 14cf9a5a093..4521acd7f2b 100644 --- a/pkg/services/sqlstore/migrations/star_mig.go +++ b/pkg/services/sqlstore/migrations/star_mig.go @@ -82,15 +82,6 @@ func RunStarMigrations(sess *xorm.Session, driverName string) error { star.org_id = dashboard.org_id, star.updated = NOW() WHERE star.dashboard_uid IS NULL OR star.org_id IS NULL;` - case Spanner: - sql = `UPDATE star - SET - dashboard_uid = (SELECT uid FROM dashboard WHERE dashboard.id = star.dashboard_id), - org_id = (SELECT org_id FROM dashboard WHERE dashboard.id = star.dashboard_id), - updated = CURRENT_TIMESTAMP() - WHERE - (dashboard_uid IS NULL OR org_id IS NULL) - AND EXISTS (SELECT 1 FROM dashboard WHERE dashboard.id = star.dashboard_id)` } if _, err := sess.Exec(sql); err != nil { diff --git a/pkg/services/sqlstore/migrations/user_mig.go b/pkg/services/sqlstore/migrations/user_mig.go index 0d3ced3fbb8..af9f0d235cd 100644 --- a/pkg/services/sqlstore/migrations/user_mig.go +++ b/pkg/services/sqlstore/migrations/user_mig.go @@ -154,8 +154,7 @@ func addUserMigrations(mg *Migrator) { mg.AddMigration("Make sure users uid are set", NewRawSQLMigration(""). SQLite("UPDATE user SET uid=printf('u%09d',id) WHERE uid is NULL OR uid = '';"). Postgres("UPDATE `user` SET uid='u' || lpad('' || id::text,9,'0') WHERE uid is NULL OR uid = '';"). - Mysql("UPDATE user SET uid=concat('u',lpad(id,9,'0')) WHERE uid is NULL OR uid = '';"). - Spanner("UPDATE user SET uid=concat('u',lpad(CAST(id AS STRING),9,'0')) WHERE uid IS NULL OR uid = '';")) + Mysql("UPDATE user SET uid=concat('u',lpad(id,9,'0')) WHERE uid is NULL OR uid = '';")) mg.AddMigration("Add unique index user_uid", NewAddIndexMigration(userV2, &Index{ Cols: []string{"uid"}, Type: UniqueIndex, diff --git a/pkg/services/sqlstore/migrations/usermig/service_account_multiple_org_login_migrator.go b/pkg/services/sqlstore/migrations/usermig/service_account_multiple_org_login_migrator.go index bf06e698339..a690c6d459d 100644 --- a/pkg/services/sqlstore/migrations/usermig/service_account_multiple_org_login_migrator.go +++ b/pkg/services/sqlstore/migrations/usermig/service_account_multiple_org_login_migrator.go @@ -72,19 +72,6 @@ func (p *ServiceAccountsSameLoginCrossOrgs) Exec(sess *xorm.Session, mg *migrato AND is_service_account = 1 AND login NOT LIKE 'sa-' || CAST(org_id AS TEXT) || '-%'; `) - case migrator.Spanner: - _, err = p.sess.Exec(` - UPDATE user - SET login = CONCAT('sa-', CAST(org_id AS STRING), '-', - CASE - WHEN login LIKE 'sa-%' THEN SUBSTRING(login, 4) - ELSE login - END - ) - WHERE login IS NOT NULL - AND is_service_account - AND login NOT LIKE CONCAT('sa-', CAST(org_id AS STRING), '-%') - `) default: return fmt.Errorf("dialect not supported: %s", p.dialect) @@ -142,19 +129,6 @@ func (p *ServiceAccountsDeduplicateOrgInLogin) Exec(sess *xorm.Session, mg *migr WHERE u2.login = 'sa-' || CAST(u.org_id AS TEXT) || SUBSTRING(u.login, LENGTH('sa-'||CAST(u.org_id AS TEXT)||'-'||CAST(u.org_id AS TEXT))+1) );; `) - case migrator.Spanner: - _, err = sess.Exec(` - UPDATE ` + dialect.Quote("user") + ` AS u - SET login = 'sa-' || CAST(u.org_id AS STRING) || SUBSTRING(u.login, LENGTH('sa-'||CAST(u.org_id AS STRING)||'-'||CAST(u.org_id AS STRING))+1) - WHERE u.login IS NOT NULL - AND u.is_service_account - AND u.login LIKE 'sa-'||CAST(u.org_id AS STRING)||'-'||CAST(u.org_id AS STRING)||'-%' - AND NOT EXISTS ( - SELECT 1 - FROM ` + dialect.Quote("user") + `AS u2 - WHERE u2.login = 'sa-' || CAST(u.org_id AS STRING) || SUBSTRING(u.login, LENGTH('sa-'||CAST(u.org_id AS STRING)||'-'||CAST(u.org_id AS STRING))+1) - );; - `) default: return fmt.Errorf("dialect not supported: %s", dialect) } diff --git a/pkg/services/sqlstore/migrator/migrations.go b/pkg/services/sqlstore/migrator/migrations.go index 24b266e57d2..b785fd867d5 100644 --- a/pkg/services/sqlstore/migrator/migrations.go +++ b/pkg/services/sqlstore/migrator/migrations.go @@ -85,10 +85,6 @@ func (m *RawSQLMigration) Mssql(sql string) *RawSQLMigration { return m.Set(MSSQL, sql) } -func (m *RawSQLMigration) Spanner(sql string) *RawSQLMigration { - return m.Set(Spanner, sql) -} - type AddColumnMigration struct { MigrationBase tableName string diff --git a/pkg/services/sqlstore/migrator/migrator.go b/pkg/services/sqlstore/migrator/migrator.go index aded736a49d..d8e55c4ee71 100644 --- a/pkg/services/sqlstore/migrator/migrator.go +++ b/pkg/services/sqlstore/migrator/migrator.go @@ -417,11 +417,6 @@ func (mg *Migrator) InTransaction(callback dbTransactionFunc) error { sess := mg.DBEngine.NewSession() defer sess.Close() - // XXX: Spanner cannot execute DDL statements in transactions - if mg.Dialect.DriverName() == Spanner { - return callback(sess) - } - if err := sess.Begin(); err != nil { return err } diff --git a/pkg/services/sqlstore/migrator/spanner_dialect.go b/pkg/services/sqlstore/migrator/spanner_dialect.go deleted file mode 100644 index 9d490f8aff2..00000000000 --- a/pkg/services/sqlstore/migrator/spanner_dialect.go +++ /dev/null @@ -1,355 +0,0 @@ -//go:build enterprise || pro - -package migrator - -import ( - "context" - _ "embed" - "encoding/json" - "errors" - "fmt" - "strings" - "time" - - "cloud.google.com/go/spanner" - database "cloud.google.com/go/spanner/admin/database/apiv1" - "cloud.google.com/go/spanner/admin/database/apiv1/databasepb" - "github.com/googleapis/gax-go/v2" - spannerdriver "github.com/googleapis/go-sql-spanner" - "github.com/grafana/grafana/pkg/util/xorm" - "google.golang.org/grpc/codes" - - "github.com/grafana/dskit/concurrency" - utilspanner "github.com/grafana/grafana/pkg/util/spanner" - "github.com/grafana/grafana/pkg/util/xorm/core" -) - -type SpannerDialect struct { - BaseDialect - d core.Dialect -} - -func init() { - supportedDialects[Spanner] = NewSpannerDialect -} - -func NewSpannerDialect() Dialect { - d := SpannerDialect{d: core.QueryDialect(Spanner)} - d.dialect = &d - d.driverName = Spanner - return &d -} - -func (s *SpannerDialect) AutoIncrStr() string { return s.d.AutoIncrStr() } -func (s *SpannerDialect) Quote(name string) string { return s.d.Quote(name) } -func (s *SpannerDialect) SupportEngine() bool { return s.d.SupportEngine() } - -func (s *SpannerDialect) LikeOperator(column string, wildcardBefore bool, pattern string, wildcardAfter bool) (string, string) { - param := strings.ToLower(pattern) - if wildcardBefore { - param = "%" + param - } - if wildcardAfter { - param = param + "%" - } - return fmt.Sprintf("LOWER(%s) LIKE ?", column), param -} - -func (s *SpannerDialect) IndexCheckSQL(tableName, indexName string) (string, []any) { - return s.d.IndexCheckSql(tableName, indexName) -} -func (s *SpannerDialect) SQLType(col *Column) string { - c := core.NewColumn(col.Name, "", core.SQLType{Name: col.Type}, col.Length, col.Length2, col.Nullable) - return s.d.SqlType(c) -} - -func (s *SpannerDialect) BatchSize() int { return 1000 } - -func (s *SpannerDialect) BooleanValue(b bool) any { - return b -} - -func (s *SpannerDialect) BooleanStr(b bool) string { - if b { - return "true" - } - return "false" -} -func (s *SpannerDialect) ErrorMessage(err error) string { - return spanner.ErrDesc(spanner.ToSpannerError(err)) -} -func (s *SpannerDialect) IsDeadlock(err error) bool { - return spanner.ErrCode(spanner.ToSpannerError(err)) == codes.Aborted -} -func (s *SpannerDialect) IsUniqueConstraintViolation(err error) bool { - return spanner.ErrCode(spanner.ToSpannerError(err)) == codes.AlreadyExists -} - -func (s *SpannerDialect) CreateTableSQL(table *Table) string { - t := core.NewEmptyTable() - t.Name = table.Name - t.PrimaryKeys = table.PrimaryKeys - for _, c := range table.Columns { - col := core.NewColumn(c.Name, c.Name, core.SQLType{Name: c.Type}, c.Length, c.Length2, c.Nullable) - col.IsAutoIncrement = c.IsAutoIncrement - col.Default = c.Default - t.AddColumn(col) - } - if len(t.PrimaryKeys) == 0 { - for _, ix := range table.Indices { - if ix.Name == "PRIMARY_KEY" { - t.PrimaryKeys = append(t.PrimaryKeys, ix.Cols...) - } - } - } - return s.d.CreateTableSql(t, t.Name, "", "") -} - -func (s *SpannerDialect) CreateIndexSQL(tableName string, index *Index) string { - idx := core.NewIndex(index.Name, index.Type) - idx.Cols = index.Cols - return s.d.CreateIndexSql(tableName, idx) -} - -func (s *SpannerDialect) UpsertMultipleSQL(tableName string, keyCols, updateCols []string, count int) (string, error) { - return "", errors.New("not supported") -} - -func (s *SpannerDialect) DropIndexSQL(tableName string, index *Index) string { - return fmt.Sprintf("DROP INDEX %v", s.Quote(index.XName(tableName))) -} - -func (s *SpannerDialect) DropTable(tableName string) string { - return fmt.Sprintf("DROP TABLE %s", s.Quote(tableName)) -} - -func (s *SpannerDialect) ColStringNoPk(col *Column) string { - sql := s.dialect.Quote(col.Name) + " " - - sql += s.dialect.SQLType(col) + " " - - if s.dialect.ShowCreateNull() && !col.Nullable { - sql += "NOT NULL " - } - - if col.Default != "" { - // Default value must be in parentheses. - sql += "DEFAULT (" + s.dialect.Default(col) + ") " - } - - return sql -} - -func (s *SpannerDialect) TruncateDBTables(engine *xorm.Engine) error { - // Get tables names only, no columns or indexes. - tables, err := engine.Dialect().GetTables() - if err != nil { - return err - } - sess := engine.NewSession() - defer sess.Close() - - var statements []string - - for _, table := range tables { - switch table.Name { - case "": - continue - case "autoincrement_sequences": - // Don't delete sequence number for migration_log.id column. - statements = append(statements, fmt.Sprintf("DELETE FROM %v WHERE name <> 'migration_log:id'", s.Quote(table.Name))) - case "migration_log": - continue - case "dashboard_acl": - // keep default dashboard permissions - statements = append(statements, fmt.Sprintf("DELETE FROM %v WHERE dashboard_id != -1 AND org_id != -1;", s.Quote(table.Name))) - default: - statements = append(statements, fmt.Sprintf("DELETE FROM %v WHERE TRUE;", s.Quote(table.Name))) - } - } - - // Run statements concurrently. - return concurrency.ForEachJob(context.Background(), len(statements), 10, func(ctx context.Context, idx int) error { - _, err := sess.Exec(statements[idx]) - return err - }) -} - -// CleanDB drops all existing tables and their indexes. -func (s *SpannerDialect) CleanDB(engine *xorm.Engine) error { - tables, err := engine.DBMetas() - if err != nil { - return err - } - - // Collect all DROP statements. - changeStreams, err := s.findChangeStreams(engine) - if err != nil { - return err - } - statements := make([]string, 0, len(tables)+len(changeStreams)) - for _, cs := range changeStreams { - statements = append(statements, fmt.Sprintf("DROP CHANGE STREAM `%s`", cs)) - } - - for _, table := range tables { - // Indexes must be dropped first, otherwise dropping tables fails. - for _, index := range table.Indexes { - if !index.IsRegular { - // Don't drop primary key. - continue - } - sql := fmt.Sprintf("DROP INDEX %s", s.Quote(index.XName(table.Name))) - statements = append(statements, sql) - } - - sql := fmt.Sprintf("DROP TABLE %s", s.Quote(table.Name)) - statements = append(statements, sql) - } - - if len(statements) == 0 { - return nil - } - - return s.executeDDLStatements(context.Background(), engine, statements) -} - -//go:embed snapshot/spanner-ddl.json -var snapshotDDL string - -//go:embed snapshot/spanner-log.json -var snapshotMigrations string - -func (s *SpannerDialect) CreateDatabaseFromSnapshot(ctx context.Context, engine *xorm.Engine, tableName string) error { - var statements, migrationIDs []string - err := json.Unmarshal([]byte(snapshotDDL), &statements) - if err != nil { - return err - } - err = json.Unmarshal([]byte(snapshotMigrations), &migrationIDs) - if err != nil { - return err - } - - err = s.executeDDLStatements(ctx, engine, statements) - if err != nil { - return err - } - - return s.recordMigrationsToMigrationLog(engine, migrationIDs, tableName) -} - -func (s *SpannerDialect) recordMigrationsToMigrationLog(engine *xorm.Engine, migrationIDs []string, tableName string) error { - now := time.Now() - makeRecord := func(id string) MigrationLog { - return MigrationLog{ - MigrationID: id, - SQL: "", - Success: true, - Timestamp: now, - } - } - - sess := engine.NewSession() - defer sess.Close() - - // Insert records in batches to avoid many roundtrips to database. - // Inserting all records at once fails due to "Number of parameters in query exceeds the maximum - // allowed limit of 950." error, so we use smaller batches. - const batchSize = 100 - - if err := sess.Begin(); err != nil { - return err - } - - records := make([]MigrationLog, 0, len(migrationIDs)) - for _, mid := range migrationIDs { - records = append(records, makeRecord(mid)) - - if len(records) >= batchSize { - if _, err := sess.Table(tableName).InsertMulti(records); err != nil { - err2 := sess.Rollback() - return errors.Join(fmt.Errorf("failed to insert migration logs: %w", err), err2) - } - records = records[:0] - } - } - - // Insert remaining records. - if len(records) > 0 { - if _, err := sess.Table(tableName).InsertMulti(records); err != nil { - err2 := sess.Rollback() - return errors.Join(fmt.Errorf("failed to insert migration logs: %w", err), err2) - } - } - - if err := sess.Commit(); err != nil { - return err - } - - return nil -} - -// Spanner can be very slow at executing single DDL statements (it can take up to a minute), but when -// many DDL statements are batched together, Spanner is *much* faster (total time to execute all statements -// is often in tens of seconds). We can't execute batch of DDL statements using sql wrapper, we use "database admin client" -// from Spanner library instead. -func (s *SpannerDialect) executeDDLStatements(ctx context.Context, engine *xorm.Engine, statements []string) error { - // Datasource name contains string used for sql.Open. - dsn := engine.Dialect().DataSourceName() - cfg, err := spannerdriver.ExtractConnectorConfig(dsn) - if err != nil { - return err - } - - opts := utilspanner.ConnectorConfigToClientOptions(cfg) - - databaseAdminClient, err := database.NewDatabaseAdminClient(ctx, opts...) - if err != nil { - return fmt.Errorf("failed to create database admin client: %v", err) - } - //nolint:errcheck // If the databaseAdminClient.Close fails, we simply don't care. - defer databaseAdminClient.Close() - - databaseName := fmt.Sprintf("projects/%s/instances/%s/databases/%s", cfg.Project, cfg.Instance, cfg.Database) - - op, err := databaseAdminClient.UpdateDatabaseDdl(ctx, &databasepb.UpdateDatabaseDdlRequest{ - Database: databaseName, - Statements: statements, - }, gax.WithTimeout(0)) /* disable default timeout */ - if err != nil { - return fmt.Errorf("failed to start database DDL update: %v", err) - } - - err = op.Wait(ctx, gax.WithTimeout(0)) /* disable default timeout */ - if err != nil { - return fmt.Errorf("failed to apply database DDL update: %v", err) - } - return nil -} - -func (s *SpannerDialect) UnionDistinct() string { - return "UNION DISTINCT" -} - -func (s *SpannerDialect) findChangeStreams(engine *xorm.Engine) ([]string, error) { - var result []string - query := `SELECT c.CHANGE_STREAM_NAME - FROM INFORMATION_SCHEMA.CHANGE_STREAMS AS C - WHERE C.CHANGE_STREAM_CATALOG='' - AND C.CHANGE_STREAM_SCHEMA=''` - rows, err := engine.DB().Query(query) - if err != nil { - return nil, err - } - //nolint:errcheck // If the rows.Close fails, we simply don't care. - defer rows.Close() - for rows.Next() { - var name string - if err := rows.Scan(&name); err != nil { - return nil, err - } - result = append(result, name) - } - return result, nil -} diff --git a/pkg/services/sqlstore/migrator/types.go b/pkg/services/sqlstore/migrator/types.go index 38ea5fabca5..15be7996dcc 100644 --- a/pkg/services/sqlstore/migrator/types.go +++ b/pkg/services/sqlstore/migrator/types.go @@ -12,7 +12,6 @@ const ( SQLite = "sqlite3" MySQL = "mysql" MSSQL = "mssql" - Spanner = "spanner" ) type Migration interface { diff --git a/pkg/services/sqlstore/permissions/dashboard_test.go b/pkg/services/sqlstore/permissions/dashboard_test.go index d6fcf89797e..7dc57810a2a 100644 --- a/pkg/services/sqlstore/permissions/dashboard_test.go +++ b/pkg/services/sqlstore/permissions/dashboard_test.go @@ -444,9 +444,6 @@ func TestIntegration_DashboardNestedPermissionFilter(t *testing.T) { expectedResult: []string{"parent"}, }, } - if db.IsTestDBSpanner() { - t.Skip("skipping integration test") - } var orgID int64 = 1 @@ -554,9 +551,6 @@ func TestIntegration_DashboardNestedPermissionFilter_WithSelfContainedPermission expectedResult: []string{"parent"}, }, } - if db.IsTestDBSpanner() { - t.Skip("skipping integration test") - } var orgID int64 = 1 @@ -665,10 +659,6 @@ func TestIntegration_DashboardNestedPermissionFilter_WithActionSets(t *testing.T }, } - if db.IsTestDBSpanner() { - t.Skip("skipping integration test") - } - var orgID int64 = 1 for _, tc := range testCases { @@ -757,9 +747,6 @@ func setupTest(t *testing.T, numFolders, numDashboards int, permissions []access // Insert dashboards in batches batchSize := 500 - if db.IsTestDBSpanner() { - batchSize = 30 // spanner has a limit of 950 parameters per query - } for i := 0; i < len(dashes); i += batchSize { end := i + batchSize if end > len(dashes) { diff --git a/pkg/services/sqlstore/session.go b/pkg/services/sqlstore/session.go index 155e63ab517..1ded27a3559 100644 --- a/pkg/services/sqlstore/session.go +++ b/pkg/services/sqlstore/session.go @@ -146,11 +146,6 @@ func (sess *DBSession) WithReturningID(driverName string, query string, args []a return id, err } } else { - if driverName == migrator.Spanner { - // Only works with INSERT statements. - query = fmt.Sprintf("%s THEN RETURN id", query) - } - sqlOrArgs := append([]any{query}, args...) res, err := sess.Exec(sqlOrArgs...) if err != nil { diff --git a/pkg/services/sqlstore/session/session.go b/pkg/services/sqlstore/session/session.go index d1f79d6f294..92bd29ff25e 100644 --- a/pkg/services/sqlstore/session/session.go +++ b/pkg/services/sqlstore/session/session.go @@ -110,11 +110,6 @@ func execWithReturningId(ctx context.Context, driverName string, query string, s } return id, nil } else { - if driverName == "spanner" { - // LastInsertId requires THEN RETURN directive. - query = fmt.Sprintf("%s THEN RETURN id", query) - } - res, err := sess.Exec(ctx, query, args...) if err != nil { return id, err diff --git a/pkg/services/sqlstore/session_test.go b/pkg/services/sqlstore/session_test.go index 3624b8f1905..3986874d48b 100644 --- a/pkg/services/sqlstore/session_test.go +++ b/pkg/services/sqlstore/session_test.go @@ -8,8 +8,6 @@ import ( "github.com/mattn/go-sqlite3" "github.com/stretchr/testify/require" - "google.golang.org/grpc/codes" - grpcstatus "google.golang.org/grpc/status" "github.com/grafana/grafana/pkg/services/sqlstore/migrator" ) @@ -141,12 +139,10 @@ func getRetryErrors(t *testing.T, store *SQLStore) []error { switch store.GetDialect().DriverName() { case migrator.SQLite: retryErrors = []error{sqlite3.Error{Code: sqlite3.ErrBusy}, sqlite3.Error{Code: sqlite3.ErrLocked}} - case migrator.Spanner: - retryErrors = []error{grpcstatus.Error(codes.Aborted, "aborted transaction")} } if len(retryErrors) == 0 { - t.Skip("This test only works with sqlite or spanner") + t.Skip("This test only works with sqlite") } return retryErrors } diff --git a/pkg/services/sqlstore/sqlstore.go b/pkg/services/sqlstore/sqlstore.go index 306a031562f..31065d93ec5 100644 --- a/pkg/services/sqlstore/sqlstore.go +++ b/pkg/services/sqlstore/sqlstore.go @@ -373,11 +373,6 @@ func (ss *SQLStore) RecursiveQueriesAreSupported() (bool, error) { return *ss.recursiveQueriesAreSupported, nil } recursiveQueriesAreSupported := func() (bool, error) { - if ss.GetDBType() == migrator.Spanner { - // no need to try... - return false, nil - } - var result []int if err := ss.WithDbSession(context.Background(), func(sess *DBSession) error { recQry := `WITH RECURSIVE cte (n) AS @@ -414,7 +409,6 @@ var testSQLStoreSetup = false var testSQLStore *SQLStore var testSQLStoreMutex sync.Mutex var testSQLStoreCleanup []func() -var testSQLStoreSkipTestsOnBackend string // When not empty and matches DB type, test is skipped. // InitTestDBOpt contains options for InitTestDB. type InitTestDBOpt struct { @@ -459,12 +453,6 @@ func SetupTestDB() { testSQLStoreSetup = true } -func SkipTestsOnSpanner() { - testSQLStoreMutex.Lock() - defer testSQLStoreMutex.Unlock() - testSQLStoreSkipTestsOnBackend = "spanner" -} - func CleanupTestDB() { testSQLStoreMutex.Lock() defer testSQLStoreMutex.Unlock() @@ -549,10 +537,6 @@ func TestMain(m *testing.M) { if testSQLStore == nil { dbType := sqlutil.GetTestDBType() - if testSQLStoreSkipTestsOnBackend != "" && testSQLStoreSkipTestsOnBackend == dbType { - t.Skipf("test skipped when using DB type %s", testSQLStoreSkipTestsOnBackend) - } - // set test db config cfg := setting.NewCfg() // nolint:staticcheck diff --git a/pkg/services/sqlstore/sqlstore_testinfra.go b/pkg/services/sqlstore/sqlstore_testinfra.go index 3ed8da0462e..194cf843c2f 100644 --- a/pkg/services/sqlstore/sqlstore_testinfra.go +++ b/pkg/services/sqlstore/sqlstore_testinfra.go @@ -9,18 +9,9 @@ import ( "encoding/hex" "fmt" "os" - "strconv" "testing" "time" - database "cloud.google.com/go/spanner/admin/database/apiv1" - "cloud.google.com/go/spanner/admin/database/apiv1/databasepb" - "cloud.google.com/go/spanner/spannertest" - spannerdriver "github.com/googleapis/go-sql-spanner" - "google.golang.org/api/option" - "google.golang.org/grpc" - "google.golang.org/grpc/credentials/insecure" - "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/registry" @@ -269,9 +260,6 @@ func createTemporaryDatabase(tb TestingTB) (*testDB, error) { // SQLite doesn't have a concept of a database server, so we always create a new file with no connections required. return newSQLite3DB(tb) } - if dbType == "spanner" { - return newSpannerDB(tb) - } // On the remaining databases, we first connect to the configured credentials, create a new database, then return this new database's info as a connection string. // We use databases rather than schemas as MySQL has no concept of schemas, so this aligns them more closely. @@ -326,121 +314,7 @@ func createTemporaryDatabase(tb TestingTB) (*testDB, error) { func generateDatabaseName() string { // The database name has to be unique amongst all tests. It is highly unlikely we will have a collision here. // The database name has to be <= 64 chars long on MySQL, and <= 31 chars on Postgres. - // Database ID length on Spanner must be between 2 and 30 characters. (https://cloud.google.com/spanner/quotas#database-limits) - return "grafana_test_" + randomLowerHex(17) -} - -func newSpannerDB(tb TestingTB) (*testDB, error) { - // See https://github.com/googleapis/go-sql-spanner/blob/main/driver.go#L56-L81 for connection string options. - spannerDB := env("SPANNER_DB", "emulator") - if spannerDB == "spannertest" { - // Start new in-memory spannertest instance. This is mostly useless for our tests - // (spannertest doesn't support many things that we use), but added for completion. - // Each spannertest instance is a separate db. - srv, err := spannertest.NewServer("localhost:0") - if err != nil { - return nil, err - } - tb.Cleanup(srv.Close) - - return &testDB{ - Driver: "spanner", - Conn: fmt.Sprintf("%s/projects/grafanatest/instances/grafanatest/databases/grafanatest;usePlainText=true", srv.Addr), - }, nil - } - - conn := spannerDB - if spannerDB == "emulator" { - host := env("SPANNER_EMULATOR_HOST", "localhost:9010") - conn = fmt.Sprintf("%s/projects/grafanatest/instances/grafanatest/databases/grafanatest;usePlainText=true", host) - } - - cfg, err := spannerdriver.ExtractConnectorConfig(conn) - if err != nil { - return nil, err - } - - clientOptions := spannerConnectorConfigToClientOptions(cfg) - - dbname := generateDatabaseName() - fullDbName := fmt.Sprintf("projects/%s/instances/%s/databases/%s", cfg.Project, cfg.Instance, dbname) - dbCreated := false - - databaseAdminClient, err := database.NewDatabaseAdminClient(tb.Context(), clientOptions...) - if err != nil { - return nil, fmt.Errorf("failed to create database admin client: %v", err) - } - tb.Cleanup(func() { - if dbCreated { - // Drop database in the cleanup. - // Can't use tb.Context() here, since that is canceled before calling Cleanup functions. - err := databaseAdminClient.DropDatabase(context.Background(), &databasepb.DropDatabaseRequest{ - Database: fullDbName, - }) - if err != nil { - tb.Logf("Failed to drop Spanner database %s due to error %v", fullDbName, err) - } else { - tb.Logf("Dropped temporary Spanner database %s", fullDbName) - } - } - - _ = databaseAdminClient.Close() - }) - - op, err := databaseAdminClient.CreateDatabase(tb.Context(), &databasepb.CreateDatabaseRequest{ - Parent: fmt.Sprintf("projects/%s/instances/%s", cfg.Project, cfg.Instance), - CreateStatement: fmt.Sprintf("CREATE DATABASE `%s`", dbname), - DatabaseDialect: databasepb.DatabaseDialect_GOOGLE_STANDARD_SQL, - }) - if err != nil { - return nil, fmt.Errorf("failed to create database: %v", err) - } - _, err = op.Wait(tb.Context()) - if err != nil { - return nil, fmt.Errorf("failed to create database: %v", err) - } - tb.Logf("Created temporary Spanner database %s", fullDbName) - - dbCreated = true - - // Rebuild connection string, but change database to ID of just-created database. - // Example: `localhost:9010/projects/test-project/instances/test-instance/databases/test-database;usePlainText=true;disableRouteToLeader=true;enableEndToEndTracing=true` - connString := "" - if cfg.Host != "" { - connString = fmt.Sprintf("%s/", cfg.Host) - } - // Use new DB name instead of cfg.Database. - connString = connString + fmt.Sprintf("projects/%s/instances/%s/databases/%s", cfg.Project, cfg.Instance, dbname) - for k, v := range cfg.Params { - connString = connString + fmt.Sprintf(";%s=%s", k, v) - } - - return &testDB{ - Driver: "spanner", - Conn: connString, - }, nil -} - -// This is same code as xorm.SpannerConnectorConfigToClientOptions, but we cannot use that because it's under "enterprise" build tag. -func spannerConnectorConfigToClientOptions(connectorConfig spannerdriver.ConnectorConfig) []option.ClientOption { - var opts []option.ClientOption - if connectorConfig.Host != "" { - opts = append(opts, option.WithEndpoint(connectorConfig.Host)) - } - if strval, ok := connectorConfig.Params["credentials"]; ok { - opts = append(opts, option.WithCredentialsFile(strval)) - } - if strval, ok := connectorConfig.Params["credentialsjson"]; ok { - opts = append(opts, option.WithCredentialsJSON([]byte(strval))) - } - if strval, ok := connectorConfig.Params["useplaintext"]; ok { - if val, err := strconv.ParseBool(strval); err == nil && val { - opts = append(opts, - option.WithGRPCDialOption(grpc.WithTransportCredentials(insecure.NewCredentials())), - option.WithoutAuthentication()) - } - } - return opts + return "grafana_test_" + randomLowerHex(18) } func env(name, fallback string) string { diff --git a/pkg/services/sqlstore/sqlutil/sqlutil.go b/pkg/services/sqlstore/sqlutil/sqlutil.go index 3b6e449ff4b..8506acdab79 100644 --- a/pkg/services/sqlstore/sqlutil/sqlutil.go +++ b/pkg/services/sqlstore/sqlutil/sqlutil.go @@ -6,8 +6,6 @@ import ( "io/fs" "os" "path/filepath" - - "cloud.google.com/go/spanner/spannertest" ) // ITestDB is an interface of arguments for testing db @@ -45,8 +43,6 @@ func GetTestDB(dbType string) (*TestDB, error) { return postgresTestDB() case "sqlite3": return sqLite3TestDB() - case "spanner": - return spannerTestDB() } return nil, fmt.Errorf("unknown test db type: %s", dbType) @@ -156,49 +152,3 @@ func postgresTestDB() (*TestDB, error) { Cleanup: func() {}, }, nil } - -func spannerTestDB() (*TestDB, error) { - // See https://github.com/googleapis/go-sql-spanner/blob/main/driver.go#L56-L81 for connection string options. - - spannerDB := os.Getenv("SPANNER_DB") - if spannerDB == "" { - spannerDB = "emulator" - } - - if spannerDB == "spannertest" { - // Start in-memory spannertest instance. - srv, err := spannertest.NewServer("localhost:0") - if err != nil { - return nil, err - } - - return &TestDB{ - DriverName: "spanner", - ConnStr: fmt.Sprintf("%s/projects/grafanatest/instances/grafanatest/databases/grafanatest;usePlainText=true", srv.Addr), - Cleanup: srv.Close, - }, nil - } - - if spannerDB == "emulator" { - host := os.Getenv("SPANNER_EMULATOR_HOST") - if host == "" { - host = "localhost:9010" - } - - // To create instance and database manually, run: - // - // $ curl "localhost:9020/v1/projects/grafanatest/instances" --data '{"instanceId": "'grafanatest'"}' - // $ curl "localhost:9020/v1/projects/grafanatest/instances/grafanatest/databases" --data '{"createStatement": "CREATE DATABASE `grafanatest`"}' - return &TestDB{ - DriverName: "spanner", - ConnStr: fmt.Sprintf("%s/projects/grafanatest/instances/grafanatest/databases/grafanatest;usePlainText=true;inMemSequenceGenerator=true", host), - Cleanup: func() {}, - }, nil - } - - return &TestDB{ - DriverName: "spanner", - ConnStr: spannerDB, - Cleanup: func() {}, - }, nil -} diff --git a/pkg/services/sqlstore/transactions.go b/pkg/services/sqlstore/transactions.go index c93f1516986..8df2cc6c0d0 100644 --- a/pkg/services/sqlstore/transactions.go +++ b/pkg/services/sqlstore/transactions.go @@ -61,7 +61,7 @@ func (ss *SQLStore) inTransactionWithRetryCtx(ctx context.Context, engine *xorm. return err } - // special handling of database locked errors for sqlite and spanner, then we can retry 5 times + // special handling of database locked errors for sqlite, then we can retry 5 times if r, ok := engine.Dialect().(xorm.DialectWithRetryableErrors); ok { if retry < ss.dbCfg.TransactionRetries && r.RetryOnError(err) { if rollErr := sess.Rollback(); rollErr != nil { diff --git a/pkg/services/ssosettings/database/database.go b/pkg/services/ssosettings/database/database.go index 6d9a97c3dc7..2d7097e690c 100644 --- a/pkg/services/ssosettings/database/database.go +++ b/pkg/services/ssosettings/database/database.go @@ -144,7 +144,7 @@ func (s *SSOSettingsStore) Delete(ctx context.Context, provider string) error { existing.Updated = time.Now().UTC() existing.IsDeleted = true - // We must explicitly omit ID column from updates, because some databases (e.g. Spanner) don't allow updating + // We must explicitly omit ID column from updates, because some databases don't allow updating // primary key. Xorm ignores autoincrement columns during updates, but since ID column here is a string, // it's not ignored by default. _, err = sess.ID(existing.ID).Omit(idColumn).MustCols(updatedColumn, isDeletedColumn).Update(existing) diff --git a/pkg/storage/unified/sql/sqltemplate/dialect.go b/pkg/storage/unified/sql/sqltemplate/dialect.go index 0789ab7f214..918545fdb25 100644 --- a/pkg/storage/unified/sql/sqltemplate/dialect.go +++ b/pkg/storage/unified/sql/sqltemplate/dialect.go @@ -24,8 +24,6 @@ func DialectForDriver(driverName string) Dialect { return PostgreSQL case "sqlite", "sqlite3": return SQLite - case "spanner": - return Spanner default: return nil } diff --git a/pkg/storage/unified/sql/sqltemplate/dialect_spanner.go b/pkg/storage/unified/sql/sqltemplate/dialect_spanner.go deleted file mode 100644 index 4fba8375d3a..00000000000 --- a/pkg/storage/unified/sql/sqltemplate/dialect_spanner.go +++ /dev/null @@ -1,32 +0,0 @@ -package sqltemplate - -// Spanner is an implementation of Dialect for the Google Spanner database. -var Spanner = spanner{} - -var _ Dialect = Spanner - -type spanner struct{} - -func (s spanner) DialectName() string { - return "spanner" -} - -func (s spanner) Ident(a string) (string, error) { - return backtickIdent{}.Ident(a) -} - -func (s spanner) ArgPlaceholder(argNum int) string { - return argFmtSQL92.ArgPlaceholder(argNum) -} - -func (s spanner) SelectFor(a ...string) (string, error) { - return rowLockingClauseSpanner.SelectFor(a...) -} - -func (spanner) CurrentEpoch() string { - return "UNIX_MICROS(CURRENT_TIMESTAMP())" -} - -var rowLockingClauseSpanner = rowLockingClauseMap{ - SelectForUpdate: SelectForUpdate, -} diff --git a/pkg/storage/unified/sql/test/benchmark_test.go b/pkg/storage/unified/sql/test/benchmark_test.go index b29975fc61d..d937abb70ab 100644 --- a/pkg/storage/unified/sql/test/benchmark_test.go +++ b/pkg/storage/unified/sql/test/benchmark_test.go @@ -6,6 +6,8 @@ import ( "testing" "time" + "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/services/featuremgmt" @@ -15,7 +17,6 @@ import ( "github.com/grafana/grafana/pkg/storage/unified/sql" "github.com/grafana/grafana/pkg/storage/unified/sql/db/dbimpl" test "github.com/grafana/grafana/pkg/storage/unified/testing" - "github.com/stretchr/testify/require" ) func newTestBackend(b testing.TB) resource.StorageBackend { @@ -40,9 +41,6 @@ func TestIntegrationBenchmarkSQLStorageBackend(t *testing.T) { if testing.Short() { t.Skip("skipping integration test in short mode") } - if db.IsTestDBSpanner() { - t.Skip("Skipping benchmark on Spanner") - } opts := test.DefaultBenchmarkOptions() if db.IsTestDbSQLite() { opts.Concurrency = 1 // to avoid SQLite database is locked error @@ -54,9 +52,6 @@ func TestIntegrationBenchmarkResourceServer(t *testing.T) { if testing.Short() { t.Skip("skipping integration test in short mode") } - if db.IsTestDBSpanner() { - t.Skip("Skipping benchmark on Spanner") - } ctx := context.Background() opts := &test.BenchmarkOptions{ diff --git a/pkg/storage/unified/sql/test/integration_test.go b/pkg/storage/unified/sql/test/integration_test.go index 1110498d485..edddcf8ef1d 100644 --- a/pkg/storage/unified/sql/test/integration_test.go +++ b/pkg/storage/unified/sql/test/integration_test.go @@ -35,9 +35,6 @@ func TestMain(m *testing.M) { } func TestIntegrationStorageServer(t *testing.T) { - if db.IsTestDBSpanner() { - t.Skip("skipping integration test") - } unitest.RunStorageServerTest(t, func(ctx context.Context) resource.StorageBackend { dbstore := db.InitTestDB(t) eDB, err := dbimpl.ProvideResourceDB(dbstore, setting.NewCfg(), nil) @@ -58,10 +55,6 @@ func TestIntegrationStorageServer(t *testing.T) { // TestStorageBackend is a test for the StorageBackend interface. func TestIntegrationSQLStorageBackend(t *testing.T) { - if db.IsTestDBSpanner() { - t.Skip("skipping integration test") - } - t.Run("IsHA (polling notifier)", func(t *testing.T) { unitest.RunStorageBackendTest(t, func(ctx context.Context) resource.StorageBackend { dbstore := db.InitTestDB(t) @@ -105,9 +98,6 @@ func TestIntegrationSearchAndStorage(t *testing.T) { if testing.Short() { t.Skip("skipping integration test in short mode") } - if db.IsTestDBSpanner() { - t.Skip("Skipping benchmark on Spanner") - } ctx := context.Background() @@ -145,9 +135,6 @@ func TestClientServer(t *testing.T) { if db.IsTestDbSQLite() { t.Skip("TODO: test blocking, skipping to unblock Enterprise until we fix this") } - if db.IsTestDBSpanner() { - t.Skip("skipping integration test") - } ctx := testutil.NewTestContext(t, time.Now().Add(5*time.Second)) dbstore := db.InitTestDB(t) diff --git a/pkg/tests/api/alerting/api_testing_test.go b/pkg/tests/api/alerting/api_testing_test.go index ccba1810f65..dd04d3b8301 100644 --- a/pkg/tests/api/alerting/api_testing_test.go +++ b/pkg/tests/api/alerting/api_testing_test.go @@ -33,7 +33,7 @@ const ( ) func TestMain(m *testing.M) { - testsuite.RunButSkipOnSpanner(m) + testsuite.Run(m) } func TestGrafanaRuleConfig(t *testing.T) { diff --git a/pkg/tests/apis/alerting/notifications/receivers/receiver_test.go b/pkg/tests/apis/alerting/notifications/receivers/receiver_test.go index 88ecf6e96bc..a854671376f 100644 --- a/pkg/tests/apis/alerting/notifications/receivers/receiver_test.go +++ b/pkg/tests/apis/alerting/notifications/receivers/receiver_test.go @@ -54,7 +54,7 @@ import ( var testData embed.FS func TestMain(m *testing.M) { - testsuite.RunButSkipOnSpanner(m) + testsuite.Run(m) } func getTestHelper(t *testing.T) *apis.K8sTestHelper { diff --git a/pkg/tests/apis/alerting/notifications/routingtree/routing_tree_test.go b/pkg/tests/apis/alerting/notifications/routingtree/routing_tree_test.go index fb5ac172eb6..56a9ecf2706 100644 --- a/pkg/tests/apis/alerting/notifications/routingtree/routing_tree_test.go +++ b/pkg/tests/apis/alerting/notifications/routingtree/routing_tree_test.go @@ -39,7 +39,7 @@ import ( ) func TestMain(m *testing.M) { - testsuite.RunButSkipOnSpanner(m) + testsuite.Run(m) } func getTestHelper(t *testing.T) *apis.K8sTestHelper { diff --git a/pkg/tests/apis/alerting/notifications/templategroup/templates_group_test.go b/pkg/tests/apis/alerting/notifications/templategroup/templates_group_test.go index e8b932e2cf0..3d1e83cbd4a 100644 --- a/pkg/tests/apis/alerting/notifications/templategroup/templates_group_test.go +++ b/pkg/tests/apis/alerting/notifications/templategroup/templates_group_test.go @@ -32,7 +32,7 @@ import ( ) func TestMain(m *testing.M) { - testsuite.RunButSkipOnSpanner(m) + testsuite.Run(m) } func getTestHelper(t *testing.T) *apis.K8sTestHelper { diff --git a/pkg/tests/apis/alerting/notifications/timeinterval/timeinterval_test.go b/pkg/tests/apis/alerting/notifications/timeinterval/timeinterval_test.go index 4fc752f8db1..8ea61ab6716 100644 --- a/pkg/tests/apis/alerting/notifications/timeinterval/timeinterval_test.go +++ b/pkg/tests/apis/alerting/notifications/timeinterval/timeinterval_test.go @@ -44,7 +44,7 @@ import ( var testData embed.FS func TestMain(m *testing.M) { - testsuite.RunButSkipOnSpanner(m) + testsuite.Run(m) } func getTestHelper(t *testing.T) *apis.K8sTestHelper { diff --git a/pkg/tests/testinfra/testinfra.go b/pkg/tests/testinfra/testinfra.go index bb59b505c1f..0d6510df906 100644 --- a/pkg/tests/testinfra/testinfra.go +++ b/pkg/tests/testinfra/testinfra.go @@ -18,6 +18,7 @@ import ( "gopkg.in/ini.v1" "github.com/grafana/dskit/kv" + "github.com/grafana/grafana/pkg/api" "github.com/grafana/grafana/pkg/extensions" "github.com/grafana/grafana/pkg/infra/db" @@ -508,17 +509,9 @@ func CreateGrafDir(t *testing.T, opts GrafanaOpts) (string, string) { require.NoError(t, err) _, err = dbSection.NewKey("query_retries", fmt.Sprintf("%d", queryRetries)) require.NoError(t, err) - if db.IsTestDBSpanner() { - _, err = dbSection.NewKey("max_open_conn", "20") - } else { - _, err = dbSection.NewKey("max_open_conn", "2") - } + _, err = dbSection.NewKey("max_open_conn", "2") require.NoError(t, err) - if db.IsTestDBSpanner() { - _, err = dbSection.NewKey("max_idle_conn", "20") - } else { - _, err = dbSection.NewKey("max_idle_conn", "2") - } + _, err = dbSection.NewKey("max_idle_conn", "2") require.NoError(t, err) cfgPath := filepath.Join(cfgDir, "test.ini") diff --git a/pkg/tests/testsuite/testsuite.go b/pkg/tests/testsuite/testsuite.go index 80378e2530b..87ce57eaa15 100644 --- a/pkg/tests/testsuite/testsuite.go +++ b/pkg/tests/testsuite/testsuite.go @@ -13,8 +13,3 @@ func Run(m *testing.M) { db.CleanupTestDB() os.Exit(code) } - -func RunButSkipOnSpanner(m *testing.M) { - db.SkipTestsOnSpanner() - Run(m) -} diff --git a/pkg/util/spanner/spanner.go b/pkg/util/spanner/spanner.go deleted file mode 100644 index 4e04f267be2..00000000000 --- a/pkg/util/spanner/spanner.go +++ /dev/null @@ -1,40 +0,0 @@ -// Package spanner should only be used from tests, or from enterprise code (eg. protected by build tags). -package spanner - -import ( - "strconv" - - spannerdriver "github.com/googleapis/go-sql-spanner" - "google.golang.org/api/option" - "google.golang.org/grpc" - "google.golang.org/grpc/credentials/insecure" -) - -func UsePlainText(connectorConfig spannerdriver.ConnectorConfig) bool { - if strval, ok := connectorConfig.Params["useplaintext"]; ok { - if val, err := strconv.ParseBool(strval); err == nil { - return val - } - } - return false -} - -// ConnectorConfigToClientOptions is adapted from https://github.com/googleapis/go-sql-spanner/blob/main/driver.go#L341-L477, from version 1.11.1. -func ConnectorConfigToClientOptions(connectorConfig spannerdriver.ConnectorConfig) []option.ClientOption { - var opts []option.ClientOption - if connectorConfig.Host != "" { - opts = append(opts, option.WithEndpoint(connectorConfig.Host)) - } - if strval, ok := connectorConfig.Params["credentials"]; ok { - opts = append(opts, option.WithCredentialsFile(strval)) - } - if strval, ok := connectorConfig.Params["credentialsjson"]; ok { - opts = append(opts, option.WithCredentialsJSON([]byte(strval))) - } - if UsePlainText(connectorConfig) { - opts = append(opts, - option.WithGRPCDialOption(grpc.WithTransportCredentials(insecure.NewCredentials())), - option.WithoutAuthentication()) - } - return opts -} diff --git a/pkg/util/xorm/dialect_spanner.go b/pkg/util/xorm/dialect_spanner.go deleted file mode 100644 index fbc361ce6b8..00000000000 --- a/pkg/util/xorm/dialect_spanner.go +++ /dev/null @@ -1,399 +0,0 @@ -//go:build enterprise || pro - -package xorm - -import ( - "database/sql" - "fmt" - "strconv" - "strings" - - spannerclient "cloud.google.com/go/spanner" - _ "github.com/googleapis/go-sql-spanner" - spannerdriver "github.com/googleapis/go-sql-spanner" - "github.com/grafana/grafana/pkg/util/xorm/core" - "google.golang.org/grpc/codes" -) - -func init() { - core.RegisterDriver("spanner", &spannerDriver{}) - core.RegisterDialect("spanner", func() core.Dialect { return &spanner{} }) -} - -// https://cloud.google.com/spanner/docs/reference/standard-sql/lexical#reserved_keywords -var spannerReservedKeywords = map[string]struct{}{ - "ALL": {}, - "AND": {}, - "ANY": {}, - "ARRAY": {}, - "AS": {}, - "ASC": {}, - "ASSERT_ROWS_MODIFIED": {}, - "AT": {}, - "BETWEEN": {}, - "BY": {}, - "CASE": {}, - "CAST": {}, - "COLLATE": {}, - "CONTAINS": {}, - "CREATE": {}, - "CROSS": {}, - "CUBE": {}, - "CURRENT": {}, - "DEFAULT": {}, - "DEFINE": {}, - "DESC": {}, - "DISTINCT": {}, - "ELSE": {}, - "END": {}, - "ENUM": {}, - "ESCAPE": {}, - "EXCEPT": {}, - "EXCLUDE": {}, - "EXISTS": {}, - "EXTRACT": {}, - "FALSE": {}, - "FETCH": {}, - "FOLLOWING": {}, - "FOR": {}, - "FROM": {}, - "FULL": {}, - "GROUP": {}, - "GROUPING": {}, - "GROUPS": {}, - "HASH": {}, - "HAVING": {}, - "IF": {}, - "IGNORE": {}, - "IN": {}, - "INNER": {}, - "INTERSECT": {}, - "INTERVAL": {}, - "INTO": {}, - "IS": {}, - "JOIN": {}, - "LATERAL": {}, - "LEFT": {}, - "LIKE": {}, - "LIMIT": {}, - "LOOKUP": {}, - "MERGE": {}, - "NATURAL": {}, - "NEW": {}, - "NO": {}, - "NOT": {}, - "NULL": {}, - "NULLS": {}, - "OF": {}, - "ON": {}, - "OR": {}, - "ORDER": {}, - "OUTER": {}, - "OVER": {}, - "PARTITION": {}, - "PRECEDING": {}, - "PROTO": {}, - "RANGE": {}, - "RECURSIVE": {}, - "RESPECT": {}, - "RIGHT": {}, - "ROLLUP": {}, - "ROWS": {}, - "SELECT": {}, - "SET": {}, - "SOME": {}, - "STRUCT": {}, - "TABLESAMPLE": {}, - "THEN": {}, - "TO": {}, - "TREAT": {}, - "TRUE": {}, - "UNBOUNDED": {}, - "UNION": {}, - "UNNEST": {}, - "USING": {}, - "WHEN": {}, - "WHERE": {}, - "WINDOW": {}, - "WITH": {}, - "WITHIN": {}, -} - -type spannerDriver struct{} - -func (d *spannerDriver) Parse(_driverName, datasourceName string) (*core.Uri, error) { - return &core.Uri{DbType: "spanner", DbName: datasourceName}, nil -} - -type spanner struct { - core.Base -} - -func (s *spanner) Init(db *core.DB, uri *core.Uri, driverName string, datasourceName string) error { - return s.Base.Init(db, s, uri, driverName, datasourceName) -} -func (s *spanner) Filters() []core.Filter { return []core.Filter{&core.IdFilter{}} } -func (s *spanner) IsReserved(name string) bool { - _, exists := spannerReservedKeywords[name] - return exists -} -func (s *spanner) AndStr() string { return "AND" } -func (s *spanner) OrStr() string { return "OR" } -func (s *spanner) EqStr() string { return "=" } -func (s *spanner) RollBackStr() string { return "ROLL BACK" } -func (s *spanner) AutoIncrStr() string { - // Spanner does not support auto-increment, but supports unique generated IDs (not sequential!). - return "GENERATED BY DEFAULT AS IDENTITY (BIT_REVERSED_POSITIVE)" -} -func (s *spanner) SupportInsertMany() bool { return false } // Needs manual transaction batching -func (s *spanner) SupportEngine() bool { return false } // No support for engine selection -func (s *spanner) SupportCharset() bool { return false } // ...or charsets -func (s *spanner) SupportDropIfExists() bool { return false } // Drop should be handled differently -func (s *spanner) IndexOnTable() bool { return false } -func (s *spanner) ShowCreateNull() bool { return false } -func (s *spanner) Quote(name string) string { return "`" + name + "`" } -func (s *spanner) SqlType(col *core.Column) string { - switch col.SQLType.Name { - case core.Int, core.SmallInt, core.BigInt: - return "INT64" - case core.Varchar, core.Text, core.MediumText, core.LongText, core.Char, core.NVarchar, core.NChar, core.NText: - l := col.Length - if l == 0 { - l = col.SQLType.DefaultLength - } - if l > 0 { - return fmt.Sprintf("STRING(%d)", l) - } - return "STRING(MAX)" - case core.Jsonb: - return "STRING(MAX)" - case core.Bool, core.TinyInt: - return "BOOL" - case core.Float, core.Double: - return "FLOAT64" - case core.Bytea, core.Blob, core.MediumBlob, core.LongBlob: - l := col.Length - if l == 0 { - l = col.SQLType.DefaultLength - } - if l > 0 { - return fmt.Sprintf("BYTES(%d)", l) - } - return "BYTES(MAX)" - case core.DateTime, core.TimeStamp: - return "TIMESTAMP" - default: - panic("unknown column type: " + col.SQLType.Name) - //default: - // return "STRING(MAX)" // XXX: more types to add - } -} - -func (s *spanner) GetColumns(tableName string) ([]string, map[string]*core.Column, error) { - query := `SELECT COLUMN_NAME, SPANNER_TYPE, IS_NULLABLE, IS_IDENTITY, IDENTITY_GENERATION, IDENTITY_KIND, COLUMN_DEFAULT - FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = ? AND TABLE_SCHEMA="" ORDER BY ORDINAL_POSITION` - rows, err := s.DB().Query(query, tableName) - if err != nil { - return nil, nil, err - } - defer rows.Close() - - columns := make(map[string]*core.Column) - var colNames []string - - var name, sqlType, isNullable string - var isIdentity, identityGeneration, identityKind, columnDefault sql.NullString - for rows.Next() { - if err := rows.Scan(&name, &sqlType, &isNullable, &isIdentity, &identityGeneration, &identityKind, &columnDefault); err != nil { - return nil, nil, err - } - - var length int - switch { - case sqlType == "INT64": - sqlType = core.Int - case sqlType == "FLOAT32" || sqlType == "FLOAT64": - sqlType = core.Float - case sqlType == "BOOL": - sqlType = core.Bool - case sqlType == "BYTES(MAX)": - sqlType = core.Blob - case sqlType == "STRING(MAX)": - sqlType = core.NVarchar - case sqlType == "TIMESTAMP": - sqlType = core.DateTime - case strings.HasPrefix(sqlType, "BYTES("): - // 6 == len(`BYTES(`), we also remove ")" from the end. - if l, err := strconv.Atoi(sqlType[6 : len(sqlType)-1]); err == nil { - length = l - } - sqlType = core.Blob - case strings.HasPrefix(sqlType, "STRING("): - // 7 == len(`STRING(`), we also remove ")" from the end. - if l, err := strconv.Atoi(sqlType[7 : len(sqlType)-1]); err == nil { - length = l - } - sqlType = core.Varchar - default: - panic("unknown column type: " + sqlType) - } - - autoincrement := isIdentity.Valid && isIdentity.String == "YES" && - identityGeneration.Valid && identityGeneration.String == "BY DEFAULT" && - identityKind.Valid && identityKind.String == "BIT_REVERSED_POSITIVE_SEQUENCE" - - defValue := "" - defEmpty := true - if columnDefault.Valid { - defValue = columnDefault.String - defEmpty = false - } - - col := &core.Column{ - Name: name, - SQLType: core.SQLType{Name: sqlType}, - Length: length, - Nullable: isNullable == "YES", - IsAutoIncrement: autoincrement, - Indexes: map[string]int{}, - Default: defValue, - DefaultIsEmpty: defEmpty, - } - columns[name] = col - colNames = append(colNames, name) - } - - return colNames, columns, rows.Err() -} - -func (s *spanner) CreateTableSql(table *core.Table, tableName, _, charset string) string { - sql := "CREATE TABLE " + s.Quote(tableName) + " (" - - for i, col := range table.Columns() { - if i > 0 { - sql += ", " - } - - sql += s.Quote(col.Name) + " " + s.SqlType(col) - if !col.Nullable { - sql += " NOT NULL" - } - if col.Default != "" { - sql += " DEFAULT (" + col.Default + ")" - } - if col.IsAutoIncrement { - sql += " GENERATED BY DEFAULT AS IDENTITY (BIT_REVERSED_POSITIVE)" - } - } - - sql += ") PRIMARY KEY (" + strings.Join(table.PrimaryKeys, ",") + ")" - return sql -} - -func (s *spanner) CreateIndexSql(tableName string, index *core.Index) string { - sql := "CREATE " - if index.Type == core.UniqueType { - sql += "UNIQUE NULL_FILTERED " - } - sql += "INDEX " + s.Quote(index.XName(tableName)) + " ON " + s.Quote(tableName) + " (" + strings.Join(index.Cols, ", ") + ")" - return sql -} - -func (s *spanner) IndexCheckSql(tableName, indexName string) (string, []any) { - return `SELECT index_name FROM information_schema.indexes - WHERE table_name = ? AND table_schema = "" AND index_name = ?`, - []any{tableName, indexName} -} - -func (s *spanner) TableCheckSql(tableName string) (string, []any) { - return `SELECT table_name FROM information_schema.tables - WHERE table_name = ? AND table_schema = ""`, - []any{tableName} -} - -func (s *spanner) GetTables() ([]*core.Table, error) { - res, err := s.DB().Query(`SELECT table_name FROM information_schema.tables WHERE table_schema = ""`) - if err != nil { - return nil, err - } - defer res.Close() - - tables := []*core.Table{} - for res.Next() { - var name string - if err := res.Scan(&name); err != nil { - return nil, err - } - t := core.NewEmptyTable() - t.Name = name - tables = append(tables, t) - } - return tables, res.Err() -} - -func (s *spanner) GetIndexes(tableName string) (map[string]*core.Index, error) { - res, err := s.DB().Query(`SELECT ix.INDEX_NAME, ix.INDEX_TYPE, ix.IS_UNIQUE, c.COLUMN_NAME - FROM INFORMATION_SCHEMA.INDEXES ix - JOIN INFORMATION_SCHEMA.INDEX_COLUMNS c ON (ix.TABLE_NAME=c.TABLE_NAME AND ix.INDEX_NAME=c.INDEX_NAME) - WHERE ix.TABLE_SCHEMA = "" AND ix.TABLE_NAME=? - ORDER BY ix.INDEX_NAME, c.ORDINAL_POSITION`, tableName) - if err != nil { - return nil, err - } - defer res.Close() - - indexes := map[string]*core.Index{} - var ixName, ixType, colName string - var isUnique bool - for res.Next() { - err := res.Scan(&ixName, &ixType, &isUnique, &colName) - if err != nil { - return nil, err - } - - isRegular := false - if strings.HasPrefix(ixName, "IDX_"+tableName) || strings.HasPrefix(ixName, "UQE_"+tableName) { - ixName = ixName[5+len(tableName):] - isRegular = true - } - - var index *core.Index - var ok bool - if index, ok = indexes[ixName]; !ok { - t := core.IndexType // ixType == "INDEX" && !isUnique - if ixType == "PRIMARY KEY" || isUnique { - t = core.UniqueType - } - - index = &core.Index{} - index.IsRegular = isRegular - index.Type = t - index.Name = ixName - indexes[ixName] = index - } - index.AddColumn(colName) - } - return indexes, res.Err() -} - -func (s *spanner) CreateSequenceGenerator(db *sql.DB) (SequenceGenerator, error) { - dsn := s.DataSourceName() - connectorConfig, err := spannerdriver.ExtractConnectorConfig(dsn) - if err != nil { - return nil, err - } - - if connectorConfig.Params[strings.ToLower("inMemSequenceGenerator")] == "true" { - // Switch to using in-memory sequence number generator. - // Using database-based sequence generator doesn't work with emulator, as emulator - // only supports single transaction. If there is already another transaction started - // generating new ID via database-based sequence generator would always fail. - return newInMemSequenceGenerator(), nil - } - - return newSequenceGenerator(db), nil -} - -func (s *spanner) RetryOnError(err error) bool { - return err != nil && spannerclient.ErrCode(spannerclient.ToSpannerError(err)) == codes.Aborted -} diff --git a/pkg/util/xorm/session_insert.go b/pkg/util/xorm/session_insert.go index d411f3d15ef..0b2b79e9f21 100644 --- a/pkg/util/xorm/session_insert.go +++ b/pkg/util/xorm/session_insert.go @@ -15,8 +15,9 @@ import ( "sync" "time" - "github.com/grafana/grafana/pkg/util/xorm/core" "xorm.io/builder" + + "github.com/grafana/grafana/pkg/util/xorm/core" ) // ErrNoElementsOnSlice represents an error there is no element when insert @@ -422,10 +423,6 @@ func (session *Session) innerInsert(bean any) (int64, error) { buf.WriteString(" RETURNING " + session.engine.Quote(table.AutoIncrement)) } - if len(table.AutoIncrement) > 0 && session.engine.dialect.DBType() == "spanner" { - buf.WriteString(" THEN RETURN " + session.engine.Quote(table.AutoIncrement)) - } - sqlStr := buf.String() args = buf.Args() diff --git a/pkg/util/xorm/xorm.go b/pkg/util/xorm/xorm.go index bf2e4bc7ea5..da15e50762d 100644 --- a/pkg/util/xorm/xorm.go +++ b/pkg/util/xorm/xorm.go @@ -24,8 +24,6 @@ import ( const ( // Version show the xorm's version Version string = "0.8.0.1015" - - Spanner = "spanner" ) func regDrvsNDialects() bool { @@ -102,12 +100,6 @@ func NewEngine(driverName string, dataSourceName string) (*Engine, error) { switch uri.DbType { case core.SQLITE: engine.DatabaseTZ = time.UTC - case Spanner: - engine.DatabaseTZ = time.UTC - // We need to specify "Z" to indicate that timestamp is in UTC. - // Otherwise Spanner uses default America/Los_Angeles timezone. - // https://cloud.google.com/spanner/docs/reference/standard-sql/data-types#time_zones - engine.timestampFormat = "2006-01-02 15:04:05Z" default: engine.DatabaseTZ = time.Local } diff --git a/pkg/util/xorm/xorm_spanner_test.go b/pkg/util/xorm/xorm_spanner_test.go deleted file mode 100644 index a9c43a0b77d..00000000000 --- a/pkg/util/xorm/xorm_spanner_test.go +++ /dev/null @@ -1,29 +0,0 @@ -//go:build enterprise || pro - -package xorm - -import ( - "fmt" - "testing" - - "cloud.google.com/go/spanner/spannertest" - _ "github.com/mattn/go-sqlite3" - "github.com/stretchr/testify/require" -) - -func TestBasicOperationsWithSpanner(t *testing.T) { - span, err := spannertest.NewServer("localhost:0") - require.NoError(t, err) - defer span.Close() - - eng, err := NewEngine("spanner", fmt.Sprintf("%s/projects/test/instances/test/databases/test;usePlainText=true", span.Addr)) - require.NoError(t, err) - require.NotNil(t, eng) - require.Equal(t, "spanner", eng.DriverName()) - - _, err = eng.Exec("CREATE TABLE test_struct (id int64, comment string(max), json string(max)) primary key (id)") - require.NoError(t, err) - - // Currently broken because simple INSERT into spannertest doesn't work: https://github.com/googleapis/go-sql-spanner/issues/392 - // testBasicOperations(t, eng) -} From 667e1080c2faf0badae2e3c09da6de85544fb0c4 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 23 May 2025 09:36:20 +0000 Subject: [PATCH 52/73] Update dependency ml-regression-polynomial to v3.0.2 (#105923) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- yarn.lock | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/yarn.lock b/yarn.lock index 5c79a2842dc..bbd5f5c0198 100644 --- a/yarn.lock +++ b/yarn.lock @@ -22407,13 +22407,13 @@ __metadata: languageName: node linkType: hard -"ml-matrix@npm:^6.11.0": - version: 6.11.0 - resolution: "ml-matrix@npm:6.11.0" +"ml-matrix@npm:^6.12.0": + version: 6.12.1 + resolution: "ml-matrix@npm:6.12.1" dependencies: is-any-array: "npm:^2.0.1" ml-array-rescale: "npm:^1.3.7" - checksum: 10/0fe34b43ae23a012ee616435766fafad2c35c69f1f5195b798975b89b279c940a87171d05906af2226c40d908310289dda7d6135a0d12a1a3d719a5d6e482e67 + checksum: 10/b59a6f0fc2657b2ff2718327f1cf49db7f87a0370528e0f92cecb79c716d9887d0e5996d1228d66e3881927c7aa26ee8e3bd1035dc0c9efe0b3b98506f02618d languageName: node linkType: hard @@ -22428,13 +22428,12 @@ __metadata: linkType: hard "ml-regression-polynomial@npm:^3.0.0": - version: 3.0.1 - resolution: "ml-regression-polynomial@npm:3.0.1" + version: 3.0.2 + resolution: "ml-regression-polynomial@npm:3.0.2" dependencies: - cheminfo-types: "npm:^1.7.3" - ml-matrix: "npm:^6.11.0" + ml-matrix: "npm:^6.12.0" ml-regression-base: "npm:^4.0.0" - checksum: 10/ae0dd3a05458af2a75ea24bcd9e0c1201650eca8a2bee699d9a432a6ba89074b0961496b7b6ccc32c57e8913524620fda6383dfe4f0c9735844cf2d98bd21d90 + checksum: 10/a36eedb96882465f10cc0c56ad57284195c35ee3404a29d4761781499c53ebbdc5efcc4769b17f27dbe5629b049ae581fa64bff473119bc9e8a787e384fb66b2 languageName: node linkType: hard From 6ffccf825fe4999485058deac7838b4c07bb1bee Mon Sep 17 00:00:00 2001 From: Alex Khomenko Date: Fri, 23 May 2025 12:44:23 +0300 Subject: [PATCH 53/73] Provisioning: Fix save form for readOnly repository (#105849) * Provisioning: Fix save form for readOnly repository * betterer --- .betterer.results | 9 ------- .../SaveProvisionedDashboardForm.test.tsx | 19 +++++++++++++-- .../SaveProvisionedDashboardForm.tsx | 24 ++++++++++++------- 3 files changed, 33 insertions(+), 19 deletions(-) diff --git a/.betterer.results b/.betterer.results index 9c867b86097..a52202f2626 100644 --- a/.betterer.results +++ b/.betterer.results @@ -1641,15 +1641,6 @@ exports[`better eslint`] = { [0, 0, 0, "Do not use any type assertions.", "5"], [0, 0, 0, "Unexpected any. Specify a different type.", "6"] ], - "public/app/features/dashboard-scene/saving/provisioned/SaveProvisionedDashboardForm.tsx:5381": [ - [0, 0, 0, "Add noMargin prop to Field components to remove built-in margins. Use layout components like Stack or Grid with the gap prop instead for consistent spacing.", "0"], - [0, 0, 0, "Add noMargin prop to Field components to remove built-in margins. Use layout components like Stack or Grid with the gap prop instead for consistent spacing.", "1"], - [0, 0, 0, "Add noMargin prop to Field components to remove built-in margins. Use layout components like Stack or Grid with the gap prop instead for consistent spacing.", "2"], - [0, 0, 0, "Add noMargin prop to Field components to remove built-in margins. Use layout components like Stack or Grid with the gap prop instead for consistent spacing.", "3"], - [0, 0, 0, "Add noMargin prop to Field components to remove built-in margins. Use layout components like Stack or Grid with the gap prop instead for consistent spacing.", "4"], - [0, 0, 0, "Add noMargin prop to Field components to remove built-in margins. Use layout components like Stack or Grid with the gap prop instead for consistent spacing.", "5"], - [0, 0, 0, "Add noMargin prop to Field components to remove built-in margins. Use layout components like Stack or Grid with the gap prop instead for consistent spacing.", "6"] - ], "public/app/features/dashboard-scene/scene/PanelMenuBehavior.tsx:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"] ], diff --git a/public/app/features/dashboard-scene/saving/provisioned/SaveProvisionedDashboardForm.test.tsx b/public/app/features/dashboard-scene/saving/provisioned/SaveProvisionedDashboardForm.test.tsx index 12540d371c9..eec8d44b8d8 100644 --- a/public/app/features/dashboard-scene/saving/provisioned/SaveProvisionedDashboardForm.test.tsx +++ b/public/app/features/dashboard-scene/saving/provisioned/SaveProvisionedDashboardForm.test.tsx @@ -390,8 +390,9 @@ describe('SaveProvisionedDashboardForm', () => { expect(screen.getByRole('button', { name: /save/i })).toBeDisabled(); }); - it('should show read-only alert when repository has no workflows', () => { + it('should properly handle read-only state for a repository without workflows', () => { setup({ + isNew: false, repository: { name: 'repo-abc', type: 'github', @@ -401,6 +402,20 @@ describe('SaveProvisionedDashboardForm', () => { }, }); - expect(screen.getByText('This repository is read only')).toBeInTheDocument(); + // Alert is shown + expect(screen.getByRole('alert', { name: 'This repository is read only' })).toBeInTheDocument(); + + // Save button is disabled + const saveButton = screen.getByRole('button', { name: /save/i }); + expect(saveButton).toBeDisabled(); + + // Common options are not shown for existing dashboards + expect(screen.queryByTestId('common-options')).not.toBeInTheDocument(); + + // Workflow options are not shown + expect(screen.queryByRole('radiogroup')).not.toBeInTheDocument(); + + // Branch field is not shown + expect(screen.queryByRole('textbox', { name: /branch/i })).not.toBeInTheDocument(); }); }); diff --git a/public/app/features/dashboard-scene/saving/provisioned/SaveProvisionedDashboardForm.tsx b/public/app/features/dashboard-scene/saving/provisioned/SaveProvisionedDashboardForm.tsx index 907a8031ef2..a177964f7e2 100644 --- a/public/app/features/dashboard-scene/saving/provisioned/SaveProvisionedDashboardForm.tsx +++ b/public/app/features/dashboard-scene/saving/provisioned/SaveProvisionedDashboardForm.tsx @@ -159,11 +159,11 @@ export function SaveProvisionedDashboardForm({ }; const workflowOptions = getWorkflowOptions(repository, loadedFromRef); - + const readOnly = !repository?.workflows?.length; return (
- {!repository?.workflows?.length && ( + {readOnly && ( - + )} - {!isNew && } + {!isNew && !readOnly && } - +