From 22a6dc6b52c40bbf0fff60abe3b20585622f473e Mon Sep 17 00:00:00 2001 From: Adela Almasan <88068998+adela-almasan@users.noreply.github.com> Date: Wed, 26 Feb 2025 20:36:09 -0600 Subject: [PATCH 01/32] Canvas: Fix no series timestamp (#101390) --- public/app/plugins/panel/canvas/components/CanvasTooltip.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/app/plugins/panel/canvas/components/CanvasTooltip.tsx b/public/app/plugins/panel/canvas/components/CanvasTooltip.tsx index f2a5959edc6..6234a4a7693 100644 --- a/public/app/plugins/panel/canvas/components/CanvasTooltip.tsx +++ b/public/app/plugins/panel/canvas/components/CanvasTooltip.tsx @@ -49,7 +49,7 @@ export const CanvasTooltip = ({ scene }: Props) => { } // Retrieve timestamp of the last data point if available - const timeField = scene.data?.series[0].fields?.find((field) => field.type === FieldType.time); + const timeField = scene.data?.series[0]?.fields?.find((field) => field.type === FieldType.time); const lastTimeValue = timeField?.values[timeField.values.length - 1]; const shouldDisplayTimeContentItem = timeField && lastTimeValue && element.data.field && getFieldDisplayName(timeField) !== element.data.field; From e8e79e9c79b5eacd88c2e6cb7461e3a84ce22507 Mon Sep 17 00:00:00 2001 From: Hugo Kiyodi Oshiro Date: Thu, 27 Feb 2025 09:16:00 +0100 Subject: [PATCH 02/32] Plugins: Fix version tab breaking for non semantic version (#101225) --- .../components/VersionInstallButton.test.tsx | 40 ++++++++++++ .../admin/components/VersionInstallButton.tsx | 61 +++++++++++-------- 2 files changed, 77 insertions(+), 24 deletions(-) diff --git a/public/app/features/plugins/admin/components/VersionInstallButton.test.tsx b/public/app/features/plugins/admin/components/VersionInstallButton.test.tsx index 38511a2dfea..16ec1d3ca70 100644 --- a/public/app/features/plugins/admin/components/VersionInstallButton.test.tsx +++ b/public/app/features/plugins/admin/components/VersionInstallButton.test.tsx @@ -177,6 +177,46 @@ describe('VersionInstallButton', () => { ); expect(screen.getByText('Downgrade')).not.toBeVisible(); }); + + it('should show the installation button if invalid semver version is provided', () => { + const version: Version = { + version: '1.0.a', + createdAt: '', + isCompatible: false, + grafanaDependency: null, + }; + const installedVersion = '1.0.1'; + renderWithStore( + {}} + /> + ); + expect(screen.getByText('Install')).toBeInTheDocument(); + }); + + it('should show the installation button if invalid semver installed version is provided', () => { + const version: Version = { + version: '1.0.0', + createdAt: '', + isCompatible: false, + grafanaDependency: null, + }; + const installedVersion = '1.0.a'; + renderWithStore( + {}} + /> + ); + expect(screen.getByText('Install')).toBeInTheDocument(); + }); }); function renderWithStore(component: JSX.Element) { diff --git a/public/app/features/plugins/admin/components/VersionInstallButton.tsx b/public/app/features/plugins/admin/components/VersionInstallButton.tsx index 1da8bfe01f7..f0bec011875 100644 --- a/public/app/features/plugins/admin/components/VersionInstallButton.tsx +++ b/public/app/features/plugins/admin/components/VersionInstallButton.tsx @@ -1,6 +1,6 @@ import { css } from '@emotion/css'; import { useEffect, useState } from 'react'; -import { gt } from 'semver'; +import { gt, valid } from 'semver'; import { GrafanaTheme2 } from '@grafana/data'; import { config, reportInteraction } from '@grafana/runtime'; @@ -14,6 +14,12 @@ import { Version } from '../types'; const PLUGINS_VERSION_PAGE_UPGRADE_INTERACTION_EVENT_NAME = 'plugins_upgrade_clicked'; const PLUGINS_VERSION_PAGE_CHANGE_INTERACTION_EVENT_NAME = 'plugins_downgrade_clicked'; +enum InstallState { + INSTALL = 'Install', + UPGRADE = 'Upgrade', + DOWNGRADE = 'Downgrade', +} + interface Props { pluginId: string; version: Version; @@ -38,7 +44,7 @@ export const VersionInstallButton = ({ const [isModalOpen, setIsModalOpen] = useState(false); const styles = useStyles2(getStyles); - const isDowngrade = installedVersion && gt(installedVersion, version.version); + const installState = getInstallState(installedVersion, version.version); useEffect(() => { if (installedVersion === version.version) { @@ -61,7 +67,7 @@ export const VersionInstallButton = ({ schema_version: '1.0.0', }; - if (!installedVersion || gt(version.version, installedVersion)) { + if (installState === InstallState.UPGRADE) { reportInteraction(PLUGINS_VERSION_PAGE_UPGRADE_INTERACTION_EVENT_NAME, trackProps); } else { reportInteraction(PLUGINS_VERSION_PAGE_CHANGE_INTERACTION_EVENT_NAME, { @@ -76,7 +82,7 @@ export const VersionInstallButton = ({ }; const onInstallClick = () => { - if (isDowngrade) { + if (installState === InstallState.DOWNGRADE) { setIsModalOpen(true); } else { performInstallation(); @@ -91,24 +97,9 @@ export const VersionInstallButton = ({ setIsModalOpen(false); }; - let label = 'Downgrade'; - let hidden = false; const isPreinstalled = isPreinstalledPlugin(pluginId); - if (!installedVersion) { - label = 'Install'; - } else if (gt(version.version, installedVersion)) { - label = 'Upgrade'; - if (isPreinstalled.withVersion) { - // Hide button if the plugin is preinstalled with a specific version - hidden = true; - } - } else { - if (isPreinstalled.found && Boolean(config.featureToggles.preinstallAutoUpdate)) { - // Hide the downgrade button if the plugin is preinstalled since it will be auto-updated - hidden = true; - } - } + const hidden = getButtonHiddenState(installState, isPreinstalled); return ( <> @@ -124,7 +115,7 @@ export const VersionInstallButton = ({ tooltip={tooltip} tooltipPlacement="bottom-start" > - {label} {isInstalling ? : getIcon(label)} + {installState} {isInstalling ? : getIcon(installState)} ; } - if (label === 'Upgrade') { + if (installState === InstallState.UPGRADE) { return ; } return ''; } +function getInstallState(installedVersion?: string, version?: string): InstallState { + if (!installedVersion || !version || !valid(installedVersion) || !valid(version)) { + return InstallState.INSTALL; + } + return gt(installedVersion, version) ? InstallState.DOWNGRADE : InstallState.UPGRADE; +} + +function getButtonHiddenState(installState: InstallState, isPreinstalled: { found: boolean; withVersion: boolean }) { + // Default state for initial install + if (installState === InstallState.INSTALL) { + return false; + } + + // Handle downgrade case + if (installState === InstallState.DOWNGRADE) { + return isPreinstalled.found && Boolean(config.featureToggles.preinstallAutoUpdate); + } + + // Handle upgrade case + return isPreinstalled.withVersion; +} + const getStyles = (theme: GrafanaTheme2) => ({ spinner: css({ marginLeft: theme.spacing(1), From 9ad01fda649aa508589a588f4acaa5044174e582 Mon Sep 17 00:00:00 2001 From: "grafana-pr-automation[bot]" <140550294+grafana-pr-automation[bot]@users.noreply.github.com> Date: Thu, 27 Feb 2025 11:59:44 +0200 Subject: [PATCH 03/32] I18n: Download translations from Crowdin (#101387) New Crowdin translations by GitHub Action Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- public/locales/de-DE/grafana.json | 112 ++++++++++++++++++++++++++-- public/locales/es-ES/grafana.json | 112 ++++++++++++++++++++++++++-- public/locales/fr-FR/grafana.json | 112 ++++++++++++++++++++++++++-- public/locales/pt-BR/grafana.json | 112 ++++++++++++++++++++++++++-- public/locales/zh-Hans/grafana.json | 112 ++++++++++++++++++++++++++-- 5 files changed, 535 insertions(+), 25 deletions(-) diff --git a/public/locales/de-DE/grafana.json b/public/locales/de-DE/grafana.json index 1222cec2022..355610c995d 100644 --- a/public/locales/de-DE/grafana.json +++ b/public/locales/de-DE/grafana.json @@ -295,6 +295,9 @@ "export-all": "", "loading": "", "search-by-matchers": "", + "titles": { + "notification-templates": "" + }, "view": "" }, "contact-points": { @@ -404,6 +407,20 @@ "title": "", "uninitialized": "" }, + "notification-templates": { + "duplicate": { + "subTitle": "", + "title": "" + }, + "edit": { + "subTitle": "", + "title": "" + }, + "new": { + "subTitle": "", + "title": "" + } + }, "policies": { "default-policy": { "description": "", @@ -592,6 +609,7 @@ } }, "rule-viewer": { + "error-loading": "", "prometheus-consistency-check": { "alert-message": "", "alert-title": "" @@ -1688,10 +1706,19 @@ "one-click-description": "" } }, + "alert": { + "close-button": "" + }, "auto-save-field": { "saved": "", "saving": "" }, + "card": { + "option": "" + }, + "cascader": { + "clear-button": "" + }, "color-picker-popover": { "palette-tab": "", "spectrum-tab": "" @@ -1699,8 +1726,15 @@ "confirm-button": { "cancel": "" }, + "confirm-content": { + "placeholder": "" + }, "data-link-editor": { - "info": "" + "info": "", + "new-tab-label": "", + "title-label": "", + "title-placeholder": "", + "url-label": "" }, "data-link-editor-modal": { "cancel": "", @@ -1720,32 +1754,77 @@ "tooltip-remove": "", "url-not-provided": "" }, + "data-source-basic-auth-settings": { + "user-label": "", + "user-placeholder": "" + }, + "data-source-http-proxy-settings": { + "oauth-identity-label": "", + "oauth-identity-tooltip": "", + "skip-tls-verify-label": "", + "ts-client-auth-label": "", + "with-ca-cert-label": "", + "with-ca-cert-tooltip": "" + }, "data-source-http-settings": { "access-help": "", "access-help-details": "", + "access-label": "", + "access-options-browser": "", + "access-options-proxy": "", "allowed-cookies": "", + "allowed-cookies-tooltip": "", "auth": "", + "azure-auth-label": "", + "azure-auth-tooltip": "", "basic-auth": "", + "basic-auth-label": "", "browser-mode-description": "", "browser-mode-title": "", + "default-url-access-select": "", "default-url-tooltip": "", "direct-url-tooltip": "", "heading": "", "proxy-url-tooltip": "", "server-mode-description": "", - "server-mode-title": "" + "server-mode-title": "", + "timeout-form-label": "", + "timeout-label": "", + "timeout-tooltip": "", + "url-label": "", + "with-credential-label": "", + "with-credential-tooltip": "" }, "data-source-settings": { "alerting-settings-heading": "", + "alerting-settings-label": "", + "alerting-settings-tooltip": "", "cert-key-reset": "", "custom-headers-add": "", + "custom-headers-header": "", + "custom-headers-header-placeholder": "", + "custom-headers-header-remove": "", + "custom-headers-header-value": "", "custom-headers-title": "", "secure-socks-heading": "", - "tls-heading": "" + "secure-socks-label": "", + "secure-socks-tooltip": "", + "tls-certification-label": "", + "tls-certification-placeholder": "", + "tls-client-certification-label": "", + "tls-client-key-label": "", + "tls-client-key-placeholder": "", + "tls-heading": "", + "tls-server-name-label": "", + "tls-tooltip": "" }, "date-time-picker": { "apply": "", - "cancel": "" + "calendar-icon-label": "", + "cancel": "", + "next-label": "", + "previous-label": "", + "select-placeholder": "" }, "drawer": { "close": "Schließen" @@ -1769,6 +1848,10 @@ "modal": { "close-tooltip": "Schließen" }, + "named-colors-palette": { + "text-color-swatch": "", + "transparent-swatch": "" + }, "secret-form-field": { "reset": "" }, @@ -1782,6 +1865,9 @@ "no-options-label": "Keine Optionen gefunden", "placeholder": "Auswählen" }, + "series-color-picker-popover": { + "y-axis-usage": "" + }, "spinner": { "aria-label": "" }, @@ -1801,6 +1887,9 @@ "user-icon": { "active-text": "" }, + "value-pill": { + "remove-button": "" + }, "viz-legend": { "right-axis-indicator": "" }, @@ -3637,6 +3726,16 @@ "title": "" } }, + "theme-preview": { + "breadcrumbs": { + "dashboards": "", + "home": "" + }, + "panel": { + "form-label": "", + "title": "" + } + }, "time-picker": { "absolute": { "recent-title": "Kürzlich verwendete absolute Bereiche", @@ -3693,10 +3792,13 @@ "example": "", "example-details": "", "example-title": "Zeitbereiche-Beispiel", + "from-label": "", "from-to": "", "more-info": "", "specify": "Zeitbereich festlegen <1>", - "supported-formats": "" + "submit-button-label": "", + "supported-formats": "", + "to-label": "" }, "zone": { "select-aria-label": "Zeitzonen-Auswähler", diff --git a/public/locales/es-ES/grafana.json b/public/locales/es-ES/grafana.json index e6d9efb5f9b..1fa664da890 100644 --- a/public/locales/es-ES/grafana.json +++ b/public/locales/es-ES/grafana.json @@ -295,6 +295,9 @@ "export-all": "", "loading": "", "search-by-matchers": "", + "titles": { + "notification-templates": "" + }, "view": "" }, "contact-points": { @@ -404,6 +407,20 @@ "title": "", "uninitialized": "" }, + "notification-templates": { + "duplicate": { + "subTitle": "", + "title": "" + }, + "edit": { + "subTitle": "", + "title": "" + }, + "new": { + "subTitle": "", + "title": "" + } + }, "policies": { "default-policy": { "description": "", @@ -592,6 +609,7 @@ } }, "rule-viewer": { + "error-loading": "", "prometheus-consistency-check": { "alert-message": "", "alert-title": "" @@ -1688,10 +1706,19 @@ "one-click-description": "" } }, + "alert": { + "close-button": "" + }, "auto-save-field": { "saved": "", "saving": "" }, + "card": { + "option": "" + }, + "cascader": { + "clear-button": "" + }, "color-picker-popover": { "palette-tab": "", "spectrum-tab": "" @@ -1699,8 +1726,15 @@ "confirm-button": { "cancel": "" }, + "confirm-content": { + "placeholder": "" + }, "data-link-editor": { - "info": "" + "info": "", + "new-tab-label": "", + "title-label": "", + "title-placeholder": "", + "url-label": "" }, "data-link-editor-modal": { "cancel": "", @@ -1720,32 +1754,77 @@ "tooltip-remove": "", "url-not-provided": "" }, + "data-source-basic-auth-settings": { + "user-label": "", + "user-placeholder": "" + }, + "data-source-http-proxy-settings": { + "oauth-identity-label": "", + "oauth-identity-tooltip": "", + "skip-tls-verify-label": "", + "ts-client-auth-label": "", + "with-ca-cert-label": "", + "with-ca-cert-tooltip": "" + }, "data-source-http-settings": { "access-help": "", "access-help-details": "", + "access-label": "", + "access-options-browser": "", + "access-options-proxy": "", "allowed-cookies": "", + "allowed-cookies-tooltip": "", "auth": "", + "azure-auth-label": "", + "azure-auth-tooltip": "", "basic-auth": "", + "basic-auth-label": "", "browser-mode-description": "", "browser-mode-title": "", + "default-url-access-select": "", "default-url-tooltip": "", "direct-url-tooltip": "", "heading": "", "proxy-url-tooltip": "", "server-mode-description": "", - "server-mode-title": "" + "server-mode-title": "", + "timeout-form-label": "", + "timeout-label": "", + "timeout-tooltip": "", + "url-label": "", + "with-credential-label": "", + "with-credential-tooltip": "" }, "data-source-settings": { "alerting-settings-heading": "", + "alerting-settings-label": "", + "alerting-settings-tooltip": "", "cert-key-reset": "", "custom-headers-add": "", + "custom-headers-header": "", + "custom-headers-header-placeholder": "", + "custom-headers-header-remove": "", + "custom-headers-header-value": "", "custom-headers-title": "", "secure-socks-heading": "", - "tls-heading": "" + "secure-socks-label": "", + "secure-socks-tooltip": "", + "tls-certification-label": "", + "tls-certification-placeholder": "", + "tls-client-certification-label": "", + "tls-client-key-label": "", + "tls-client-key-placeholder": "", + "tls-heading": "", + "tls-server-name-label": "", + "tls-tooltip": "" }, "date-time-picker": { "apply": "", - "cancel": "" + "calendar-icon-label": "", + "cancel": "", + "next-label": "", + "previous-label": "", + "select-placeholder": "" }, "drawer": { "close": "Cerrar" @@ -1769,6 +1848,10 @@ "modal": { "close-tooltip": "Cerrar" }, + "named-colors-palette": { + "text-color-swatch": "", + "transparent-swatch": "" + }, "secret-form-field": { "reset": "" }, @@ -1782,6 +1865,9 @@ "no-options-label": "No se ha encontrado ninguna opción", "placeholder": "Elegir" }, + "series-color-picker-popover": { + "y-axis-usage": "" + }, "spinner": { "aria-label": "" }, @@ -1801,6 +1887,9 @@ "user-icon": { "active-text": "" }, + "value-pill": { + "remove-button": "" + }, "viz-legend": { "right-axis-indicator": "" }, @@ -3637,6 +3726,16 @@ "title": "" } }, + "theme-preview": { + "breadcrumbs": { + "dashboards": "", + "home": "" + }, + "panel": { + "form-label": "", + "title": "" + } + }, "time-picker": { "absolute": { "recent-title": "Intervalos absolutos utilizados recientemente", @@ -3693,10 +3792,13 @@ "example": "", "example-details": "", "example-title": "Ejemplos de intervalos de tiempo", + "from-label": "", "from-to": "", "more-info": "", "specify": "Especificar el intervalo de tiempo <1>", - "supported-formats": "" + "submit-button-label": "", + "supported-formats": "", + "to-label": "" }, "zone": { "select-aria-label": "Selector de huso horario", diff --git a/public/locales/fr-FR/grafana.json b/public/locales/fr-FR/grafana.json index f6ab1a8be74..4def0fc0e73 100644 --- a/public/locales/fr-FR/grafana.json +++ b/public/locales/fr-FR/grafana.json @@ -295,6 +295,9 @@ "export-all": "", "loading": "", "search-by-matchers": "", + "titles": { + "notification-templates": "" + }, "view": "" }, "contact-points": { @@ -404,6 +407,20 @@ "title": "", "uninitialized": "" }, + "notification-templates": { + "duplicate": { + "subTitle": "", + "title": "" + }, + "edit": { + "subTitle": "", + "title": "" + }, + "new": { + "subTitle": "", + "title": "" + } + }, "policies": { "default-policy": { "description": "", @@ -592,6 +609,7 @@ } }, "rule-viewer": { + "error-loading": "", "prometheus-consistency-check": { "alert-message": "", "alert-title": "" @@ -1688,10 +1706,19 @@ "one-click-description": "" } }, + "alert": { + "close-button": "" + }, "auto-save-field": { "saved": "", "saving": "" }, + "card": { + "option": "" + }, + "cascader": { + "clear-button": "" + }, "color-picker-popover": { "palette-tab": "", "spectrum-tab": "" @@ -1699,8 +1726,15 @@ "confirm-button": { "cancel": "" }, + "confirm-content": { + "placeholder": "" + }, "data-link-editor": { - "info": "" + "info": "", + "new-tab-label": "", + "title-label": "", + "title-placeholder": "", + "url-label": "" }, "data-link-editor-modal": { "cancel": "", @@ -1720,32 +1754,77 @@ "tooltip-remove": "", "url-not-provided": "" }, + "data-source-basic-auth-settings": { + "user-label": "", + "user-placeholder": "" + }, + "data-source-http-proxy-settings": { + "oauth-identity-label": "", + "oauth-identity-tooltip": "", + "skip-tls-verify-label": "", + "ts-client-auth-label": "", + "with-ca-cert-label": "", + "with-ca-cert-tooltip": "" + }, "data-source-http-settings": { "access-help": "", "access-help-details": "", + "access-label": "", + "access-options-browser": "", + "access-options-proxy": "", "allowed-cookies": "", + "allowed-cookies-tooltip": "", "auth": "", + "azure-auth-label": "", + "azure-auth-tooltip": "", "basic-auth": "", + "basic-auth-label": "", "browser-mode-description": "", "browser-mode-title": "", + "default-url-access-select": "", "default-url-tooltip": "", "direct-url-tooltip": "", "heading": "", "proxy-url-tooltip": "", "server-mode-description": "", - "server-mode-title": "" + "server-mode-title": "", + "timeout-form-label": "", + "timeout-label": "", + "timeout-tooltip": "", + "url-label": "", + "with-credential-label": "", + "with-credential-tooltip": "" }, "data-source-settings": { "alerting-settings-heading": "", + "alerting-settings-label": "", + "alerting-settings-tooltip": "", "cert-key-reset": "", "custom-headers-add": "", + "custom-headers-header": "", + "custom-headers-header-placeholder": "", + "custom-headers-header-remove": "", + "custom-headers-header-value": "", "custom-headers-title": "", "secure-socks-heading": "", - "tls-heading": "" + "secure-socks-label": "", + "secure-socks-tooltip": "", + "tls-certification-label": "", + "tls-certification-placeholder": "", + "tls-client-certification-label": "", + "tls-client-key-label": "", + "tls-client-key-placeholder": "", + "tls-heading": "", + "tls-server-name-label": "", + "tls-tooltip": "" }, "date-time-picker": { "apply": "", - "cancel": "" + "calendar-icon-label": "", + "cancel": "", + "next-label": "", + "previous-label": "", + "select-placeholder": "" }, "drawer": { "close": "Fermer" @@ -1769,6 +1848,10 @@ "modal": { "close-tooltip": "Fermer" }, + "named-colors-palette": { + "text-color-swatch": "", + "transparent-swatch": "" + }, "secret-form-field": { "reset": "" }, @@ -1782,6 +1865,9 @@ "no-options-label": "Aucune option trouvée", "placeholder": "Choisir" }, + "series-color-picker-popover": { + "y-axis-usage": "" + }, "spinner": { "aria-label": "" }, @@ -1801,6 +1887,9 @@ "user-icon": { "active-text": "" }, + "value-pill": { + "remove-button": "" + }, "viz-legend": { "right-axis-indicator": "" }, @@ -3637,6 +3726,16 @@ "title": "" } }, + "theme-preview": { + "breadcrumbs": { + "dashboards": "", + "home": "" + }, + "panel": { + "form-label": "", + "title": "" + } + }, "time-picker": { "absolute": { "recent-title": "Périodes absolues récemment utilisées", @@ -3693,10 +3792,13 @@ "example": "", "example-details": "", "example-title": "Exemple de plages de temps", + "from-label": "", "from-to": "", "more-info": "", "specify": "Spécifiez la plage de temps <1>", - "supported-formats": "" + "submit-button-label": "", + "supported-formats": "", + "to-label": "" }, "zone": { "select-aria-label": "Outil de sélection du fuseau horaire", diff --git a/public/locales/pt-BR/grafana.json b/public/locales/pt-BR/grafana.json index c10c3c0eaa4..6e0f3ba0687 100644 --- a/public/locales/pt-BR/grafana.json +++ b/public/locales/pt-BR/grafana.json @@ -295,6 +295,9 @@ "export-all": "", "loading": "", "search-by-matchers": "", + "titles": { + "notification-templates": "" + }, "view": "" }, "contact-points": { @@ -404,6 +407,20 @@ "title": "", "uninitialized": "" }, + "notification-templates": { + "duplicate": { + "subTitle": "", + "title": "" + }, + "edit": { + "subTitle": "", + "title": "" + }, + "new": { + "subTitle": "", + "title": "" + } + }, "policies": { "default-policy": { "description": "", @@ -592,6 +609,7 @@ } }, "rule-viewer": { + "error-loading": "", "prometheus-consistency-check": { "alert-message": "", "alert-title": "" @@ -1688,10 +1706,19 @@ "one-click-description": "" } }, + "alert": { + "close-button": "" + }, "auto-save-field": { "saved": "", "saving": "" }, + "card": { + "option": "" + }, + "cascader": { + "clear-button": "" + }, "color-picker-popover": { "palette-tab": "", "spectrum-tab": "" @@ -1699,8 +1726,15 @@ "confirm-button": { "cancel": "" }, + "confirm-content": { + "placeholder": "" + }, "data-link-editor": { - "info": "" + "info": "", + "new-tab-label": "", + "title-label": "", + "title-placeholder": "", + "url-label": "" }, "data-link-editor-modal": { "cancel": "", @@ -1720,32 +1754,77 @@ "tooltip-remove": "", "url-not-provided": "" }, + "data-source-basic-auth-settings": { + "user-label": "", + "user-placeholder": "" + }, + "data-source-http-proxy-settings": { + "oauth-identity-label": "", + "oauth-identity-tooltip": "", + "skip-tls-verify-label": "", + "ts-client-auth-label": "", + "with-ca-cert-label": "", + "with-ca-cert-tooltip": "" + }, "data-source-http-settings": { "access-help": "", "access-help-details": "", + "access-label": "", + "access-options-browser": "", + "access-options-proxy": "", "allowed-cookies": "", + "allowed-cookies-tooltip": "", "auth": "", + "azure-auth-label": "", + "azure-auth-tooltip": "", "basic-auth": "", + "basic-auth-label": "", "browser-mode-description": "", "browser-mode-title": "", + "default-url-access-select": "", "default-url-tooltip": "", "direct-url-tooltip": "", "heading": "", "proxy-url-tooltip": "", "server-mode-description": "", - "server-mode-title": "" + "server-mode-title": "", + "timeout-form-label": "", + "timeout-label": "", + "timeout-tooltip": "", + "url-label": "", + "with-credential-label": "", + "with-credential-tooltip": "" }, "data-source-settings": { "alerting-settings-heading": "", + "alerting-settings-label": "", + "alerting-settings-tooltip": "", "cert-key-reset": "", "custom-headers-add": "", + "custom-headers-header": "", + "custom-headers-header-placeholder": "", + "custom-headers-header-remove": "", + "custom-headers-header-value": "", "custom-headers-title": "", "secure-socks-heading": "", - "tls-heading": "" + "secure-socks-label": "", + "secure-socks-tooltip": "", + "tls-certification-label": "", + "tls-certification-placeholder": "", + "tls-client-certification-label": "", + "tls-client-key-label": "", + "tls-client-key-placeholder": "", + "tls-heading": "", + "tls-server-name-label": "", + "tls-tooltip": "" }, "date-time-picker": { "apply": "", - "cancel": "" + "calendar-icon-label": "", + "cancel": "", + "next-label": "", + "previous-label": "", + "select-placeholder": "" }, "drawer": { "close": "Fechar" @@ -1769,6 +1848,10 @@ "modal": { "close-tooltip": "Fechar" }, + "named-colors-palette": { + "text-color-swatch": "", + "transparent-swatch": "" + }, "secret-form-field": { "reset": "" }, @@ -1782,6 +1865,9 @@ "no-options-label": "Nenhuma opção encontrada", "placeholder": "Escolher" }, + "series-color-picker-popover": { + "y-axis-usage": "" + }, "spinner": { "aria-label": "" }, @@ -1801,6 +1887,9 @@ "user-icon": { "active-text": "" }, + "value-pill": { + "remove-button": "" + }, "viz-legend": { "right-axis-indicator": "" }, @@ -3637,6 +3726,16 @@ "title": "" } }, + "theme-preview": { + "breadcrumbs": { + "dashboards": "", + "home": "" + }, + "panel": { + "form-label": "", + "title": "" + } + }, "time-picker": { "absolute": { "recent-title": "Intervalos absolutos usados recentemente", @@ -3693,10 +3792,13 @@ "example": "", "example-details": "", "example-title": "Exemplos de intervalos de tempo", + "from-label": "", "from-to": "", "more-info": "", "specify": "Especifique o intervalo de tempo <1>", - "supported-formats": "" + "submit-button-label": "", + "supported-formats": "", + "to-label": "" }, "zone": { "select-aria-label": "Seletor de fuso horário", diff --git a/public/locales/zh-Hans/grafana.json b/public/locales/zh-Hans/grafana.json index eb52a53913b..d1f6b954db5 100644 --- a/public/locales/zh-Hans/grafana.json +++ b/public/locales/zh-Hans/grafana.json @@ -295,6 +295,9 @@ "export-all": "", "loading": "", "search-by-matchers": "", + "titles": { + "notification-templates": "" + }, "view": "" }, "contact-points": { @@ -402,6 +405,20 @@ "title": "", "uninitialized": "" }, + "notification-templates": { + "duplicate": { + "subTitle": "", + "title": "" + }, + "edit": { + "subTitle": "", + "title": "" + }, + "new": { + "subTitle": "", + "title": "" + } + }, "policies": { "default-policy": { "description": "", @@ -588,6 +605,7 @@ } }, "rule-viewer": { + "error-loading": "", "prometheus-consistency-check": { "alert-message": "", "alert-title": "" @@ -1679,10 +1697,19 @@ "one-click-description": "" } }, + "alert": { + "close-button": "" + }, "auto-save-field": { "saved": "", "saving": "" }, + "card": { + "option": "" + }, + "cascader": { + "clear-button": "" + }, "color-picker-popover": { "palette-tab": "", "spectrum-tab": "" @@ -1690,8 +1717,15 @@ "confirm-button": { "cancel": "" }, + "confirm-content": { + "placeholder": "" + }, "data-link-editor": { - "info": "" + "info": "", + "new-tab-label": "", + "title-label": "", + "title-placeholder": "", + "url-label": "" }, "data-link-editor-modal": { "cancel": "", @@ -1711,32 +1745,77 @@ "tooltip-remove": "", "url-not-provided": "" }, + "data-source-basic-auth-settings": { + "user-label": "", + "user-placeholder": "" + }, + "data-source-http-proxy-settings": { + "oauth-identity-label": "", + "oauth-identity-tooltip": "", + "skip-tls-verify-label": "", + "ts-client-auth-label": "", + "with-ca-cert-label": "", + "with-ca-cert-tooltip": "" + }, "data-source-http-settings": { "access-help": "", "access-help-details": "", + "access-label": "", + "access-options-browser": "", + "access-options-proxy": "", "allowed-cookies": "", + "allowed-cookies-tooltip": "", "auth": "", + "azure-auth-label": "", + "azure-auth-tooltip": "", "basic-auth": "", + "basic-auth-label": "", "browser-mode-description": "", "browser-mode-title": "", + "default-url-access-select": "", "default-url-tooltip": "", "direct-url-tooltip": "", "heading": "", "proxy-url-tooltip": "", "server-mode-description": "", - "server-mode-title": "" + "server-mode-title": "", + "timeout-form-label": "", + "timeout-label": "", + "timeout-tooltip": "", + "url-label": "", + "with-credential-label": "", + "with-credential-tooltip": "" }, "data-source-settings": { "alerting-settings-heading": "", + "alerting-settings-label": "", + "alerting-settings-tooltip": "", "cert-key-reset": "", "custom-headers-add": "", + "custom-headers-header": "", + "custom-headers-header-placeholder": "", + "custom-headers-header-remove": "", + "custom-headers-header-value": "", "custom-headers-title": "", "secure-socks-heading": "", - "tls-heading": "" + "secure-socks-label": "", + "secure-socks-tooltip": "", + "tls-certification-label": "", + "tls-certification-placeholder": "", + "tls-client-certification-label": "", + "tls-client-key-label": "", + "tls-client-key-placeholder": "", + "tls-heading": "", + "tls-server-name-label": "", + "tls-tooltip": "" }, "date-time-picker": { "apply": "", - "cancel": "" + "calendar-icon-label": "", + "cancel": "", + "next-label": "", + "previous-label": "", + "select-placeholder": "" }, "drawer": { "close": "关闭" @@ -1760,6 +1839,10 @@ "modal": { "close-tooltip": "关闭" }, + "named-colors-palette": { + "text-color-swatch": "", + "transparent-swatch": "" + }, "secret-form-field": { "reset": "" }, @@ -1773,6 +1856,9 @@ "no-options-label": "未找到选项", "placeholder": "选择" }, + "series-color-picker-popover": { + "y-axis-usage": "" + }, "spinner": { "aria-label": "" }, @@ -1792,6 +1878,9 @@ "user-icon": { "active-text": "" }, + "value-pill": { + "remove-button": "" + }, "viz-legend": { "right-axis-indicator": "" }, @@ -3623,6 +3712,16 @@ "title": "" } }, + "theme-preview": { + "breadcrumbs": { + "dashboards": "", + "home": "" + }, + "panel": { + "form-label": "", + "title": "" + } + }, "time-picker": { "absolute": { "recent-title": "最近使用的绝对范围", @@ -3679,10 +3778,13 @@ "example": "", "example-details": "", "example-title": "示例时间范围", + "from-label": "", "from-to": "", "more-info": "", "specify": "指定时间范围 <1>", - "supported-formats": "" + "submit-button-label": "", + "supported-formats": "", + "to-label": "" }, "zone": { "select-aria-label": "时区选择器", From b16904651fbae4eaf413baa44c83c51a1b2edd71 Mon Sep 17 00:00:00 2001 From: ismail simsek Date: Thu, 27 Feb 2025 11:13:58 +0100 Subject: [PATCH 04/32] Chore: Bump grafana-plugin-sdk-go to v0.267.0 (#101376) * bump grafana-plugin-sdk-go to v0.267.0 * make update-workspace --- apps/alerting/notifications/go.mod | 4 ++-- apps/alerting/notifications/go.sum | 8 ++++---- apps/investigations/go.mod | 4 ++-- apps/investigations/go.sum | 8 ++++---- apps/playlist/go.mod | 4 ++-- apps/playlist/go.sum | 8 ++++---- go.mod | 8 ++++---- go.sum | 15 ++++++++------- go.work.sum | 20 ++++---------------- pkg/aggregator/go.mod | 8 ++++---- pkg/aggregator/go.sum | 15 ++++++++------- pkg/apimachinery/go.mod | 3 ++- pkg/apimachinery/go.sum | 8 ++++---- pkg/apiserver/go.mod | 4 ++-- pkg/apiserver/go.sum | 8 ++++---- pkg/build/go.mod | 4 ++-- pkg/build/go.sum | 8 ++++---- pkg/build/wire/go.mod | 2 +- pkg/build/wire/go.sum | 4 ++-- pkg/codegen/go.mod | 2 +- pkg/codegen/go.sum | 4 ++-- pkg/plugins/codegen/go.mod | 2 +- pkg/plugins/codegen/go.sum | 4 ++-- pkg/promlib/go.mod | 8 ++++---- pkg/promlib/go.sum | 15 ++++++++------- pkg/semconv/go.mod | 1 + pkg/semconv/go.sum | 4 ++-- pkg/storage/unified/apistore/go.mod | 8 ++++---- pkg/storage/unified/apistore/go.sum | 15 ++++++++------- pkg/storage/unified/resource/go.mod | 8 ++++---- pkg/storage/unified/resource/go.sum | 15 ++++++++------- 31 files changed, 112 insertions(+), 117 deletions(-) diff --git a/apps/alerting/notifications/go.mod b/apps/alerting/notifications/go.mod index 74dc1dfe028..cf49c4e862d 100644 --- a/apps/alerting/notifications/go.mod +++ b/apps/alerting/notifications/go.mod @@ -32,7 +32,7 @@ require ( github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/protobuf v1.5.4 // indirect github.com/google/gnostic-models v0.6.8 // indirect - github.com/google/go-cmp v0.6.0 // indirect + github.com/google/go-cmp v0.7.0 // indirect github.com/google/gofuzz v1.2.0 // indirect github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad // indirect github.com/google/uuid v1.6.0 // indirect @@ -83,7 +83,7 @@ require ( google.golang.org/genproto/googleapis/api v0.0.0-20250115164207-1a7da9e5054f // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f // indirect google.golang.org/grpc v1.70.0 // indirect - google.golang.org/protobuf v1.36.4 // indirect + google.golang.org/protobuf v1.36.5 // indirect gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/apps/alerting/notifications/go.sum b/apps/alerting/notifications/go.sum index cae5be00879..3b3312aebf7 100644 --- a/apps/alerting/notifications/go.sum +++ b/apps/alerting/notifications/go.sum @@ -60,8 +60,8 @@ github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl76 github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= -github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= @@ -269,8 +269,8 @@ google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f/go. google.golang.org/grpc v1.18.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= google.golang.org/grpc v1.70.0 h1:pWFv03aZoHzlRKHWicjsZytKAiYCtNS0dHbXnIdq7jQ= google.golang.org/grpc v1.70.0/go.mod h1:ofIJqVKDXx/JiXrwr2IG4/zwdH9txy3IlF40RmcJSQw= -google.golang.org/protobuf v1.36.4 h1:6A3ZDJHn/eNqc1i+IdefRzy/9PokBTPvcqMySR7NNIM= -google.golang.org/protobuf v1.36.4/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= +google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= diff --git a/apps/investigations/go.mod b/apps/investigations/go.mod index 12da166af27..7e9c06424d0 100644 --- a/apps/investigations/go.mod +++ b/apps/investigations/go.mod @@ -27,7 +27,7 @@ require ( github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/protobuf v1.5.4 // indirect github.com/google/gnostic-models v0.6.8 // indirect - github.com/google/go-cmp v0.6.0 // indirect + github.com/google/go-cmp v0.7.0 // indirect github.com/google/gofuzz v1.2.0 // indirect github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad // indirect github.com/google/uuid v1.6.0 // indirect @@ -73,7 +73,7 @@ require ( google.golang.org/genproto/googleapis/api v0.0.0-20250115164207-1a7da9e5054f // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f // indirect google.golang.org/grpc v1.70.0 // indirect - google.golang.org/protobuf v1.36.4 // indirect + google.golang.org/protobuf v1.36.5 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect k8s.io/api v0.32.1 // indirect diff --git a/apps/investigations/go.sum b/apps/investigations/go.sum index d3caa3d08e3..bc0a2332d93 100644 --- a/apps/investigations/go.sum +++ b/apps/investigations/go.sum @@ -40,8 +40,8 @@ github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6 github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= -github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= @@ -195,8 +195,8 @@ google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f h1: google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f/go.mod h1:+2Yz8+CLJbIfL9z73EW45avw8Lmge3xVElCP9zEKi50= google.golang.org/grpc v1.70.0 h1:pWFv03aZoHzlRKHWicjsZytKAiYCtNS0dHbXnIdq7jQ= google.golang.org/grpc v1.70.0/go.mod h1:ofIJqVKDXx/JiXrwr2IG4/zwdH9txy3IlF40RmcJSQw= -google.golang.org/protobuf v1.36.4 h1:6A3ZDJHn/eNqc1i+IdefRzy/9PokBTPvcqMySR7NNIM= -google.golang.org/protobuf v1.36.4/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= +google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= diff --git a/apps/playlist/go.mod b/apps/playlist/go.mod index cd07f8c199e..474148a6799 100644 --- a/apps/playlist/go.mod +++ b/apps/playlist/go.mod @@ -28,7 +28,7 @@ require ( github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/protobuf v1.5.4 // indirect github.com/google/gnostic-models v0.6.8 // indirect - github.com/google/go-cmp v0.6.0 // indirect + github.com/google/go-cmp v0.7.0 // indirect github.com/google/gofuzz v1.2.0 // indirect github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad // indirect github.com/google/uuid v1.6.0 // indirect @@ -74,7 +74,7 @@ require ( google.golang.org/genproto/googleapis/api v0.0.0-20250115164207-1a7da9e5054f // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f // indirect google.golang.org/grpc v1.70.0 // indirect - google.golang.org/protobuf v1.36.4 // indirect + google.golang.org/protobuf v1.36.5 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect k8s.io/api v0.32.1 // indirect diff --git a/apps/playlist/go.sum b/apps/playlist/go.sum index d3caa3d08e3..bc0a2332d93 100644 --- a/apps/playlist/go.sum +++ b/apps/playlist/go.sum @@ -40,8 +40,8 @@ github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6 github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= -github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= @@ -195,8 +195,8 @@ google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f h1: google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f/go.mod h1:+2Yz8+CLJbIfL9z73EW45avw8Lmge3xVElCP9zEKi50= google.golang.org/grpc v1.70.0 h1:pWFv03aZoHzlRKHWicjsZytKAiYCtNS0dHbXnIdq7jQ= google.golang.org/grpc v1.70.0/go.mod h1:ofIJqVKDXx/JiXrwr2IG4/zwdH9txy3IlF40RmcJSQw= -google.golang.org/protobuf v1.36.4 h1:6A3ZDJHn/eNqc1i+IdefRzy/9PokBTPvcqMySR7NNIM= -google.golang.org/protobuf v1.36.4/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= +google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= diff --git a/go.mod b/go.mod index d32c2843663..e664a31975a 100644 --- a/go.mod +++ b/go.mod @@ -65,7 +65,7 @@ require ( github.com/golang/mock v1.7.0-rc.1 // @grafana/alerting-backend github.com/golang/protobuf v1.5.4 // @grafana/grafana-backend-group github.com/golang/snappy v0.0.4 // @grafana/alerting-backend - github.com/google/go-cmp v0.6.0 // @grafana/grafana-backend-group + github.com/google/go-cmp v0.7.0 // @grafana/grafana-backend-group github.com/google/go-querystring v1.1.0 // indirect; @grafana/oss-big-tent github.com/google/uuid v1.6.0 // @grafana/grafana-backend-group github.com/google/wire v0.6.0 // @grafana/grafana-backend-group @@ -88,7 +88,7 @@ require ( github.com/grafana/grafana-cloud-migration-snapshot v1.6.0 // @grafana/grafana-operator-experience-squad github.com/grafana/grafana-google-sdk-go v0.2.1 // @grafana/partner-datasources github.com/grafana/grafana-openapi-client-go v0.0.0-20231213163343-bd475d63fb79 // @grafana/grafana-backend-group - github.com/grafana/grafana-plugin-sdk-go v0.266.0 // @grafana/plugins-platform-backend + github.com/grafana/grafana-plugin-sdk-go v0.267.0 // @grafana/plugins-platform-backend github.com/grafana/loki/v3 v3.2.1 // @grafana/observability-logs github.com/grafana/otel-profiling-go v0.5.1 // @grafana/grafana-backend-group github.com/grafana/pyroscope-go/godeltaprof v0.1.8 // @grafana/observability-traces-and-profiling @@ -180,7 +180,7 @@ require ( gonum.org/v1/gonum v0.15.1 // @grafana/oss-big-tent google.golang.org/api v0.216.0 // @grafana/grafana-backend-group google.golang.org/grpc v1.70.0 // @grafana/plugins-platform-backend - google.golang.org/protobuf v1.36.4 // @grafana/plugins-platform-backend + google.golang.org/protobuf v1.36.5 // @grafana/plugins-platform-backend gopkg.in/ini.v1 v1.67.0 // @grafana/alerting-backend gopkg.in/mail.v2 v2.3.1 // @grafana/grafana-backend-group gopkg.in/yaml.v3 v3.0.1 // @grafana/alerting-backend @@ -331,7 +331,7 @@ require ( github.com/dolthub/maphash v0.1.0 // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/edsrzf/mmap-go v1.2.0 // indirect - github.com/elazarl/goproxy v1.7.0 // indirect + github.com/elazarl/goproxy v1.7.1 // indirect github.com/emicklei/go-restful/v3 v3.11.0 // indirect github.com/emicklei/proto v1.13.2 // indirect github.com/emirpasic/gods v1.18.1 // indirect diff --git a/go.sum b/go.sum index 96e57fa263f..29c820b8964 100644 --- a/go.sum +++ b/go.sum @@ -1094,8 +1094,8 @@ github.com/edsrzf/mmap-go v1.2.0/go.mod h1:19H/e8pUPLicwkyNgOykDXkJ9F0MHE+Z52B8E github.com/efficientgo/core v1.0.0-rc.3 h1:X6CdgycYWDcbYiJr1H1+lQGzx13o7bq3EUkbB9DsSPc= github.com/efficientgo/core v1.0.0-rc.3/go.mod h1:FfGdkzWarkuzOlY04VY+bGfb1lWrjaL6x/GLcQ4vJps= github.com/elazarl/goproxy v0.0.0-20170405201442-c4fc26588b6e/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= -github.com/elazarl/goproxy v1.7.0 h1:EXv2nV4EjM60ZtsEVLYJG4oBXhDGutMKperpHsZ/v+0= -github.com/elazarl/goproxy v1.7.0/go.mod h1:X/5W/t+gzDyLfHW4DrMdpjqYjpXsURlBt9lpBDxZZZQ= +github.com/elazarl/goproxy v1.7.1 h1:1P7LPSxbqtNxusFnXclj6O56pjfq1xOQZ6a0mwwKUlY= +github.com/elazarl/goproxy v1.7.1/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE= github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= @@ -1422,8 +1422,9 @@ github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ= github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= @@ -1558,8 +1559,8 @@ github.com/grafana/grafana-google-sdk-go v0.2.1 h1:XeFdKnkXBjOJjXc1gf4iMx4h5aCHT github.com/grafana/grafana-google-sdk-go v0.2.1/go.mod h1:RiITSHwBhqVTTd3se3HQq5Ncs/wzzhTB9OK5N0J0PEU= github.com/grafana/grafana-openapi-client-go v0.0.0-20231213163343-bd475d63fb79 h1:r+mU5bGMzcXCRVAuOrTn54S80qbfVkvTdUJZfSfTNbs= github.com/grafana/grafana-openapi-client-go v0.0.0-20231213163343-bd475d63fb79/go.mod h1:wc6Hbh3K2TgCUSfBC/BOzabItujtHMESZeFk5ZhdxhQ= -github.com/grafana/grafana-plugin-sdk-go v0.266.0 h1:YP+iEpXH3HRX9Xo4NHjsrJhN2W7uVTtkLNzMHYbmiLI= -github.com/grafana/grafana-plugin-sdk-go v0.266.0/go.mod h1:bxkXrBQ4QSmOncsWdIOcpgP+M6wajQNMAPXlbWrqAWY= +github.com/grafana/grafana-plugin-sdk-go v0.267.0 h1:4AcuLEE0UeFG0Eo3P8r1FDS89hgZZ73K550DRz1fgjQ= +github.com/grafana/grafana-plugin-sdk-go v0.267.0/go.mod h1:OuwS4c/JYgn0rr/w5zhJBpLo4gKm/vw15RsfpYAvK9Q= github.com/grafana/grafana/apps/advisor v0.0.0-20250220163425-b4c4b9abbdc8 h1:mG/6nDlEBVxWlo2GQJVASzucw3ByPIBsec06XcPrjgQ= github.com/grafana/grafana/apps/advisor v0.0.0-20250220163425-b4c4b9abbdc8/go.mod h1:9I1dKV3Dqr0NPR9Af0WJGxOytp5/6W3JLiNChOz8r+c= github.com/grafana/grafana/apps/alerting/notifications v0.0.0-20250220163425-b4c4b9abbdc8 h1:w42GlvkmHG4nM/p1kb2nKmROVP+AHtL3qWEYMhnhCVM= @@ -3357,8 +3358,8 @@ google.golang.org/protobuf v1.29.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqw google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= -google.golang.org/protobuf v1.36.4 h1:6A3ZDJHn/eNqc1i+IdefRzy/9PokBTPvcqMySR7NNIM= -google.golang.org/protobuf v1.36.4/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= +google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk= gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk= diff --git a/go.work.sum b/go.work.sum index 0e79edfd1d2..25fc8254a99 100644 --- a/go.work.sum +++ b/go.work.sum @@ -142,8 +142,6 @@ cloud.google.com/go/memcache v1.11.1 h1:2FGuyd3WY7buNDAkMBdmeIOheNWA3gwaXrttLrEd cloud.google.com/go/memcache v1.11.1/go.mod h1:3zF+dEqmEmElHuO4NtHiShekQY5okQtssjPBv7jpmZ8= cloud.google.com/go/metastore v1.14.1 h1:kGx+IUSSYCVn8LisCT4fpxCC9rauEVonzi7RlygdqWY= cloud.google.com/go/metastore v1.14.1/go.mod h1:WDvsAcbQLl9M4xL+eIpbKogH7aEaPWMhO9aRBcFOnJE= -cloud.google.com/go/monitoring v1.21.1 h1:zWtbIoBMnU5LP9A/fz8LmWMGHpk4skdfeiaa66QdFGc= -cloud.google.com/go/monitoring v1.21.1/go.mod h1:Rj++LKrlht9uBi8+Eb530dIrzG/cU/lB8mt+lbeFK1c= cloud.google.com/go/networkconnectivity v1.15.1 h1:EizN+cFGHzRAyiFTK8jT1PqTo+cSnbc2IGh6OmllS7Y= cloud.google.com/go/networkconnectivity v1.15.1/go.mod h1:tYAcT4Ahvq+BiePXL/slYipf/8FF0oNJw3MqFhBnSPI= cloud.google.com/go/networkmanagement v1.14.1 h1:0x3hVI6xbp3N/choffKPHMSxbzaPdHSD92cBElebXEk= @@ -204,8 +202,6 @@ cloud.google.com/go/servicemanagement v1.8.0 h1:fopAQI/IAzlxnVeiKn/8WiV6zKndjFkv cloud.google.com/go/serviceusage v1.6.0 h1:rXyq+0+RSIm3HFypctp7WoXxIA563rn206CfMWdqXX4= cloud.google.com/go/shell v1.8.1 h1:etoJal+LB7Pn8+5vE2aAh6QcFbBmerIOh5MxNDoXykw= cloud.google.com/go/shell v1.8.1/go.mod h1:jaU7OHeldDhTwgs3+clM0KYEDYnBAPevUI6wNLf7ycE= -cloud.google.com/go/spanner v1.70.0 h1:nj6p/GJTgMDiSQ1gQ034ItsKuJgHiMOjtOlONOg8PSo= -cloud.google.com/go/spanner v1.70.0/go.mod h1:X5T0XftydYp0K1adeJQDJtdWpbrOeJ7wHecM4tK6FiE= 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= cloud.google.com/go/storagetransfer v1.11.1 h1:Hd7H1zXGQGEWyWXxWVXDMuNCGasNQim1y9CIaMZIBX8= @@ -280,10 +276,6 @@ github.com/DmitriyVTitov/size v1.5.0 h1:/PzqxYrOyOUX1BXj6J9OuVRVGe+66VL4D9FlUaW5 github.com/DmitriyVTitov/size v1.5.0/go.mod h1:le6rNI4CoLQV1b9gzp1+3d7hMAD/uu2QcJ+aYbNgiU0= github.com/GoogleCloudPlatform/cloudsql-proxy v1.36.0 h1:kAtNAWwvTt5+iew6baV0kbOrtjYTXPtWNSyOFlcxkBU= github.com/GoogleCloudPlatform/cloudsql-proxy v1.36.0/go.mod h1:VRKXU8C7Y/aUKjRBTGfw0Ndv4YqNxlB8zAPJJDxbASE= -github.com/GoogleCloudPlatform/grpc-gcp-go/grpcgcp v1.5.0 h1:oVLqHXhnYtUwM89y9T1fXGaK9wTkXHgNp8/ZNMQzUxE= -github.com/GoogleCloudPlatform/grpc-gcp-go/grpcgcp v1.5.0/go.mod h1:dppbR7CwXD4pgtV9t3wD1812RaLDcBjtblcDF5f1vI0= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.25.0 h1:3c8yed4lgqTt+oTQ+JNMDo+F4xprBf+O/il4ZC0nRLw= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.25.0/go.mod h1:obipzmGjfSjam60XLwGfqUkJsfiheAl+TUjG+4yzyPM= github.com/IBM/go-sdk-core/v5 v5.17.4 h1:VGb9+mRrnS2HpHZFM5hy4J6ppIWnwNrw0G+tLSgcJLc= github.com/IBM/go-sdk-core/v5 v5.17.4/go.mod h1:KsAAI7eStAWwQa4F96MLy+whYSh39JzNjklZRbN/8ns= github.com/IBM/ibm-cos-sdk-go v1.11.0 h1:Jp55NLN3OvBwucMGpP5wNybyjncsmTZ9+GPHai/1cE8= @@ -387,7 +379,6 @@ github.com/bytedance/sonic v1.10.0-rc3 h1:uNSnscRapXTwUgTyOF0GVljYD08p9X/Lbr9Mwe github.com/bytedance/sonic v1.10.0-rc3/go.mod h1:iZcSUejdk5aukTND/Eu/ivjQuEL0Cu9/rf50Hi0u/g4= github.com/campoy/embedmd v1.0.0 h1:V4kI2qTJJLf4J29RzI/MAt2c3Bl4dQSYPuflzwFH2hY= github.com/campoy/embedmd v1.0.0/go.mod h1:oxyr9RCiSXg0M3VJ3ks0UGfp98BpSSGr0kpiX3MzVl8= -github.com/census-instrumentation/opencensus-proto v0.4.1 h1:iKLQ0xPNFxR/2hzXZMrBo8f1j86j5WHzznCCQxV/b8g= github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d h1:77cEq6EriyTZ0g/qfRdp61a3Uu/AWrgIq2s0ClJV1g0= github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d/go.mod h1:8EPpVsBuRksnlj1mLy4AWzRNQYxauNi62uWcE3to6eA= github.com/chenzhuoyu/iasm v0.9.0 h1:9fhXjVzq5hUy2gkhhgHl95zG2cEAhw9OSGs8toWWAwo= @@ -510,6 +501,7 @@ github.com/elastic/go-sysinfo v1.11.2/go.mod h1:GKqR8bbMK/1ITnez9NIsIfXQr25aLhRJ github.com/elastic/go-windows v1.0.1 h1:AlYZOldA+UJ0/2nBuqWdo90GFCgG9xuyw9SYzGUtJm0= github.com/elastic/go-windows v1.0.1/go.mod h1:FoVvqWSun28vaDQPbj2Elfc0JahhPB7WQEGa3c814Ss= github.com/elazarl/goproxy v1.3.0/go.mod h1:X/5W/t+gzDyLfHW4DrMdpjqYjpXsURlBt9lpBDxZZZQ= +github.com/elazarl/goproxy v1.7.1 h1:1P7LPSxbqtNxusFnXclj6O56pjfq1xOQZ6a0mwwKUlY= github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633 h1:H2pdYOb3KQ1/YsqVWoWNLQO+fusocsw354rqGTZtAgw= github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw= github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= @@ -593,6 +585,7 @@ github.com/gomodule/redigo v1.8.9/go.mod h1:7ArFNvsTjH8GMMzB4uy1snslv2BwmginuMs0 github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA= github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= github.com/google/cel-go v0.22.0/go.mod h1:BuznPXXfQDpXKWQ9sPW3TzlAJN5zzFe+i9tIs0yC4s8= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-github v17.0.0+incompatible h1:N0LgJ1j65A7kfXrZnUDaYCs/Sf4rEjNlfyDHW9dolSY= github.com/google/go-jsonnet v0.18.0 h1:/6pTy6g+Jh1a1I2UMoAODkqELFiVIdOxbNwv0DDzoOg= github.com/google/go-jsonnet v0.18.0/go.mod h1:C3fTzyVJDslXdiTqw/bTFk7vSGyCtH3MGRbDfvEwGd0= @@ -907,7 +900,6 @@ github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad h1:fiWzISvDn0Csy5H0iwgAuJ github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8= github.com/stoewer/parquet-cli v0.0.7 h1:rhdZODIbyMS3twr4OM3am8BPPT5pbfMcHLH93whDM5o= github.com/stoewer/parquet-cli v0.0.7/go.mod h1:bskxHdj8q3H1EmfuCqjViFoeO3NEvs5lzZAQvI8Nfjk= -github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/substrait-io/substrait v0.57.1 h1:GW8nnYfSowMseHR8Os82/X6lNtQGIK7p4p+lr6r+auw= github.com/substrait-io/substrait v0.57.1/go.mod h1:q9s+tjo+gK0lsA+SqYB0lhojNuxvdPdfYlGUP0hjbrA= github.com/substrait-io/substrait-go v1.2.0 h1:3ZNRkc8FYD7ifCagKEOZQtUcgMceMQfwo2N1NGaK4Q4= @@ -1055,8 +1047,6 @@ go.opentelemetry.io/contrib/bridges/prometheus v0.53.0 h1:BdkKDtcrHThgjcEia1737O go.opentelemetry.io/contrib/bridges/prometheus v0.53.0/go.mod h1:ZkhVxcJgeXlL/lVyT/vxNHVFiSG5qOaDwYaSgD8IfZo= go.opentelemetry.io/contrib/config v0.7.0 h1:b1rK5tGTuhhPirJiMxOcyQfZs76j2VapY6ODn3b2Dbs= go.opentelemetry.io/contrib/config v0.7.0/go.mod h1:8tdiFd8N5etOi3XzBmAoMxplEzI3TcL8dU5rM5/xcOQ= -go.opentelemetry.io/contrib/detectors/gcp v1.32.0 h1:P78qWqkLSShicHmAzfECaTgvslqHxblNE9j62Ws1NK8= -go.opentelemetry.io/contrib/detectors/gcp v1.32.0/go.mod h1:TVqo0Sda4Cv8gCIixd7LuLwW4EylumVWfhjZJjDD4DU= go.opentelemetry.io/contrib/exporters/autoexport v0.53.0 h1:13K+tY7E8GJInkrvRiPAhC0gi/7vKjzDNhtmCf+QXG8= go.opentelemetry.io/contrib/exporters/autoexport v0.53.0/go.mod h1:lyQF6xQ4iDnMg4sccNdFs1zf62xd79YI8vZqKjOTwMs= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.53.0/go.mod h1:azvtTADFQJA8mX80jIH/akaE7h+dbm/sVuaHqN13w74= @@ -1137,12 +1127,9 @@ golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0= golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= golang.org/x/net v0.32.0/go.mod h1:CwU0IoeOlnQQWJ6ioyFrfRuomB8GKF6KbYXZVyeXNfs= golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= -golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8= -golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk= golang.org/x/oauth2 v0.21.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= -golang.org/x/oauth2 v0.27.0 h1:da9Vo7/tDv5RH/7nZDz1eMGS/q1Vv1N/7FCrBhI9I3M= -golang.org/x/oauth2 v0.27.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= +golang.org/x/oauth2 v0.26.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -1200,6 +1187,7 @@ google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojt google.golang.org/protobuf v1.35.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= google.golang.org/protobuf v1.36.0/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= google.golang.org/protobuf v1.36.3/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= gopkg.in/alecthomas/kingpin.v2 v2.2.6 h1:jMFz6MfLP0/4fUyZle81rXUoxOBFi19VUFKVDOQfozc= gopkg.in/errgo.v2 v2.1.0 h1:0vLT13EuvQ0hNvakwLuFZ/jYrLp5F3kcWHXdRggjCE8= gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= diff --git a/pkg/aggregator/go.mod b/pkg/aggregator/go.mod index edfe54e00b2..cac9067a6a9 100644 --- a/pkg/aggregator/go.mod +++ b/pkg/aggregator/go.mod @@ -6,7 +6,7 @@ toolchain go1.23.6 require ( github.com/emicklei/go-restful/v3 v3.11.0 - github.com/grafana/grafana-plugin-sdk-go v0.266.0 + github.com/grafana/grafana-plugin-sdk-go v0.267.0 github.com/grafana/grafana/pkg/apimachinery v0.0.0-20240808213237-f4d2e064f435 github.com/grafana/grafana/pkg/semconv v0.0.0-20240808213237-f4d2e064f435 github.com/mattbaird/jsonpatch v0.0.0-20240118010651-0ba75a80ca38 @@ -39,7 +39,7 @@ require ( github.com/coreos/go-systemd/v22 v22.5.0 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect - github.com/elazarl/goproxy v1.7.0 // indirect + github.com/elazarl/goproxy v1.7.1 // indirect github.com/evanphx/json-patch v5.6.0+incompatible // indirect github.com/fatih/color v1.17.0 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect @@ -59,7 +59,7 @@ require ( github.com/google/cel-go v0.22.1 // indirect github.com/google/flatbuffers v24.3.25+incompatible // indirect github.com/google/gnostic-models v0.6.8 // indirect - github.com/google/go-cmp v0.6.0 // indirect + github.com/google/go-cmp v0.7.0 // indirect github.com/google/gofuzz v1.2.0 // indirect github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad // indirect github.com/google/uuid v1.6.0 // indirect @@ -150,7 +150,7 @@ require ( google.golang.org/genproto/googleapis/api v0.0.0-20250115164207-1a7da9e5054f // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f // indirect google.golang.org/grpc v1.70.0 // indirect - google.golang.org/protobuf v1.36.4 // indirect + google.golang.org/protobuf v1.36.5 // indirect gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect gopkg.in/fsnotify/fsnotify.v1 v1.4.7 // indirect gopkg.in/inf.v0 v0.9.1 // indirect diff --git a/pkg/aggregator/go.sum b/pkg/aggregator/go.sum index eb715898bf0..11bd9b72803 100644 --- a/pkg/aggregator/go.sum +++ b/pkg/aggregator/go.sum @@ -49,8 +49,8 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= -github.com/elazarl/goproxy v1.7.0 h1:EXv2nV4EjM60ZtsEVLYJG4oBXhDGutMKperpHsZ/v+0= -github.com/elazarl/goproxy v1.7.0/go.mod h1:X/5W/t+gzDyLfHW4DrMdpjqYjpXsURlBt9lpBDxZZZQ= +github.com/elazarl/goproxy v1.7.1 h1:1P7LPSxbqtNxusFnXclj6O56pjfq1xOQZ6a0mwwKUlY= +github.com/elazarl/goproxy v1.7.1/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE= github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= @@ -118,8 +118,9 @@ github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvR github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= @@ -134,8 +135,8 @@ github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/grafana/grafana-plugin-sdk-go v0.266.0 h1:YP+iEpXH3HRX9Xo4NHjsrJhN2W7uVTtkLNzMHYbmiLI= -github.com/grafana/grafana-plugin-sdk-go v0.266.0/go.mod h1:bxkXrBQ4QSmOncsWdIOcpgP+M6wajQNMAPXlbWrqAWY= +github.com/grafana/grafana-plugin-sdk-go v0.267.0 h1:4AcuLEE0UeFG0Eo3P8r1FDS89hgZZ73K550DRz1fgjQ= +github.com/grafana/grafana-plugin-sdk-go v0.267.0/go.mod h1:OuwS4c/JYgn0rr/w5zhJBpLo4gKm/vw15RsfpYAvK9Q= github.com/grafana/grafana/pkg/apimachinery v0.0.0-20240808213237-f4d2e064f435 h1:lmw60EW7JWlAEvgggktOyVkH4hF1m/+LSF/Ap0NCyi8= github.com/grafana/grafana/pkg/apimachinery v0.0.0-20240808213237-f4d2e064f435/go.mod h1:ORVFiW/KNRY52lNjkGwnFWCxNVfE97bJG2jr2fetq0I= github.com/grafana/grafana/pkg/semconv v0.0.0-20240808213237-f4d2e064f435 h1:SNEeqY22DrGr5E9kGF1mKSqlOom14W9+b1u4XEGJowA= @@ -495,8 +496,8 @@ google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8 google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= google.golang.org/grpc v1.70.0 h1:pWFv03aZoHzlRKHWicjsZytKAiYCtNS0dHbXnIdq7jQ= google.golang.org/grpc v1.70.0/go.mod h1:ofIJqVKDXx/JiXrwr2IG4/zwdH9txy3IlF40RmcJSQw= -google.golang.org/protobuf v1.36.4 h1:6A3ZDJHn/eNqc1i+IdefRzy/9PokBTPvcqMySR7NNIM= -google.golang.org/protobuf v1.36.4/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= +google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= diff --git a/pkg/apimachinery/go.mod b/pkg/apimachinery/go.mod index 033f48738ec..a3f8b199c92 100644 --- a/pkg/apimachinery/go.mod +++ b/pkg/apimachinery/go.mod @@ -23,6 +23,7 @@ require ( github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/protobuf v1.5.4 // indirect github.com/google/gnostic-models v0.6.8 // indirect + github.com/google/go-cmp v0.7.0 // indirect github.com/google/gofuzz v1.2.0 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect @@ -43,7 +44,7 @@ require ( golang.org/x/text v0.22.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f // indirect google.golang.org/grpc v1.70.0 // indirect - google.golang.org/protobuf v1.36.4 // indirect + google.golang.org/protobuf v1.36.5 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect k8s.io/klog/v2 v2.130.1 // indirect diff --git a/pkg/apimachinery/go.sum b/pkg/apimachinery/go.sum index bd82bcbb03f..a2532bad3ff 100644 --- a/pkg/apimachinery/go.sum +++ b/pkg/apimachinery/go.sum @@ -25,8 +25,8 @@ github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6 github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= -github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= @@ -151,8 +151,8 @@ google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f h1: google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f/go.mod h1:+2Yz8+CLJbIfL9z73EW45avw8Lmge3xVElCP9zEKi50= google.golang.org/grpc v1.70.0 h1:pWFv03aZoHzlRKHWicjsZytKAiYCtNS0dHbXnIdq7jQ= google.golang.org/grpc v1.70.0/go.mod h1:ofIJqVKDXx/JiXrwr2IG4/zwdH9txy3IlF40RmcJSQw= -google.golang.org/protobuf v1.36.4 h1:6A3ZDJHn/eNqc1i+IdefRzy/9PokBTPvcqMySR7NNIM= -google.golang.org/protobuf v1.36.4/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= +google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= diff --git a/pkg/apiserver/go.mod b/pkg/apiserver/go.mod index 0c45aa74e58..59a16f25782 100644 --- a/pkg/apiserver/go.mod +++ b/pkg/apiserver/go.mod @@ -5,7 +5,7 @@ go 1.23.4 toolchain go1.23.6 require ( - github.com/google/go-cmp v0.6.0 + github.com/google/go-cmp v0.7.0 github.com/grafana/authlib/types v0.0.0-20250224151205-5ef97131cc82 github.com/grafana/grafana-app-sdk/logging v0.30.0 github.com/grafana/grafana/pkg/apimachinery v0.0.0-20240701135906-559738ce6ae1 @@ -93,7 +93,7 @@ require ( google.golang.org/genproto/googleapis/api v0.0.0-20250115164207-1a7da9e5054f // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f // indirect google.golang.org/grpc v1.70.0 // indirect - google.golang.org/protobuf v1.36.4 // indirect + google.golang.org/protobuf v1.36.5 // indirect gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/pkg/apiserver/go.sum b/pkg/apiserver/go.sum index b3d15d3aee2..f1076d70d55 100644 --- a/pkg/apiserver/go.sum +++ b/pkg/apiserver/go.sum @@ -68,8 +68,8 @@ github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvR github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= -github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= @@ -312,8 +312,8 @@ google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8 google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= google.golang.org/grpc v1.70.0 h1:pWFv03aZoHzlRKHWicjsZytKAiYCtNS0dHbXnIdq7jQ= google.golang.org/grpc v1.70.0/go.mod h1:ofIJqVKDXx/JiXrwr2IG4/zwdH9txy3IlF40RmcJSQw= -google.golang.org/protobuf v1.36.4 h1:6A3ZDJHn/eNqc1i+IdefRzy/9PokBTPvcqMySR7NNIM= -google.golang.org/protobuf v1.36.4/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= +google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= diff --git a/pkg/build/go.mod b/pkg/build/go.mod index 1d5f29a7bed..fd52527bf94 100644 --- a/pkg/build/go.mod +++ b/pkg/build/go.mod @@ -14,7 +14,7 @@ require ( github.com/docker/docker v27.4.1+incompatible // @grafana/grafana-developer-enablement-squad github.com/drone/drone-cli v1.8.0 // @grafana/grafana-developer-enablement-squad github.com/gogo/protobuf v1.3.2 // indirect; @grafana/alerting-backend - github.com/google/go-cmp v0.6.0 // @grafana/grafana-backend-group + github.com/google/go-cmp v0.7.0 // @grafana/grafana-backend-group github.com/google/go-github/v69 v69.2.0 // @grafana/grafana-developer-enablement-squad github.com/google/uuid v1.6.0 // indirect; @grafana/grafana-backend-group github.com/googleapis/gax-go/v2 v2.14.1 // indirect; @grafana/grafana-backend-group @@ -35,7 +35,7 @@ require ( golang.org/x/time v0.9.0 // indirect; @grafana/grafana-backend-group google.golang.org/api v0.216.0 // @grafana/grafana-backend-group google.golang.org/grpc v1.70.0 // indirect; @grafana/plugins-platform-backend - google.golang.org/protobuf v1.36.4 // indirect; @grafana/plugins-platform-backend + google.golang.org/protobuf v1.36.5 // indirect; @grafana/plugins-platform-backend gopkg.in/yaml.v3 v3.0.1 // @grafana/alerting-backend ) diff --git a/pkg/build/go.sum b/pkg/build/go.sum index 7c868c04a4c..b1e8e4c3aa9 100644 --- a/pkg/build/go.sum +++ b/pkg/build/go.sum @@ -126,8 +126,8 @@ github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= -github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-github/v69 v69.2.0 h1:wR+Wi/fN2zdUx9YxSmYE0ktiX9IAR/BeePzeaUUbEHE= github.com/google/go-github/v69 v69.2.0/go.mod h1:xne4jymxLR6Uj9b7J7PyTpkMYstEMMwGZa0Aehh1azM= github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= @@ -362,8 +362,8 @@ google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2 google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= -google.golang.org/protobuf v1.36.4 h1:6A3ZDJHn/eNqc1i+IdefRzy/9PokBTPvcqMySR7NNIM= -google.golang.org/protobuf v1.36.4/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= +google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/pkg/build/wire/go.mod b/pkg/build/wire/go.mod index ee43dc13181..61a2bb2b9db 100644 --- a/pkg/build/wire/go.mod +++ b/pkg/build/wire/go.mod @@ -3,7 +3,7 @@ module github.com/grafana/grafana/pkg/build/wire go 1.23.1 require ( - github.com/google/go-cmp v0.6.0 + github.com/google/go-cmp v0.7.0 github.com/google/subcommands v1.2.0 github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 golang.org/x/tools v0.29.0 diff --git a/pkg/build/wire/go.sum b/pkg/build/wire/go.sum index 56cfeb71f60..07103d75876 100644 --- a/pkg/build/wire/go.sum +++ b/pkg/build/wire/go.sum @@ -1,5 +1,5 @@ -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= -github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/subcommands v1.2.0 h1:vWQspBTo2nEqTUFita5/KeEWlUL8kQObDFbub/EN9oE= github.com/google/subcommands v1.2.0/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= diff --git a/pkg/codegen/go.mod b/pkg/codegen/go.mod index 3c3b0bfa94b..f6f900fc6b8 100644 --- a/pkg/codegen/go.mod +++ b/pkg/codegen/go.mod @@ -21,7 +21,7 @@ require ( github.com/go-openapi/jsonpointer v0.21.0 // indirect github.com/go-openapi/swag v0.23.0 // indirect github.com/golang/glog v1.2.4 // indirect - github.com/google/go-cmp v0.6.0 // indirect + github.com/google/go-cmp v0.7.0 // indirect github.com/google/uuid v1.6.0 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect diff --git a/pkg/codegen/go.sum b/pkg/codegen/go.sum index 25214c4b99e..e125a5bdc0f 100644 --- a/pkg/codegen/go.sum +++ b/pkg/codegen/go.sum @@ -25,8 +25,8 @@ github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfU github.com/golang/glog v1.2.4 h1:CNNw5U8lSiiBk7druxtSHHTsRWcxKoac6kZKm2peBBc= github.com/golang/glog v1.2.4/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w= github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= -github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/grafana/codejen v0.0.4-0.20230321061741-77f656893a3d h1:hrXbGJ5jgp6yNITzs5o+zXq0V5yT3siNJ+uM8LGwWKk= diff --git a/pkg/plugins/codegen/go.mod b/pkg/plugins/codegen/go.mod index 5d415a7f052..8205acbb824 100644 --- a/pkg/plugins/codegen/go.mod +++ b/pkg/plugins/codegen/go.mod @@ -21,7 +21,7 @@ require ( github.com/getkin/kin-openapi v0.129.0 // indirect github.com/go-openapi/jsonpointer v0.21.0 // indirect github.com/go-openapi/swag v0.23.0 // indirect - github.com/google/go-cmp v0.6.0 // indirect + github.com/google/go-cmp v0.7.0 // indirect github.com/google/uuid v1.6.0 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect diff --git a/pkg/plugins/codegen/go.sum b/pkg/plugins/codegen/go.sum index a940ced9650..c6205d86509 100644 --- a/pkg/plugins/codegen/go.sum +++ b/pkg/plugins/codegen/go.sum @@ -24,8 +24,8 @@ github.com/go-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7 github.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZsDC26tQJow= github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM= github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= -github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/grafana/codejen v0.0.4-0.20230321061741-77f656893a3d h1:hrXbGJ5jgp6yNITzs5o+zXq0V5yT3siNJ+uM8LGwWKk= diff --git a/pkg/promlib/go.mod b/pkg/promlib/go.mod index 95380de2939..fb2946bb264 100644 --- a/pkg/promlib/go.mod +++ b/pkg/promlib/go.mod @@ -6,7 +6,7 @@ toolchain go1.23.6 require ( github.com/grafana/dskit v0.0.0-20241105154643-a6b453a88040 - github.com/grafana/grafana-plugin-sdk-go v0.266.0 + github.com/grafana/grafana-plugin-sdk-go v0.267.0 github.com/json-iterator/go v1.1.12 github.com/prometheus/client_golang v1.20.5 github.com/prometheus/common v0.62.0 @@ -33,7 +33,7 @@ require ( github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/dennwc/varint v1.0.0 // indirect - github.com/elazarl/goproxy v1.7.0 // indirect + github.com/elazarl/goproxy v1.7.1 // indirect github.com/emicklei/go-restful/v3 v3.11.0 // indirect github.com/fatih/color v1.17.0 // indirect github.com/fxamacker/cbor/v2 v2.7.0 // indirect @@ -48,7 +48,7 @@ require ( github.com/golang/protobuf v1.5.4 // indirect github.com/google/flatbuffers v24.3.25+incompatible // indirect github.com/google/gnostic-models v0.6.8 // indirect - github.com/google/go-cmp v0.6.0 // indirect + github.com/google/go-cmp v0.7.0 // indirect github.com/google/gofuzz v1.2.0 // indirect github.com/google/uuid v1.6.0 // indirect github.com/gorilla/mux v1.8.1 // indirect @@ -121,7 +121,7 @@ require ( google.golang.org/genproto/googleapis/api v0.0.0-20250115164207-1a7da9e5054f // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f // indirect google.golang.org/grpc v1.70.0 // indirect - google.golang.org/protobuf v1.36.4 // indirect + google.golang.org/protobuf v1.36.5 // indirect gopkg.in/fsnotify/fsnotify.v1 v1.4.7 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/pkg/promlib/go.sum b/pkg/promlib/go.sum index 66a0da7e790..f6b5b3b3d42 100644 --- a/pkg/promlib/go.sum +++ b/pkg/promlib/go.sum @@ -55,8 +55,8 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dennwc/varint v1.0.0 h1:kGNFFSSw8ToIy3obO/kKr8U9GZYUAxQEVuix4zfDWzE= github.com/dennwc/varint v1.0.0/go.mod h1:hnItb35rvZvJrbTALZtY/iQfDs48JKRG1RPpgziApxA= -github.com/elazarl/goproxy v1.7.0 h1:EXv2nV4EjM60ZtsEVLYJG4oBXhDGutMKperpHsZ/v+0= -github.com/elazarl/goproxy v1.7.0/go.mod h1:X/5W/t+gzDyLfHW4DrMdpjqYjpXsURlBt9lpBDxZZZQ= +github.com/elazarl/goproxy v1.7.1 h1:1P7LPSxbqtNxusFnXclj6O56pjfq1xOQZ6a0mwwKUlY= +github.com/elazarl/goproxy v1.7.1/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE= github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= @@ -100,8 +100,9 @@ github.com/google/flatbuffers v24.3.25+incompatible/go.mod h1:1AeVuKshWv4vARoZat github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= @@ -120,8 +121,8 @@ github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= github.com/grafana/dskit v0.0.0-20241105154643-a6b453a88040 h1:IR+UNYHqaU31t8/TArJk8K/GlDwOyxMpGNkWCXeZ28g= github.com/grafana/dskit v0.0.0-20241105154643-a6b453a88040/go.mod h1:SPLNCARd4xdjCkue0O6hvuoveuS1dGJjDnfxYe405YQ= -github.com/grafana/grafana-plugin-sdk-go v0.266.0 h1:YP+iEpXH3HRX9Xo4NHjsrJhN2W7uVTtkLNzMHYbmiLI= -github.com/grafana/grafana-plugin-sdk-go v0.266.0/go.mod h1:bxkXrBQ4QSmOncsWdIOcpgP+M6wajQNMAPXlbWrqAWY= +github.com/grafana/grafana-plugin-sdk-go v0.267.0 h1:4AcuLEE0UeFG0Eo3P8r1FDS89hgZZ73K550DRz1fgjQ= +github.com/grafana/grafana-plugin-sdk-go v0.267.0/go.mod h1:OuwS4c/JYgn0rr/w5zhJBpLo4gKm/vw15RsfpYAvK9Q= github.com/grafana/otel-profiling-go v0.5.1 h1:stVPKAFZSa7eGiqbYuG25VcqYksR6iWvF3YH66t4qL8= github.com/grafana/otel-profiling-go v0.5.1/go.mod h1:ftN/t5A/4gQI19/8MoWurBEtC6gFw8Dns1sJZ9W4Tls= github.com/grafana/pyroscope-go/godeltaprof v0.1.8 h1:iwOtYXeeVSAeYefJNaxDytgjKtUuKQbJqgAIjlnicKg= @@ -394,8 +395,8 @@ google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f h1: google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f/go.mod h1:+2Yz8+CLJbIfL9z73EW45avw8Lmge3xVElCP9zEKi50= google.golang.org/grpc v1.70.0 h1:pWFv03aZoHzlRKHWicjsZytKAiYCtNS0dHbXnIdq7jQ= google.golang.org/grpc v1.70.0/go.mod h1:ofIJqVKDXx/JiXrwr2IG4/zwdH9txy3IlF40RmcJSQw= -google.golang.org/protobuf v1.36.4 h1:6A3ZDJHn/eNqc1i+IdefRzy/9PokBTPvcqMySR7NNIM= -google.golang.org/protobuf v1.36.4/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= +google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= diff --git a/pkg/semconv/go.mod b/pkg/semconv/go.mod index b79133b4a5b..d015899f50c 100644 --- a/pkg/semconv/go.mod +++ b/pkg/semconv/go.mod @@ -6,5 +6,6 @@ require go.opentelemetry.io/otel v1.34.0 require ( github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/google/go-cmp v0.7.0 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect ) diff --git a/pkg/semconv/go.sum b/pkg/semconv/go.sum index 2b997e160c5..2d9cf378678 100644 --- a/pkg/semconv/go.sum +++ b/pkg/semconv/go.sum @@ -1,7 +1,7 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= -github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= diff --git a/pkg/storage/unified/apistore/go.mod b/pkg/storage/unified/apistore/go.mod index 748b5e929ff..c515663008a 100644 --- a/pkg/storage/unified/apistore/go.mod +++ b/pkg/storage/unified/apistore/go.mod @@ -134,7 +134,7 @@ require ( github.com/dolthub/jsonpath v0.0.2-0.20240227200619-19675ab05c71 // indirect github.com/dolthub/vitess v0.0.0-20250123002143-3b45b8cacbfa // indirect github.com/dustin/go-humanize v1.0.1 // indirect - github.com/elazarl/goproxy v1.7.0 // indirect + github.com/elazarl/goproxy v1.7.1 // indirect github.com/emicklei/go-restful/v3 v3.11.0 // indirect github.com/emirpasic/gods v1.18.1 // indirect github.com/envoyproxy/protoc-gen-validate v1.1.0 // indirect @@ -183,7 +183,7 @@ require ( github.com/google/cel-go v0.22.1 // indirect github.com/google/flatbuffers v24.3.25+incompatible // indirect github.com/google/gnostic-models v0.6.8 // indirect - github.com/google/go-cmp v0.6.0 // indirect + github.com/google/go-cmp v0.7.0 // indirect github.com/google/go-querystring v1.1.0 // indirect github.com/google/gofuzz v1.2.0 // indirect github.com/google/s2a-go v0.1.8 // indirect @@ -199,7 +199,7 @@ require ( github.com/grafana/grafana-app-sdk/logging v0.30.0 // indirect github.com/grafana/grafana-aws-sdk v0.31.5 // indirect github.com/grafana/grafana-azure-sdk-go/v2 v2.1.6 // indirect - github.com/grafana/grafana-plugin-sdk-go v0.266.0 // indirect + github.com/grafana/grafana-plugin-sdk-go v0.267.0 // indirect github.com/grafana/grafana/pkg/aggregator v0.0.0-20250220163425-b4c4b9abbdc8 // indirect github.com/grafana/grafana/pkg/promlib v0.0.8 // indirect github.com/grafana/grafana/pkg/semconv v0.0.0-20250220164708-c8d4ff28a450 // indirect @@ -382,7 +382,7 @@ require ( google.golang.org/genproto v0.0.0-20241021214115-324edc3d5d38 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20250115164207-1a7da9e5054f // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f // indirect - google.golang.org/protobuf v1.36.4 // indirect + google.golang.org/protobuf v1.36.5 // indirect gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect gopkg.in/fsnotify/fsnotify.v1 v1.4.7 // indirect diff --git a/pkg/storage/unified/apistore/go.sum b/pkg/storage/unified/apistore/go.sum index 35a9215a782..11a44305649 100644 --- a/pkg/storage/unified/apistore/go.sum +++ b/pkg/storage/unified/apistore/go.sum @@ -334,8 +334,8 @@ github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5m github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU= github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I= github.com/edsrzf/mmap-go v0.0.0-20170320065105-0bce6a688712/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M= -github.com/elazarl/goproxy v1.7.0 h1:EXv2nV4EjM60ZtsEVLYJG4oBXhDGutMKperpHsZ/v+0= -github.com/elazarl/goproxy v1.7.0/go.mod h1:X/5W/t+gzDyLfHW4DrMdpjqYjpXsURlBt9lpBDxZZZQ= +github.com/elazarl/goproxy v1.7.1 h1:1P7LPSxbqtNxusFnXclj6O56pjfq1xOQZ6a0mwwKUlY= +github.com/elazarl/goproxy v1.7.1/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE= github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= @@ -515,8 +515,9 @@ github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ= github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= @@ -584,8 +585,8 @@ github.com/grafana/grafana-aws-sdk v0.31.5 h1:4HpMQx7n4Qqoi7Bgu8KHQ2QKT9fYYdHilX github.com/grafana/grafana-aws-sdk v0.31.5/go.mod h1:5p4Cjyr5ZiR6/RT2nFWkJ8XpIKgX4lAUmUMu70m2yCM= github.com/grafana/grafana-azure-sdk-go/v2 v2.1.6 h1:OfCkitCuomzZKW1WYHrG8MxKwtMhALb7jqoj+487eTg= github.com/grafana/grafana-azure-sdk-go/v2 v2.1.6/go.mod h1:V7y2BmsWxS3A9Ohebwn4OiSfJJqi//4JQydQ8fHTduo= -github.com/grafana/grafana-plugin-sdk-go v0.266.0 h1:YP+iEpXH3HRX9Xo4NHjsrJhN2W7uVTtkLNzMHYbmiLI= -github.com/grafana/grafana-plugin-sdk-go v0.266.0/go.mod h1:bxkXrBQ4QSmOncsWdIOcpgP+M6wajQNMAPXlbWrqAWY= +github.com/grafana/grafana-plugin-sdk-go v0.267.0 h1:4AcuLEE0UeFG0Eo3P8r1FDS89hgZZ73K550DRz1fgjQ= +github.com/grafana/grafana-plugin-sdk-go v0.267.0/go.mod h1:OuwS4c/JYgn0rr/w5zhJBpLo4gKm/vw15RsfpYAvK9Q= github.com/grafana/grafana/pkg/aggregator v0.0.0-20250220163425-b4c4b9abbdc8 h1:9qOLpC21AmXZqZ6rUhrBWl2mVqS3CzV53pzw0BCuHt0= github.com/grafana/grafana/pkg/aggregator v0.0.0-20250220163425-b4c4b9abbdc8/go.mod h1:deLQ/ywLvpVGbncRGUA4UDGt8a5Ei9sivOP+x6AQ2ko= github.com/grafana/grafana/pkg/promlib v0.0.8 h1:VUWsqttdf0wMI4j9OX9oNrykguQpZcruudDAFpJJVw0= @@ -1552,8 +1553,8 @@ google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGj google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.36.4 h1:6A3ZDJHn/eNqc1i+IdefRzy/9PokBTPvcqMySR7NNIM= -google.golang.org/protobuf v1.36.4/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= +google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk= gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk= diff --git a/pkg/storage/unified/resource/go.mod b/pkg/storage/unified/resource/go.mod index 42bb07c9e45..75b69ced369 100644 --- a/pkg/storage/unified/resource/go.mod +++ b/pkg/storage/unified/resource/go.mod @@ -15,7 +15,7 @@ require ( github.com/grafana/authlib/types v0.0.0-20250224151205-5ef97131cc82 github.com/grafana/dskit v0.0.0-20241105154643-a6b453a88040 github.com/grafana/grafana v11.4.0-00010101000000-000000000000+incompatible - github.com/grafana/grafana-plugin-sdk-go v0.266.0 + github.com/grafana/grafana-plugin-sdk-go v0.267.0 github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250220154326-6e5de80ef295 github.com/grafana/grafana/pkg/apiserver v0.0.0-20250220154326-6e5de80ef295 // indirect github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.2.0 @@ -28,7 +28,7 @@ require ( gocloud.dev v0.40.0 golang.org/x/sync v0.11.0 google.golang.org/grpc v1.70.0 - google.golang.org/protobuf v1.36.4 + google.golang.org/protobuf v1.36.5 k8s.io/apimachinery v0.32.1 ) @@ -82,7 +82,7 @@ require ( github.com/coreos/go-systemd/v22 v22.5.0 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect - github.com/elazarl/goproxy v1.7.0 // indirect + github.com/elazarl/goproxy v1.7.1 // indirect github.com/emicklei/go-restful/v3 v3.11.0 // indirect github.com/fatih/color v1.17.0 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect @@ -110,7 +110,7 @@ require ( github.com/google/btree v1.1.3 // indirect github.com/google/flatbuffers v24.3.25+incompatible // indirect github.com/google/gnostic-models v0.6.8 // indirect - github.com/google/go-cmp v0.6.0 // indirect + github.com/google/go-cmp v0.7.0 // indirect github.com/google/gofuzz v1.2.0 // indirect github.com/google/s2a-go v0.1.8 // indirect github.com/google/wire v0.6.0 // indirect diff --git a/pkg/storage/unified/resource/go.sum b/pkg/storage/unified/resource/go.sum index 2bc9e773911..83d766f41aa 100644 --- a/pkg/storage/unified/resource/go.sum +++ b/pkg/storage/unified/resource/go.sum @@ -216,8 +216,8 @@ github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5m github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU= github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I= github.com/edsrzf/mmap-go v0.0.0-20170320065105-0bce6a688712/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M= -github.com/elazarl/goproxy v1.7.0 h1:EXv2nV4EjM60ZtsEVLYJG4oBXhDGutMKperpHsZ/v+0= -github.com/elazarl/goproxy v1.7.0/go.mod h1:X/5W/t+gzDyLfHW4DrMdpjqYjpXsURlBt9lpBDxZZZQ= +github.com/elazarl/goproxy v1.7.1 h1:1P7LPSxbqtNxusFnXclj6O56pjfq1xOQZ6a0mwwKUlY= +github.com/elazarl/goproxy v1.7.1/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE= github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= @@ -361,8 +361,9 @@ github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ= github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= github.com/google/go-replayers/grpcreplay v1.3.0 h1:1Keyy0m1sIpqstQmgz307zhiJ1pV4uIlFds5weTmxbo= @@ -413,8 +414,8 @@ github.com/grafana/grafana-aws-sdk v0.31.5 h1:4HpMQx7n4Qqoi7Bgu8KHQ2QKT9fYYdHilX github.com/grafana/grafana-aws-sdk v0.31.5/go.mod h1:5p4Cjyr5ZiR6/RT2nFWkJ8XpIKgX4lAUmUMu70m2yCM= github.com/grafana/grafana-azure-sdk-go/v2 v2.1.6 h1:OfCkitCuomzZKW1WYHrG8MxKwtMhALb7jqoj+487eTg= github.com/grafana/grafana-azure-sdk-go/v2 v2.1.6/go.mod h1:V7y2BmsWxS3A9Ohebwn4OiSfJJqi//4JQydQ8fHTduo= -github.com/grafana/grafana-plugin-sdk-go v0.266.0 h1:YP+iEpXH3HRX9Xo4NHjsrJhN2W7uVTtkLNzMHYbmiLI= -github.com/grafana/grafana-plugin-sdk-go v0.266.0/go.mod h1:bxkXrBQ4QSmOncsWdIOcpgP+M6wajQNMAPXlbWrqAWY= +github.com/grafana/grafana-plugin-sdk-go v0.267.0 h1:4AcuLEE0UeFG0Eo3P8r1FDS89hgZZ73K550DRz1fgjQ= +github.com/grafana/grafana-plugin-sdk-go v0.267.0/go.mod h1:OuwS4c/JYgn0rr/w5zhJBpLo4gKm/vw15RsfpYAvK9Q= github.com/grafana/otel-profiling-go v0.5.1 h1:stVPKAFZSa7eGiqbYuG25VcqYksR6iWvF3YH66t4qL8= github.com/grafana/otel-profiling-go v0.5.1/go.mod h1:ftN/t5A/4gQI19/8MoWurBEtC6gFw8Dns1sJZ9W4Tls= github.com/grafana/pyroscope-go/godeltaprof v0.1.8 h1:iwOtYXeeVSAeYefJNaxDytgjKtUuKQbJqgAIjlnicKg= @@ -1021,8 +1022,8 @@ google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpAD google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.36.4 h1:6A3ZDJHn/eNqc1i+IdefRzy/9PokBTPvcqMySR7NNIM= -google.golang.org/protobuf v1.36.4/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= +google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk= gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk= From 58457d41d3257d712b8fa88c9c88b83793bcdb80 Mon Sep 17 00:00:00 2001 From: Ryan McKinley Date: Thu, 27 Feb 2025 13:27:28 +0300 Subject: [PATCH 05/32] K8s/DualWriter: Remove legacy interface (#101395) --- pkg/apiserver/rest/dualwriter.go | 41 ++++----- pkg/apiserver/rest/dualwriter_mode1.go | 4 +- pkg/apiserver/rest/dualwriter_mode1_test.go | 48 +++++----- pkg/apiserver/rest/dualwriter_mode2.go | 10 +- pkg/apiserver/rest/dualwriter_mode2_test.go | 24 ++--- pkg/apiserver/rest/dualwriter_mode3.go | 4 +- pkg/apiserver/rest/dualwriter_mode3_test.go | 28 +++--- pkg/apiserver/rest/dualwriter_syncer.go | 2 +- pkg/apiserver/rest/dualwriter_syncer_test.go | 8 +- pkg/apiserver/rest/dualwriter_test.go | 4 +- pkg/apiserver/rest/storage_mocks_test.go | 91 ------------------- .../notifications/receiver/legacy_storage.go | 4 +- .../routingtree/legacy_storage.go | 4 +- .../templategroup/legacy_storage.go | 5 +- .../timeinterval/legacy_storage.go | 4 +- pkg/registry/apis/dashboard/legacy_storage.go | 2 +- pkg/registry/apis/dashboard/register.go | 5 +- pkg/registry/apps/playlist/register.go | 2 +- pkg/services/apiserver/builder/helper.go | 2 +- .../apiserver/builder/runner/builder.go | 2 +- pkg/storage/legacysql/dualwrite/mock.go | 2 +- pkg/storage/legacysql/dualwrite/runtime.go | 4 +- .../legacysql/dualwrite/runtime_test.go | 12 +-- pkg/storage/legacysql/dualwrite/static.go | 2 +- .../legacysql/dualwrite/storage_mocks_test.go | 91 ------------------- pkg/storage/legacysql/dualwrite/types.go | 2 +- 26 files changed, 108 insertions(+), 299 deletions(-) diff --git a/pkg/apiserver/rest/dualwriter.go b/pkg/apiserver/rest/dualwriter.go index 15be881b250..31ca7e9c97e 100644 --- a/pkg/apiserver/rest/dualwriter.go +++ b/pkg/apiserver/rest/dualwriter.go @@ -26,8 +26,18 @@ var ( _ rest.SingularNameProvider = (DualWriter)(nil) ) +type dualWriteContextKey struct{} + +func IsDualWriteUpdate(ctx context.Context) bool { + return ctx.Value(dualWriteContextKey{}) == true +} + +func WithDualWriteUpdate(ctx context.Context) context.Context { + return context.WithValue(ctx, dualWriteContextKey{}, true) +} + // Function that will create a dual writer -type DualWriteBuilder func(gr schema.GroupResource, legacy LegacyStorage, storage Storage) (Storage, error) +type DualWriteBuilder func(gr schema.GroupResource, legacy Storage, unified Storage) (Storage, error) // Storage is a storage implementation that satisfies the same interfaces as genericregistry.Store. type Storage interface { @@ -36,26 +46,12 @@ type Storage interface { rest.TableConvertor rest.SingularNameProvider rest.Getter - // TODO: when watch is implemented, we can replace all the below with rest.StandardStorage rest.Lister rest.CreaterUpdater rest.GracefulDeleter rest.CollectionDeleter } -// LegacyStorage is a storage implementation that writes to the Grafana SQL database. -type LegacyStorage interface { - rest.Storage - rest.Scoper - rest.SingularNameProvider - rest.CreaterUpdater - rest.Lister - rest.GracefulDeleter - rest.CollectionDeleter - rest.TableConvertor - rest.Getter -} - // DualWriter is a storage implementation that writes first to LegacyStorage and then to Storage. // If writing to LegacyStorage fails, the write to Storage is skipped and the error is returned. // Storage is used for all read operations. This is useful as a migration step from SQL based @@ -79,7 +75,6 @@ type LegacyStorage interface { type DualWriter interface { Storage - LegacyStorage Mode() DualWriterMode } @@ -110,8 +105,8 @@ const ( // NewDualWriter returns a new DualWriter. func NewDualWriter( mode DualWriterMode, - legacy LegacyStorage, - storage Storage, + legacy Storage, + unified Storage, reg prometheus.Registerer, resource string, ) Storage { @@ -122,17 +117,17 @@ func NewDualWriter( return legacy case Mode1: // read and write only from legacy storage - return newDualWriterMode1(legacy, storage, metrics, resource) + return newDualWriterMode1(legacy, unified, metrics, resource) case Mode2: // write to both, read from storage but use legacy as backup - return newDualWriterMode2(legacy, storage, metrics, resource) + return newDualWriterMode2(legacy, unified, metrics, resource) case Mode3: // write to both, read from storage only - return newDualWriterMode3(legacy, storage, metrics, resource) + return newDualWriterMode3(legacy, unified, metrics, resource) case Mode4, Mode5: - return storage + return unified default: - return newDualWriterMode1(legacy, storage, metrics, resource) + return newDualWriterMode1(legacy, unified, metrics, resource) } } diff --git a/pkg/apiserver/rest/dualwriter_mode1.go b/pkg/apiserver/rest/dualwriter_mode1.go index 2ce24e52008..7395fc5d13b 100644 --- a/pkg/apiserver/rest/dualwriter_mode1.go +++ b/pkg/apiserver/rest/dualwriter_mode1.go @@ -15,7 +15,7 @@ import ( ) type DualWriterMode1 struct { - Legacy LegacyStorage + Legacy Storage Storage Storage *dualWriterMetrics resource string @@ -26,7 +26,7 @@ const mode1Str = "1" // NewDualWriterMode1 returns a new DualWriter in mode 1. // Mode 1 represents writing to and reading from LegacyStorage. -func newDualWriterMode1(legacy LegacyStorage, storage Storage, dwm *dualWriterMetrics, resource string) *DualWriterMode1 { +func newDualWriterMode1(legacy Storage, storage Storage, dwm *dualWriterMetrics, resource string) *DualWriterMode1 { return &DualWriterMode1{ Legacy: legacy, Storage: storage, diff --git a/pkg/apiserver/rest/dualwriter_mode1_test.go b/pkg/apiserver/rest/dualwriter_mode1_test.go index e5504e4df4e..398b25a3acc 100644 --- a/pkg/apiserver/rest/dualwriter_mode1_test.go +++ b/pkg/apiserver/rest/dualwriter_mode1_test.go @@ -60,10 +60,10 @@ func TestMode1_Create(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - l := (LegacyStorage)(nil) + l := (Storage)(nil) s := (Storage)(nil) - ls := legacyStoreMock{&mock.Mock{}, l} + ls := storageMock{&mock.Mock{}, l} us := storageMock{&mock.Mock{}, s} if tt.setupLegacyFn != nil { @@ -122,10 +122,10 @@ func TestMode1_CreateOnUnifiedStorage(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - l := (LegacyStorage)(nil) + l := (Storage)(nil) s := (Storage)(nil) - ls := legacyStoreMock{&mock.Mock{}, l} + ls := storageMock{&mock.Mock{}, l} us := storageMock{&mock.Mock{}, s} if tt.setupLegacyFn != nil { @@ -190,10 +190,10 @@ func TestMode1_Get(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - l := (LegacyStorage)(nil) + l := (Storage)(nil) s := (Storage)(nil) - ls := legacyStoreMock{&mock.Mock{}, l} + ls := storageMock{&mock.Mock{}, l} us := storageMock{&mock.Mock{}, s} if tt.setupLegacyFn != nil { @@ -251,10 +251,10 @@ func TestMode1_GetFromUnifiedStorage(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - l := (LegacyStorage)(nil) + l := (Storage)(nil) s := (Storage)(nil) - ls := legacyStoreMock{&mock.Mock{}, l} + ls := storageMock{&mock.Mock{}, l} us := storageMock{&mock.Mock{}, s} if tt.setupLegacyFn != nil { @@ -308,10 +308,10 @@ func TestMode1_List(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - l := (LegacyStorage)(nil) + l := (Storage)(nil) s := (Storage)(nil) - ls := legacyStoreMock{&mock.Mock{}, l} + ls := storageMock{&mock.Mock{}, l} us := storageMock{&mock.Mock{}, s} if tt.setupLegacyFn != nil { @@ -365,10 +365,10 @@ func TestMode1_ListFromUnifiedStorage(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - l := (LegacyStorage)(nil) + l := (Storage)(nil) s := (Storage)(nil) - ls := legacyStoreMock{&mock.Mock{}, l} + ls := storageMock{&mock.Mock{}, l} us := storageMock{&mock.Mock{}, s} if tt.setupLegacyFn != nil { @@ -434,10 +434,10 @@ func TestMode1_Delete(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - l := (LegacyStorage)(nil) + l := (Storage)(nil) s := (Storage)(nil) - ls := legacyStoreMock{&mock.Mock{}, l} + ls := storageMock{&mock.Mock{}, l} us := storageMock{&mock.Mock{}, s} if tt.setupLegacyFn != nil { @@ -494,10 +494,10 @@ func TestMode1_DeleteFromUnifiedStorage(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - l := (LegacyStorage)(nil) + l := (Storage)(nil) s := (Storage)(nil) - ls := legacyStoreMock{&mock.Mock{}, l} + ls := storageMock{&mock.Mock{}, l} us := storageMock{&mock.Mock{}, s} if tt.setupLegacyFn != nil { @@ -565,10 +565,10 @@ func TestMode1_DeleteCollection(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - l := (LegacyStorage)(nil) + l := (Storage)(nil) s := (Storage)(nil) - ls := legacyStoreMock{&mock.Mock{}, l} + ls := storageMock{&mock.Mock{}, l} us := storageMock{&mock.Mock{}, s} if tt.setupLegacyFn != nil { @@ -626,10 +626,10 @@ func TestMode1_DeleteCollectionFromUnifiedStorage(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - l := (LegacyStorage)(nil) + l := (Storage)(nil) s := (Storage)(nil) - ls := legacyStoreMock{&mock.Mock{}, l} + ls := storageMock{&mock.Mock{}, l} us := storageMock{&mock.Mock{}, s} if tt.setupLegacyFn != nil { @@ -695,10 +695,10 @@ func TestMode1_Update(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - l := (LegacyStorage)(nil) + l := (Storage)(nil) s := (Storage)(nil) - ls := legacyStoreMock{&mock.Mock{}, l} + ls := storageMock{&mock.Mock{}, l} us := storageMock{&mock.Mock{}, s} if tt.setupLegacyFn != nil { @@ -761,10 +761,10 @@ func TestMode1_UpdateOnUnifiedStorage(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - l := (LegacyStorage)(nil) + l := (Storage)(nil) s := (Storage)(nil) - ls := legacyStoreMock{&mock.Mock{}, l} + ls := storageMock{&mock.Mock{}, l} us := storageMock{&mock.Mock{}, s} if tt.setupLegacyFn != nil { diff --git a/pkg/apiserver/rest/dualwriter_mode2.go b/pkg/apiserver/rest/dualwriter_mode2.go index 84e841270e5..17aae84e2fb 100644 --- a/pkg/apiserver/rest/dualwriter_mode2.go +++ b/pkg/apiserver/rest/dualwriter_mode2.go @@ -16,15 +16,9 @@ import ( "github.com/grafana/grafana/pkg/apimachinery/utils" ) -type dualWriteContextKey struct{} - -func IsDualWriteUpdate(ctx context.Context) bool { - return ctx.Value(dualWriteContextKey{}) == true -} - type DualWriterMode2 struct { Storage Storage - Legacy LegacyStorage + Legacy Storage *dualWriterMetrics resource string Log klog.Logger @@ -35,7 +29,7 @@ const mode2Str = "2" // newDualWriterMode2 returns a new DualWriter in mode 2. // Mode 2 represents writing to LegacyStorage first, then to Storage. // When reading, values from LegacyStorage will be returned. -func newDualWriterMode2(legacy LegacyStorage, storage Storage, dwm *dualWriterMetrics, resource string) *DualWriterMode2 { +func newDualWriterMode2(legacy Storage, storage Storage, dwm *dualWriterMetrics, resource string) *DualWriterMode2 { return &DualWriterMode2{ Legacy: legacy, Storage: storage, diff --git a/pkg/apiserver/rest/dualwriter_mode2_test.go b/pkg/apiserver/rest/dualwriter_mode2_test.go index dfd7b6db0f5..b0d653053df 100644 --- a/pkg/apiserver/rest/dualwriter_mode2_test.go +++ b/pkg/apiserver/rest/dualwriter_mode2_test.go @@ -54,10 +54,10 @@ func TestMode2_Create(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - l := (LegacyStorage)(nil) + l := (Storage)(nil) s := (Storage)(nil) - ls := legacyStoreMock{&mock.Mock{}, l} + ls := storageMock{&mock.Mock{}, l} us := storageMock{&mock.Mock{}, s} if tt.setupLegacyFn != nil { @@ -138,10 +138,10 @@ func TestMode2_Get(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - l := (LegacyStorage)(nil) + l := (Storage)(nil) s := (Storage)(nil) - ls := legacyStoreMock{&mock.Mock{}, l} + ls := storageMock{&mock.Mock{}, l} us := storageMock{&mock.Mock{}, s} if tt.setupLegacyFn != nil { @@ -212,10 +212,10 @@ func TestMode2_List(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - l := (LegacyStorage)(nil) + l := (Storage)(nil) s := (Storage)(nil) - ls := legacyStoreMock{&mock.Mock{}, l} + ls := storageMock{&mock.Mock{}, l} us := storageMock{&mock.Mock{}, s} if tt.setupLegacyFn != nil { @@ -313,10 +313,10 @@ func TestMode2_Delete(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - l := (LegacyStorage)(nil) + l := (Storage)(nil) s := (Storage)(nil) - ls := legacyStoreMock{&mock.Mock{}, l} + ls := storageMock{&mock.Mock{}, l} us := storageMock{&mock.Mock{}, s} if tt.setupLegacyFn != nil { @@ -382,10 +382,10 @@ func TestMode2_DeleteCollection(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - l := (LegacyStorage)(nil) + l := (Storage)(nil) s := (Storage)(nil) - ls := legacyStoreMock{&mock.Mock{}, l} + ls := storageMock{&mock.Mock{}, l} us := storageMock{&mock.Mock{}, s} if tt.setupLegacyFn != nil { @@ -451,10 +451,10 @@ func TestMode2_Update(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - l := (LegacyStorage)(nil) + l := (Storage)(nil) s := (Storage)(nil) - ls := legacyStoreMock{&mock.Mock{}, l} + ls := storageMock{&mock.Mock{}, l} us := storageMock{&mock.Mock{}, s} if tt.setupLegacyFn != nil { diff --git a/pkg/apiserver/rest/dualwriter_mode3.go b/pkg/apiserver/rest/dualwriter_mode3.go index bd28ef3c31c..db61f0ed7f7 100644 --- a/pkg/apiserver/rest/dualwriter_mode3.go +++ b/pkg/apiserver/rest/dualwriter_mode3.go @@ -17,7 +17,7 @@ import ( ) type DualWriterMode3 struct { - Legacy LegacyStorage + Legacy Storage Storage Storage watchImp rest.Watcher // watch is only available in mode 3 and 4 *dualWriterMetrics @@ -27,7 +27,7 @@ type DualWriterMode3 struct { // newDualWriterMode3 returns a new DualWriter in mode 3. // Mode 3 represents writing to LegacyStorage and Storage and reading from Storage. -func newDualWriterMode3(legacy LegacyStorage, storage Storage, dwm *dualWriterMetrics, resource string) *DualWriterMode3 { +func newDualWriterMode3(legacy Storage, storage Storage, dwm *dualWriterMetrics, resource string) *DualWriterMode3 { return &DualWriterMode3{ Legacy: legacy, Storage: storage, diff --git a/pkg/apiserver/rest/dualwriter_mode3_test.go b/pkg/apiserver/rest/dualwriter_mode3_test.go index f68287f4ead..832c8957aeb 100644 --- a/pkg/apiserver/rest/dualwriter_mode3_test.go +++ b/pkg/apiserver/rest/dualwriter_mode3_test.go @@ -61,10 +61,10 @@ func TestMode3_Create(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - l := (LegacyStorage)(nil) + l := (Storage)(nil) s := (Storage)(nil) - ls := legacyStoreMock{&mock.Mock{}, l} + ls := storageMock{&mock.Mock{}, l} us := storageMock{&mock.Mock{}, s} if tt.setupLegacyFn != nil { @@ -128,10 +128,10 @@ func TestMode3_Get(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - l := (LegacyStorage)(nil) + l := (Storage)(nil) s := (Storage)(nil) - ls := legacyStoreMock{&mock.Mock{}, l} + ls := storageMock{&mock.Mock{}, l} us := storageMock{&mock.Mock{}, s} if tt.setupLegacyFn != nil { @@ -187,10 +187,10 @@ func TestMode1_GetFromLegacyStorage(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - l := (LegacyStorage)(nil) + l := (Storage)(nil) s := (Storage)(nil) - ls := legacyStoreMock{&mock.Mock{}, l} + ls := storageMock{&mock.Mock{}, l} us := storageMock{&mock.Mock{}, s} if tt.setupLegacyFn != nil { @@ -234,10 +234,10 @@ func TestMode3_List(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - l := (LegacyStorage)(nil) + l := (Storage)(nil) s := (Storage)(nil) - ls := legacyStoreMock{&mock.Mock{}, l} + ls := storageMock{&mock.Mock{}, l} us := storageMock{&mock.Mock{}, s} if tt.setupStorageFn != nil { @@ -311,10 +311,10 @@ func TestMode3_Delete(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - l := (LegacyStorage)(nil) + l := (Storage)(nil) s := (Storage)(nil) - ls := legacyStoreMock{&mock.Mock{}, l} + ls := storageMock{&mock.Mock{}, l} us := storageMock{&mock.Mock{}, s} if tt.setupLegacyFn != nil { @@ -380,10 +380,10 @@ func TestMode3_DeleteCollection(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - l := (LegacyStorage)(nil) + l := (Storage)(nil) s := (Storage)(nil) - ls := legacyStoreMock{&mock.Mock{}, l} + ls := storageMock{&mock.Mock{}, l} us := storageMock{&mock.Mock{}, s} if tt.setupLegacyFn != nil { @@ -449,10 +449,10 @@ func TestMode3_Update(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - l := (LegacyStorage)(nil) + l := (Storage)(nil) s := (Storage)(nil) - ls := legacyStoreMock{&mock.Mock{}, l} + ls := storageMock{&mock.Mock{}, l} us := storageMock{&mock.Mock{}, s} if tt.setupLegacyFn != nil { diff --git a/pkg/apiserver/rest/dualwriter_syncer.go b/pkg/apiserver/rest/dualwriter_syncer.go index 9c36fdd22b7..b15cf5376ac 100644 --- a/pkg/apiserver/rest/dualwriter_syncer.go +++ b/pkg/apiserver/rest/dualwriter_syncer.go @@ -33,7 +33,7 @@ type SyncerConfig struct { RequestInfo *request.RequestInfo Mode DualWriterMode - LegacyStorage LegacyStorage + LegacyStorage Storage Storage Storage ServerLockService ServerLockService diff --git a/pkg/apiserver/rest/dualwriter_syncer_test.go b/pkg/apiserver/rest/dualwriter_syncer_test.go index d9d94cea54c..c1e0a216e9f 100644 --- a/pkg/apiserver/rest/dualwriter_syncer_test.go +++ b/pkg/apiserver/rest/dualwriter_syncer_test.go @@ -181,12 +181,12 @@ func TestLegacyToUnifiedStorage_DataSyncer(t *testing.T) { // mode 1 for _, tt := range tests { t.Run("Mode-1-"+tt.name, func(t *testing.T) { - l := (LegacyStorage)(nil) + l := (Storage)(nil) s := (Storage)(nil) lm := &mock.Mock{} um := &mock.Mock{} - ls := legacyStoreMock{lm, l} + ls := storageMock{lm, l} us := storageMock{um, s} if tt.setupLegacyFn != nil { @@ -221,12 +221,12 @@ func TestLegacyToUnifiedStorage_DataSyncer(t *testing.T) { // mode 2 for _, tt := range tests { t.Run("Mode-2-"+tt.name, func(t *testing.T) { - l := (LegacyStorage)(nil) + l := (Storage)(nil) s := (Storage)(nil) lm := &mock.Mock{} um := &mock.Mock{} - ls := legacyStoreMock{lm, l} + ls := storageMock{lm, l} us := storageMock{um, s} if tt.setupLegacyFn != nil { diff --git a/pkg/apiserver/rest/dualwriter_test.go b/pkg/apiserver/rest/dualwriter_test.go index 13fc007dc53..3faf131424e 100644 --- a/pkg/apiserver/rest/dualwriter_test.go +++ b/pkg/apiserver/rest/dualwriter_test.go @@ -64,7 +64,7 @@ func TestSetDualWritingMode(t *testing.T) { } for _, tt := range tests { - l := (LegacyStorage)(nil) + l := (Storage)(nil) s := (Storage)(nil) sm := &mock.Mock{} @@ -75,7 +75,7 @@ func TestSetDualWritingMode(t *testing.T) { lm := &mock.Mock{} lm.On("List", mock.Anything, mock.Anything).Return(exampleList, nil) - ls := legacyStoreMock{lm, l} + ls := storageMock{lm, l} serverLockSvc := &fakeServerLock{ err: tt.serverLockError, diff --git a/pkg/apiserver/rest/storage_mocks_test.go b/pkg/apiserver/rest/storage_mocks_test.go index 582bcdc067c..3baaa14c526 100644 --- a/pkg/apiserver/rest/storage_mocks_test.go +++ b/pkg/apiserver/rest/storage_mocks_test.go @@ -11,102 +11,11 @@ import ( "k8s.io/apiserver/pkg/registry/rest" ) -type legacyStoreMock struct { - *mock.Mock - LegacyStorage -} - type storageMock struct { *mock.Mock Storage } -func (m legacyStoreMock) Get(ctx context.Context, name string, options *metav1.GetOptions) (runtime.Object, error) { - select { - case <-ctx.Done(): - return nil, errors.New("context canceled") - default: - } - - args := m.Called(ctx, name, options) - if err := args.Get(1); err != nil { - return nil, err.(error) - } - return args.Get(0).(runtime.Object), args.Error(1) -} - -func (m legacyStoreMock) Create(ctx context.Context, obj runtime.Object, createValidation rest.ValidateObjectFunc, options *metav1.CreateOptions) (runtime.Object, error) { - select { - case <-ctx.Done(): - return nil, errors.New("context canceled") - default: - } - - args := m.Called(ctx, obj, createValidation, options) - if err := args.Get(1); err != nil { - return nil, err.(error) - } - return args.Get(0).(runtime.Object), args.Error(1) -} - -func (m legacyStoreMock) List(ctx context.Context, options *metainternalversion.ListOptions) (runtime.Object, error) { - select { - case <-ctx.Done(): - return nil, errors.New("context canceled") - default: - } - - args := m.Called(ctx, options) - if err := args.Get(1); err != nil { - return nil, err.(error) - } - return args.Get(0).(runtime.Object), args.Error(1) -} - -func (m legacyStoreMock) NewList() runtime.Object { - return nil -} - -func (m legacyStoreMock) Update(ctx context.Context, name string, objInfo rest.UpdatedObjectInfo, createValidation rest.ValidateObjectFunc, updateValidation rest.ValidateObjectUpdateFunc, forceAllowCreate bool, options *metav1.UpdateOptions) (runtime.Object, bool, error) { - select { - case <-ctx.Done(): - return nil, false, errors.New("context canceled") - default: - } - args := m.Called(ctx, name, objInfo, createValidation, updateValidation, forceAllowCreate, options) - if err := args.Get(2); err != nil { - return nil, false, err.(error) - } - return args.Get(0).(runtime.Object), args.Bool(1), args.Error(2) -} - -func (m legacyStoreMock) Delete(ctx context.Context, name string, deleteValidation rest.ValidateObjectFunc, options *metav1.DeleteOptions) (runtime.Object, bool, error) { - select { - case <-ctx.Done(): - return nil, false, errors.New("context canceled") - default: - } - - args := m.Called(ctx, name, deleteValidation, options) - if err := args.Get(2); err != nil { - return nil, false, err.(error) - } - return args.Get(0).(runtime.Object), args.Bool(1), args.Error(2) -} - -func (m legacyStoreMock) DeleteCollection(ctx context.Context, deleteValidation rest.ValidateObjectFunc, options *metav1.DeleteOptions, listOptions *metainternalversion.ListOptions) (runtime.Object, error) { - select { - case <-ctx.Done(): - return nil, errors.New("context canceled") - default: - } - args := m.Called(ctx, deleteValidation, options, listOptions) - if err := args.Get(1); err != nil { - return nil, err.(error) - } - return args.Get(0).(runtime.Object), args.Error(1) -} - // Unified Store func (m storageMock) Get(ctx context.Context, name string, options *metav1.GetOptions) (runtime.Object, error) { select { diff --git a/pkg/registry/apis/alerting/notifications/receiver/legacy_storage.go b/pkg/registry/apis/alerting/notifications/receiver/legacy_storage.go index 6b1377dc1f3..65f5593cf3a 100644 --- a/pkg/registry/apis/alerting/notifications/receiver/legacy_storage.go +++ b/pkg/registry/apis/alerting/notifications/receiver/legacy_storage.go @@ -13,7 +13,7 @@ import ( model "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/resource/receiver/v0alpha1" "github.com/grafana/grafana/pkg/apimachinery/identity" - grafanaRest "github.com/grafana/grafana/pkg/apiserver/rest" + grafanarest "github.com/grafana/grafana/pkg/apiserver/rest" "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request" alertingac "github.com/grafana/grafana/pkg/services/ngalert/accesscontrol" "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" @@ -22,7 +22,7 @@ import ( ) var ( - _ grafanaRest.LegacyStorage = (*legacyStorage)(nil) + _ grafanarest.Storage = (*legacyStorage)(nil) ) type ReceiverService interface { diff --git a/pkg/registry/apis/alerting/notifications/routingtree/legacy_storage.go b/pkg/registry/apis/alerting/notifications/routingtree/legacy_storage.go index 00c20880212..07f5138aca8 100644 --- a/pkg/registry/apis/alerting/notifications/routingtree/legacy_storage.go +++ b/pkg/registry/apis/alerting/notifications/routingtree/legacy_storage.go @@ -11,14 +11,14 @@ import ( "k8s.io/apiserver/pkg/registry/rest" model "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/resource/routingtree/v0alpha1" - grafanaRest "github.com/grafana/grafana/pkg/apiserver/rest" + grafanarest "github.com/grafana/grafana/pkg/apiserver/rest" "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request" "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" alerting_models "github.com/grafana/grafana/pkg/services/ngalert/models" ) var ( - _ grafanaRest.LegacyStorage = (*legacyStorage)(nil) + _ grafanarest.Storage = (*legacyStorage)(nil) ) type RouteService interface { diff --git a/pkg/registry/apis/alerting/notifications/templategroup/legacy_storage.go b/pkg/registry/apis/alerting/notifications/templategroup/legacy_storage.go index a492c4b3b49..ada3f463820 100644 --- a/pkg/registry/apis/alerting/notifications/templategroup/legacy_storage.go +++ b/pkg/registry/apis/alerting/notifications/templategroup/legacy_storage.go @@ -4,13 +4,14 @@ import ( "context" "fmt" - "github.com/grafana/alerting/templates" "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/apis/meta/internalversion" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apiserver/pkg/registry/rest" + "github.com/grafana/alerting/templates" + model "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/resource/templategroup/v0alpha1" grafanarest "github.com/grafana/grafana/pkg/apiserver/rest" "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request" @@ -19,7 +20,7 @@ import ( ) var ( - _ grafanarest.LegacyStorage = (*legacyStorage)(nil) + _ grafanarest.Storage = (*legacyStorage)(nil) ) type TemplateService interface { diff --git a/pkg/registry/apis/alerting/notifications/timeinterval/legacy_storage.go b/pkg/registry/apis/alerting/notifications/timeinterval/legacy_storage.go index 7a7ef74165a..bb382ee9525 100644 --- a/pkg/registry/apis/alerting/notifications/timeinterval/legacy_storage.go +++ b/pkg/registry/apis/alerting/notifications/timeinterval/legacy_storage.go @@ -11,14 +11,14 @@ import ( "k8s.io/apiserver/pkg/registry/rest" model "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/resource/timeinterval/v0alpha1" - grafanaRest "github.com/grafana/grafana/pkg/apiserver/rest" + grafanarest "github.com/grafana/grafana/pkg/apiserver/rest" "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request" "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models" ) var ( - _ grafanaRest.LegacyStorage = (*legacyStorage)(nil) + _ grafanarest.Storage = (*legacyStorage)(nil) ) type TimeIntervalService interface { diff --git a/pkg/registry/apis/dashboard/legacy_storage.go b/pkg/registry/apis/dashboard/legacy_storage.go index 214c2ad2202..b5e033df390 100644 --- a/pkg/registry/apis/dashboard/legacy_storage.go +++ b/pkg/registry/apis/dashboard/legacy_storage.go @@ -28,7 +28,7 @@ type DashboardStorage struct { Features featuremgmt.FeatureToggles } -func (s *DashboardStorage) NewStore(scheme *runtime.Scheme, defaultOptsGetter generic.RESTOptionsGetter, reg prometheus.Registerer) (grafanarest.LegacyStorage, error) { +func (s *DashboardStorage) NewStore(scheme *runtime.Scheme, defaultOptsGetter generic.RESTOptionsGetter, reg prometheus.Registerer) (grafanarest.Storage, error) { server, err := resource.NewResourceServer(resource.ResourceServerOptions{ Backend: s.Access, Reg: reg, diff --git a/pkg/registry/apis/dashboard/register.go b/pkg/registry/apis/dashboard/register.go index c223026576f..a06dfe50a0f 100644 --- a/pkg/registry/apis/dashboard/register.go +++ b/pkg/registry/apis/dashboard/register.go @@ -17,6 +17,8 @@ import ( "k8s.io/kube-openapi/pkg/spec3" "k8s.io/kube-openapi/pkg/validation/spec" + "github.com/prometheus/client_golang/prometheus" + claims "github.com/grafana/authlib/types" "github.com/grafana/grafana/pkg/apimachinery/utils" dashboardinternal "github.com/grafana/grafana/pkg/apis/dashboard" @@ -42,7 +44,6 @@ import ( "github.com/grafana/grafana/pkg/storage/legacysql/dualwrite" "github.com/grafana/grafana/pkg/storage/unified/apistore" "github.com/grafana/grafana/pkg/storage/unified/resource" - "github.com/prometheus/client_golang/prometheus" ) var ( @@ -247,7 +248,7 @@ func (b *DashboardsAPIBuilder) UpdateAPIGroupInfo(apiGroupInfo *genericapiserver func (b *DashboardsAPIBuilder) storageForVersion( opts builder.APIGroupOptions, - legacyStore grafanarest.LegacyStorage, + legacyStore grafanarest.Storage, largeObjects apistore.LargeObjectSupport, newDTOFunc func() runtime.Object, ) (map[string]rest.Storage, error) { diff --git a/pkg/registry/apps/playlist/register.go b/pkg/registry/apps/playlist/register.go index 9f79bb22f1d..a4215d880d4 100644 --- a/pkg/registry/apps/playlist/register.go +++ b/pkg/registry/apps/playlist/register.go @@ -48,7 +48,7 @@ func RegisterApp( return provider } -func (p *PlaylistAppProvider) legacyStorageGetter(requested schema.GroupVersionResource) grafanarest.LegacyStorage { +func (p *PlaylistAppProvider) legacyStorageGetter(requested schema.GroupVersionResource) grafanarest.Storage { gvr := schema.GroupVersionResource{ Group: playlistv0alpha1.PlaylistKind().Group(), Version: playlistv0alpha1.PlaylistKind().Version(), diff --git a/pkg/services/apiserver/builder/helper.go b/pkg/services/apiserver/builder/helper.go index 33e06c2580b..8f6fe0e3783 100644 --- a/pkg/services/apiserver/builder/helper.go +++ b/pkg/services/apiserver/builder/helper.go @@ -286,7 +286,7 @@ func InstallAPIs( // nolint:staticcheck if storageOpts.StorageType != options.StorageTypeLegacy { - dualWrite = func(gr schema.GroupResource, legacy grafanarest.LegacyStorage, storage grafanarest.Storage) (grafanarest.Storage, error) { + dualWrite = func(gr schema.GroupResource, legacy grafanarest.Storage, storage grafanarest.Storage) (grafanarest.Storage, error) { // Dashboards + Folders may be managed (depends on feature toggles and database state) if dualWriteService != nil && dualWriteService.ShouldManage(gr) { return dualWriteService.NewStorage(gr, legacy, storage) // eventually this can replace this whole function diff --git a/pkg/services/apiserver/builder/runner/builder.go b/pkg/services/apiserver/builder/runner/builder.go index 1bc052fb46e..8a7b18a5fe2 100644 --- a/pkg/services/apiserver/builder/runner/builder.go +++ b/pkg/services/apiserver/builder/runner/builder.go @@ -18,7 +18,7 @@ import ( var _ AppBuilder = (*appBuilder)(nil) -type LegacyStorageGetter func(schema.GroupVersionResource) grafanarest.LegacyStorage +type LegacyStorageGetter func(schema.GroupVersionResource) grafanarest.Storage type AppBuilderConfig struct { Authorizer authorizer.Authorizer diff --git a/pkg/storage/legacysql/dualwrite/mock.go b/pkg/storage/legacysql/dualwrite/mock.go index 130c5c0feff..3dbf71a126a 100644 --- a/pkg/storage/legacysql/dualwrite/mock.go +++ b/pkg/storage/legacysql/dualwrite/mock.go @@ -26,7 +26,7 @@ type mockService struct { } // NewStorage implements Service. -func (m *mockService) NewStorage(gr schema.GroupResource, legacy rest.LegacyStorage, storage rest.Storage) (rest.Storage, error) { +func (m *mockService) NewStorage(gr schema.GroupResource, legacy rest.Storage, storage rest.Storage) (rest.Storage, error) { return nil, fmt.Errorf("not implemented") } diff --git a/pkg/storage/legacysql/dualwrite/runtime.go b/pkg/storage/legacysql/dualwrite/runtime.go index 5fe705c3409..33896e669e2 100644 --- a/pkg/storage/legacysql/dualwrite/runtime.go +++ b/pkg/storage/legacysql/dualwrite/runtime.go @@ -15,7 +15,7 @@ import ( ) func (m *service) NewStorage(gr schema.GroupResource, - legacy grafanarest.LegacyStorage, + legacy grafanarest.Storage, storage grafanarest.Storage, ) (grafanarest.Storage, error) { status, err := m.Status(context.Background(), gr) @@ -53,7 +53,7 @@ func (m *service) NewStorage(gr schema.GroupResource, // When a resource is marked as "migrating", all write requests will be 503 unavailable type runtimeDualWriter struct { service Service - legacy grafanarest.LegacyStorage + legacy grafanarest.Storage unified grafanarest.Storage dualwrite grafanarest.Storage gr schema.GroupResource diff --git a/pkg/storage/legacysql/dualwrite/runtime_test.go b/pkg/storage/legacysql/dualwrite/runtime_test.go index c97139948f2..9c148eaf07f 100644 --- a/pkg/storage/legacysql/dualwrite/runtime_test.go +++ b/pkg/storage/legacysql/dualwrite/runtime_test.go @@ -76,10 +76,10 @@ func TestManagedMode3_Create(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - l := (rest.LegacyStorage)(nil) + l := (rest.Storage)(nil) s := (rest.Storage)(nil) - ls := legacyStoreMock{&mock.Mock{}, l} + ls := storageMock{&mock.Mock{}, l} us := storageMock{&mock.Mock{}, s} if tt.setupLegacyFn != nil { @@ -148,10 +148,10 @@ func TestManagedMode3_Get(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - l := (rest.LegacyStorage)(nil) + l := (rest.Storage)(nil) s := (rest.Storage)(nil) - ls := legacyStoreMock{&mock.Mock{}, l} + ls := storageMock{&mock.Mock{}, l} us := storageMock{&mock.Mock{}, s} if tt.setupLegacyFn != nil { @@ -249,10 +249,10 @@ func TestManagedMode3_CreateWhileMigrating(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - l := (rest.LegacyStorage)(nil) + l := (rest.Storage)(nil) s := (rest.Storage)(nil) - ls := legacyStoreMock{&mock.Mock{}, l} + ls := storageMock{&mock.Mock{}, l} us := storageMock{&mock.Mock{}, s} if tt.setupLegacyFn != nil { diff --git a/pkg/storage/legacysql/dualwrite/static.go b/pkg/storage/legacysql/dualwrite/static.go index 64e8c14a85a..d74e2a5b914 100644 --- a/pkg/storage/legacysql/dualwrite/static.go +++ b/pkg/storage/legacysql/dualwrite/static.go @@ -14,7 +14,7 @@ type staticService struct { cfg *setting.Cfg } -func (m *staticService) NewStorage(gr schema.GroupResource, legacy rest.LegacyStorage, storage rest.Storage) (rest.Storage, error) { +func (m *staticService) NewStorage(gr schema.GroupResource, legacy rest.Storage, storage rest.Storage) (rest.Storage, error) { return nil, fmt.Errorf("not implemented") } diff --git a/pkg/storage/legacysql/dualwrite/storage_mocks_test.go b/pkg/storage/legacysql/dualwrite/storage_mocks_test.go index 62ce9be2344..b3905d1c3d3 100644 --- a/pkg/storage/legacysql/dualwrite/storage_mocks_test.go +++ b/pkg/storage/legacysql/dualwrite/storage_mocks_test.go @@ -13,102 +13,11 @@ import ( grafanarest "github.com/grafana/grafana/pkg/apiserver/rest" ) -type legacyStoreMock struct { - *mock.Mock - grafanarest.LegacyStorage -} - type storageMock struct { *mock.Mock grafanarest.Storage } -func (m legacyStoreMock) Get(ctx context.Context, name string, options *metav1.GetOptions) (runtime.Object, error) { - select { - case <-ctx.Done(): - return nil, errors.New("context canceled") - default: - } - - args := m.Called(ctx, name, options) - if err := args.Get(1); err != nil { - return nil, err.(error) - } - return args.Get(0).(runtime.Object), args.Error(1) -} - -func (m legacyStoreMock) Create(ctx context.Context, obj runtime.Object, createValidation rest.ValidateObjectFunc, options *metav1.CreateOptions) (runtime.Object, error) { - select { - case <-ctx.Done(): - return nil, errors.New("context canceled") - default: - } - - args := m.Called(ctx, obj, createValidation, options) - if err := args.Get(1); err != nil { - return nil, err.(error) - } - return args.Get(0).(runtime.Object), args.Error(1) -} - -func (m legacyStoreMock) List(ctx context.Context, options *metainternalversion.ListOptions) (runtime.Object, error) { - select { - case <-ctx.Done(): - return nil, errors.New("context canceled") - default: - } - - args := m.Called(ctx, options) - if err := args.Get(1); err != nil { - return nil, err.(error) - } - return args.Get(0).(runtime.Object), args.Error(1) -} - -func (m legacyStoreMock) NewList() runtime.Object { - return nil -} - -func (m legacyStoreMock) Update(ctx context.Context, name string, objInfo rest.UpdatedObjectInfo, createValidation rest.ValidateObjectFunc, updateValidation rest.ValidateObjectUpdateFunc, forceAllowCreate bool, options *metav1.UpdateOptions) (runtime.Object, bool, error) { - select { - case <-ctx.Done(): - return nil, false, errors.New("context canceled") - default: - } - args := m.Called(ctx, name, objInfo, createValidation, updateValidation, forceAllowCreate, options) - if err := args.Get(2); err != nil { - return nil, false, err.(error) - } - return args.Get(0).(runtime.Object), args.Bool(1), args.Error(2) -} - -func (m legacyStoreMock) Delete(ctx context.Context, name string, deleteValidation rest.ValidateObjectFunc, options *metav1.DeleteOptions) (runtime.Object, bool, error) { - select { - case <-ctx.Done(): - return nil, false, errors.New("context canceled") - default: - } - - args := m.Called(ctx, name, deleteValidation, options) - if err := args.Get(2); err != nil { - return nil, false, err.(error) - } - return args.Get(0).(runtime.Object), args.Bool(1), args.Error(2) -} - -func (m legacyStoreMock) DeleteCollection(ctx context.Context, deleteValidation rest.ValidateObjectFunc, options *metav1.DeleteOptions, listOptions *metainternalversion.ListOptions) (runtime.Object, error) { - select { - case <-ctx.Done(): - return nil, errors.New("context canceled") - default: - } - args := m.Called(ctx, deleteValidation, options, listOptions) - if err := args.Get(1); err != nil { - return nil, err.(error) - } - return args.Get(0).(runtime.Object), args.Error(1) -} - // Unified Store func (m storageMock) Get(ctx context.Context, name string, options *metav1.GetOptions) (runtime.Object, error) { select { diff --git a/pkg/storage/legacysql/dualwrite/types.go b/pkg/storage/legacysql/dualwrite/types.go index 4b43f0d39a8..0478aa5438e 100644 --- a/pkg/storage/legacysql/dualwrite/types.go +++ b/pkg/storage/legacysql/dualwrite/types.go @@ -36,7 +36,7 @@ type Service interface { ShouldManage(gr schema.GroupResource) bool // Create a managed k8s storage instance - NewStorage(gr schema.GroupResource, legacy grafanarest.LegacyStorage, storage grafanarest.Storage) (grafanarest.Storage, error) + NewStorage(gr schema.GroupResource, legacy grafanarest.Storage, storage grafanarest.Storage) (grafanarest.Storage, error) // Check if the dual writes is reading from unified storage (mode3++) ReadFromUnified(ctx context.Context, gr schema.GroupResource) (bool, error) From 03dcd25a3213255930b14ce0e0285c593ce14933 Mon Sep 17 00:00:00 2001 From: Matias Chomicki Date: Thu, 27 Feb 2025 10:31:55 +0000 Subject: [PATCH 06/32] New Logs Panel: Displayed fields support (#100643) * LogList: add displayedFields and getFieldLinks props * Render displayed fields * LogLine: rename function * Refactor log dimensions * Generate styles in parent component * Log List: implement tabular unwrapped logs * Rename class * Log line: center fields * Parametrize field gap * Virtualization: update measurement to support displayed fields * Shorten visible level * Do not calculate dimensions when logs are wrapped * Logs Navigation: fix width when flag is enabled * Pass styles to LogLineMessage * Formatting * Fix unwrapped logs when showTime is off * LogLine: update css selectors for fields --- public/app/features/explore/Logs/Logs.tsx | 2 + .../features/explore/Logs/LogsNavigation.tsx | 2 +- .../logs/components/panel/InfiniteScroll.tsx | 28 +++++- .../logs/components/panel/LogLine.tsx | 86 +++++++++++++++--- .../logs/components/panel/LogLineMessage.tsx | 9 +- .../logs/components/panel/LogList.tsx | 56 ++++++++++-- .../logs/components/panel/processing.ts | 89 ++++++++++++++++--- .../logs/components/panel/virtualization.ts | 24 ++++- .../app/plugins/panel/logs-new/LogsPanel.tsx | 1 + 9 files changed, 250 insertions(+), 47 deletions(-) diff --git a/public/app/features/explore/Logs/Logs.tsx b/public/app/features/explore/Logs/Logs.tsx index feefbb1bee7..24bc46790a5 100644 --- a/public/app/features/explore/Logs/Logs.tsx +++ b/public/app/features/explore/Logs/Logs.tsx @@ -1070,8 +1070,10 @@ const UnthemedLogs: React.FunctionComponent = (props: Props) => { { return { navContainer: css({ maxHeight: navContainerHeight, - width: oldestLogsFirst ? '58px' : 'auto', + width: oldestLogsFirst && !config.featureToggles.newLogsPanel ? '58px' : 'auto', display: 'flex', flexDirection: 'column', justifyContent: config.featureToggles.logsInfiniteScrolling diff --git a/public/app/features/logs/components/panel/InfiniteScroll.tsx b/public/app/features/logs/components/panel/InfiniteScroll.tsx index 4f0e7a9584b..3790f5f0b0f 100644 --- a/public/app/features/logs/components/panel/InfiniteScroll.tsx +++ b/public/app/features/logs/components/panel/InfiniteScroll.tsx @@ -4,12 +4,12 @@ import { ListChildComponentProps, ListOnItemsRenderedProps } from 'react-window' import { AbsoluteTimeRange, LogsSortOrder, TimeRange } from '@grafana/data'; import { config, reportInteraction } from '@grafana/runtime'; -import { Spinner } from '@grafana/ui'; +import { Spinner, useTheme2 } from '@grafana/ui'; import { t } from 'app/core/internationalization'; import { canScrollBottom, getVisibleRange, ScrollDirection, shouldLoadMore } from '../InfiniteScroll'; -import { LogLine } from './LogLine'; +import { getStyles, LogLine } from './LogLine'; import { LogLineMessage } from './LogLineMessage'; import { LogListModel } from './processing'; @@ -22,6 +22,7 @@ interface ChildrenProps { interface Props { children: (props: ChildrenProps) => ReactNode; + displayedFields: string[]; handleOverflow: (index: number, id: string, height: number) => void; loadMore?: (range: AbsoluteTimeRange) => void; logs: LogListModel[]; @@ -38,6 +39,7 @@ type InfiniteLoaderState = 'idle' | 'out-of-bounds' | 'pre-scroll' | 'loading'; export const InfiniteScroll = ({ children, + displayedFields, handleOverflow, loadMore, logs, @@ -57,6 +59,8 @@ export const InfiniteScroll = ({ const lastEvent = useRef(null); const countRef = useRef(0); const lastLogOfPage = useRef([]); + const theme = useTheme2(); + const styles = getStyles(theme); useEffect(() => { // Logs have not changed, ignore effect @@ -132,24 +136,40 @@ export const InfiniteScroll = ({ ({ index, style }: ListChildComponentProps) => { if (!logs[index] && infiniteLoaderState !== 'idle') { return ( - + {getMessageFromInfiniteLoaderState(infiniteLoaderState, sortOrder)} ); } return ( ); }, - [handleOverflow, infiniteLoaderState, logs, onLoadMore, showTime, sortOrder, wrapLogMessage] + [ + displayedFields, + handleOverflow, + infiniteLoaderState, + logs, + onLoadMore, + showTime, + sortOrder, + styles, + wrapLogMessage, + ] ); const onItemsRendered = useCallback( diff --git a/public/app/features/logs/components/panel/LogLine.tsx b/public/app/features/logs/components/panel/LogLine.tsx index bba06ff1ab0..d484675d424 100644 --- a/public/app/features/logs/components/panel/LogLine.tsx +++ b/public/app/features/logs/components/panel/LogLine.tsx @@ -2,24 +2,35 @@ import { css } from '@emotion/css'; import { CSSProperties, useEffect, useRef } from 'react'; import { GrafanaTheme2 } from '@grafana/data'; -import { useTheme2 } from '@grafana/ui'; -import { LogListModel } from './processing'; -import { hasUnderOrOverflow } from './virtualization'; +import { LOG_LINE_BODY_FIELD_NAME } from '../LogDetailsBody'; + +import { LogFieldDimension, LogListModel } from './processing'; +import { FIELD_GAP_MULTIPLIER, hasUnderOrOverflow } from './virtualization'; interface Props { + displayedFields: string[]; index: number; log: LogListModel; showTime: boolean; style: CSSProperties; + styles: LogLineStyles; onOverflow?: (index: number, id: string, height: number) => void; variant?: 'infinite-scroll'; wrapLogMessage: boolean; } -export const LogLine = ({ index, log, style, onOverflow, showTime, variant, wrapLogMessage }: Props) => { - const theme = useTheme2(); - const styles = getStyles(theme); +export const LogLine = ({ + displayedFields, + index, + log, + style, + styles, + onOverflow, + showTime, + variant, + wrapLogMessage, +}: Props) => { const logLineRef = useRef(null); useEffect(() => { @@ -34,16 +45,59 @@ export const LogLine = ({ index, log, style, onOverflow, showTime, variant, wrap }, [index, log.uid, onOverflow, style.height]); return ( -
-
- {showTime && {log.timestamp}} - {log.logLevel && {log.logLevel}} - {log.body} +
+
+
); }; +interface LogProps { + displayedFields: string[]; + log: LogListModel; + showTime: boolean; + styles: ReturnType; +} + +const Log = ({ displayedFields, log, showTime, styles }: LogProps) => { + return ( + <> + {showTime && {log.timestamp}} + {log.displayLevel} + {displayedFields.length > 0 ? ( + displayedFields.map((field) => ( + + {getDisplayedFieldValue(field, log)} + + )) + ) : ( + {log.body} + )} + + ); +}; + +export function getDisplayedFieldValue(fieldName: string, log: LogListModel): string { + if (fieldName === LOG_LINE_BODY_FIELD_NAME) { + return log.body; + } + if (log.labels[fieldName] != null) { + return log.labels[fieldName]; + } + const field = log.fields.find((field) => { + return field.keys[0] === fieldName; + }); + + return field ? field.values.toString() : ''; +} + +export function getGridTemplateColumns(dimensions: LogFieldDimension[]) { + const columns = dimensions.map((dimension) => dimension.width).join('px '); + return `${columns}px 1fr`; +} + +export type LogLineStyles = ReturnType; export const getStyles = (theme: GrafanaTheme2) => { const colors = { critical: '#B877D9', @@ -82,7 +136,6 @@ export const getStyles = (theme: GrafanaTheme2) => { timestamp: css({ color: theme.colors.text.secondary, display: 'inline-block', - marginRight: theme.spacing(1), '&.level-critical': { color: colors.critical, }, @@ -103,7 +156,6 @@ export const getStyles = (theme: GrafanaTheme2) => { color: theme.colors.text.secondary, fontWeight: theme.typography.fontWeightBold, display: 'inline-block', - marginRight: theme.spacing(1), '&.level-critical': { color: colors.critical, }, @@ -129,12 +181,20 @@ export const getStyles = (theme: GrafanaTheme2) => { outline: 'solid 1px red', }), unwrappedLogLine: css({ + display: 'grid', + gridColumnGap: theme.spacing(FIELD_GAP_MULTIPLIER), whiteSpace: 'pre', paddingBottom: theme.spacing(0.75), }), wrappedLogLine: css({ whiteSpace: 'pre-wrap', paddingBottom: theme.spacing(0.75), + '& .field': { + marginRight: theme.spacing(FIELD_GAP_MULTIPLIER), + }, + '& .field:last-child': { + marginRight: 0, + }, }), }; }; diff --git a/public/app/features/logs/components/panel/LogLineMessage.tsx b/public/app/features/logs/components/panel/LogLineMessage.tsx index 2bdff1c03f5..9d2bfba0eed 100644 --- a/public/app/features/logs/components/panel/LogLineMessage.tsx +++ b/public/app/features/logs/components/panel/LogLineMessage.tsx @@ -1,18 +1,15 @@ import { CSSProperties, ReactNode } from 'react'; -import { useTheme2 } from '@grafana/ui'; - -import { getStyles } from './LogLine'; +import { LogLineStyles } from './LogLine'; interface Props { children: ReactNode; onClick?: () => void; style: CSSProperties; + styles: LogLineStyles; } -export const LogLineMessage = ({ children, onClick, style }: Props) => { - const theme = useTheme2(); - const styles = getStyles(theme); +export const LogLineMessage = ({ children, onClick, style, styles }: Props) => { return (
{onClick ? ( diff --git a/public/app/features/logs/components/panel/LogList.tsx b/public/app/features/logs/components/panel/LogList.tsx index 9e42cb73214..abc48cbe9ce 100644 --- a/public/app/features/logs/components/panel/LogList.tsx +++ b/public/app/features/logs/components/panel/LogList.tsx @@ -1,12 +1,24 @@ +import { css } from '@emotion/css'; import { debounce } from 'lodash'; -import { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react'; +import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'; import { VariableSizeList } from 'react-window'; -import { AbsoluteTimeRange, CoreApp, EventBus, LogRowModel, LogsSortOrder, TimeRange } from '@grafana/data'; +import { + AbsoluteTimeRange, + CoreApp, + DataFrame, + EventBus, + Field, + LinkModel, + LogRowModel, + LogsSortOrder, + TimeRange, +} from '@grafana/data'; import { useTheme2 } from '@grafana/ui'; import { InfiniteScroll } from './InfiniteScroll'; -import { preProcessLogs, LogListModel } from './processing'; +import { getGridTemplateColumns } from './LogLine'; +import { preProcessLogs, LogListModel, calculateFieldDimensions, LogFieldDimension } from './processing'; import { getLogLineSize, init as initVirtualization, @@ -15,14 +27,18 @@ import { storeLogLineSize, } from './virtualization'; +export type GetFieldLinksFn = (field: Field, rowIndex: number, dataFrame: DataFrame) => Array>; + interface Props { app: CoreApp; - logs: LogRowModel[]; containerElement: HTMLDivElement; + displayedFields: string[]; eventBus: EventBus; forceEscape?: boolean; + getFieldLinks?: GetFieldLinksFn; initialScrollPosition?: 'top' | 'bottom'; loadMore?: (range: AbsoluteTimeRange) => void; + logs: LogRowModel[]; showTime: boolean; sortOrder: LogsSortOrder; timeRange: TimeRange; @@ -33,8 +49,10 @@ interface Props { export const LogList = ({ app, containerElement, + displayedFields = [], eventBus, forceEscape = false, + getFieldLinks, initialScrollPosition = 'top', loadMore, logs, @@ -52,6 +70,11 @@ export const LogList = ({ const listRef = useRef(null); const widthRef = useRef(containerElement.clientWidth); const scrollRef = useRef(null); + const dimensions = useMemo( + () => (wrapLogMessage ? [] : calculateFieldDimensions(processedLogs, displayedFields)), + [displayedFields, processedLogs, wrapLogMessage] + ); + const styles = getStyles(dimensions, { showTime }); useEffect(() => { initVirtualization(theme); @@ -65,9 +88,11 @@ export const LogList = ({ }, [eventBus, logs.length]); useEffect(() => { - setProcessedLogs(preProcessLogs(logs, { wrap: wrapLogMessage, escape: forceEscape, order: sortOrder, timeZone })); + setProcessedLogs( + preProcessLogs(logs, { getFieldLinks, wrap: wrapLogMessage, escape: forceEscape, order: sortOrder, timeZone }) + ); listRef.current?.resetAfterIndex(0); - }, [forceEscape, logs, sortOrder, timeZone, wrapLogMessage]); + }, [forceEscape, getFieldLinks, logs, sortOrder, timeZone, wrapLogMessage]); useEffect(() => { const handleResize = debounce(() => { @@ -110,6 +135,7 @@ export const LogList = ({ return ( {({ getItemKey, itemCount, onItemsRendered, Renderer }) => ( index > 0); + return { + logList: css({ + '& .unwrapped-log-line': { + display: 'grid', + gridTemplateColumns: getGridTemplateColumns(columns), + }, + }), + }; +} + function handleScrollToEvent(event: ScrollToLogsEvent, logsCount: number, list: VariableSizeList | null) { if (event.payload.scrollTo === 'top') { list?.scrollTo(0); diff --git a/public/app/features/logs/components/panel/processing.ts b/public/app/features/logs/components/panel/processing.ts index 77f525614b5..2d62db31370 100644 --- a/public/app/features/logs/components/panel/processing.ts +++ b/public/app/features/logs/components/panel/processing.ts @@ -1,22 +1,28 @@ -import { dateTimeFormat, LogRowModel, LogsSortOrder } from '@grafana/data'; +import { dateTimeFormat, LogLevel, LogRowModel, LogsSortOrder } from '@grafana/data'; import { escapeUnescapedString, sortLogRows } from '../../utils'; +import { LOG_LINE_BODY_FIELD_NAME } from '../LogDetailsBody'; +import { FieldDef, getAllFields } from '../logParser'; +import { getDisplayedFieldValue } from './LogLine'; +import { GetFieldLinksFn } from './LogList'; import { measureTextWidth } from './virtualization'; export interface LogListModel extends LogRowModel { body: string; + displayLevel: string; + fields: FieldDef[]; timestamp: string; - dimensions: LogDimensions; } -export interface LogDimensions { - timestampWidth: number; - levelWidth: number; +export interface LogFieldDimension { + field: string; + width: number; } interface PreProcessOptions { escape: boolean; + getFieldLinks?: GetFieldLinksFn; order: LogsSortOrder; timeZone: string; wrap: boolean; @@ -24,19 +30,23 @@ interface PreProcessOptions { export const preProcessLogs = ( logs: LogRowModel[], - { escape, order, timeZone, wrap }: PreProcessOptions + { escape, getFieldLinks, order, timeZone, wrap }: PreProcessOptions ): LogListModel[] => { const orderedLogs = sortLogRows(logs, order); - return orderedLogs.map((log) => preProcessLog(log, { wrap, escape, timeZone, expanded: false })); + return orderedLogs.map((log) => preProcessLog(log, { escape, expanded: false, getFieldLinks, timeZone, wrap })); }; interface PreProcessLogOptions { escape: boolean; expanded: boolean; // Not yet implemented + getFieldLinks?: GetFieldLinksFn; timeZone: string; wrap: boolean; } -const preProcessLog = (log: LogRowModel, { escape, expanded, timeZone, wrap }: PreProcessLogOptions): LogListModel => { +const preProcessLog = ( + log: LogRowModel, + { escape, expanded, getFieldLinks, timeZone, wrap }: PreProcessLogOptions +): LogListModel => { let body = log.entry; const timestamp = dateTimeFormat(log.timeEpochMs, { timeZone, @@ -54,10 +64,65 @@ const preProcessLog = (log: LogRowModel, { escape, expanded, timeZone, wrap }: P return { ...log, body, + displayLevel: logLevelToDisplayLevel(log.logLevel), + fields: getAllFields(log, getFieldLinks), timestamp, - dimensions: { - timestampWidth: measureTextWidth(timestamp), - levelWidth: measureTextWidth(log.logLevel), - }, }; }; + +function logLevelToDisplayLevel(level = '') { + switch (level) { + case LogLevel.critical: + return 'crit'; + case LogLevel.warning: + return 'warn'; + case LogLevel.unknown: + return ''; + default: + return level; + } +} + +export const calculateFieldDimensions = (logs: LogListModel[], displayedFields: string[] = []) => { + if (!logs.length) { + return []; + } + let timestampWidth = 0; + let levelWidth = 0; + const fieldWidths: Record = {}; + for (let i = 0; i < logs.length; i++) { + let width = measureTextWidth(logs[i].timestamp); + if (width > timestampWidth) { + timestampWidth = Math.round(width); + } + width = measureTextWidth(logs[i].displayLevel); + if (width > levelWidth) { + levelWidth = Math.round(width); + } + for (const field of displayedFields) { + width = measureTextWidth(getDisplayedFieldValue(field, logs[i])); + fieldWidths[field] = !fieldWidths[field] || width > fieldWidths[field] ? Math.round(width) : fieldWidths[field]; + } + } + const dimensions: LogFieldDimension[] = [ + { + field: 'timestamp', + width: timestampWidth, + }, + { + field: 'level', + width: levelWidth, + }, + ]; + for (const field in fieldWidths) { + // Skip the log line when it's a displayed field + if (field === LOG_LINE_BODY_FIELD_NAME) { + continue; + } + dimensions.push({ + field, + width: fieldWidths[field], + }); + } + return dimensions; +}; diff --git a/public/app/features/logs/components/panel/virtualization.ts b/public/app/features/logs/components/panel/virtualization.ts index 201cc4c82ed..a367b41c050 100644 --- a/public/app/features/logs/components/panel/virtualization.ts +++ b/public/app/features/logs/components/panel/virtualization.ts @@ -1,5 +1,6 @@ import { BusEventWithPayload, GrafanaTheme2 } from '@grafana/data'; +import { getDisplayedFieldValue } from './LogLine'; import { LogListModel } from './processing'; let ctx: CanvasRenderingContext2D | null = null; @@ -8,6 +9,9 @@ let paddingBottom = gridSize * 0.75; let lineHeight = 22; let measurementMode: 'canvas' | 'dom' = 'canvas'; +// Controls the space between fields in the log line, timestamp, level, displayed fields, and log line body +export const FIELD_GAP_MULTIPLIER = 1.5; + export function init(theme: GrafanaTheme2) { const font = `${theme.typography.fontSize}px ${theme.typography.fontFamilyMonospace}`; const letterSpacing = theme.typography.body.letterSpacing; @@ -146,6 +150,7 @@ interface DisplayOptions { export function getLogLineSize( logs: LogListModel[], container: HTMLDivElement | null, + displayedFields: string[], { wrap, showTime }: DisplayOptions, index: number ) { @@ -160,15 +165,26 @@ export function getLogLineSize( if (storedSize) { return storedSize; } - const gap = gridSize; + + let textToMeasure = ''; + const gap = gridSize * FIELD_GAP_MULTIPLIER; let optionsWidth = 0; if (showTime) { - optionsWidth += logs[index].dimensions.timestampWidth + gap; + optionsWidth += gap; + textToMeasure += logs[index].timestamp; } if (logs[index].logLevel) { - optionsWidth += logs[index].dimensions.levelWidth + gap; + optionsWidth += gap; + textToMeasure += logs[index].logLevel; } - const { height } = measureTextHeight(logs[index].body, getLogContainerWidth(container), optionsWidth); + for (const field of displayedFields) { + textToMeasure = getDisplayedFieldValue(field, logs[index]) + textToMeasure; + } + if (!displayedFields.length) { + textToMeasure += logs[index].body; + } + + const { height } = measureTextHeight(textToMeasure, getLogContainerWidth(container), optionsWidth); return height; } diff --git a/public/app/plugins/panel/logs-new/LogsPanel.tsx b/public/app/plugins/panel/logs-new/LogsPanel.tsx index 44dea0e5f54..9b0b26b4c23 100644 --- a/public/app/plugins/panel/logs-new/LogsPanel.tsx +++ b/public/app/plugins/panel/logs-new/LogsPanel.tsx @@ -102,6 +102,7 @@ export const LogsPanel = ({ Date: Thu, 27 Feb 2025 10:47:39 +0000 Subject: [PATCH 07/32] Add GitHub Actions workflow for feature toggle tests (#101270) ci: Add GitHub Actions workflow for feature toggle tests Signed-off-by: Jack Baldry --- .github/CODEOWNERS | 1 + .github/workflows/feature-toggles-ci.yml | 25 ++++++++++++++++++++++++ 2 files changed, 26 insertions(+) create mode 100644 .github/workflows/feature-toggles-ci.yml diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 92581c315aa..4e3ca236ebf 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -776,6 +776,7 @@ embed.go @grafana/grafana-as-code /.github/workflows/doc-validator.yml @grafana/docs-tooling /.github/workflows/deploy-pr-preview.yml @grafana/docs-tooling /.github/workflows/epic-add-to-platform-ux-parent-project.yml @meanmina +/.github/workflows/feature-toggles-ci.yml @grafana/docs-tooling /.github/workflows/github-release.yml @grafana/grafana-developer-enablement-squad /.github/workflows/issue-opened.yml @grafana/grafana-community-support /.github/workflows/metrics-collector.yml @torkelo diff --git a/.github/workflows/feature-toggles-ci.yml b/.github/workflows/feature-toggles-ci.yml new file mode 100644 index 00000000000..a6c9f5c52dc --- /dev/null +++ b/.github/workflows/feature-toggles-ci.yml @@ -0,0 +1,25 @@ +name: Feature toggles CI + +on: + pull_request: + paths: + - 'pkg/services/featuremgmt/toggles_gen_test.go' + - 'pkg/services/featuremgmt/registry.go' + - 'docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md' + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + persist-credentials: false + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version-file: 'go.mod' + cache: true + + - name: Run feature toggle tests + run: go test -v -run TestFeatureToggleFiles ./pkg/services/featuremgmt/ From 8f465f12492461c7f32e7d4b7babd018fd77f9f7 Mon Sep 17 00:00:00 2001 From: Will Browne Date: Thu, 27 Feb 2025 10:56:25 +0000 Subject: [PATCH 08/32] Plugins: Add confirmation modal for uninstalling updateable plugin (#101297) * add confirmation modal for uninstalling updateable plugin * shush betterer * refactor with master Levi * update betterer * update name --- .../InstallControls/InstallControlsButton.tsx | 40 ++++++++++--------- 1 file changed, 21 insertions(+), 19 deletions(-) diff --git a/public/app/features/plugins/admin/components/InstallControls/InstallControlsButton.tsx b/public/app/features/plugins/admin/components/InstallControls/InstallControlsButton.tsx index 6af5c5904ad..9e0bdf9f811 100644 --- a/public/app/features/plugins/admin/components/InstallControls/InstallControlsButton.tsx +++ b/public/app/features/plugins/admin/components/InstallControls/InstallControlsButton.tsx @@ -126,24 +126,28 @@ export function InstallControlsButton({ uninstallTitle = 'Preinstalled plugin. Remove from Grafana config before uninstalling.'; } + const uninstallControls = ( + <> + + + + ); + if (pluginStatus === PluginStatus.UNINSTALL) { return ( - <> - - - - - + + {uninstallControls} + ); } @@ -162,9 +166,7 @@ export function InstallControlsButton({ {isInstalling ? 'Updating' : 'Update'} )} - + {uninstallControls} ); } From 743991e30212f8afb26ca808b5f89236d0d6350c Mon Sep 17 00:00:00 2001 From: Oscar Kilhed Date: Thu, 27 Feb 2025 12:02:00 +0100 Subject: [PATCH 09/32] Chore: Bump scenes to v6.1.3 (#101370) * Bump scenes * Fix profile name --- package.json | 4 ++-- .../pages/DashboardScenePageStateManager.ts | 2 +- yarn.lock | 22 +++++++++---------- 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/package.json b/package.json index 012ee9da149..9e5f4588c92 100644 --- a/package.json +++ b/package.json @@ -276,8 +276,8 @@ "@grafana/prometheus": "workspace:*", "@grafana/runtime": "workspace:*", "@grafana/saga-icons": "workspace:*", - "@grafana/scenes": "6.0.2", - "@grafana/scenes-react": "6.0.2", + "@grafana/scenes": "6.1.3", + "@grafana/scenes-react": "6.1.3", "@grafana/schema": "workspace:*", "@grafana/sql": "workspace:*", "@grafana/ui": "workspace:*", diff --git a/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.ts b/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.ts index b9f37706bad..63da7075646 100644 --- a/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.ts +++ b/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.ts @@ -151,7 +151,7 @@ abstract class DashboardScenePageStateManagerBase const queryController = sceneGraph.getQueryController(dashboard); trackDashboardSceneLoaded(dashboard, measure?.duration); - queryController?.startProfile(dashboard); + queryController?.startProfile('DashboardScene'); if (options.route !== DashboardRoutes.New) { emitDashboardViewEvent({ diff --git a/yarn.lock b/yarn.lock index eb8360924ac..3d8020c64dd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3772,11 +3772,11 @@ __metadata: languageName: unknown linkType: soft -"@grafana/scenes-react@npm:6.0.2": - version: 6.0.2 - resolution: "@grafana/scenes-react@npm:6.0.2" +"@grafana/scenes-react@npm:6.1.3": + version: 6.1.3 + resolution: "@grafana/scenes-react@npm:6.1.3" dependencies: - "@grafana/scenes": "npm:6.0.2" + "@grafana/scenes": "npm:6.1.3" lru-cache: "npm:^10.2.2" react-use: "npm:^17.4.0" peerDependencies: @@ -3788,13 +3788,13 @@ __metadata: react: ^18.0.0 react-dom: ^18.0.0 react-router-dom: ^6.28.0 - checksum: 10/9744e01f2ff912229e43cedfa41d626ccdfd034f5b9718b57c593bc90edadade960f76baf1d8ad19eed03709c17c62397df1871b89acc635172aa14f6a20e096 + checksum: 10/51b023de3ad6c3c9c1d08b1e0f3f5f9d99c65bc3a0871e34cbfd1bc542b0e5b945d9075baacb1d07009a731fb3c94a59a5631361c30961c7fea59be127878d93 languageName: node linkType: hard -"@grafana/scenes@npm:6.0.2": - version: 6.0.2 - resolution: "@grafana/scenes@npm:6.0.2" +"@grafana/scenes@npm:6.1.3": + version: 6.1.3 + resolution: "@grafana/scenes@npm:6.1.3" dependencies: "@floating-ui/react": "npm:^0.26.16" "@leeoniya/ufuzzy": "npm:^1.0.16" @@ -3812,7 +3812,7 @@ __metadata: react: ^18.0.0 react-dom: ^18.0.0 react-router-dom: ^6.28.0 - checksum: 10/2584f296db6299ef0a09d51f5c267ebcf7e44bd17b4d6516e38d3220f8f1d7aebc63c5fc6523979c4ac4d3f555416ca573e85e03bd36eb33a11941a5b3497149 + checksum: 10/10db2094648bf1e308b2f3ca1725acf9a5ea5bfa7f1015f9da0765abc34f5c718ec410275f45f78e5428ecd3ff6285343e95c02841eeef39eef5be7c8875fe9f languageName: node linkType: hard @@ -18126,8 +18126,8 @@ __metadata: "@grafana/prometheus": "workspace:*" "@grafana/runtime": "workspace:*" "@grafana/saga-icons": "workspace:*" - "@grafana/scenes": "npm:6.0.2" - "@grafana/scenes-react": "npm:6.0.2" + "@grafana/scenes": "npm:6.1.3" + "@grafana/scenes-react": "npm:6.1.3" "@grafana/schema": "workspace:*" "@grafana/sql": "workspace:*" "@grafana/tsconfig": "npm:^2.0.0" From c6a78978c39df988121b0289b6e039e3872186b6 Mon Sep 17 00:00:00 2001 From: Karl Persson <23356117+kalleep@users.noreply.github.com> Date: Thu, 27 Feb 2025 12:03:03 +0100 Subject: [PATCH 10/32] ContextHandler: unexport cfg (#101396) --- pkg/services/contexthandler/contexthandler.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/pkg/services/contexthandler/contexthandler.go b/pkg/services/contexthandler/contexthandler.go index df738065715..ee01105e37c 100644 --- a/pkg/services/contexthandler/contexthandler.go +++ b/pkg/services/contexthandler/contexthandler.go @@ -29,7 +29,7 @@ import ( func ProvideService(cfg *setting.Cfg, authenticator authn.Authenticator, features featuremgmt.FeatureToggles, ) *ContextHandler { return &ContextHandler{ - Cfg: cfg, + cfg: cfg, authenticator: authenticator, features: features, } @@ -37,7 +37,7 @@ func ProvideService(cfg *setting.Cfg, authenticator authn.Authenticator, feature // ContextHandler is a middleware. type ContextHandler struct { - Cfg *setting.Cfg + cfg *setting.Cfg authenticator authn.Authenticator features featuremgmt.FeatureToggles } @@ -104,7 +104,7 @@ func (h *ContextHandler) Middleware(next http.Handler) http.Handler { // inject ReqContext in the context ctx = context.WithValue(ctx, reqContextKey{}, reqContext) // store list of possible auth header in context - ctx = WithAuthHTTPHeaders(ctx, h.Cfg) + ctx = WithAuthHTTPHeaders(ctx, h.cfg) // Set the context for the http.Request.Context // This modifies both r and reqContext.Req since they point to the same value *reqContext.Req = *reqContext.Req.WithContext(ctx) @@ -137,7 +137,7 @@ func (h *ContextHandler) Middleware(next http.Handler) http.Handler { attribute.Int64("userId", reqContext.UserID), )) - if h.Cfg.IDResponseHeaderEnabled && reqContext.SignedInUser != nil { + if h.cfg.IDResponseHeaderEnabled && reqContext.SignedInUser != nil { reqContext.Resp.Before(h.addIDHeaderEndOfRequestFunc(reqContext.SignedInUser)) } @@ -167,11 +167,11 @@ func (h *ContextHandler) addIDHeaderEndOfRequestFunc(ident identity.Requester) w return } - if _, ok := h.Cfg.IDResponseHeaderNamespaces[string(ident.GetIdentityType())]; !ok { + if _, ok := h.cfg.IDResponseHeaderNamespaces[string(ident.GetIdentityType())]; !ok { return } - headerName := fmt.Sprintf("%s-Identity-Id", h.Cfg.IDResponseHeaderPrefix) + headerName := fmt.Sprintf("%s-Identity-Id", h.cfg.IDResponseHeaderPrefix) w.Header().Add(headerName, ident.GetID()) } } From 2372508e9e736ab5fad6ce6905ac84d4e37f5233 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 27 Feb 2025 13:25:30 +0200 Subject: [PATCH 11/32] Update scenes to v6.1.4 (#101402) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package.json | 4 ++-- yarn.lock | 22 +++++++++++----------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/package.json b/package.json index 9e5f4588c92..3eb2cbbeabf 100644 --- a/package.json +++ b/package.json @@ -276,8 +276,8 @@ "@grafana/prometheus": "workspace:*", "@grafana/runtime": "workspace:*", "@grafana/saga-icons": "workspace:*", - "@grafana/scenes": "6.1.3", - "@grafana/scenes-react": "6.1.3", + "@grafana/scenes": "6.1.4", + "@grafana/scenes-react": "6.1.4", "@grafana/schema": "workspace:*", "@grafana/sql": "workspace:*", "@grafana/ui": "workspace:*", diff --git a/yarn.lock b/yarn.lock index 3d8020c64dd..d5a786396d4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3772,11 +3772,11 @@ __metadata: languageName: unknown linkType: soft -"@grafana/scenes-react@npm:6.1.3": - version: 6.1.3 - resolution: "@grafana/scenes-react@npm:6.1.3" +"@grafana/scenes-react@npm:6.1.4": + version: 6.1.4 + resolution: "@grafana/scenes-react@npm:6.1.4" dependencies: - "@grafana/scenes": "npm:6.1.3" + "@grafana/scenes": "npm:6.1.4" lru-cache: "npm:^10.2.2" react-use: "npm:^17.4.0" peerDependencies: @@ -3788,13 +3788,13 @@ __metadata: react: ^18.0.0 react-dom: ^18.0.0 react-router-dom: ^6.28.0 - checksum: 10/51b023de3ad6c3c9c1d08b1e0f3f5f9d99c65bc3a0871e34cbfd1bc542b0e5b945d9075baacb1d07009a731fb3c94a59a5631361c30961c7fea59be127878d93 + checksum: 10/69a344f30937a80e25201c8ce1261f85c10663e68d70103c28603abbee2496c7855d999b2a212132f48af597f3f528de65bade66c05b02c719ca25f8f8c682bd languageName: node linkType: hard -"@grafana/scenes@npm:6.1.3": - version: 6.1.3 - resolution: "@grafana/scenes@npm:6.1.3" +"@grafana/scenes@npm:6.1.4": + version: 6.1.4 + resolution: "@grafana/scenes@npm:6.1.4" dependencies: "@floating-ui/react": "npm:^0.26.16" "@leeoniya/ufuzzy": "npm:^1.0.16" @@ -3812,7 +3812,7 @@ __metadata: react: ^18.0.0 react-dom: ^18.0.0 react-router-dom: ^6.28.0 - checksum: 10/10db2094648bf1e308b2f3ca1725acf9a5ea5bfa7f1015f9da0765abc34f5c718ec410275f45f78e5428ecd3ff6285343e95c02841eeef39eef5be7c8875fe9f + checksum: 10/708652236c3b4a5bb0e1cd84739bef530b06244e2798e754347eabd8ab040b24c7f39376fd7b93d1049da6c0ee40785294882cba0125ce8171d5627a71ae63db languageName: node linkType: hard @@ -18126,8 +18126,8 @@ __metadata: "@grafana/prometheus": "workspace:*" "@grafana/runtime": "workspace:*" "@grafana/saga-icons": "workspace:*" - "@grafana/scenes": "npm:6.1.3" - "@grafana/scenes-react": "npm:6.1.3" + "@grafana/scenes": "npm:6.1.4" + "@grafana/scenes-react": "npm:6.1.4" "@grafana/schema": "workspace:*" "@grafana/sql": "workspace:*" "@grafana/tsconfig": "npm:^2.0.0" From 77305325c26d228404865635655ccdba0b9dd734 Mon Sep 17 00:00:00 2001 From: Victor Marin <36818606+mdvictor@users.noreply.github.com> Date: Thu, 27 Feb 2025 13:33:32 +0200 Subject: [PATCH 12/32] [Scopes]: Pass formatted scope filters to adhoc (#101217) * pass formatted scope filters to adhoc * fix * fix * fix scenario where we have equals and not-equals filters with the same key * add canary packages for testing * WIP * refactor to pass all filter values * rename property * refactor * update canary scenes * update scenes version * fix tests * fix arg startProfile bug that arised with scenes update --- .betterer.results | 3 + packages/grafana-data/src/index.ts | 2 + packages/grafana-data/src/types/scopes.ts | 11 + .../scene/DashboardScopesFacade.ts | 31 +- .../scene/convertScopesToAdHocFilters.test.ts | 288 ++++++++++++++++++ .../scene/convertScopesToAdHocFilters.ts | 92 ++++++ 6 files changed, 426 insertions(+), 1 deletion(-) create mode 100644 public/app/features/dashboard-scene/scene/convertScopesToAdHocFilters.test.ts create mode 100644 public/app/features/dashboard-scene/scene/convertScopesToAdHocFilters.ts diff --git a/.betterer.results b/.betterer.results index b593c39ca68..552f1bfd1a5 100644 --- a/.betterer.results +++ b/.betterer.results @@ -285,6 +285,9 @@ exports[`better eslint`] = { [0, 0, 0, "Do not use any type assertions.", "0"], [0, 0, 0, "Unexpected any. Specify a different type.", "1"] ], + "packages/grafana-data/src/types/scopes.ts:5381": [ + [0, 0, 0, "Do not use any type assertions.", "0"] + ], "packages/grafana-data/src/types/select.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"], [0, 0, 0, "Unexpected any. Specify a different type.", "1"] diff --git a/packages/grafana-data/src/index.ts b/packages/grafana-data/src/index.ts index ec84e8f0d7b..67c8483ed11 100644 --- a/packages/grafana-data/src/index.ts +++ b/packages/grafana-data/src/index.ts @@ -576,6 +576,8 @@ export { type ScopeNodeSpec, type ScopeNode, scopeFilterOperatorMap, + reverseScopeFilterOperatorMap, + isEqualityOrMultiOperator, } from './types/scopes'; export { PluginState, diff --git a/packages/grafana-data/src/types/scopes.ts b/packages/grafana-data/src/types/scopes.ts index 90579b72017..2f85a19237d 100644 --- a/packages/grafana-data/src/types/scopes.ts +++ b/packages/grafana-data/src/types/scopes.ts @@ -18,6 +18,12 @@ export interface ScopeDashboardBinding { } export type ScopeFilterOperator = 'equals' | 'not-equals' | 'regex-match' | 'regex-not-match' | 'one-of' | 'not-one-of'; +export type EqualityOrMultiOperator = Extract; + +export function isEqualityOrMultiOperator(value: string): value is EqualityOrMultiOperator { + const operators = new Set(['equals', 'not-equals', 'one-of', 'not-one-of']); + return operators.has(value); +} export const scopeFilterOperatorMap: Record = { '=': 'equals', @@ -28,6 +34,11 @@ export const scopeFilterOperatorMap: Record = { '!=|': 'not-one-of', }; +export const reverseScopeFilterOperatorMap: Record = Object.fromEntries( + Object.entries(scopeFilterOperatorMap).map(([symbol, operator]) => [operator, symbol]) + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions +) as Record; + export interface ScopeSpecFilter { key: string; value: string; diff --git a/public/app/features/dashboard-scene/scene/DashboardScopesFacade.ts b/public/app/features/dashboard-scene/scene/DashboardScopesFacade.ts index 93b81e735ce..ccde822ce13 100644 --- a/public/app/features/dashboard-scene/scene/DashboardScopesFacade.ts +++ b/public/app/features/dashboard-scene/scene/DashboardScopesFacade.ts @@ -1,6 +1,10 @@ -import { sceneGraph } from '@grafana/scenes'; +import { AdHocFiltersVariable, sceneGraph } from '@grafana/scenes'; import { ScopesFacade } from 'app/features/scopes'; +import { getDashboardSceneFor } from '../utils/utils'; + +import { convertScopesToAdHocFilters } from './convertScopesToAdHocFilters'; + export interface DashboardScopesFacadeState { reloadOnParamsChange?: boolean; uid?: string; @@ -13,7 +17,32 @@ export class DashboardScopesFacade extends ScopesFacade { if (!reloadOnParamsChange || !uid) { sceneGraph.getTimeRange(facade).onRefresh(); } + + // push filters as soon as they come + this.pushScopeFiltersToAdHocVariable(); }, }); + + this.addActivationHandler(() => { + // also try to push filters on activation, for + // when the dashboard is changed + this.pushScopeFiltersToAdHocVariable(); + }); + } + + private pushScopeFiltersToAdHocVariable() { + const dashboard = getDashboardSceneFor(this); + + const adhoc = dashboard.state.$variables?.state.variables.find((v) => v instanceof AdHocFiltersVariable); + + if (!adhoc) { + return; + } + + const filters = convertScopesToAdHocFilters(this.value); + + adhoc.setState({ + baseFilters: filters, + }); } } diff --git a/public/app/features/dashboard-scene/scene/convertScopesToAdHocFilters.test.ts b/public/app/features/dashboard-scene/scene/convertScopesToAdHocFilters.test.ts new file mode 100644 index 00000000000..c6b84a4d39f --- /dev/null +++ b/public/app/features/dashboard-scene/scene/convertScopesToAdHocFilters.test.ts @@ -0,0 +1,288 @@ +import { Scope, ScopeSpecFilter } from '@grafana/data'; +import { FilterOrigin } from '@grafana/scenes'; + +import { convertScopesToAdHocFilters } from './convertScopesToAdHocFilters'; + +describe('convertScopesToAdHocFilters', () => { + it('should return empty filters when no scopes are provided', () => { + let scopes = generateScopes([]); + + expect(scopes).toEqual([]); + expect(convertScopesToAdHocFilters(scopes)).toEqual([]); + + scopes = generateScopes([[], []]); + + expect(convertScopesToAdHocFilters(scopes)).toEqual([]); + }); + + it('should return filters formatted for adHoc from a single scope', () => { + let scopes = generateScopes([ + [ + { key: 'key1', value: 'value1', operator: 'equals' }, + { key: 'key2', value: 'value2', operator: 'not-equals' }, + { key: 'key3', value: 'value3', operator: 'regex-not-match' }, + ], + ]); + + expect(convertScopesToAdHocFilters(scopes)).toEqual([ + { key: 'key1', value: 'value1', operator: '=', origin: FilterOrigin.Scopes, values: ['value1'] }, + { key: 'key2', value: 'value2', operator: '!=', origin: FilterOrigin.Scopes, values: ['value2'] }, + { key: 'key3', value: 'value3', operator: '!~', origin: FilterOrigin.Scopes, values: ['value3'] }, + ]); + + scopes = generateScopes([[{ key: 'key3', value: 'value3', operator: 'regex-match' }]]); + + expect(convertScopesToAdHocFilters(scopes)).toEqual([ + { key: 'key3', value: 'value3', operator: '=~', origin: FilterOrigin.Scopes, values: ['value3'] }, + ]); + }); + + it('should return filters formatted for adHoc from multiple scopes with single values', () => { + let scopes = generateScopes([ + [{ key: 'key1', value: 'value1', operator: 'equals' }], + [{ key: 'key2', value: 'value2', operator: 'regex-match' }], + ]); + + expect(convertScopesToAdHocFilters(scopes)).toEqual([ + { key: 'key1', value: 'value1', operator: '=', origin: FilterOrigin.Scopes, values: ['value1'] }, + { key: 'key2', value: 'value2', operator: '=~', origin: FilterOrigin.Scopes, values: ['value2'] }, + ]); + }); + + it('should return filters formatted for adHoc from multiple scopes with multiple values', () => { + let scopes = generateScopes([ + [ + { key: 'key1', value: 'value1', operator: 'equals' }, + { key: 'key2', value: 'value2', operator: 'not-equals' }, + ], + [ + { key: 'key3', value: 'value3', operator: 'regex-match' }, + { key: 'key4', value: 'value4', operator: 'regex-match' }, + ], + ]); + + expect(convertScopesToAdHocFilters(scopes)).toEqual([ + { key: 'key1', value: 'value1', operator: '=', origin: FilterOrigin.Scopes, values: ['value1'] }, + { key: 'key2', value: 'value2', operator: '!=', origin: FilterOrigin.Scopes, values: ['value2'] }, + { key: 'key3', value: 'value3', operator: '=~', origin: FilterOrigin.Scopes, values: ['value3'] }, + { key: 'key4', value: 'value4', operator: '=~', origin: FilterOrigin.Scopes, values: ['value4'] }, + ]); + }); + + it('should return formatted filters and concat values of the same key, coming from different scopes, if operator supports multi-value', () => { + let scopes = generateScopes([ + [ + { key: 'key1', value: 'value1', operator: 'equals' }, + { key: 'key2', value: 'value2', operator: 'not-equals' }, + ], + [ + { key: 'key1', value: 'value3', operator: 'equals' }, + { key: 'key2', value: 'value4', operator: 'not-equals' }, + ], + [{ key: 'key1', value: 'value5', operator: 'equals' }], + ]); + + expect(convertScopesToAdHocFilters(scopes)).toEqual([ + { + key: 'key1', + value: 'value1', + operator: '=|', + origin: FilterOrigin.Scopes, + values: ['value1', 'value3', 'value5'], + }, + { key: 'key2', value: 'value2', operator: '!=|', origin: FilterOrigin.Scopes, values: ['value2', 'value4'] }, + ]); + }); + + it('should ignore the rest of the duplicate filters, if they are a combination of equals and not-equals', () => { + let scopes = generateScopes([ + [{ key: 'key1', value: 'value1', operator: 'equals' }], + [{ key: 'key1', value: 'value2', operator: 'not-equals' }], + [{ key: 'key1', value: 'value3', operator: 'equals' }], + ]); + + expect(convertScopesToAdHocFilters(scopes)).toEqual([ + { + key: 'key1', + value: 'value1', + operator: '=|', + origin: FilterOrigin.Scopes, + values: ['value1', 'value3'], + }, + { + key: 'key1', + value: 'value2', + operator: '!=', + origin: FilterOrigin.Scopes, + values: ['value2'], + }, + ]); + }); + + it('should return formatted filters and keep only the first filter of the same key if operator is not multi-value', () => { + let scopes = generateScopes([ + [ + { key: 'key1', value: 'value1', operator: 'regex-match' }, + { key: 'key2', value: 'value2', operator: 'not-equals' }, + ], + [ + { key: 'key1', value: 'value3', operator: 'regex-match' }, + { key: 'key2', value: 'value4', operator: 'not-equals' }, + ], + [{ key: 'key1', value: 'value5', operator: 'equals' }], + ]); + + expect(convertScopesToAdHocFilters(scopes)).toEqual([ + { + key: 'key1', + value: 'value1', + operator: '=~', + origin: FilterOrigin.Scopes, + values: ['value1'], + }, + { key: 'key2', value: 'value2', operator: '!=|', origin: FilterOrigin.Scopes, values: ['value2', 'value4'] }, + { + key: 'key1', + value: 'value3', + operator: '=~', + origin: FilterOrigin.Scopes, + values: ['value3'], + }, + { + key: 'key1', + value: 'value5', + operator: '=', + origin: FilterOrigin.Scopes, + values: ['value5'], + }, + ]); + + scopes = generateScopes([ + [{ key: 'key1', value: 'value1', operator: 'regex-match' }], + [{ key: 'key1', value: 'value5', operator: 'equals' }], + [{ key: 'key1', value: 'value3', operator: 'regex-match' }], + ]); + + expect(convertScopesToAdHocFilters(scopes)).toEqual([ + { + key: 'key1', + value: 'value1', + operator: '=~', + origin: FilterOrigin.Scopes, + values: ['value1'], + }, + { + key: 'key1', + value: 'value5', + operator: '=', + origin: FilterOrigin.Scopes, + values: ['value5'], + }, + { + key: 'key1', + value: 'value3', + operator: '=~', + origin: FilterOrigin.Scopes, + values: ['value3'], + }, + ]); + }); + + it('should return formatted filters and concat values that are multi-value and drop duplicates with non multi-value operator', () => { + let scopes = generateScopes([ + [{ key: 'key1', value: 'value1', operator: 'equals' }], + [{ key: 'key1', value: 'value2', operator: 'regex-match' }], + [{ key: 'key1', value: 'value3', operator: 'equals' }], + ]); + + expect(convertScopesToAdHocFilters(scopes)).toEqual([ + { + key: 'key1', + value: 'value1', + operator: '=|', + origin: FilterOrigin.Scopes, + values: ['value1', 'value3'], + }, + { + key: 'key1', + value: 'value2', + operator: '=~', + origin: FilterOrigin.Scopes, + values: ['value2'], + }, + ]); + + scopes = generateScopes([ + [ + { key: 'key1', value: 'value1', operator: 'equals' }, + { key: 'key2', value: 'value2', operator: 'equals' }, + ], + [ + { key: 'key1', value: 'value3', operator: 'equals' }, + { key: 'key2', value: 'value4', operator: 'equals' }, + ], + [ + { key: 'key1', value: 'value5', operator: 'regex-match' }, + { key: 'key2', value: 'value6', operator: 'equals' }, + ], + [ + { key: 'key1', value: 'value7', operator: 'equals' }, + { key: 'key2', value: 'value8', operator: 'regex-match' }, + ], + [ + { key: 'key1', value: 'value9', operator: 'equals' }, + { key: 'key2', value: 'value10', operator: 'equals' }, + ], + ]); + + expect(convertScopesToAdHocFilters(scopes)).toEqual([ + { + key: 'key1', + value: 'value1', + operator: '=|', + origin: FilterOrigin.Scopes, + values: ['value1', 'value3', 'value7', 'value9'], + }, + { + key: 'key2', + value: 'value2', + operator: '=|', + origin: FilterOrigin.Scopes, + values: ['value2', 'value4', 'value6', 'value10'], + }, + { + key: 'key1', + value: 'value5', + operator: '=~', + origin: FilterOrigin.Scopes, + values: ['value5'], + }, + { + key: 'key2', + value: 'value8', + operator: '=~', + origin: FilterOrigin.Scopes, + values: ['value8'], + }, + ]); + }); +}); + +function generateScopes(filtersSpec: ScopeSpecFilter[][]) { + const scopes: Scope[] = []; + + for (let i = 0; i < filtersSpec.length; i++) { + scopes.push({ + metadata: { name: `name-${i}` }, + spec: { + title: `scope-${i}`, + type: '', + description: 'desc', + category: '', + filters: filtersSpec[i], + }, + }); + } + + return scopes; +} diff --git a/public/app/features/dashboard-scene/scene/convertScopesToAdHocFilters.ts b/public/app/features/dashboard-scene/scene/convertScopesToAdHocFilters.ts new file mode 100644 index 00000000000..1066a8f7b15 --- /dev/null +++ b/public/app/features/dashboard-scene/scene/convertScopesToAdHocFilters.ts @@ -0,0 +1,92 @@ +import { + Scope, + ScopeSpecFilter, + isEqualityOrMultiOperator, + reverseScopeFilterOperatorMap, + scopeFilterOperatorMap, +} from '@grafana/data'; +import { AdHocFilterWithLabels, FilterOrigin } from '@grafana/scenes'; + +export function convertScopesToAdHocFilters(scopes: Scope[]): AdHocFilterWithLabels[] { + const formattedFilters: Map = new Map(); + // duplicated filters that could not be processed in any way are just appended to the list + const duplicatedFilters: AdHocFilterWithLabels[] = []; + const allFilters = scopes.flatMap((scope) => scope.spec.filters); + + for (const filter of allFilters) { + processFilter(formattedFilters, duplicatedFilters, filter); + } + + return [...formattedFilters.values(), ...duplicatedFilters]; +} + +function processFilter( + formattedFilters: Map, + duplicatedFilters: AdHocFilterWithLabels[], + filter: ScopeSpecFilter +) { + const existingFilter = formattedFilters.get(filter.key); + + if (existingFilter && canValueBeMerged(existingFilter.operator, filter.operator)) { + mergeFilterValues(existingFilter, filter); + } else if (!existingFilter) { + // Add filter to map either only if it is new. + // Otherwise it is an existing filter that cannot be converted to multi-value + // and thus will be moved to the duplicatedFilters list + formattedFilters.set(filter.key, { + key: filter.key, + operator: reverseScopeFilterOperatorMap[filter.operator], + value: filter.value, + values: filter.values ?? [filter.value], + origin: FilterOrigin.Scopes, + }); + } else { + duplicatedFilters.push({ + key: filter.key, + operator: reverseScopeFilterOperatorMap[filter.operator], + value: filter.value, + values: filter.values ?? [filter.value], + origin: FilterOrigin.Scopes, + }); + } +} + +function mergeFilterValues(adHocFilter: AdHocFilterWithLabels, filter: ScopeSpecFilter) { + const values = filter.values ?? [filter.value]; + + for (const value of values) { + if (!adHocFilter.values?.includes(value)) { + adHocFilter.values?.push(value); + } + } + + // If there's only one value, there's no need to update the + // operator to its multi-value equivalent + if (adHocFilter.values?.length === 1) { + return; + } + + // Otherwise update it to the equivalent multi-value operator + if (filter.operator === 'equals' && adHocFilter.operator === reverseScopeFilterOperatorMap['equals']) { + adHocFilter.operator = reverseScopeFilterOperatorMap['one-of']; + } else if (filter.operator === 'not-equals' && adHocFilter.operator === reverseScopeFilterOperatorMap['not-equals']) { + adHocFilter.operator = reverseScopeFilterOperatorMap['not-one-of']; + } +} + +function canValueBeMerged(adHocFilterOperator: string, filterOperator: string) { + const scopeConvertedOperator = scopeFilterOperatorMap[adHocFilterOperator]; + + if (!isEqualityOrMultiOperator(scopeConvertedOperator) || !isEqualityOrMultiOperator(filterOperator)) { + return false; + } + + if ( + (scopeConvertedOperator.includes('not') && !filterOperator.includes('not')) || + (!scopeConvertedOperator.includes('not') && filterOperator.includes('not')) + ) { + return false; + } + + return true; +} From d947433d19ed95a1a6ebb76123c113cbd1c7011f Mon Sep 17 00:00:00 2001 From: Alexander Akhmetov Date: Thu, 27 Feb 2025 13:04:47 +0100 Subject: [PATCH 13/32] Alerting: API to delete rule groups using mimirtool (#100687) * Alerting: API to delete rule groups using mimirtool --- .../ngalert/api/api_convert_prometheus.go | 46 ++- .../api/api_convert_prometheus_test.go | 288 +++++++++++++++++- pkg/services/ngalert/api/api_provisioning.go | 1 + pkg/services/ngalert/api/errors.go | 5 +- pkg/services/ngalert/api/tooling/api.json | 1 - .../ngalert/provisioning/alert_rules.go | 75 +++-- .../ngalert/provisioning/alert_rules_test.go | 250 ++++++++++++++- pkg/services/ngalert/store/deltas.go | 54 +++- pkg/services/ngalert/store/deltas_test.go | 89 +++++- pkg/services/ngalert/tests/fakes/rules.go | 18 ++ .../alerting/api_convert_prometheus_test.go | 111 ++++++- pkg/tests/api/alerting/testing.go | 16 + public/api-merged.json | 1 - public/openapi3.json | 1 - 14 files changed, 908 insertions(+), 48 deletions(-) diff --git a/pkg/services/ngalert/api/api_convert_prometheus.go b/pkg/services/ngalert/api/api_convert_prometheus.go index e9dec2d4481..5dca89080a5 100644 --- a/pkg/services/ngalert/api/api_convert_prometheus.go +++ b/pkg/services/ngalert/api/api_convert_prometheus.go @@ -103,12 +103,46 @@ func (srv *ConvertPrometheusSrv) RouteConvertPrometheusGetRules(c *contextmodel. // RouteConvertPrometheusDeleteNamespace deletes all rule groups that were imported from a Prometheus-compatible source // within a specified namespace. func (srv *ConvertPrometheusSrv) RouteConvertPrometheusDeleteNamespace(c *contextmodel.ReqContext, namespaceTitle string) response.Response { - return response.Error(501, "Not implemented", nil) + logger := srv.logger.FromContext(c.Req.Context()) + + logger.Debug("Looking up folder in the root by title", "folder_title", namespaceTitle) + namespace, err := srv.ruleStore.GetNamespaceInRootByTitle(c.Req.Context(), namespaceTitle, c.SignedInUser.GetOrgID(), c.SignedInUser) + if err != nil { + return namespaceErrorResponse(err) + } + logger.Info("Deleting all Prometheus-imported rule groups", "folder_uid", namespace.UID, "folder_title", namespaceTitle) + + filterOpts := &provisioning.FilterOptions{ + NamespaceUIDs: []string{namespace.UID}, + ImportedPrometheusRule: util.Pointer(true), + } + err = srv.alertRuleService.DeleteRuleGroups(c.Req.Context(), c.SignedInUser, models.ProvenanceConvertedPrometheus, filterOpts) + if err != nil { + logger.Error("Failed to delete rule groups", "folder_uid", namespace.UID, "error", err) + return errorToResponse(err) + } + + return successfulResponse() } // RouteConvertPrometheusDeleteRuleGroup deletes a specific rule group if it was imported from a Prometheus-compatible source. func (srv *ConvertPrometheusSrv) RouteConvertPrometheusDeleteRuleGroup(c *contextmodel.ReqContext, namespaceTitle string, group string) response.Response { - return response.Error(501, "Not implemented", nil) + logger := srv.logger.FromContext(c.Req.Context()) + + logger.Debug("Looking up folder in the root by title", "folder_title", namespaceTitle) + folder, err := srv.ruleStore.GetNamespaceInRootByTitle(c.Req.Context(), namespaceTitle, c.SignedInUser.GetOrgID(), c.SignedInUser) + if err != nil { + return namespaceErrorResponse(err) + } + logger.Info("Deleting Prometheus-imported rule group", "folder_uid", folder.UID, "folder_title", namespaceTitle, "group", group) + + err = srv.alertRuleService.DeleteRuleGroup(c.Req.Context(), c.SignedInUser, folder.UID, group, models.ProvenanceConvertedPrometheus) + if err != nil { + logger.Error("Failed to delete rule group", "folder_uid", folder.UID, "group", group, "error", err) + return errorToResponse(err) + } + + return successfulResponse() } // RouteConvertPrometheusGetNamespace returns the Grafana-managed alert rules for a specified namespace (folder). @@ -220,7 +254,7 @@ func (srv *ConvertPrometheusSrv) RouteConvertPrometheusPostRuleGroup(c *contextm return errorToResponse(err) } - return response.JSON(http.StatusAccepted, map[string]string{"status": "success"}) + return successfulResponse() } func (srv *ConvertPrometheusSrv) getOrCreateNamespace(c *contextmodel.ReqContext, title string, logger log.Logger) (*folder.Folder, response.Response) { @@ -363,3 +397,9 @@ func namespaceErrorResponse(err error) response.Response { return toNamespaceErrorResponse(err) } + +func successfulResponse() response.Response { + return response.JSON(http.StatusAccepted, apimodels.ConvertPrometheusResponse{ + Status: "success", + }) +} diff --git a/pkg/services/ngalert/api/api_convert_prometheus_test.go b/pkg/services/ngalert/api/api_convert_prometheus_test.go index 05f84347334..51b378d437c 100644 --- a/pkg/services/ngalert/api/api_convert_prometheus_test.go +++ b/pkg/services/ngalert/api/api_convert_prometheus_test.go @@ -2,6 +2,7 @@ package api import ( "context" + "fmt" "net/http" "net/http/httptest" "testing" @@ -77,6 +78,79 @@ func TestRouteConvertPrometheusPostRuleGroup(t *testing.T) { require.Equal(t, http.StatusAccepted, response.Status()) }) + t.Run("should replace an existing rule group", func(t *testing.T) { + provenanceStore := fakes.NewFakeProvisioningStore() + srv, _, ruleStore, folderService := createConvertPrometheusSrv(t, withProvenanceStore(provenanceStore)) + + // Create a folder in the root + fldr := randFolder() + fldr.ParentUID = "" + folderService.ExpectedFolder = fldr + folderService.ExpectedFolders = []*folder.Folder{fldr} + ruleStore.Folders[1] = append(ruleStore.Folders[1], fldr) + + // And a rule + rule := models.RuleGen. + With(models.RuleGen.WithNamespaceUID(fldr.UID)). + With(models.RuleGen.WithGroupName(simpleGroup.Name)). + With(models.RuleGen.WithOrgID(1)). + With(models.RuleGen.WithPrometheusOriginalRuleDefinition("123")). + GenerateRef() + ruleStore.PutRule(context.Background(), rule) + + rc := createRequestCtx() + response := srv.RouteConvertPrometheusPostRuleGroup(rc, fldr.Title, simpleGroup) + require.Equal(t, http.StatusAccepted, response.Status()) + + // Get the updated rule + remaining, err := ruleStore.ListAlertRules(context.Background(), &models.ListAlertRulesQuery{ + OrgID: 1, + }) + require.NoError(t, err) + require.Len(t, remaining, 1) + + require.Equal(t, simpleGroup.Name, remaining[0].RuleGroup) + require.Equal(t, fmt.Sprintf("[%s] %s", simpleGroup.Name, simpleGroup.Rules[0].Alert), remaining[0].Title) + promRuleYAML, err := yaml.Marshal(simpleGroup.Rules[0]) + require.NoError(t, err) + require.Equal(t, string(promRuleYAML), remaining[0].PrometheusRuleDefinition()) + }) + + t.Run("should fail to replace a provisioned rule group", func(t *testing.T) { + provenanceStore := fakes.NewFakeProvisioningStore() + srv, _, ruleStore, folderService := createConvertPrometheusSrv(t, withProvenanceStore(provenanceStore)) + + // Create a folder in the root + fldr := randFolder() + fldr.ParentUID = "" + folderService.ExpectedFolder = fldr + folderService.ExpectedFolders = []*folder.Folder{fldr} + ruleStore.Folders[1] = append(ruleStore.Folders[1], fldr) + + rule := models.RuleGen. + With(models.RuleGen.WithNamespaceUID(fldr.UID)). + With(models.RuleGen.WithGroupName(simpleGroup.Name)). + With(models.RuleGen.WithOrgID(1)). + With(models.RuleGen.WithPrometheusOriginalRuleDefinition("123")). + GenerateRef() + ruleStore.PutRule(context.Background(), rule) + // mark the rule as provisioned + err := provenanceStore.SetProvenance(context.Background(), rule, 1, models.ProvenanceAPI) + require.NoError(t, err) + + rc := createRequestCtx() + response := srv.RouteConvertPrometheusPostRuleGroup(rc, fldr.Title, simpleGroup) + require.Equal(t, http.StatusConflict, response.Status()) + + // Verify the rule is still present + remaining, err := ruleStore.GetAlertRuleByUID(context.Background(), &models.GetAlertRuleByUIDQuery{ + UID: rule.UID, + OrgID: rule.OrgID, + }) + require.NoError(t, err) + require.NotNil(t, remaining) + }) + t.Run("with valid pause header values should return 202", func(t *testing.T) { testCases := []struct { name string @@ -420,9 +494,219 @@ func TestRouteConvertPrometheusGetRules(t *testing.T) { }) } -func createConvertPrometheusSrv(t *testing.T) (*ConvertPrometheusSrv, datasources.CacheService, *fakes.RuleStore, *foldertest.FakeService) { +func TestRouteConvertPrometheusDeleteNamespace(t *testing.T) { + t.Run("for non-existent folder should return 404", func(t *testing.T) { + srv, _, _, _ := createConvertPrometheusSrv(t) + rc := createRequestCtx() + + response := srv.RouteConvertPrometheusDeleteNamespace(rc, "non-existent") + require.Equal(t, http.StatusNotFound, response.Status()) + }) + + t.Run("valid request should delete rules", func(t *testing.T) { + initNamespace := func(promDefinition string, opts ...convertPrometheusSrvOptionsFunc) (*ConvertPrometheusSrv, *fakes.RuleStore, *folder.Folder, *models.AlertRule) { + srv, _, ruleStore, folderService := createConvertPrometheusSrv(t, opts...) + + // Create a folder in the root + fldr := randFolder() + fldr.ParentUID = "" + folderService.ExpectedFolder = fldr + folderService.ExpectedFolders = []*folder.Folder{fldr} + ruleStore.Folders[1] = append(ruleStore.Folders[1], fldr) + + rule := models.RuleGen. + With(models.RuleGen.WithNamespaceUID(fldr.UID)). + With(models.RuleGen.WithOrgID(1)). + With(models.RuleGen.WithPrometheusOriginalRuleDefinition(promDefinition)). + GenerateRef() + ruleStore.PutRule(context.Background(), rule) + + return srv, ruleStore, fldr, rule + } + + t.Run("valid request should delete rules", func(t *testing.T) { + srv, ruleStore, fldr, rule := initNamespace("prometheus definition") + + // Create another rule group in a different namespace that should not be deleted + otherGroupName := "other-group" + otherRule := models.RuleGen. + With(models.RuleGen.WithOrgID(1)). + With(models.RuleGen.WithGroupName(otherGroupName)). + With(models.RuleGen.WithPrometheusOriginalRuleDefinition("other prometheus definition")). + GenerateRef() + ruleStore.PutRule(context.Background(), otherRule) + + rc := createRequestCtx() + + response := srv.RouteConvertPrometheusDeleteNamespace(rc, fldr.Title) + require.Equal(t, http.StatusAccepted, response.Status()) + + // Verify the rule in the specified group was deleted + remaining, err := ruleStore.GetAlertRuleByUID(context.Background(), &models.GetAlertRuleByUIDQuery{ + UID: rule.UID, + OrgID: rule.OrgID, + }) + require.Error(t, err) + require.Nil(t, remaining) + + // Verify the rule in the other group still exists + remainingOther, err := ruleStore.GetAlertRuleByUID(context.Background(), &models.GetAlertRuleByUIDQuery{ + UID: otherRule.UID, + OrgID: otherRule.OrgID, + }) + require.NoError(t, err) + require.NotNil(t, remainingOther) + }) + + t.Run("fails to delete rules when they are provisioned", func(t *testing.T) { + provenanceStore := fakes.NewFakeProvisioningStore() + srv, ruleStore, fldr, rule := initNamespace("", withProvenanceStore(provenanceStore)) + rc := createRequestCtx() + + // Create a provisioned rule + rule2 := models.RuleGen. + With(models.RuleGen.WithNamespaceUID(fldr.UID)). + With(models.RuleGen.WithOrgID(1)). + With(models.RuleGen.WithPrometheusOriginalRuleDefinition("prometheus definition")). + GenerateRef() + ruleStore.PutRule(context.Background(), rule2) + err := provenanceStore.SetProvenance(context.Background(), rule2, 1, models.ProvenanceAPI) + require.NoError(t, err) + + response := srv.RouteConvertPrometheusDeleteNamespace(rc, fldr.Title) + require.Equal(t, http.StatusConflict, response.Status()) + + // Verify the rule is still present + remaining, err := ruleStore.GetAlertRuleByUID(context.Background(), &models.GetAlertRuleByUIDQuery{ + UID: rule.UID, + OrgID: rule.OrgID, + }) + require.NoError(t, err) + require.NotNil(t, remaining) + }) + }) +} + +func TestRouteConvertPrometheusDeleteRuleGroup(t *testing.T) { + t.Run("for non-existent folder should return 404", func(t *testing.T) { + srv, _, _, _ := createConvertPrometheusSrv(t) + rc := createRequestCtx() + + response := srv.RouteConvertPrometheusDeleteRuleGroup(rc, "non-existent", "test-group") + require.Equal(t, http.StatusNotFound, response.Status()) + }) + + const groupName = "test-group" + + t.Run("valid request should delete rules", func(t *testing.T) { + initGroup := func(promDefinition string, groupName string, opts ...convertPrometheusSrvOptionsFunc) (*ConvertPrometheusSrv, *fakes.RuleStore, *folder.Folder, *models.AlertRule) { + srv, _, ruleStore, folderService := createConvertPrometheusSrv(t, opts...) + + // Create a folder in the root + fldr := randFolder() + fldr.ParentUID = "" + folderService.ExpectedFolder = fldr + folderService.ExpectedFolders = []*folder.Folder{fldr} + ruleStore.Folders[1] = append(ruleStore.Folders[1], fldr) + + rule := models.RuleGen. + With(models.RuleGen.WithNamespaceUID(fldr.UID)). + With(models.RuleGen.WithOrgID(1)). + With(models.RuleGen.WithGroupName(groupName)). + With(models.RuleGen.WithPrometheusOriginalRuleDefinition(promDefinition)). + GenerateRef() + ruleStore.PutRule(context.Background(), rule) + + return srv, ruleStore, fldr, rule + } + + t.Run("valid request should delete rules", func(t *testing.T) { + srv, ruleStore, fldr, rule := initGroup("prometheus definition", groupName) + rc := createRequestCtx() + + // Create another rule in a different group that should not be deleted + otherGroupName := "other-group" + otherRule := models.RuleGen. + With(models.RuleGen.WithNamespaceUID(fldr.UID)). + With(models.RuleGen.WithOrgID(1)). + With(models.RuleGen.WithGroupName(otherGroupName)). + With(models.RuleGen.WithPrometheusOriginalRuleDefinition("other prometheus definition")). + GenerateRef() + ruleStore.PutRule(context.Background(), otherRule) + + response := srv.RouteConvertPrometheusDeleteRuleGroup(rc, fldr.Title, groupName) + require.Equal(t, http.StatusAccepted, response.Status()) + + // Verify the rule was deleted + remaining, err := ruleStore.GetAlertRuleByUID(context.Background(), &models.GetAlertRuleByUIDQuery{ + UID: rule.UID, + OrgID: rule.OrgID, + }) + require.Error(t, err) + require.Nil(t, remaining) + + // Verify the otherRule from the "other-group" is still present + otherRuleRefreshed, err := ruleStore.GetAlertRuleByUID(context.Background(), &models.GetAlertRuleByUIDQuery{ + UID: otherRule.UID, + OrgID: otherRule.OrgID, + }) + require.NoError(t, err) + require.NotNil(t, otherRuleRefreshed) + }) + + t.Run("fails to delete rules when they are provisioned", func(t *testing.T) { + provenanceStore := fakes.NewFakeProvisioningStore() + srv, ruleStore, fldr, rule := initGroup("", groupName, withProvenanceStore(provenanceStore)) + rc := createRequestCtx() + + // Create a provisioned rule + rule2 := models.RuleGen. + With(models.RuleGen.WithNamespaceUID(fldr.UID)). + With(models.RuleGen.WithOrgID(1)). + With(models.RuleGen.WithGroupName(groupName)). + With(models.RuleGen.WithPrometheusOriginalRuleDefinition("prometheus definition")). + GenerateRef() + ruleStore.PutRule(context.Background(), rule2) + err := provenanceStore.SetProvenance(context.Background(), rule2, 1, models.ProvenanceAPI) + require.NoError(t, err) + + response := srv.RouteConvertPrometheusDeleteRuleGroup(rc, fldr.Title, groupName) + require.Equal(t, http.StatusConflict, response.Status()) + + // Verify the rule is still present + remaining, err := ruleStore.GetAlertRuleByUID(context.Background(), &models.GetAlertRuleByUIDQuery{ + UID: rule.UID, + OrgID: rule.OrgID, + }) + require.NoError(t, err) + require.NotNil(t, remaining) + }) + }) +} + +type convertPrometheusSrvOptions struct { + provenanceStore provisioning.ProvisioningStore +} + +type convertPrometheusSrvOptionsFunc func(*convertPrometheusSrvOptions) + +func withProvenanceStore(store provisioning.ProvisioningStore) convertPrometheusSrvOptionsFunc { + return func(opts *convertPrometheusSrvOptions) { + opts.provenanceStore = store + } +} + +func createConvertPrometheusSrv(t *testing.T, opts ...convertPrometheusSrvOptionsFunc) (*ConvertPrometheusSrv, datasources.CacheService, *fakes.RuleStore, *foldertest.FakeService) { t.Helper() + options := convertPrometheusSrvOptions{ + provenanceStore: fakes.NewFakeProvisioningStore(), + } + + for _, opt := range opts { + opt(&options) + } + ruleStore := fakes.NewRuleStore(t) folder := randFolder() ruleStore.Folders[1] = append(ruleStore.Folders[1], folder) @@ -441,7 +725,7 @@ func createConvertPrometheusSrv(t *testing.T) (*ConvertPrometheusSrv, datasource alertRuleService := provisioning.NewAlertRuleService( ruleStore, - fakes.NewFakeProvisioningStore(), + options.provenanceStore, folderService, quotas, &provisioning.NopTransactionManager{}, diff --git a/pkg/services/ngalert/api/api_provisioning.go b/pkg/services/ngalert/api/api_provisioning.go index 6eb8f38a3c4..b5c7f5ab1c2 100644 --- a/pkg/services/ngalert/api/api_provisioning.go +++ b/pkg/services/ngalert/api/api_provisioning.go @@ -75,6 +75,7 @@ type AlertRuleService interface { GetRuleGroup(ctx context.Context, user identity.Requester, folder, group string) (alerting_models.AlertRuleGroup, error) ReplaceRuleGroup(ctx context.Context, user identity.Requester, group alerting_models.AlertRuleGroup, provenance alerting_models.Provenance) error DeleteRuleGroup(ctx context.Context, user identity.Requester, folder, group string, provenance alerting_models.Provenance) error + DeleteRuleGroups(ctx context.Context, user identity.Requester, provenance alerting_models.Provenance, opts *provisioning.FilterOptions) error GetAlertRuleWithFolderFullpath(ctx context.Context, u identity.Requester, ruleUID string) (provisioning.AlertRuleWithFolderFullpath, error) GetAlertRuleGroupWithFolderFullpath(ctx context.Context, u identity.Requester, folder, group string) (alerting_models.AlertRuleGroupWithFolderFullpath, error) GetAlertGroupsWithFolderFullpath(ctx context.Context, u identity.Requester, opts *provisioning.FilterOptions) ([]alerting_models.AlertRuleGroupWithFolderFullpath, error) diff --git a/pkg/services/ngalert/api/errors.go b/pkg/services/ngalert/api/errors.go index 0207d06cc43..a66db1e4fb2 100644 --- a/pkg/services/ngalert/api/errors.go +++ b/pkg/services/ngalert/api/errors.go @@ -3,6 +3,7 @@ package api import ( "errors" "fmt" + "net/http" "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/apimachinery/errutil" @@ -33,7 +34,7 @@ func errorToResponse(err error) response.Response { return response.Err(err) } if errors.Is(err, datasources.ErrDataSourceNotFound) { - return ErrResp(404, err, "") + return ErrResp(http.StatusNotFound, err, "") } if errors.Is(err, errUnexpectedDatasourceType) { return ErrResp(400, err, "") @@ -41,5 +42,5 @@ func errorToResponse(err error) response.Response { if errors.Is(err, errFolderAccess) { return toNamespaceErrorResponse(err) } - return ErrResp(500, err, "") + return ErrResp(http.StatusInternalServerError, err, "") } diff --git a/pkg/services/ngalert/api/tooling/api.json b/pkg/services/ngalert/api/tooling/api.json index 7fc64a8291f..0a2fb0af0d4 100644 --- a/pkg/services/ngalert/api/tooling/api.json +++ b/pkg/services/ngalert/api/tooling/api.json @@ -4932,7 +4932,6 @@ "type": "object" }, "gettableAlerts": { - "description": "GettableAlerts gettable alerts", "items": { "$ref": "#/definitions/gettableAlert", "type": "object" diff --git a/pkg/services/ngalert/provisioning/alert_rules.go b/pkg/services/ngalert/provisioning/alert_rules.go index 7e6f8387273..17b95ef314d 100644 --- a/pkg/services/ngalert/provisioning/alert_rules.go +++ b/pkg/services/ngalert/provisioning/alert_rules.go @@ -6,6 +6,7 @@ import ( "fmt" "time" + "github.com/grafana/grafana/pkg/apimachinery/errutil" "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/services/dashboards" @@ -29,6 +30,11 @@ type ruleAccessControlService interface { CanWriteAllRules(ctx context.Context, user identity.Requester) (bool, error) } +var errProvenanceMismatch = errutil.NewBase(errutil.StatusConflict, "alerting.provenanceMismatch").MustTemplate( + "cannot {{ .Public.Operation }} with provided provenance '{{ .Public.ProvidedProvenance }}', needs '{{ .Public.StoredProvenance }}'", + errutil.WithPublic("cannot {{ .Public.Operation }} with provided provenance '{{ .Public.ProvidedProvenance }}', needs '{{ .Public.StoredProvenance }}'"), +) + type NotificationSettingsValidatorProvider interface { Validator(ctx context.Context, orgID int64) (notifier.NotificationSettingsValidator, error) } @@ -445,27 +451,42 @@ func (service *AlertRuleService) ReplaceRuleGroup(ctx context.Context, user iden } func (service *AlertRuleService) DeleteRuleGroup(ctx context.Context, user identity.Requester, namespaceUID, group string, provenance models.Provenance) error { - delta, err := store.CalculateRuleGroupDelete(ctx, service.ruleStore, models.AlertRuleGroupKey{ - OrgID: user.GetOrgID(), - NamespaceUID: namespaceUID, - RuleGroup: group, + return service.DeleteRuleGroups(ctx, user, provenance, &FilterOptions{ + NamespaceUIDs: []string{namespaceUID}, + RuleGroups: []string{group}, }) +} + +// DeleteRuleGroups deletes alert rule groups by the specified filter options. +func (service *AlertRuleService) DeleteRuleGroups(ctx context.Context, user identity.Requester, provenance models.Provenance, filterOpts *FilterOptions) error { + q := models.ListAlertRulesQuery{} + q = filterOpts.apply(q) + q.OrgID = user.GetOrgID() + + deltas, err := store.CalculateRuleGroupsDelete(ctx, service.ruleStore, user.GetOrgID(), &q) if err != nil { return err } - // check if the current user has permissions to all rules and can bypass the regular authorization validation. - can, err := service.authz.CanWriteAllRules(ctx, user) - if err != nil { - return err - } - if !can { - if err := service.authz.AuthorizeRuleGroupWrite(ctx, user, delta); err != nil { - return err + // Perform all deletions in a transaction + return service.xact.InTransaction(ctx, func(ctx context.Context) error { + for _, delta := range deltas { + can, err := service.authz.CanWriteAllRules(ctx, user) + if err != nil { + return err + } + if !can { + if err := service.authz.AuthorizeRuleGroupWrite(ctx, user, delta); err != nil { + return err + } + } + err = service.persistDelta(ctx, user, delta, provenance) + if err != nil { + return err + } } - } - - return service.persistDelta(ctx, user, delta, provenance) + return nil + }) } func (service *AlertRuleService) calcDelta(ctx context.Context, user identity.Requester, group models.AlertRuleGroup) (*store.GroupDelta, error) { @@ -526,7 +547,13 @@ func (service *AlertRuleService) persistDelta(ctx context.Context, user identity return err } if canUpdate := validation.CanUpdateProvenanceInRuleGroup(storedProvenance, provenance); !canUpdate { - return fmt.Errorf("cannot delete with provided provenance '%s', needs '%s'", provenance, storedProvenance) + return errProvenanceMismatch.Build(errutil.TemplateData{ + Public: map[string]interface{}{ + "ProvidedProvenance": provenance, + "StoredProvenance": storedProvenance, + "Operation": "delete", + }, + }) } } if err := service.deleteRules(ctx, user.GetOrgID(), delta.Delete...); err != nil { @@ -543,7 +570,13 @@ func (service *AlertRuleService) persistDelta(ctx context.Context, user identity return err } if canUpdate := validation.CanUpdateProvenanceInRuleGroup(storedProvenance, provenance); !canUpdate { - return fmt.Errorf("cannot update with provided provenance '%s', needs '%s'", provenance, storedProvenance) + return errProvenanceMismatch.Build(errutil.TemplateData{ + Public: map[string]interface{}{ + "ProvidedProvenance": provenance, + "StoredProvenance": storedProvenance, + "Operation": "update", + }, + }) } updates = append(updates, models.UpdateRule{ Existing: update.Existing, @@ -689,7 +722,13 @@ func (service *AlertRuleService) DeleteAlertRule(ctx context.Context, user ident return err } if storedProvenance != provenance && storedProvenance != models.ProvenanceNone { - return fmt.Errorf("cannot delete with provided provenance '%s', needs '%s'", provenance, storedProvenance) + return errProvenanceMismatch.Build(errutil.TemplateData{ + Public: map[string]interface{}{ + "ProvidedProvenance": provenance, + "StoredProvenance": storedProvenance, + "Operation": "delete", + }, + }) } can, err := service.authz.CanWriteAllRules(ctx, user) diff --git a/pkg/services/ngalert/provisioning/alert_rules_test.go b/pkg/services/ngalert/provisioning/alert_rules_test.go index 3a10b26fd39..c269a92c664 100644 --- a/pkg/services/ngalert/provisioning/alert_rules_test.go +++ b/pkg/services/ngalert/provisioning/alert_rules_test.go @@ -5,6 +5,7 @@ import ( "encoding/json" "errors" "math/rand" + "slices" "strconv" "strings" "testing" @@ -1577,7 +1578,7 @@ func TestDeleteRuleGroup(t *testing.T) { assert.Equal(t, u, user) assert.Equal(t, groupKey, change.GroupKey) assert.Contains(t, change.AffectedGroups, groupKey) - assert.EqualValues(t, rules, change.AffectedGroups[groupKey]) + assert.ElementsMatch(t, rules, change.AffectedGroups[groupKey]) assert.Empty(t, change.Update) assert.Empty(t, change.New) assert.Len(t, change.Delete, len(rules)) @@ -1597,6 +1598,228 @@ func TestDeleteRuleGroup(t *testing.T) { }) } +func TestDeleteRuleGroups(t *testing.T) { + orgID1 := rand.Int63() + orgID2 := rand.Int63() + u := &user.SignedInUser{OrgID: orgID1} + + // Create groups across different orgs and namespaces + groupKey1 := models.AlertRuleGroupKey{ + OrgID: orgID1, + NamespaceUID: "namespace1", + RuleGroup: "group1", + } + groupKey2 := models.AlertRuleGroupKey{ + OrgID: orgID1, + NamespaceUID: "namespace2", + RuleGroup: "group2", + } + groupKey3 := models.AlertRuleGroupKey{ + OrgID: orgID1, + NamespaceUID: "namespace3", + RuleGroup: "group3", + } + groupKey4 := models.AlertRuleGroupKey{ + OrgID: orgID2, // Different org + NamespaceUID: "namespace1", + RuleGroup: "group1", + } + + gen := models.RuleGen + // Create rules for each group + rules1 := gen.With(gen.WithGroupKey(groupKey1)).GenerateManyRef(2) + rules2 := gen.With(gen.WithGroupKey(groupKey2)).GenerateManyRef(3) + rules3 := gen.With(gen.WithGroupKey(groupKey3)).GenerateManyRef(2) + rules4 := gen.With(gen.WithGroupKey(groupKey4)).GenerateManyRef(2) + + org1Rules := slices.Concat(rules1, rules2, rules3) + org2Rules := rules4 + + initServiceWithData := func(t *testing.T) (*AlertRuleService, *fakes.RuleStore, *fakes.FakeProvisioningStore, *fakeRuleAccessControlService) { + service, ruleStore, provenanceStore, ac := initService(t) + ruleStore.Rules = map[int64][]*models.AlertRule{ + orgID1: org1Rules, + orgID2: org2Rules, + } + // Set provenance for all rules + for _, rules := range []([]*models.AlertRule){org1Rules, org2Rules} { + for _, rule := range rules { + err := provenanceStore.SetProvenance(context.Background(), rule, rule.OrgID, models.ProvenanceAPI) + require.NoError(t, err) + } + } + return service, ruleStore, provenanceStore, ac + } + + getUIDs := func(rules []*models.AlertRule) []string { + uids := make([]string, 0, len(rules)) + for _, rule := range rules { + uids = append(uids, rule.UID) + } + return uids + } + + t.Run("when deleting specific groups", func(t *testing.T) { + filterOpts := &FilterOptions{ + NamespaceUIDs: []string{"namespace1"}, + RuleGroups: []string{"group1"}, + } + + t.Run("when user can write all rules", func(t *testing.T) { + service, ruleStore, _, ac := initServiceWithData(t) + ac.CanWriteAllRulesFunc = func(ctx context.Context, user identity.Requester) (bool, error) { + return true, nil + } + + err := service.DeleteRuleGroups(context.Background(), u, models.ProvenanceAPI, filterOpts) + require.NoError(t, err) + + require.Len(t, ac.Calls, 1) + assert.Equal(t, "CanWriteAllRules", ac.Calls[0].Method) + + // Verify only rules from group1 in org1 were deleted + deletes := getDeletedRules(t, ruleStore) + require.Len(t, deletes, 1) + require.ElementsMatch(t, getUIDs(rules1), deletes[0].uids) + }) + + t.Run("when user cannot write all rules", func(t *testing.T) { + t.Run("should not delete if not authorized", func(t *testing.T) { + service, ruleStore, _, ac := initServiceWithData(t) + ac.CanWriteAllRulesFunc = func(ctx context.Context, user identity.Requester) (bool, error) { + return false, nil + } + expectedErr := errors.New("test error") + ac.AuthorizeRuleChangesFunc = func(ctx context.Context, user identity.Requester, change *store.GroupDelta) error { + return expectedErr + } + + err := service.DeleteRuleGroups(context.Background(), u, models.ProvenanceAPI, filterOpts) + require.ErrorIs(t, err, expectedErr) + + require.Len(t, ac.Calls, 2) + assert.Equal(t, "CanWriteAllRules", ac.Calls[0].Method) + assert.Equal(t, "AuthorizeRuleGroupWrite", ac.Calls[1].Method) + + deletes := getDeletedRules(t, ruleStore) + require.Empty(t, deletes) + }) + + t.Run("should delete group1 when authorized", func(t *testing.T) { + service, ruleStore, _, ac := initServiceWithData(t) + ac.CanWriteAllRulesFunc = func(ctx context.Context, user identity.Requester) (bool, error) { + return false, nil + } + ac.AuthorizeRuleChangesFunc = func(ctx context.Context, user identity.Requester, change *store.GroupDelta) error { + assert.Equal(t, u, user) + assert.Equal(t, groupKey1, change.GroupKey) + assert.ElementsMatch(t, rules1, change.AffectedGroups[groupKey1]) + assert.Empty(t, change.Update) + assert.Empty(t, change.New) + assert.Len(t, change.Delete, len(rules1)) + return nil + } + + err := service.DeleteRuleGroups(context.Background(), u, models.ProvenanceAPI, filterOpts) + require.NoError(t, err) + + require.Len(t, ac.Calls, 2) + assert.Equal(t, "CanWriteAllRules", ac.Calls[0].Method) + assert.Equal(t, "AuthorizeRuleGroupWrite", ac.Calls[1].Method) + + deletes := getDeletedRules(t, ruleStore) + require.Len(t, deletes, 1) + require.ElementsMatch(t, getUIDs(rules1), deletes[0].uids) + }) + }) + }) + + t.Run("when deleting multiple groups from multiple namespaces", func(t *testing.T) { + filterOpts := &FilterOptions{ + NamespaceUIDs: []string{"namespace1", "namespace2"}, + RuleGroups: []string{"group1", "group2"}, + } + + t.Run("should delete all matching groups from correct org", func(t *testing.T) { + service, ruleStore, _, ac := initServiceWithData(t) + ac.CanWriteAllRulesFunc = func(ctx context.Context, user identity.Requester) (bool, error) { + return true, nil + } + + err := service.DeleteRuleGroups(context.Background(), u, models.ProvenanceAPI, filterOpts) + require.NoError(t, err) + + deletes := getDeletedRules(t, ruleStore) + require.Len(t, deletes, 2) + require.ElementsMatch( + t, + slices.Concat(getUIDs(rules1), getUIDs(rules2)), + slices.Concat(deletes[0].uids, deletes[1].uids), + ) + }) + }) + + t.Run("when filtering by imported Prometheus rules", func(t *testing.T) { + filterOpts := &FilterOptions{ + ImportedPrometheusRule: util.Pointer(true), + NamespaceUIDs: []string{"namespace1"}, + } + + t.Run("when the group is not imported", func(t *testing.T) { + filterOpts.RuleGroups = []string{groupKey1.RuleGroup} + service, _, _, ac := initServiceWithData(t) + ac.CanWriteAllRulesFunc = func(ctx context.Context, user identity.Requester) (bool, error) { + return true, nil + } + + err := service.DeleteRuleGroups(context.Background(), u, models.ProvenanceAPI, filterOpts) + require.ErrorIs(t, err, models.ErrAlertRuleGroupNotFound) + }) + + t.Run("when the group is imported", func(t *testing.T) { + importedGroup := models.AlertRuleGroupKey{ + OrgID: orgID1, + NamespaceUID: "namespace1", + RuleGroup: "newgroup", + } + importedRules := gen.With( + gen.WithGroupKey(importedGroup), + gen.WithPrometheusOriginalRuleDefinition("something"), + ).GenerateManyRef(2) + filterOpts.RuleGroups = []string{importedGroup.RuleGroup} + service, ruleStore, _, ac := initServiceWithData(t) + ruleStore.Rules[orgID1] = append(ruleStore.Rules[orgID1], importedRules...) + ac.CanWriteAllRulesFunc = func(ctx context.Context, user identity.Requester) (bool, error) { + return true, nil + } + + err := service.DeleteRuleGroups(context.Background(), u, models.ProvenanceAPI, filterOpts) + require.NoError(t, err) + deletes := getDeletedRules(t, ruleStore) + require.Len(t, deletes, 1) + require.ElementsMatch(t, getUIDs(importedRules), deletes[0].uids) + }) + }) + + t.Run("with no matching rule groups", func(t *testing.T) { + filterOpts := &FilterOptions{ + NamespaceUIDs: []string{"non-existent"}, + RuleGroups: []string{"non-existent"}, + } + + service, ruleStore, _, ac := initServiceWithData(t) + ac.CanWriteAllRulesFunc = func(ctx context.Context, user identity.Requester) (bool, error) { + return true, nil + } + + err := service.DeleteRuleGroups(context.Background(), u, models.ProvenanceAPI, filterOpts) + require.ErrorIs(t, err, models.ErrAlertRuleGroupNotFound) + + deletes := getDeletedRules(t, ruleStore) + require.Empty(t, deletes) + }) +} + func TestProvisiongWithFullpath(t *testing.T) { tracer := tracing.InitializeTracerForTest() inProcBus := bus.ProvideBus(tracer) @@ -1696,6 +1919,31 @@ func getDeleteQueries(ruleStore *fakes.RuleStore) []fakes.GenericRecordedQuery { return result } +type deleteRuleOperation struct { + orgID int64 + uids []string +} + +func getDeletedRules(t *testing.T, ruleStore *fakes.RuleStore) []deleteRuleOperation { + t.Helper() + + queries := getDeleteQueries(ruleStore) + operations := make([]deleteRuleOperation, 0, len(queries)) + for _, q := range queries { + orgID, ok := q.Params[0].(int64) + require.True(t, ok, "orgID parameter should be int64") + + uids, ok := q.Params[1].([]string) + require.True(t, ok, "uids parameter should be []string") + + operations = append(operations, deleteRuleOperation{ + orgID: orgID, + uids: uids, + }) + } + return operations +} + func createAlertRuleService(t *testing.T, folderService folder.Service) AlertRuleService { t.Helper() sqlStore := db.InitTestDB(t) diff --git a/pkg/services/ngalert/store/deltas.go b/pkg/services/ngalert/store/deltas.go index a7b00d27adf..d59980604f1 100644 --- a/pkg/services/ngalert/store/deltas.go +++ b/pkg/services/ngalert/store/deltas.go @@ -221,15 +221,13 @@ func CalculateRuleUpdate(ctx context.Context, ruleReader RuleReader, rule *model return calculateChanges(ctx, ruleReader, rule.GetGroupKey(), existingGroupRules, newGroup) } -// CalculateRuleGroupDelete calculates GroupDelta that reflects an operation of removing entire group -func CalculateRuleGroupDelete(ctx context.Context, ruleReader RuleReader, groupKey models.AlertRuleGroupKey) (*GroupDelta, error) { - // List all rules in the group. - q := models.ListAlertRulesQuery{ - OrgID: groupKey.OrgID, - NamespaceUIDs: []string{groupKey.NamespaceUID}, - RuleGroups: []string{groupKey.RuleGroup}, +// CalculateRuleGroupsDelete calculates []*GroupDelta that reflects an operation of removing multiple groups +func CalculateRuleGroupsDelete(ctx context.Context, ruleReader RuleReader, orgID int64, query *models.ListAlertRulesQuery) ([]*GroupDelta, error) { + if query == nil { + query = &models.ListAlertRulesQuery{} } - ruleList, err := ruleReader.ListAlertRules(ctx, &q) + query.OrgID = orgID + ruleList, err := ruleReader.ListAlertRules(ctx, query) if err != nil { return nil, err } @@ -237,14 +235,40 @@ func CalculateRuleGroupDelete(ctx context.Context, ruleReader RuleReader, groupK return nil, models.ErrAlertRuleGroupNotFound.Errorf("") } - delta := &GroupDelta{ - GroupKey: groupKey, - Delete: ruleList, - AffectedGroups: map[models.AlertRuleGroupKey]models.RulesGroup{ - groupKey: ruleList, - }, + groups := models.GroupByAlertRuleGroupKey(ruleList) + deltas := make([]*GroupDelta, 0, len(groups)) + for groupKey := range groups { + delta := &GroupDelta{ + GroupKey: groupKey, + Delete: groups[groupKey], + AffectedGroups: map[models.AlertRuleGroupKey]models.RulesGroup{ + groupKey: groups[groupKey], + }, + } + if err != nil { + return nil, err + } + deltas = append(deltas, delta) } - return delta, nil + + return deltas, nil +} + +// CalculateRuleGroupDelete calculates GroupDelta that reflects an operation of removing entire group +func CalculateRuleGroupDelete(ctx context.Context, ruleReader RuleReader, groupKey models.AlertRuleGroupKey) (*GroupDelta, error) { + q := &models.ListAlertRulesQuery{ + NamespaceUIDs: []string{groupKey.NamespaceUID}, + RuleGroups: []string{groupKey.RuleGroup}, + } + deltas, err := CalculateRuleGroupsDelete(ctx, ruleReader, groupKey.OrgID, q) + if err != nil { + return nil, err + } + if len(deltas) != 1 { + return nil, fmt.Errorf("expected to get a single group delta, got %d", len(deltas)) + } + + return deltas[0], nil } // CalculateRuleDelete calculates GroupDelta that reflects an operation of removing a rule from the group. diff --git a/pkg/services/ngalert/store/deltas_test.go b/pkg/services/ngalert/store/deltas_test.go index fde5a8e252f..fc27fbe9af2 100644 --- a/pkg/services/ngalert/store/deltas_test.go +++ b/pkg/services/ngalert/store/deltas_test.go @@ -428,6 +428,91 @@ func TestCalculateAutomaticChanges(t *testing.T) { }) } +func TestCalculateRuleGroupsDelete(t *testing.T) { + orgId := int64(rand.Int31()) + gen := models.RuleGen + + t.Run("returns ErrAlertRuleGroupNotFound when namespace has no rules", func(t *testing.T) { + fakeStore := fakes.NewRuleStore(t) + otherRules := gen.With(gen.WithOrgID(orgId), gen.WithNamespaceUID("ns-1")).GenerateManyRef(3) + fakeStore.Rules[orgId] = otherRules + + query := &models.ListAlertRulesQuery{ + NamespaceUIDs: []string{"ns-2"}, + } + deltas, err := CalculateRuleGroupsDelete(context.Background(), fakeStore, orgId, query) + require.ErrorIs(t, err, models.ErrAlertRuleGroupNotFound) + require.Nil(t, deltas) + }) + + t.Run("returns deltas for all affected groups in namespace", func(t *testing.T) { + fakeStore := fakes.NewRuleStore(t) + folder := randFolder() + + // Create rules in two groups in target namespace + group1Key := models.AlertRuleGroupKey{ + OrgID: orgId, + NamespaceUID: folder.UID, + RuleGroup: util.GenerateShortUID(), + } + group2Key := models.AlertRuleGroupKey{ + OrgID: orgId, + NamespaceUID: folder.UID, + RuleGroup: util.GenerateShortUID(), + } + + group1Rules := gen.With(gen.WithGroupKey(group1Key)).GenerateManyRef(3) + group2Rules := gen.With(gen.WithGroupKey(group2Key)).GenerateManyRef(2) + allNamespaceRules := append(group1Rules, group2Rules...) + + // Create rules in different namespace + otherRules := gen.With(gen.WithOrgID(orgId), gen.WithNamespaceUIDNotIn(folder.UID)).GenerateManyRef(3) + + fakeStore.Rules[orgId] = append(allNamespaceRules, otherRules...) + + query := &models.ListAlertRulesQuery{ + NamespaceUIDs: []string{folder.UID}, + } + + deltas, err := CalculateRuleGroupsDelete(context.Background(), fakeStore, orgId, query) + require.NoError(t, err) + + require.Len(t, deltas, 2, "expected deltas for two groups") + + // Verify each group's delta + for _, delta := range deltas { + require.True(t, delta.GroupKey == group1Key || delta.GroupKey == group2Key) + require.Empty(t, delta.Update) + require.Empty(t, delta.New) + + require.Contains(t, delta.AffectedGroups, delta.GroupKey) + if delta.GroupKey == group1Key { + require.ElementsMatch(t, group1Rules, delta.Delete) + require.ElementsMatch(t, group1Rules, delta.AffectedGroups[delta.GroupKey]) + } else { + require.ElementsMatch(t, group2Rules, delta.Delete) + require.ElementsMatch(t, group2Rules, delta.AffectedGroups[delta.GroupKey]) + } + } + }) + + t.Run("fails if store returns error", func(t *testing.T) { + fakeStore := fakes.NewRuleStore(t) + expectedErr := errors.New("store error") + fakeStore.Hook = func(cmd any) error { + switch cmd.(type) { + case models.ListAlertRulesQuery: + return expectedErr + } + return nil + } + + deltas, err := CalculateRuleGroupsDelete(context.Background(), fakeStore, orgId, nil) + require.ErrorIs(t, err, expectedErr) + require.Nil(t, deltas) + }) +} + func TestCalculateRuleGroupDelete(t *testing.T) { gen := models.RuleGen fakeStore := fakes.NewRuleStore(t) @@ -449,13 +534,13 @@ func TestCalculateRuleGroupDelete(t *testing.T) { require.NoError(t, err) assert.Equal(t, groupKey, delta.GroupKey) - assert.EqualValues(t, groupRules, delta.Delete) + assert.ElementsMatch(t, groupRules, delta.Delete) assert.Empty(t, delta.Update) assert.Empty(t, delta.New) assert.Len(t, delta.AffectedGroups, 1) - assert.Equal(t, models.RulesGroup(groupRules), delta.AffectedGroups[delta.GroupKey]) + assert.ElementsMatch(t, groupRules, delta.AffectedGroups[delta.GroupKey]) }) } diff --git a/pkg/services/ngalert/tests/fakes/rules.go b/pkg/services/ngalert/tests/fakes/rules.go index 2fa4714b19d..728ae6bd76f 100644 --- a/pkg/services/ngalert/tests/fakes/rules.go +++ b/pkg/services/ngalert/tests/fakes/rules.go @@ -214,6 +214,15 @@ func (f *RuleStore) ListAlertRules(_ context.Context, q *models.ListAlertRulesQu if len(q.RuleUIDs) > 0 && !slices.Contains(q.RuleUIDs, r.UID) { continue } + if q.ImportedPrometheusRule != nil { + hasOriginalRuleDefinition := r.PrometheusRuleDefinition() != "" + if *q.ImportedPrometheusRule && !hasOriginalRuleDefinition { + continue + } + if !*q.ImportedPrometheusRule && hasOriginalRuleDefinition { + continue + } + } ruleList = append(ruleList, r) } @@ -308,12 +317,21 @@ func (f *RuleStore) InsertAlertRules(_ context.Context, _ *models.UserUID, q []m defer f.mtx.Unlock() f.RecordedOps = append(f.RecordedOps, q) ids := make([]models.AlertRuleKeyWithId, 0, len(q)) + rulesPerOrg := map[int64][]models.AlertRule{} for _, rule := range q { ids = append(ids, models.AlertRuleKeyWithId{ AlertRuleKey: rule.GetKey(), ID: rand.Int63(), }) + rulesPerOrg[rule.OrgID] = append(rulesPerOrg[rule.OrgID], rule) } + + for orgID, rules := range rulesPerOrg { + for _, rule := range rules { + f.Rules[orgID] = append(f.Rules[orgID], &rule) + } + } + if err := f.Hook(q); err != nil { return ids, err } diff --git a/pkg/tests/api/alerting/api_convert_prometheus_test.go b/pkg/tests/api/alerting/api_convert_prometheus_test.go index 674d708250f..c92d5f8e428 100644 --- a/pkg/tests/api/alerting/api_convert_prometheus_test.go +++ b/pkg/tests/api/alerting/api_convert_prometheus_test.go @@ -1,6 +1,7 @@ package alerting import ( + "encoding/json" "net/http" "testing" "time" @@ -8,6 +9,7 @@ import ( prommodel "github.com/prometheus/common/model" "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/expr" "github.com/grafana/grafana/pkg/services/datasources" apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" "github.com/grafana/grafana/pkg/services/org" @@ -100,7 +102,7 @@ func TestIntegrationConvertPrometheusEndpoints(t *testing.T) { testinfra.SQLiteIntegrationTest(t) // Setup Grafana and its Database - dir, path := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{ + dir, gpath := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{ DisableLegacyAlerting: true, EnableUnifiedAlerting: true, DisableAnonymous: true, @@ -108,7 +110,7 @@ func TestIntegrationConvertPrometheusEndpoints(t *testing.T) { EnableFeatureToggles: []string{"alertingConversionAPI"}, }) - grafanaListedAddr, env := testinfra.StartGrafanaEnv(t, dir, path) + grafanaListedAddr, env := testinfra.StartGrafanaEnv(t, dir, gpath) // Create users to make authenticated requests createUser(t, env.SQLStore, env.Cfg, user.CreateUserCommand{ @@ -169,6 +171,111 @@ func TestIntegrationConvertPrometheusEndpoints(t *testing.T) { _, status, raw := viewerClient.ConvertPrometheusPostRuleGroup(t, namespace1, ds.Body.Datasource.UID, promGroup1, nil) requireStatusCode(t, http.StatusForbidden, status, raw) }) + + t.Run("delete one rule group", func(t *testing.T) { + _, status, body := apiClient.ConvertPrometheusPostRuleGroup(t, namespace1, ds.Body.Datasource.UID, promGroup1, nil) + requireStatusCode(t, http.StatusAccepted, status, body) + _, status, body = apiClient.ConvertPrometheusPostRuleGroup(t, namespace1, ds.Body.Datasource.UID, promGroup2, nil) + requireStatusCode(t, http.StatusAccepted, status, body) + _, status, body = apiClient.ConvertPrometheusPostRuleGroup(t, namespace2, ds.Body.Datasource.UID, promGroup3, nil) + requireStatusCode(t, http.StatusAccepted, status, body) + + apiClient.ConvertPrometheusDeleteRuleGroup(t, namespace1, promGroup1.Name) + + // Check that the promGroup2 and promGroup3 are still there + namespaces := apiClient.ConvertPrometheusGetAllRules(t) + expectedNamespaces := map[string][]apimodels.PrometheusRuleGroup{ + namespace1: {promGroup2}, + namespace2: {promGroup3}, + } + require.Equal(t, expectedNamespaces, namespaces) + + // Delete the second namespace + apiClient.ConvertPrometheusDeleteNamespace(t, namespace2) + + // Check that only the first namespace is left + namespaces = apiClient.ConvertPrometheusGetAllRules(t) + expectedNamespaces = map[string][]apimodels.PrometheusRuleGroup{ + namespace1: {promGroup2}, + } + require.Equal(t, expectedNamespaces, namespaces) + }) +} + +func TestIntegrationConvertPrometheusEndpoints_Conflict(t *testing.T) { + testinfra.SQLiteIntegrationTest(t) + + // Setup Grafana and its Database + dir, gpath := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{ + DisableLegacyAlerting: true, + EnableUnifiedAlerting: true, + DisableAnonymous: true, + AppModeProduction: true, + EnableFeatureToggles: []string{"alertingConversionAPI"}, + }) + + grafanaListedAddr, env := testinfra.StartGrafanaEnv(t, dir, gpath) + + // Create users to make authenticated requests + createUser(t, env.SQLStore, env.Cfg, user.CreateUserCommand{ + DefaultOrgRole: string(org.RoleAdmin), + Password: "password", + Login: "admin", + }) + apiClient := newAlertingApiClient(grafanaListedAddr, "admin", "password") + + createUser(t, env.SQLStore, env.Cfg, user.CreateUserCommand{ + DefaultOrgRole: string(org.RoleViewer), + Password: "password", + Login: "viewer", + }) + + namespace1 := "test-namespace-1" + + ds := apiClient.CreateDatasource(t, datasources.DS_PROMETHEUS) + + t.Run("cannot overwrite a rule group with different provenance", func(t *testing.T) { + // Create a rule group using the provisioning API and then try to overwrite it + // using the Prometheus Conversion API. It should fail because the provenance + // we set for rules in these two APIs is different and we check that when updating. + provisionedRuleGroup := apimodels.AlertRuleGroup{ + Title: promGroup1.Name, + Interval: 60, + FolderUID: namespace1, + Rules: []apimodels.ProvisionedAlertRule{ + { + Title: "Rule1", + OrgID: 1, + RuleGroup: promGroup1.Name, + Condition: "A", + NoDataState: apimodels.Alerting, + ExecErrState: apimodels.AlertingErrState, + For: prommodel.Duration(time.Duration(60) * time.Second), + Data: []apimodels.AlertQuery{ + { + RefID: "A", + RelativeTimeRange: apimodels.RelativeTimeRange{ + From: apimodels.Duration(time.Duration(5) * time.Hour), + To: apimodels.Duration(time.Duration(3) * time.Hour), + }, + DatasourceUID: expr.DatasourceUID, + Model: json.RawMessage([]byte(`{"type":"math","expression":"2 + 3 \u003e 1"}`)), + }, + }, + }, + }, + } + + // Create the folder + apiClient.CreateFolder(t, namespace1, namespace1) + // Create rule in the root folder using another API + _, status, response := apiClient.CreateOrUpdateRuleGroupProvisioning(t, provisionedRuleGroup) + require.Equalf(t, http.StatusOK, status, response) + + // Should fail to post the group + _, status, body := apiClient.ConvertPrometheusPostRuleGroup(t, namespace1, ds.Body.Datasource.UID, promGroup1, nil) + requireStatusCode(t, http.StatusConflict, status, body) + }) } func TestIntegrationConvertPrometheusEndpoints_CreatePausedRules(t *testing.T) { diff --git a/pkg/tests/api/alerting/testing.go b/pkg/tests/api/alerting/testing.go index 6594eaa4bb6..6290339dd24 100644 --- a/pkg/tests/api/alerting/testing.go +++ b/pkg/tests/api/alerting/testing.go @@ -1146,6 +1146,22 @@ func (a apiClient) ConvertPrometheusGetAllRules(t *testing.T) map[string][]apimo return result } +func (a apiClient) ConvertPrometheusDeleteRuleGroup(t *testing.T, namespaceTitle, groupName string) { + t.Helper() + req, err := http.NewRequest(http.MethodDelete, fmt.Sprintf("%s/api/convert/prometheus/config/v1/rules/%s/%s", a.url, namespaceTitle, groupName), nil) + require.NoError(t, err) + _, status, raw := sendRequestJSON[apimodels.ConvertPrometheusResponse](t, req, http.StatusAccepted) + requireStatusCode(t, http.StatusAccepted, status, raw) +} + +func (a apiClient) ConvertPrometheusDeleteNamespace(t *testing.T, namespaceTitle string) { + t.Helper() + req, err := http.NewRequest(http.MethodDelete, fmt.Sprintf("%s/api/convert/prometheus/config/v1/rules/%s", a.url, namespaceTitle), nil) + require.NoError(t, err) + _, status, raw := sendRequestJSON[apimodels.ConvertPrometheusResponse](t, req, http.StatusAccepted) + requireStatusCode(t, http.StatusAccepted, status, raw) +} + func sendRequestRaw(t *testing.T, req *http.Request) ([]byte, int, error) { t.Helper() client := &http.Client{} diff --git a/public/api-merged.json b/public/api-merged.json index 9ddc287f6f6..dc455b6a36f 100644 --- a/public/api-merged.json +++ b/public/api-merged.json @@ -22771,7 +22771,6 @@ } }, "gettableAlerts": { - "description": "GettableAlerts gettable alerts", "type": "array", "items": { "type": "object", diff --git a/public/openapi3.json b/public/openapi3.json index 8f339a39b59..fabc90f92cf 100644 --- a/public/openapi3.json +++ b/public/openapi3.json @@ -12838,7 +12838,6 @@ "type": "object" }, "gettableAlerts": { - "description": "GettableAlerts gettable alerts", "items": { "$ref": "#/components/schemas/gettableAlert" }, From 8a988d6b5a148d4374c5eb158bf295c563f53daf Mon Sep 17 00:00:00 2001 From: Ezequiel Victorero Date: Thu, 27 Feb 2025 09:14:37 -0300 Subject: [PATCH 14/32] Playlists: Add support for back button (#101374) --- .../app/features/playlist/PlaylistSrv.test.ts | 26 ++++++++++++- public/app/features/playlist/PlaylistSrv.ts | 38 +++++++++++++------ 2 files changed, 52 insertions(+), 12 deletions(-) diff --git a/public/app/features/playlist/PlaylistSrv.test.ts b/public/app/features/playlist/PlaylistSrv.test.ts index 687b5e39156..3620f3a1e02 100644 --- a/public/app/features/playlist/PlaylistSrv.test.ts +++ b/public/app/features/playlist/PlaylistSrv.test.ts @@ -1,4 +1,3 @@ -// @ts-ignore import { Store } from 'redux'; import configureMockStore from 'redux-mock-store'; @@ -139,4 +138,29 @@ describe('PlaylistSrv', () => { expect((srv as any).validPlaylistUrl).toBe('/url/to/bbb'); expect(srv.state.isPlaying).toBe(true); }); + + it('should replace playlist start page in history when starting playlist', async () => { + // Start at playlists page + locationService.push('/playlists'); + + // Navigate to playlist start page + locationService.push('/playlists/play/foo'); + + // Start the playlist + await srv.start('foo'); + + // Get history entries + const history = locationService.getHistory(); + const entries = (history as unknown as { entries: Location[] }).entries; + + // The current entry should be the first dashboard + expect(entries[entries.length - 1].pathname).toBe('/url/to/aaa'); + + // The previous entry should be the playlists page, not the start page + expect(entries[entries.length - 2].pathname).toBe('/playlists'); + + // Verify the start page (/playlists/play/foo) is not in history + const hasStartPage = entries.some((entry: { pathname: string }) => entry.pathname === '/playlists/play/foo'); + expect(hasStartPage).toBe(false); + }); }); diff --git a/public/app/features/playlist/PlaylistSrv.ts b/public/app/features/playlist/PlaylistSrv.ts index d11f817a369..99b839c857c 100644 --- a/public/app/features/playlist/PlaylistSrv.ts +++ b/public/app/features/playlist/PlaylistSrv.ts @@ -39,6 +39,28 @@ export class PlaylistSrv extends StateManagerBase { this.api = getPlaylistAPI(); } + private navigateToDashboard(replaceHistoryEntry = false) { + const url = this.urls[this.index]; + const queryParams = locationService.getSearchObject(); + const filteredParams = pickBy(queryParams, (value: unknown, key: string) => queryParamsToPreserve[key]); + const nextDashboardUrl = locationUtil.stripBaseFromUrl(url); + + this.index++; + this.validPlaylistUrl = nextDashboardUrl; + this.nextTimeoutId = setTimeout(() => this.next(), this.interval); + + const urlWithParams = nextDashboardUrl + '?' + urlUtil.toUrlParams(filteredParams); + + // When starting the playlist from the PlaylistStartPage component using the playlist URL, we want to replace the + // history entry to support the back button + // When starting the playlist from the playlist modal, we want to push a new history entry + if (replaceHistoryEntry) { + locationService.getHistory().replace(urlWithParams); + } else { + locationService.push(urlWithParams); + } + } + next() { clearTimeout(this.nextTimeoutId); @@ -55,16 +77,7 @@ export class PlaylistSrv extends StateManagerBase { this.index = 0; } - const url = this.urls[this.index]; - const queryParams = locationService.getSearchObject(); - const filteredParams = pickBy(queryParams, (value: unknown, key: string) => queryParamsToPreserve[key]); - const nextDashboardUrl = locationUtil.stripBaseFromUrl(url); - - this.index++; - this.validPlaylistUrl = nextDashboardUrl; - this.nextTimeoutId = setTimeout(() => this.next(), this.interval); - - locationService.push(nextDashboardUrl + '?' + urlUtil.toUrlParams(filteredParams)); + this.navigateToDashboard(); } prev() { @@ -115,7 +128,10 @@ export class PlaylistSrv extends StateManagerBase { this.urls = urls; this.setState({ isPlaying: true }); - this.next(); + + // Replace current history entry with first dashboard instead of pushing + // this is to avoid the back button to go back to the playlist start page which causes a redirection + this.navigateToDashboard(true); return; } From f79ce08e5090fa33e112a08bfcc1cc94b6d8cff8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Thu, 27 Feb 2025 13:42:22 +0100 Subject: [PATCH 15/32] Dashboard: Outline using EditableElement interface (#101076) --- .../edit-pane/DashboardEditPane.tsx | 9 +- .../edit-pane/DashboardEditPaneSplitter.tsx | 60 +++---- .../edit-pane/DashboardEditableElement.tsx | 7 +- .../edit-pane/DashboardOutline.tsx | 159 ++++++++++++++++++ .../edit-pane/ElementEditPane.tsx | 3 +- .../edit-pane/ElementSelection.ts | 23 +-- .../MultiSelectedObjectsEditableElement.tsx | 8 +- .../MultiSelectedVizPanelsEditableElement.tsx | 8 +- .../edit-pane/VizPanelEditableElement.tsx | 11 +- .../dashboard-scene/edit-pane/shared.ts | 50 ++++++ .../SceneGridRowEditableElement.tsx | 149 ++++++++++++++++ .../scene/layout-rows/RowItem.tsx | 7 +- .../scene/layout-rows/RowItems.tsx | 7 +- .../scene/layout-tabs/TabItem.tsx | 7 +- .../scene/layout-tabs/TabItems.tsx | 7 +- .../scene/types/EditableDashboardElement.ts | 13 +- .../MultiSelectedEditableDashboardElement.ts | 8 +- public/locales/en-US/grafana.json | 25 ++- public/locales/pseudo-LOCALE/grafana.json | 25 ++- 19 files changed, 509 insertions(+), 77 deletions(-) create mode 100644 public/app/features/dashboard-scene/edit-pane/DashboardOutline.tsx create mode 100644 public/app/features/dashboard-scene/scene/layout-default/SceneGridRowEditableElement.tsx diff --git a/public/app/features/dashboard-scene/edit-pane/DashboardEditPane.tsx b/public/app/features/dashboard-scene/edit-pane/DashboardEditPane.tsx index fbaf5aa5260..51cd262b24a 100644 --- a/public/app/features/dashboard-scene/edit-pane/DashboardEditPane.tsx +++ b/public/app/features/dashboard-scene/edit-pane/DashboardEditPane.tsx @@ -18,6 +18,7 @@ import { isInCloneChain } from '../utils/clone'; import { getDashboardSceneFor } from '../utils/utils'; import { DashboardAddPane } from './DashboardAddPane'; +import { DashboardOutline } from './DashboardOutline'; import { ElementEditPane } from './ElementEditPane'; import { ElementSelection } from './ElementSelection'; import { useEditableElement } from './useEditableElement'; @@ -181,6 +182,8 @@ export function DashboardEditPaneRenderer({ editPane, isCollapsed, onToggleColla return null; } + const { typeId } = editableElement.getEditableElementInfo(); + if (isCollapsed) { return ( <> @@ -197,7 +200,7 @@ export function DashboardEditPaneRenderer({ editPane, isCollapsed, onToggleColla {openOverlay && ( - + )} @@ -225,8 +228,8 @@ export function DashboardEditPaneRenderer({ editPane, isCollapsed, onToggleColla
{tab === 'add' && } - {tab === 'configure' && } - {tab === 'outline' &&
} + {tab === 'configure' && } + {tab === 'outline' && }
); diff --git a/public/app/features/dashboard-scene/edit-pane/DashboardEditPaneSplitter.tsx b/public/app/features/dashboard-scene/edit-pane/DashboardEditPaneSplitter.tsx index b2fc279ccde..f51540115c5 100644 --- a/public/app/features/dashboard-scene/edit-pane/DashboardEditPaneSplitter.tsx +++ b/public/app/features/dashboard-scene/edit-pane/DashboardEditPaneSplitter.tsx @@ -73,38 +73,40 @@ export function DashboardEditPaneSplitter({ dashboard, isEditing, body, controls return (
-
{ - if (evt.shiftKey) { - return; - } + +
{ + if (evt.shiftKey) { + return; + } - editPane.clearSelection(); - }} - > - -
{controls}
-
-
- {body} + editPane.clearSelection(); + }} + > + +
{controls}
+
+
+ {body} +
-
- {isEditing && ( - <> -
-
- 0} - /> -
- - )} + {isEditing && ( + <> +
+
+ 0} + /> +
+ + )} +
); } diff --git a/public/app/features/dashboard-scene/edit-pane/DashboardEditableElement.tsx b/public/app/features/dashboard-scene/edit-pane/DashboardEditableElement.tsx index 4699e8c1c3f..77255075393 100644 --- a/public/app/features/dashboard-scene/edit-pane/DashboardEditableElement.tsx +++ b/public/app/features/dashboard-scene/edit-pane/DashboardEditableElement.tsx @@ -7,14 +7,17 @@ import { OptionsPaneItemDescriptor } from 'app/features/dashboard/components/Pan import { DashboardScene } from '../scene/DashboardScene'; import { useLayoutCategory } from '../scene/layouts-shared/DashboardLayoutSelector'; -import { EditableDashboardElement } from '../scene/types/EditableDashboardElement'; +import { EditableDashboardElement, EditableDashboardElementInfo } from '../scene/types/EditableDashboardElement'; export class DashboardEditableElement implements EditableDashboardElement { public readonly isEditableDashboardElement = true; - public readonly typeName = 'Dashboard'; public constructor(private dashboard: DashboardScene) {} + public getEditableElementInfo(): EditableDashboardElementInfo { + return { typeId: 'dashboard', icon: 'apps', name: t('dashboard.edit-pane.elements.dashboard', 'Dashboard') }; + } + public useEditPaneOptions(): OptionsPaneCategoryDescriptor[] { const dashboard = this.dashboard; diff --git a/public/app/features/dashboard-scene/edit-pane/DashboardOutline.tsx b/public/app/features/dashboard-scene/edit-pane/DashboardOutline.tsx new file mode 100644 index 00000000000..4f302a638fd --- /dev/null +++ b/public/app/features/dashboard-scene/edit-pane/DashboardOutline.tsx @@ -0,0 +1,159 @@ +import { css, cx } from '@emotion/css'; +import { useMemo, useState } from 'react'; + +import { GrafanaTheme2 } from '@grafana/data'; +import { SceneObject, VizPanel } from '@grafana/scenes'; +import { Box, Icon, IconButton, Stack, Text, useElementSelection, useStyles2 } from '@grafana/ui'; +import { t, Trans } from 'app/core/internationalization'; + +import { DashboardGridItem } from '../scene/layout-default/DashboardGridItem'; +import { isInCloneChain } from '../utils/clone'; +import { getDashboardSceneFor } from '../utils/utils'; + +import { DashboardEditPane } from './DashboardEditPane'; +import { getEditableElementFor, hasEditableElement } from './shared'; + +export interface Props { + editPane: DashboardEditPane; +} + +export function DashboardOutline({ editPane }: Props) { + const dashboard = getDashboardSceneFor(editPane); + + return ( + + + + ); +} + +function DashboardOutlineNode({ sceneObject, expandable }: { sceneObject: SceneObject; expandable: boolean }) { + const [isExpanded, setIsExpanded] = useState(true); + const { key } = sceneObject.useState(); + const styles = useStyles2(getStyles); + const { isSelected, onSelect } = useElementSelection(key); + const isCloned = useMemo(() => isInCloneChain(key!), [key]); + const editableElement = useMemo(() => getEditableElementFor(sceneObject)!, [sceneObject]); + + const children = collectEditableElementChildren(sceneObject); + const elementInfo = editableElement.getEditableElementInfo(); + + return ( + <> + + {expandable && ( + setIsExpanded(!isExpanded)} + aria-label={ + isExpanded + ? t('dashboard.outline.tree.item.collapse', 'Collapse item') + : t('dashboard.outline.tree.item.expand', 'Expand item') + } + /> + )} + + + {expandable && isExpanded && ( +
+ {children.length > 0 ? ( + children.map((child) => ( + + )) + ) : ( + + (empty) + + )} +
+ )} + + ); +} + +function getStyles(theme: GrafanaTheme2) { + return { + container: css({ + display: 'flex', + flexDirection: 'column', + gap: theme.spacing(1), + marginLeft: theme.spacing(1), + paddingLeft: theme.spacing(1.5), + borderLeft: `1px solid ${theme.colors.border.medium}`, + }), + nodeButton: css({ + boxShadow: 'none', + border: 'none', + background: 'transparent', + padding: theme.spacing(0.25, 1), + borderRadius: theme.shape.radius.default, + display: 'flex', + alignItems: 'center', + gap: theme.spacing(1), + overflow: 'hidden', + '&:hover': { + backgroundColor: theme.colors.action.hover, + }, + '> span': { + whiteSpace: 'nowrap', + overflow: 'hidden', + textOverflow: 'ellipsis', + }, + }), + nodeButtonSelected: css({ + color: theme.colors.primary.text, + }), + nodeButtonClone: css({ + color: theme.colors.text.secondary, + cursor: 'not-allowed', + }), + }; +} + +interface EditableElementConfig { + sceneObject: SceneObject; + expandable: boolean; +} + +function collectEditableElementChildren( + sceneObject: SceneObject, + children: EditableElementConfig[] = [] +): EditableElementConfig[] { + sceneObject.forEachChild((child) => { + if (child instanceof DashboardGridItem) { + // DashboardGridItem is a special case as it can contain repeated panels + // In this case, we want to show the repeated panels as separate items, otherwise show the body panel + if (child.state.repeatedPanels?.length) { + children.push(...child.state.repeatedPanels.map((panel) => ({ sceneObject: panel, expandable: false }))); + } else { + children.push({ sceneObject: child.state.body, expandable: false }); + } + } else if (child instanceof VizPanel) { + children.push({ sceneObject: child, expandable: false }); + } else if (hasEditableElement(child)) { + children.push({ sceneObject: child, expandable: true }); + } else { + collectEditableElementChildren(child, children); + } + }); + + return children; +} diff --git a/public/app/features/dashboard-scene/edit-pane/ElementEditPane.tsx b/public/app/features/dashboard-scene/edit-pane/ElementEditPane.tsx index 16adfe8004a..47cd84ade4b 100644 --- a/public/app/features/dashboard-scene/edit-pane/ElementEditPane.tsx +++ b/public/app/features/dashboard-scene/edit-pane/ElementEditPane.tsx @@ -14,13 +14,14 @@ export interface Props { export function ElementEditPane({ element }: Props) { const categories = element.useEditPaneOptions ? element.useEditPaneOptions() : []; const styles = useStyles2(getStyles); + const elementInfo = element.getEditableElementInfo(); return ( {element.renderActions && ( diff --git a/public/app/features/dashboard-scene/edit-pane/ElementSelection.ts b/public/app/features/dashboard-scene/edit-pane/ElementSelection.ts index cecd28020cf..2141781408d 100644 --- a/public/app/features/dashboard-scene/edit-pane/ElementSelection.ts +++ b/public/app/features/dashboard-scene/edit-pane/ElementSelection.ts @@ -1,15 +1,14 @@ import { SceneObject, SceneObjectRef, VizPanel } from '@grafana/scenes'; import { ElementSelectionContextItem } from '@grafana/ui'; -import { DashboardScene } from '../scene/DashboardScene'; import { isBulkActionElement } from '../scene/types/BulkActionElement'; import { EditableDashboardElement, isEditableDashboardElement } from '../scene/types/EditableDashboardElement'; import { MultiSelectedEditableDashboardElement } from '../scene/types/MultiSelectedEditableDashboardElement'; -import { DashboardEditableElement } from './DashboardEditableElement'; import { MultiSelectedObjectsEditableElement } from './MultiSelectedObjectsEditableElement'; import { MultiSelectedVizPanelsEditableElement } from './MultiSelectedVizPanelsEditableElement'; import { VizPanelEditableElement } from './VizPanelEditableElement'; +import { getEditableElementFor } from './shared'; export class ElementSelection { private selectedObjects?: Map>; @@ -121,24 +120,7 @@ export class ElementSelection { private createSingleSelectedElement(): EditableDashboardElement | undefined { const sceneObj = this.selectedObjects?.values().next().value?.resolve(); - - if (!sceneObj) { - return undefined; - } - - if (isEditableDashboardElement(sceneObj)) { - return sceneObj; - } - - if (sceneObj instanceof VizPanel) { - return new VizPanelEditableElement(sceneObj); - } - - if (sceneObj instanceof DashboardScene) { - return new DashboardEditableElement(sceneObj); - } - - return undefined; + return getEditableElementFor(sceneObj); } private createMultiSelectedElement(): MultiSelectedEditableDashboardElement | undefined { @@ -161,6 +143,7 @@ export class ElementSelection { } const bulkActionElements = []; + for (const sceneObject of sceneObjects) { if (sceneObject instanceof VizPanel) { const editableElement = new VizPanelEditableElement(sceneObject); diff --git a/public/app/features/dashboard-scene/edit-pane/MultiSelectedObjectsEditableElement.tsx b/public/app/features/dashboard-scene/edit-pane/MultiSelectedObjectsEditableElement.tsx index 3278deb3a17..a55b5f0d8c9 100644 --- a/public/app/features/dashboard-scene/edit-pane/MultiSelectedObjectsEditableElement.tsx +++ b/public/app/features/dashboard-scene/edit-pane/MultiSelectedObjectsEditableElement.tsx @@ -2,20 +2,24 @@ import { ReactNode } from 'react'; import { v4 as uuidv4 } from 'uuid'; import { Stack, Text, Button } from '@grafana/ui'; -import { Trans } from 'app/core/internationalization'; +import { t, Trans } from 'app/core/internationalization'; import { BulkActionElement } from '../scene/types/BulkActionElement'; +import { EditableDashboardElementInfo } from '../scene/types/EditableDashboardElement'; import { MultiSelectedEditableDashboardElement } from '../scene/types/MultiSelectedEditableDashboardElement'; export class MultiSelectedObjectsEditableElement implements MultiSelectedEditableDashboardElement { public readonly isMultiSelectedEditableDashboardElement = true; - public readonly typeName = 'Objects'; public readonly key: string; constructor(private _elements: BulkActionElement[]) { this.key = uuidv4(); } + public getEditableElementInfo(): EditableDashboardElementInfo { + return { name: t('dashboard.edit-pane.elements.objects', 'Objects'), typeId: 'objects', icon: 'folder' }; + } + public renderActions(): ReactNode { return ( diff --git a/public/app/features/dashboard-scene/edit-pane/MultiSelectedVizPanelsEditableElement.tsx b/public/app/features/dashboard-scene/edit-pane/MultiSelectedVizPanelsEditableElement.tsx index 259ae9d91cc..afa29772adc 100644 --- a/public/app/features/dashboard-scene/edit-pane/MultiSelectedVizPanelsEditableElement.tsx +++ b/public/app/features/dashboard-scene/edit-pane/MultiSelectedVizPanelsEditableElement.tsx @@ -3,20 +3,24 @@ import { v4 as uuidv4 } from 'uuid'; import { VizPanel } from '@grafana/scenes'; import { Button, Stack, Text } from '@grafana/ui'; -import { Trans } from 'app/core/internationalization'; +import { t, Trans } from 'app/core/internationalization'; +import { EditableDashboardElementInfo } from '../scene/types/EditableDashboardElement'; import { MultiSelectedEditableDashboardElement } from '../scene/types/MultiSelectedEditableDashboardElement'; import { dashboardSceneGraph } from '../utils/dashboardSceneGraph'; export class MultiSelectedVizPanelsEditableElement implements MultiSelectedEditableDashboardElement { public readonly isMultiSelectedEditableDashboardElement = true; - public readonly typeName = 'Panels'; public readonly key: string; constructor(private _panels: VizPanel[]) { this.key = uuidv4(); } + public getEditableElementInfo(): EditableDashboardElementInfo { + return { name: t('dashboard.edit-pane.elements.panels', 'Panels'), typeId: 'panels', icon: 'folder' }; + } + renderActions(): ReactNode { return ( diff --git a/public/app/features/dashboard-scene/edit-pane/VizPanelEditableElement.tsx b/public/app/features/dashboard-scene/edit-pane/VizPanelEditableElement.tsx index 4d97803b8db..c20c61113fa 100644 --- a/public/app/features/dashboard-scene/edit-pane/VizPanelEditableElement.tsx +++ b/public/app/features/dashboard-scene/edit-pane/VizPanelEditableElement.tsx @@ -14,15 +14,22 @@ import { } from '../panel-edit/getPanelFrameOptions'; import { BulkActionElement } from '../scene/types/BulkActionElement'; import { isDashboardLayoutItem } from '../scene/types/DashboardLayoutItem'; -import { EditableDashboardElement } from '../scene/types/EditableDashboardElement'; +import { EditableDashboardElement, EditableDashboardElementInfo } from '../scene/types/EditableDashboardElement'; import { dashboardSceneGraph } from '../utils/dashboardSceneGraph'; export class VizPanelEditableElement implements EditableDashboardElement, BulkActionElement { public readonly isEditableDashboardElement = true; - public readonly typeName = 'Panel'; public constructor(private panel: VizPanel) {} + public getEditableElementInfo(): EditableDashboardElementInfo { + return { + typeId: 'panel', + icon: 'chart-line', + name: sceneGraph.interpolate(this.panel, this.panel.state.title, undefined, 'text'), + }; + } + public useEditPaneOptions(): OptionsPaneCategoryDescriptor[] { const panel = this.panel; const layoutElement = panel.parent!; diff --git a/public/app/features/dashboard-scene/edit-pane/shared.ts b/public/app/features/dashboard-scene/edit-pane/shared.ts index 59ee48dde75..72c44d5dc2b 100644 --- a/public/app/features/dashboard-scene/edit-pane/shared.ts +++ b/public/app/features/dashboard-scene/edit-pane/shared.ts @@ -1,5 +1,55 @@ import { useSessionStorage } from 'react-use'; +import { SceneGridRow, SceneObject, VizPanel } from '@grafana/scenes'; + +import { DashboardScene } from '../scene/DashboardScene'; +import { SceneGridRowEditableElement } from '../scene/layout-default/SceneGridRowEditableElement'; +import { EditableDashboardElement, isEditableDashboardElement } from '../scene/types/EditableDashboardElement'; + +import { DashboardEditableElement } from './DashboardEditableElement'; +import { VizPanelEditableElement } from './VizPanelEditableElement'; + export function useEditPaneCollapsed() { return useSessionStorage('grafana.dashboards.edit-pane.isCollapsed', false); } + +export function getEditableElementFor(sceneObj: SceneObject | undefined): EditableDashboardElement | undefined { + if (!sceneObj) { + return undefined; + } + + if (isEditableDashboardElement(sceneObj)) { + return sceneObj; + } + + if (sceneObj instanceof VizPanel) { + return new VizPanelEditableElement(sceneObj); + } + + if (sceneObj instanceof SceneGridRow) { + return new SceneGridRowEditableElement(sceneObj); + } + + if (sceneObj instanceof DashboardScene) { + return new DashboardEditableElement(sceneObj); + } + + return undefined; +} + +export function hasEditableElement(sceneObj: SceneObject | undefined): boolean { + if (!sceneObj) { + return false; + } + + if ( + isEditableDashboardElement(sceneObj) || + sceneObj instanceof VizPanel || + sceneObj instanceof SceneGridRow || + sceneObj instanceof DashboardScene + ) { + return true; + } + + return false; +} diff --git a/public/app/features/dashboard-scene/scene/layout-default/SceneGridRowEditableElement.tsx b/public/app/features/dashboard-scene/scene/layout-default/SceneGridRowEditableElement.tsx new file mode 100644 index 00000000000..497b4087da9 --- /dev/null +++ b/public/app/features/dashboard-scene/scene/layout-default/SceneGridRowEditableElement.tsx @@ -0,0 +1,149 @@ +import { ReactNode, useMemo } from 'react'; + +import { selectors } from '@grafana/e2e-selectors'; +import { sceneGraph, SceneGridRow, VizPanel } from '@grafana/scenes'; +import { Alert, Button, Input, TextLink } from '@grafana/ui'; +import { t, Trans } from 'app/core/internationalization'; +import { OptionsPaneCategoryDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneCategoryDescriptor'; +import { OptionsPaneItemDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneItemDescriptor'; +import { RepeatRowSelect2 } from 'app/features/dashboard/components/RepeatRowSelect/RepeatRowSelect'; +import { SHARED_DASHBOARD_QUERY } from 'app/plugins/datasource/dashboard/constants'; +import { MIXED_DATASOURCE_NAME } from 'app/plugins/datasource/mixed/MixedDataSource'; + +import { getDashboardSceneFor, getLayoutManagerFor, getQueryRunnerFor } from '../../utils/utils'; +import { DashboardScene } from '../DashboardScene'; +import { BulkActionElement } from '../types/BulkActionElement'; +import { EditableDashboardElement, EditableDashboardElementInfo } from '../types/EditableDashboardElement'; + +import { DefaultGridLayoutManager } from './DefaultGridLayoutManager'; +import { RowRepeaterBehavior } from './RowRepeaterBehavior'; + +export class SceneGridRowEditableElement implements EditableDashboardElement, BulkActionElement { + public readonly isEditableDashboardElement = true; + + public constructor(private _row: SceneGridRow) {} + + public getEditableElementInfo(): EditableDashboardElementInfo { + return { + typeId: 'panel', + icon: 'line-alt', + name: sceneGraph.interpolate(this._row, this._row.state.title, undefined, 'text'), + }; + } + + public useEditPaneOptions(): OptionsPaneCategoryDescriptor[] { + const row = this._row; + + const rowOptions = useMemo(() => { + return new OptionsPaneCategoryDescriptor({ + title: t('dashboard.default-layout.row-options.title', 'Row options'), + id: 'row-options', + isOpenDefault: true, + }).addItem( + new OptionsPaneItemDescriptor({ + title: t('dashboard.default-layout.row-options.form.title', 'Title'), + render: () => , + }) + ); + }, [row]); + + const rowRepeatOptions = useMemo(() => { + const dashboard = getDashboardSceneFor(row); + + return new OptionsPaneCategoryDescriptor({ + title: t('dashboard.default-layout.row-options.repeat.title', 'Repeat options'), + id: 'row-repeat-options', + isOpenDefault: true, + }).addItem( + new OptionsPaneItemDescriptor({ + title: t('dashboard.default-layout.row-options.repeat.variable.title', 'Variable'), + render: () => , + }) + ); + }, [row]); + + return [rowOptions, rowRepeatOptions]; + } + + public onDelete() { + const layoutManager = getLayoutManagerFor(this._row); + + if (layoutManager instanceof DefaultGridLayoutManager) { + layoutManager.removeRow(this._row); + } + } + + public renderActions(): ReactNode { + return ( + <> + ; } - if (installState === InstallState.UPGRADE) { + if (installState === PluginStatus.UPDATE) { return ; } return ''; } -function getInstallState(installedVersion?: string, version?: string): InstallState { +function getInstallState(installedVersion?: string, version?: string): PluginStatus { if (!installedVersion || !version || !valid(installedVersion) || !valid(version)) { - return InstallState.INSTALL; + return PluginStatus.INSTALL; } - return gt(installedVersion, version) ? InstallState.DOWNGRADE : InstallState.UPGRADE; + return gt(installedVersion, version) ? PluginStatus.DOWNGRADE : PluginStatus.UPDATE; } -function getButtonHiddenState(installState: InstallState, isPreinstalled: { found: boolean; withVersion: boolean }) { +function getButtonHiddenState(installState: PluginStatus, isPreinstalled: { found: boolean; withVersion: boolean }) { // Default state for initial install - if (installState === InstallState.INSTALL) { + if (installState === PluginStatus.INSTALL) { return false; } // Handle downgrade case - if (installState === InstallState.DOWNGRADE) { + if (installState === PluginStatus.DOWNGRADE) { return isPreinstalled.found && Boolean(config.featureToggles.preinstallAutoUpdate); } diff --git a/public/app/features/plugins/admin/state/actions.ts b/public/app/features/plugins/admin/state/actions.ts index eab93de18fa..e08e7241c42 100644 --- a/public/app/features/plugins/admin/state/actions.ts +++ b/public/app/features/plugins/admin/state/actions.ts @@ -19,7 +19,7 @@ import { } from '../api'; import { STATE_PREFIX } from '../constants'; import { mapLocalToCatalog, mergeLocalsAndRemotes, updatePanels } from '../helpers'; -import { CatalogPlugin, RemotePlugin, LocalPlugin, InstancePlugin, ProvisionedPlugin } from '../types'; +import { CatalogPlugin, RemotePlugin, LocalPlugin, InstancePlugin, ProvisionedPlugin, PluginStatus } from '../types'; // Fetches export const fetchAll = createAsyncThunk(`${STATE_PREFIX}/fetchAll`, async (_, thunkApi) => { @@ -188,17 +188,23 @@ export const install = createAsyncThunk< { id: string; version?: string; - isUpdating?: boolean; + installType?: PluginStatus; } ->(`${STATE_PREFIX}/install`, async ({ id, version, isUpdating = false }, thunkApi) => { - const changes = isUpdating - ? { isInstalled: true, installedVersion: version, hasUpdate: false } - : { isInstalled: true, installedVersion: version }; +>(`${STATE_PREFIX}/install`, async ({ id, version, installType = PluginStatus.INSTALL }, thunkApi) => { + const changes: Partial = { isInstalled: true, installedVersion: version }; + + if (installType === PluginStatus.UPDATE) { + changes.hasUpdate = false; + } + if (installType === PluginStatus.DOWNGRADE) { + changes.hasUpdate = true; + } + try { await installPlugin(id, version); await updatePanels(); - if (isUpdating) { + if (installType !== PluginStatus.INSTALL) { invalidatePluginInCache(id); } diff --git a/public/app/features/plugins/admin/state/hooks.ts b/public/app/features/plugins/admin/state/hooks.ts index da0ba859a3a..457f2f1d73e 100644 --- a/public/app/features/plugins/admin/state/hooks.ts +++ b/public/app/features/plugins/admin/state/hooks.ts @@ -4,7 +4,7 @@ import { PluginError, PluginType } from '@grafana/data'; import { useDispatch, useSelector } from 'app/types'; import { sortPlugins, Sorters, isPluginUpdatable } from '../helpers'; -import { CatalogPlugin } from '../types'; +import { CatalogPlugin, PluginStatus } from '../types'; import { fetchAll, fetchDetails, fetchRemotePlugins, install, uninstall, fetchAllLocal, unsetInstall } from './actions'; import { @@ -64,7 +64,7 @@ export const useGetErrors = (filterByPluginType?: PluginType): PluginError[] => export const useInstall = () => { const dispatch = useDispatch(); - return (id: string, version?: string, isUpdating?: boolean) => dispatch(install({ id, version, isUpdating })); + return (id: string, version?: string, installType?: PluginStatus) => dispatch(install({ id, version, installType })); }; export const useUnsetInstall = () => { diff --git a/public/app/features/plugins/admin/types.ts b/public/app/features/plugins/admin/types.ts index f3d5783ae3c..96e1d2451d2 100644 --- a/public/app/features/plugins/admin/types.ts +++ b/public/app/features/plugins/admin/types.ts @@ -257,6 +257,7 @@ export enum PluginStatus { UNINSTALL = 'UNINSTALL', UPDATE = 'UPDATE', REINSTALL = 'REINSTALL', + DOWNGRADE = 'DOWNGRADE', } export enum PluginTabLabels { From 5e61ec12580d780f088230dd8a865b98a58af500 Mon Sep 17 00:00:00 2001 From: Leonor Oliveira <9090754+leonorfmartins@users.noreply.github.com> Date: Thu, 27 Feb 2025 15:17:50 +0100 Subject: [PATCH 21/32] Prevent wrong type conversion (#101349) --- pkg/registry/apis/folders/legacy_storage.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/registry/apis/folders/legacy_storage.go b/pkg/registry/apis/folders/legacy_storage.go index 0c96769a942..34dcaa9a73b 100644 --- a/pkg/registry/apis/folders/legacy_storage.go +++ b/pkg/registry/apis/folders/legacy_storage.go @@ -116,7 +116,7 @@ func (s *legacyStorage) List(ctx context.Context, options *internalversion.ListO } list.Items = append(list.Items, *r) } - if len(list.Items) >= int(paging.limit) { + if int64(len(list.Items)) >= paging.limit { list.Continue = paging.GetNextPageToken() } return list, nil From f8b63c364b65f1c6a5e6eeed07a90ae1ddbc5389 Mon Sep 17 00:00:00 2001 From: Leonor Oliveira <9090754+leonorfmartins@users.noreply.github.com> Date: Thu, 27 Feb 2025 15:18:02 +0100 Subject: [PATCH 22/32] [CodeQL] Fix wrong type conversion (#101353) * [CodeQL] Fix wrong type conversion * Use AtyoI --- pkg/services/dashboardversion/dashverimpl/dashver.go | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/pkg/services/dashboardversion/dashverimpl/dashver.go b/pkg/services/dashboardversion/dashverimpl/dashver.go index 6130ea6e279..27bcbb845d1 100644 --- a/pkg/services/dashboardversion/dashverimpl/dashver.go +++ b/pkg/services/dashboardversion/dashverimpl/dashver.go @@ -334,10 +334,9 @@ func getRestoreVersion(msg string) (int, error) { return 0, nil } - ver, err := strconv.ParseInt(parts[1], 10, 64) + ver, err := strconv.Atoi(parts[1]) if err != nil { return 0, err } - - return int(ver), nil + return ver, nil } From 980332ae75795e55cfb66b02c687b954747c7d68 Mon Sep 17 00:00:00 2001 From: Sonia Aguilar <33540275+soniaAguilarPeiron@users.noreply.github.com> Date: Thu, 27 Feb 2025 15:20:07 +0100 Subject: [PATCH 23/32] Alerting: Fix exporting new rule with a new group (#101404) Fix exporting new rule with a new group --- .../rule-editor/alert-rule-form/ModifyExportRuleForm.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/ModifyExportRuleForm.tsx b/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/ModifyExportRuleForm.tsx index 72093ecc490..71c049e661b 100644 --- a/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/ModifyExportRuleForm.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/ModifyExportRuleForm.tsx @@ -168,7 +168,7 @@ export const getPayloadToExport = ( } else { // we have to create a new group with the updated rule return { - name: existingGroup?.name ?? '', + name: existingGroup?.name ?? formValues.group, rules: [updatedRule], }; } From 98dd977fabd06a6ef650c3f2d8894fe428dee541 Mon Sep 17 00:00:00 2001 From: Sonia Aguilar <33540275+soniaAguilarPeiron@users.noreply.github.com> Date: Thu, 27 Feb 2025 15:23:40 +0100 Subject: [PATCH 24/32] Alerting: Fix KeyValueMap input bug (#101367) * fix KeyValueMap input bug * add translations --- .betterer.results | 6 +- .../form/fields/KeyValueMapInput.tsx | 77 +++++++++++++------ public/locales/en-US/grafana.json | 4 + public/locales/pseudo-LOCALE/grafana.json | 4 + 4 files changed, 62 insertions(+), 29 deletions(-) diff --git a/.betterer.results b/.betterer.results index 5e04d7cf375..f6c07dd6633 100644 --- a/.betterer.results +++ b/.betterer.results @@ -1982,10 +1982,8 @@ exports[`better eslint`] = { [0, 0, 0, "No untranslated strings. Wrap text with ", "6"] ], "public/app/features/alerting/unified/components/receivers/form/fields/KeyValueMapInput.tsx:5381": [ - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "0"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "1"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "2"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "3"] + [0, 0, 0, "No untranslated strings. Wrap text with ", "0"], + [0, 0, 0, "No untranslated strings. Wrap text with ", "1"] ], "public/app/features/alerting/unified/components/receivers/form/fields/OptionField.tsx:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"], diff --git a/public/app/features/alerting/unified/components/receivers/form/fields/KeyValueMapInput.tsx b/public/app/features/alerting/unified/components/receivers/form/fields/KeyValueMapInput.tsx index 5a1bccdd34e..b69a9dce091 100644 --- a/public/app/features/alerting/unified/components/receivers/form/fields/KeyValueMapInput.tsx +++ b/public/app/features/alerting/unified/components/receivers/form/fields/KeyValueMapInput.tsx @@ -1,8 +1,9 @@ import { css } from '@emotion/css'; -import { useEffect, useState } from 'react'; +import { useState } from 'react'; import { GrafanaTheme2 } from '@grafana/data'; -import { Button, Input, useStyles2 } from '@grafana/ui'; +import { Button, Input, Stack, useStyles2 } from '@grafana/ui'; +import { Trans, t } from 'app/core/internationalization'; import { ActionIcon } from '../../../rules/ActionIcon'; @@ -15,7 +16,7 @@ interface Props { export const KeyValueMapInput = ({ value, onChange, readOnly = false }: Props) => { const styles = useStyles2(getStyles); const [pairs, setPairs] = useState(recordToPairs(value)); - useEffect(() => setPairs(recordToPairs(value)), [value]); + const [currentNewPair, setCurrentNewPair] = useState<[string, string] | undefined>(undefined); const emitChange = (pairs: Array<[string, string]>) => { onChange(pairsToRecord(pairs)); @@ -30,15 +31,6 @@ export const KeyValueMapInput = ({ value, onChange, readOnly = false }: Props) = } }; - const updatePair = (values: [string, string], index: number) => { - const old = pairs[index]; - const newPairs = pairs.map((pair, i) => (i === index ? values : pair)); - setPairs(newPairs); - if (values[0] || old[0]) { - emitChange(newPairs); - } - }; - return (
{!!pairs.length && ( @@ -54,22 +46,18 @@ export const KeyValueMapInput = ({ value, onChange, readOnly = false }: Props) = {pairs.map(([key, value], index) => ( - updatePair([e.currentTarget.value, value], index)} - /> + - updatePair([key, e.currentTarget.value], index)} - /> + {!readOnly && ( - deleteItem(index)} /> + deleteItem(index)} + /> )} @@ -77,6 +65,44 @@ export const KeyValueMapInput = ({ value, onChange, readOnly = false }: Props) = )} + {currentNewPair && ( + + + + + + + + +
+ setCurrentNewPair([e.currentTarget.value, currentNewPair[1]])} + /> + + setCurrentNewPair([currentNewPair[0], e.currentTarget.value])} + /> + + + { + setPairs([...pairs, currentNewPair]); + setCurrentNewPair(undefined); + emitChange([...pairs, currentNewPair]); + }} + /> + setCurrentNewPair(undefined)} + /> + +
+ )} {!readOnly && ( )}
diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index e014860e85c..a064ce993a6 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -315,6 +315,10 @@ "empty-state": { "title": "You don't have any contact points yet" }, + "key-value-map": { + "add": "Add", + "confirm-add": "Confirm to add" + }, "last-delivery-attempt": "Last delivery attempt", "last-delivery-failed": "Last delivery attempt failed", "no-contact-points-found": "No contact points found", diff --git a/public/locales/pseudo-LOCALE/grafana.json b/public/locales/pseudo-LOCALE/grafana.json index cb3c713ad28..317a107f796 100644 --- a/public/locales/pseudo-LOCALE/grafana.json +++ b/public/locales/pseudo-LOCALE/grafana.json @@ -315,6 +315,10 @@ "empty-state": { "title": "Ÿőū đőʼn'ŧ ĥävę äʼny čőʼnŧäčŧ pőįʼnŧş yęŧ" }, + "key-value-map": { + "add": "Åđđ", + "confirm-add": "Cőʼnƒįřm ŧő äđđ" + }, "last-delivery-attempt": "Ŀäşŧ đęľįvęřy äŧŧęmpŧ", "last-delivery-failed": "Ŀäşŧ đęľįvęřy äŧŧęmpŧ ƒäįľęđ", "no-contact-points-found": "Ńő čőʼnŧäčŧ pőįʼnŧş ƒőūʼnđ", From 2ec8b0b45b5a93d9c36169f24b60028ae88c78a8 Mon Sep 17 00:00:00 2001 From: Kevin Minehart <5140827+kminehart@users.noreply.github.com> Date: Thu, 27 Feb 2025 09:00:00 -0600 Subject: [PATCH 25/32] CI: Create the frontend test workflow in GHA (#101256) * Create the frontend test workflow * Give .github/workflows/test-frontend to frontend platform group * Remove file filter * rename workflow * frontend unit tests * add yarn install * update CODEOWNERS * Run on 8 core machines? * use parallelization? * add sharding * update package.json to allow sharding jest * update workflow name * yarn generate-apis * update naming --- .github/CODEOWNERS | 1 + .github/workflows/frontend-unit-tests.yml | 29 +++++++++++++++++++++++ package.json | 2 +- 3 files changed, 31 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/frontend-unit-tests.yml diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 4e3ca236ebf..64363540c79 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -813,6 +813,7 @@ embed.go @grafana/grafana-as-code /.github/workflows/trivy-scan.yml @grafana/grafana-backend-services-squad /.github/workflows/changelog.yml @zserge /.github/workflows/actions/changelog @zserge +/.github/workflows/frontend-unit-tests.yml @grafana/grafana-frontend-platform # Generated files not requiring owner approval /packages/grafana-data/src/types/featureToggles.gen.ts @grafanabot diff --git a/.github/workflows/frontend-unit-tests.yml b/.github/workflows/frontend-unit-tests.yml new file mode 100644 index 00000000000..495bb647233 --- /dev/null +++ b/.github/workflows/frontend-unit-tests.yml @@ -0,0 +1,29 @@ +name: Frontend tests +on: + pull_request: + push: + branches: + - main + - release-*.*.* + +jobs: + test-frontend: + runs-on: ubuntu-latest-8-cores + name: "Unit tests (${{ matrix.chunk }} / 8)" + strategy: + fail-fast: true + matrix: + chunk: [1, 2, 3, 4, 5, 6, 7, 8] + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version-file: '.nvmrc' + cache: 'yarn' + cache-dependency-path: 'yarn.lock' + - run: yarn install --immutable --check-cache + - run: yarn run test:ci + env: + TEST_MAX_WORKERS: 2 + TEST_SHARD: ${{ matrix.chunk }} + TEST_SHARD_TOTAL: 8 diff --git a/package.json b/package.json index 3eb2cbbeabf..a07f4835a14 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,7 @@ "lint": "yarn run lint:ts && yarn run lint:sass", "lint:ts": "eslint ./ ./public/app/extensions/ --cache --no-error-on-unmatched-pattern", "lint:sass": "yarn stylelint '{public/sass,packages}/**/*.scss' --cache", - "test:ci": "mkdir -p reports/junit && JEST_JUNIT_OUTPUT_DIR=reports/junit jest --ci --reporters=default --reporters=jest-junit -w ${TEST_MAX_WORKERS:-100%}", + "test:ci": "mkdir -p reports/junit && JEST_JUNIT_OUTPUT_DIR=reports/junit jest --ci --reporters=default --reporters=jest-junit -w ${TEST_MAX_WORKERS:-100%} --shard=${TEST_SHARD:-1}/${TEST_SHARD_TOTAL:-1}", "lint:fix": "yarn lint:ts --fix", "packages:build": "nx run-many -t build --projects='tag:scope:package'", "packages:clean": "rimraf ./npm-artifacts && nx run-many -t clean --projects='tag:scope:package' --maxParallel=100", From 9beaa3e1d146674370abe094e0933eca618d2aac Mon Sep 17 00:00:00 2001 From: Jack Westbrook Date: Thu, 27 Feb 2025 16:30:21 +0100 Subject: [PATCH 26/32] CI: Update renovate storybook config (#101424) ci(renovate): when bumping storybook always update package.json so e2e ci step runs --- .github/renovate.json5 | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/renovate.json5 b/.github/renovate.json5 index a782ac2c0a0..43a537f2fa1 100644 --- a/.github/renovate.json5 +++ b/.github/renovate.json5 @@ -32,6 +32,7 @@ extends: ["schedule:monthly"], groupName: "Storybook updates", matchPackageNames: ["/^@?storybook/"], + rangeStrategy: "bump", }, { groupName: "React Aria", From 908f4ff357c296a21bf05f4383dbe1bc1d2fa5e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Peter=20=C5=A0tibran=C3=BD?= Date: Thu, 27 Feb 2025 16:33:26 +0100 Subject: [PATCH 27/32] Use enterprise imports for pro builds as well. (#101423) --- pkg/extensions/enterprise_imports.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/extensions/enterprise_imports.go b/pkg/extensions/enterprise_imports.go index f0b4c12f7ec..7e96faa86ac 100644 --- a/pkg/extensions/enterprise_imports.go +++ b/pkg/extensions/enterprise_imports.go @@ -1,5 +1,5 @@ -//go:build enterprise -// +build enterprise +//go:build enterprise || pro +// +build enterprise pro package extensions From 843d876f1688115565949536d1d642fc4b6e6601 Mon Sep 17 00:00:00 2001 From: Eric Leijonmarck Date: Thu, 27 Feb 2025 15:34:15 +0000 Subject: [PATCH 28/32] Anon: Deprecation notice for Editor, Admin anonymous org role usage (#101411) * deprecation notice for anonymous org role usage * exclude viewer --- pkg/setting/setting_anonymous.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pkg/setting/setting_anonymous.go b/pkg/setting/setting_anonymous.go index 057b82e73a4..53e0830eb86 100644 --- a/pkg/setting/setting_anonymous.go +++ b/pkg/setting/setting_anonymous.go @@ -14,7 +14,12 @@ func (cfg *Cfg) readAnonymousSettings() { anonSettings := AnonymousSettings{} anonSettings.Enabled = anonSection.Key("enabled").MustBool(false) anonSettings.OrgName = valueAsString(anonSection, "org_name", "") + // Deprecated: + // only viewer role is supported anonSettings.OrgRole = valueAsString(anonSection, "org_role", "") + if anonSettings.OrgRole != "Viewer" { + cfg.Logger.Warn("auth.anonymous.org_role is deprecated, only viewer role is supported") + } anonSettings.HideVersion = anonSection.Key("hide_version").MustBool(false) anonSettings.DeviceLimit = anonSection.Key("device_limit").MustInt64(0) cfg.Anonymous = anonSettings From ef86582dfca33243e202ffb966bce2f8ca4ffc27 Mon Sep 17 00:00:00 2001 From: Alexander Akhmetov Date: Thu, 27 Feb 2025 17:20:49 +0100 Subject: [PATCH 29/32] Alerting: API paths for cortextool to import Loki rules (#101409) Alerting: Legacy rules paths for cortextool --- pkg/services/ngalert/api/authorization.go | 14 +- .../ngalert/api/authorization_test.go | 2 +- .../generated_base_api_convert_prometheus.go | 108 ++++ .../ngalert/api/prometheus_conversion.go | 26 + pkg/services/ngalert/api/tooling/api.json | 4 +- .../definitions/convert_prometheus_api.go | 95 +++- pkg/services/ngalert/api/tooling/post.json | 252 ++++++++- pkg/services/ngalert/api/tooling/spec.json | 252 ++++++++- .../alerting/api_convert_prometheus_test.go | 506 ++++++++++-------- pkg/tests/api/alerting/testing.go | 43 +- public/api-merged.json | 1 + public/openapi3.json | 1 + 12 files changed, 1049 insertions(+), 255 deletions(-) diff --git a/pkg/services/ngalert/api/authorization.go b/pkg/services/ngalert/api/authorization.go index a1cedae4b73..4dce6e33015 100644 --- a/pkg/services/ngalert/api/authorization.go +++ b/pkg/services/ngalert/api/authorization.go @@ -125,26 +125,32 @@ func (api *API) authorize(method, path string) web.Handler { // convert/prometheus API paths case http.MethodGet + "/api/convert/prometheus/config/v1/rules/{NamespaceTitle}/{Group}", - http.MethodGet + "/api/convert/prometheus/config/v1/rules/{NamespaceTitle}": + http.MethodGet + "/api/convert/api/prom/rules/{NamespaceTitle}/{Group}", + http.MethodGet + "/api/convert/prometheus/config/v1/rules/{NamespaceTitle}", + http.MethodGet + "/api/convert/api/prom/rules/{NamespaceTitle}": eval = ac.EvalAll( ac.EvalPermission(ac.ActionAlertingRuleRead), ac.EvalPermission(dashboards.ActionFoldersRead), ) - case http.MethodGet + "/api/convert/prometheus/config/v1/rules": + case http.MethodGet + "/api/convert/prometheus/config/v1/rules", + http.MethodGet + "/api/convert/api/prom/rules": eval = ac.EvalAll( ac.EvalPermission(ac.ActionAlertingRuleRead), ac.EvalPermission(dashboards.ActionFoldersRead), ) - case http.MethodPost + "/api/convert/prometheus/config/v1/rules/{NamespaceTitle}": + case http.MethodPost + "/api/convert/prometheus/config/v1/rules/{NamespaceTitle}", + http.MethodPost + "/api/convert/api/prom/rules/{NamespaceTitle}": eval = ac.EvalAll( ac.EvalPermission(ac.ActionAlertingRuleCreate), ac.EvalPermission(ac.ActionAlertingProvisioningSetStatus), ) case http.MethodDelete + "/api/convert/prometheus/config/v1/rules/{NamespaceTitle}/{Group}", - http.MethodDelete + "/api/convert/prometheus/config/v1/rules/{NamespaceTitle}": + http.MethodDelete + "/api/convert/api/prom/rules/{NamespaceTitle}/{Group}", + http.MethodDelete + "/api/convert/prometheus/config/v1/rules/{NamespaceTitle}", + http.MethodDelete + "/api/convert/api/prom/rules/{NamespaceTitle}": eval = ac.EvalAny( ac.EvalAll( ac.EvalPermission(ac.ActionAlertingRuleRead), diff --git a/pkg/services/ngalert/api/authorization_test.go b/pkg/services/ngalert/api/authorization_test.go index 13b0f24c36c..993a72d3ef7 100644 --- a/pkg/services/ngalert/api/authorization_test.go +++ b/pkg/services/ngalert/api/authorization_test.go @@ -41,7 +41,7 @@ func TestAuthorize(t *testing.T) { } paths[p] = methods } - require.Len(t, paths, 63) + require.Len(t, paths, 66) ac := acmock.New() api := &API{AccessControl: ac, FeatureManager: featuremgmt.WithFeatures()} diff --git a/pkg/services/ngalert/api/generated_base_api_convert_prometheus.go b/pkg/services/ngalert/api/generated_base_api_convert_prometheus.go index 3f2e0566295..2ee728172c9 100644 --- a/pkg/services/ngalert/api/generated_base_api_convert_prometheus.go +++ b/pkg/services/ngalert/api/generated_base_api_convert_prometheus.go @@ -19,6 +19,12 @@ import ( ) type ConvertPrometheusApi interface { + RouteConvertPrometheusCortexDeleteNamespace(*contextmodel.ReqContext) response.Response + RouteConvertPrometheusCortexDeleteRuleGroup(*contextmodel.ReqContext) response.Response + RouteConvertPrometheusCortexGetNamespace(*contextmodel.ReqContext) response.Response + RouteConvertPrometheusCortexGetRuleGroup(*contextmodel.ReqContext) response.Response + RouteConvertPrometheusCortexGetRules(*contextmodel.ReqContext) response.Response + RouteConvertPrometheusCortexPostRuleGroup(*contextmodel.ReqContext) response.Response RouteConvertPrometheusDeleteNamespace(*contextmodel.ReqContext) response.Response RouteConvertPrometheusDeleteRuleGroup(*contextmodel.ReqContext) response.Response RouteConvertPrometheusGetNamespace(*contextmodel.ReqContext) response.Response @@ -27,6 +33,36 @@ type ConvertPrometheusApi interface { RouteConvertPrometheusPostRuleGroup(*contextmodel.ReqContext) response.Response } +func (f *ConvertPrometheusApiHandler) RouteConvertPrometheusCortexDeleteNamespace(ctx *contextmodel.ReqContext) response.Response { + // Parse Path Parameters + namespaceTitleParam := web.Params(ctx.Req)[":NamespaceTitle"] + return f.handleRouteConvertPrometheusCortexDeleteNamespace(ctx, namespaceTitleParam) +} +func (f *ConvertPrometheusApiHandler) RouteConvertPrometheusCortexDeleteRuleGroup(ctx *contextmodel.ReqContext) response.Response { + // Parse Path Parameters + namespaceTitleParam := web.Params(ctx.Req)[":NamespaceTitle"] + groupParam := web.Params(ctx.Req)[":Group"] + return f.handleRouteConvertPrometheusCortexDeleteRuleGroup(ctx, namespaceTitleParam, groupParam) +} +func (f *ConvertPrometheusApiHandler) RouteConvertPrometheusCortexGetNamespace(ctx *contextmodel.ReqContext) response.Response { + // Parse Path Parameters + namespaceTitleParam := web.Params(ctx.Req)[":NamespaceTitle"] + return f.handleRouteConvertPrometheusCortexGetNamespace(ctx, namespaceTitleParam) +} +func (f *ConvertPrometheusApiHandler) RouteConvertPrometheusCortexGetRuleGroup(ctx *contextmodel.ReqContext) response.Response { + // Parse Path Parameters + namespaceTitleParam := web.Params(ctx.Req)[":NamespaceTitle"] + groupParam := web.Params(ctx.Req)[":Group"] + return f.handleRouteConvertPrometheusCortexGetRuleGroup(ctx, namespaceTitleParam, groupParam) +} +func (f *ConvertPrometheusApiHandler) RouteConvertPrometheusCortexGetRules(ctx *contextmodel.ReqContext) response.Response { + return f.handleRouteConvertPrometheusCortexGetRules(ctx) +} +func (f *ConvertPrometheusApiHandler) RouteConvertPrometheusCortexPostRuleGroup(ctx *contextmodel.ReqContext) response.Response { + // Parse Path Parameters + namespaceTitleParam := web.Params(ctx.Req)[":NamespaceTitle"] + return f.handleRouteConvertPrometheusCortexPostRuleGroup(ctx, namespaceTitleParam) +} func (f *ConvertPrometheusApiHandler) RouteConvertPrometheusDeleteNamespace(ctx *contextmodel.ReqContext) response.Response { // Parse Path Parameters namespaceTitleParam := web.Params(ctx.Req)[":NamespaceTitle"] @@ -60,6 +96,78 @@ func (f *ConvertPrometheusApiHandler) RouteConvertPrometheusPostRuleGroup(ctx *c func (api *API) RegisterConvertPrometheusApiEndpoints(srv ConvertPrometheusApi, m *metrics.API) { api.RouteRegister.Group("", func(group routing.RouteRegister) { + group.Delete( + toMacaronPath("/api/convert/api/prom/rules/{NamespaceTitle}"), + requestmeta.SetOwner(requestmeta.TeamAlerting), + requestmeta.SetSLOGroup(requestmeta.SLOGroupHighSlow), + api.authorize(http.MethodDelete, "/api/convert/api/prom/rules/{NamespaceTitle}"), + metrics.Instrument( + http.MethodDelete, + "/api/convert/api/prom/rules/{NamespaceTitle}", + api.Hooks.Wrap(srv.RouteConvertPrometheusCortexDeleteNamespace), + m, + ), + ) + group.Delete( + toMacaronPath("/api/convert/api/prom/rules/{NamespaceTitle}/{Group}"), + requestmeta.SetOwner(requestmeta.TeamAlerting), + requestmeta.SetSLOGroup(requestmeta.SLOGroupHighSlow), + api.authorize(http.MethodDelete, "/api/convert/api/prom/rules/{NamespaceTitle}/{Group}"), + metrics.Instrument( + http.MethodDelete, + "/api/convert/api/prom/rules/{NamespaceTitle}/{Group}", + api.Hooks.Wrap(srv.RouteConvertPrometheusCortexDeleteRuleGroup), + m, + ), + ) + group.Get( + toMacaronPath("/api/convert/api/prom/rules/{NamespaceTitle}"), + requestmeta.SetOwner(requestmeta.TeamAlerting), + requestmeta.SetSLOGroup(requestmeta.SLOGroupHighSlow), + api.authorize(http.MethodGet, "/api/convert/api/prom/rules/{NamespaceTitle}"), + metrics.Instrument( + http.MethodGet, + "/api/convert/api/prom/rules/{NamespaceTitle}", + api.Hooks.Wrap(srv.RouteConvertPrometheusCortexGetNamespace), + m, + ), + ) + group.Get( + toMacaronPath("/api/convert/api/prom/rules/{NamespaceTitle}/{Group}"), + requestmeta.SetOwner(requestmeta.TeamAlerting), + requestmeta.SetSLOGroup(requestmeta.SLOGroupHighSlow), + api.authorize(http.MethodGet, "/api/convert/api/prom/rules/{NamespaceTitle}/{Group}"), + metrics.Instrument( + http.MethodGet, + "/api/convert/api/prom/rules/{NamespaceTitle}/{Group}", + api.Hooks.Wrap(srv.RouteConvertPrometheusCortexGetRuleGroup), + m, + ), + ) + group.Get( + toMacaronPath("/api/convert/api/prom/rules"), + requestmeta.SetOwner(requestmeta.TeamAlerting), + requestmeta.SetSLOGroup(requestmeta.SLOGroupHighSlow), + api.authorize(http.MethodGet, "/api/convert/api/prom/rules"), + metrics.Instrument( + http.MethodGet, + "/api/convert/api/prom/rules", + api.Hooks.Wrap(srv.RouteConvertPrometheusCortexGetRules), + m, + ), + ) + group.Post( + toMacaronPath("/api/convert/api/prom/rules/{NamespaceTitle}"), + requestmeta.SetOwner(requestmeta.TeamAlerting), + requestmeta.SetSLOGroup(requestmeta.SLOGroupHighSlow), + api.authorize(http.MethodPost, "/api/convert/api/prom/rules/{NamespaceTitle}"), + metrics.Instrument( + http.MethodPost, + "/api/convert/api/prom/rules/{NamespaceTitle}", + api.Hooks.Wrap(srv.RouteConvertPrometheusCortexPostRuleGroup), + m, + ), + ) group.Delete( toMacaronPath("/api/convert/prometheus/config/v1/rules/{NamespaceTitle}"), requestmeta.SetOwner(requestmeta.TeamAlerting), diff --git a/pkg/services/ngalert/api/prometheus_conversion.go b/pkg/services/ngalert/api/prometheus_conversion.go index 354f6de2a38..d48aa5444b9 100644 --- a/pkg/services/ngalert/api/prometheus_conversion.go +++ b/pkg/services/ngalert/api/prometheus_conversion.go @@ -20,6 +20,7 @@ func NewConvertPrometheusApi(svc *ConvertPrometheusSrv) *ConvertPrometheusApiHan } } +// mimirtool func (f *ConvertPrometheusApiHandler) handleRouteConvertPrometheusGetRules(ctx *contextmodel.ReqContext) response.Response { return f.svc.RouteConvertPrometheusGetRules(ctx) } @@ -54,3 +55,28 @@ func (f *ConvertPrometheusApiHandler) handleRouteConvertPrometheusPostRuleGroup( return f.svc.RouteConvertPrometheusPostRuleGroup(ctx, namespaceTitle, promGroup) } + +// cortextool +func (f *ConvertPrometheusApiHandler) handleRouteConvertPrometheusCortexGetRules(ctx *contextmodel.ReqContext) response.Response { + return f.handleRouteConvertPrometheusGetRules(ctx) +} + +func (f *ConvertPrometheusApiHandler) handleRouteConvertPrometheusCortexDeleteNamespace(ctx *contextmodel.ReqContext, namespaceTitle string) response.Response { + return f.handleRouteConvertPrometheusDeleteNamespace(ctx, namespaceTitle) +} + +func (f *ConvertPrometheusApiHandler) handleRouteConvertPrometheusCortexDeleteRuleGroup(ctx *contextmodel.ReqContext, namespaceTitle string, group string) response.Response { + return f.handleRouteConvertPrometheusDeleteRuleGroup(ctx, namespaceTitle, group) +} + +func (f *ConvertPrometheusApiHandler) handleRouteConvertPrometheusCortexGetNamespace(ctx *contextmodel.ReqContext, namespaceTitle string) response.Response { + return f.handleRouteConvertPrometheusGetNamespace(ctx, namespaceTitle) +} + +func (f *ConvertPrometheusApiHandler) handleRouteConvertPrometheusCortexGetRuleGroup(ctx *contextmodel.ReqContext, namespaceTitle string, group string) response.Response { + return f.handleRouteConvertPrometheusGetRuleGroup(ctx, namespaceTitle, group) +} + +func (f *ConvertPrometheusApiHandler) handleRouteConvertPrometheusCortexPostRuleGroup(ctx *contextmodel.ReqContext, namespaceTitle string) response.Response { + return f.handleRouteConvertPrometheusPostRuleGroup(ctx, namespaceTitle) +} diff --git a/pkg/services/ngalert/api/tooling/api.json b/pkg/services/ngalert/api/tooling/api.json index 0a2fb0af0d4..7fc77c7ce19 100644 --- a/pkg/services/ngalert/api/tooling/api.json +++ b/pkg/services/ngalert/api/tooling/api.json @@ -4493,7 +4493,6 @@ "type": "object" }, "URL": { - "description": "The general form represented is:\n\n[scheme:][//[userinfo@]host][/]path[?query][#fragment]\n\nURLs that do not start with a slash after the scheme are interpreted as:\n\nscheme:opaque[?query][#fragment]\n\nThe Host field contains the host and port subcomponents of the URL.\nWhen the port is present, it is separated from the host with a colon.\nWhen the host is an IPv6 address, it must be enclosed in square brackets:\n\"[fe80::1]:80\". The [net.JoinHostPort] function combines a host and port\ninto a string suitable for the Host field, adding square brackets to\nthe host when necessary.\n\nNote that the Path field is stored in decoded form: /%47%6f%2f becomes /Go/.\nA consequence is that it is impossible to tell which slashes in the Path were\nslashes in the raw URL and which were %2f. This distinction is rarely important,\nbut when it is, the code should use the [URL.EscapedPath] method, which preserves\nthe original encoding of Path.\n\nThe RawPath field is an optional field which is only set when the default\nencoding of Path is different from the escaped path. See the EscapedPath method\nfor more details.\n\nURL's String method uses the EscapedPath method to obtain the path.", "properties": { "ForceQuery": { "type": "boolean" @@ -4529,7 +4528,7 @@ "$ref": "#/definitions/Userinfo" } }, - "title": "A URL represents a parsed URL (technically, a URI reference).", + "title": "URL is a custom URL type that allows validation at configuration load time.", "type": "object" }, "UpdateRuleGroupResponse": { @@ -4932,6 +4931,7 @@ "type": "object" }, "gettableAlerts": { + "description": "GettableAlerts gettable alerts", "items": { "$ref": "#/definitions/gettableAlert", "type": "object" diff --git a/pkg/services/ngalert/api/tooling/definitions/convert_prometheus_api.go b/pkg/services/ngalert/api/tooling/definitions/convert_prometheus_api.go index 9442ba9fc0f..f0202486e46 100644 --- a/pkg/services/ngalert/api/tooling/definitions/convert_prometheus_api.go +++ b/pkg/services/ngalert/api/tooling/definitions/convert_prometheus_api.go @@ -4,6 +4,7 @@ import ( "github.com/prometheus/common/model" ) +// Route for mimirtool // swagger:route GET /convert/prometheus/config/v1/rules convert_prometheus RouteConvertPrometheusGetRules // // Gets all Grafana-managed alert rules that were imported from Prometheus-compatible sources, grouped by namespace. @@ -16,6 +17,20 @@ import ( // 403: ForbiddenError // 404: NotFound +// Route for cortextool +// swagger:route GET /convert/api/prom/rules convert_prometheus RouteConvertPrometheusCortexGetRules +// +// Gets all Grafana-managed alert rules that were imported from Prometheus-compatible sources, grouped by namespace. +// +// Produces: +// - application/yaml +// +// Responses: +// 200: PrometheusNamespace +// 403: ForbiddenError +// 404: NotFound + +// Route for mimirtool // swagger:route GET /convert/prometheus/config/v1/rules/{NamespaceTitle} convert_prometheus RouteConvertPrometheusGetNamespace // // Gets Grafana-managed alert rules that were imported from Prometheus-compatible sources for a specified namespace (folder). @@ -28,6 +43,20 @@ import ( // 403: ForbiddenError // 404: NotFound +// Route for cortextool +// swagger:route GET /convert/api/prom/rules/{NamespaceTitle} convert_prometheus RouteConvertPrometheusCortexGetNamespace +// +// Gets Grafana-managed alert rules that were imported from Prometheus-compatible sources for a specified namespace (folder). +// +// Produces: +// - application/yaml +// +// Responses: +// 200: PrometheusNamespace +// 403: ForbiddenError +// 404: NotFound + +// Route for mimirtool // swagger:route GET /convert/prometheus/config/v1/rules/{NamespaceTitle}/{Group} convert_prometheus RouteConvertPrometheusGetRuleGroup // // Gets a single rule group in Prometheus-compatible format if it was imported from a Prometheus-compatible source. @@ -40,6 +69,20 @@ import ( // 403: ForbiddenError // 404: NotFound +// Route for cortextool +// swagger:route GET /convert/api/prom/rules/{NamespaceTitle}/{Group} convert_prometheus RouteConvertPrometheusCortexGetRuleGroup +// +// Gets a single rule group in Prometheus-compatible format if it was imported from a Prometheus-compatible source. +// +// Produces: +// - application/yaml +// +// Responses: +// 200: PrometheusRuleGroup +// 403: ForbiddenError +// 404: NotFound + +// Route for mimirtool // swagger:route POST /convert/prometheus/config/v1/rules/{NamespaceTitle} convert_prometheus RouteConvertPrometheusPostRuleGroup // // Converts a Prometheus rule group into a Grafana rule group and creates or updates it within the specified namespace. @@ -59,6 +102,27 @@ import ( // Extensions: // x-raw-request: true +// Route for cortextool +// swagger:route POST /convert/api/prom/rules/{NamespaceTitle} convert_prometheus RouteConvertPrometheusCortexPostRuleGroup +// +// Converts a Prometheus rule group into a Grafana rule group and creates or updates it within the specified namespace. +// If the group already exists and was not imported from a Prometheus-compatible source initially, +// it will not be replaced and an error will be returned. +// +// Consumes: +// - application/yaml +// +// Produces: +// - application/json +// +// Responses: +// 202: ConvertPrometheusResponse +// 403: ForbiddenError +// +// Extensions: +// x-raw-request: true + +// Route for mimirtool // swagger:route DELETE /convert/prometheus/config/v1/rules/{NamespaceTitle} convert_prometheus RouteConvertPrometheusDeleteNamespace // // Deletes all rule groups that were imported from Prometheus-compatible sources within the specified namespace. @@ -70,6 +134,19 @@ import ( // 202: ConvertPrometheusResponse // 403: ForbiddenError +// Route for cortextool +// swagger:route DELETE /convert/api/prom/rules/{NamespaceTitle} convert_prometheus RouteConvertPrometheusCortexDeleteNamespace +// +// Deletes all rule groups that were imported from Prometheus-compatible sources within the specified namespace. +// +// Produces: +// - application/json +// +// Responses: +// 202: ConvertPrometheusResponse +// 403: ForbiddenError + +// Route for mimirtool // swagger:route DELETE /convert/prometheus/config/v1/rules/{NamespaceTitle}/{Group} convert_prometheus RouteConvertPrometheusDeleteRuleGroup // // Deletes a specific rule group if it was imported from a Prometheus-compatible source. @@ -81,7 +158,19 @@ import ( // 202: ConvertPrometheusResponse // 403: ForbiddenError -// swagger:parameters RouteConvertPrometheusPostRuleGroup +// Route for cortextool +// swagger:route DELETE /convert/api/prom/rules/{NamespaceTitle}/{Group} convert_prometheus RouteConvertPrometheusCortexDeleteRuleGroup +// +// Deletes a specific rule group if it was imported from a Prometheus-compatible source. +// +// Produces: +// - application/json +// +// Responses: +// 202: ConvertPrometheusResponse +// 403: ForbiddenError + +// swagger:parameters RouteConvertPrometheusPostRuleGroup RouteConvertPrometheusCortexPostRuleGroup type RouteConvertPrometheusPostRuleGroupParams struct { // in: path NamespaceTitle string @@ -119,7 +208,7 @@ type PrometheusRule struct { Record string `yaml:"record,omitempty"` } -// swagger:parameters RouteConvertPrometheusDeleteRuleGroup RouteConvertPrometheusGetRuleGroup +// swagger:parameters RouteConvertPrometheusDeleteRuleGroup RouteConvertPrometheusCortexDeleteRuleGroup RouteConvertPrometheusGetRuleGroup RouteConvertPrometheusCortexGetRuleGroup type RouteConvertPrometheusDeleteRuleGroupParams struct { // in: path NamespaceTitle string @@ -127,7 +216,7 @@ type RouteConvertPrometheusDeleteRuleGroupParams struct { Group string } -// swagger:parameters RouteConvertPrometheusDeleteNamespace RouteConvertPrometheusGetNamespace +// swagger:parameters RouteConvertPrometheusDeleteNamespace RouteConvertPrometheusCortexDeleteNamespace RouteConvertPrometheusGetNamespace RouteConvertPrometheusCortexGetNamespace type RouteConvertPrometheusDeleteNamespaceParams struct { // in: path NamespaceTitle string diff --git a/pkg/services/ngalert/api/tooling/post.json b/pkg/services/ngalert/api/tooling/post.json index b48c13e2d7e..ab2b7542381 100644 --- a/pkg/services/ngalert/api/tooling/post.json +++ b/pkg/services/ngalert/api/tooling/post.json @@ -4493,6 +4493,7 @@ "type": "object" }, "URL": { + "description": "The general form represented is:\n\n[scheme:][//[userinfo@]host][/]path[?query][#fragment]\n\nURLs that do not start with a slash after the scheme are interpreted as:\n\nscheme:opaque[?query][#fragment]\n\nThe Host field contains the host and port subcomponents of the URL.\nWhen the port is present, it is separated from the host with a colon.\nWhen the host is an IPv6 address, it must be enclosed in square brackets:\n\"[fe80::1]:80\". The [net.JoinHostPort] function combines a host and port\ninto a string suitable for the Host field, adding square brackets to\nthe host when necessary.\n\nNote that the Path field is stored in decoded form: /%47%6f%2f becomes /Go/.\nA consequence is that it is impossible to tell which slashes in the Path were\nslashes in the raw URL and which were %2f. This distinction is rarely important,\nbut when it is, the code should use the [URL.EscapedPath] method, which preserves\nthe original encoding of Path.\n\nThe RawPath field is an optional field which is only set when the default\nencoding of Path is different from the escaped path. See the EscapedPath method\nfor more details.\n\nURL's String method uses the EscapedPath method to obtain the path.", "properties": { "ForceQuery": { "type": "boolean" @@ -4528,7 +4529,7 @@ "$ref": "#/definitions/Userinfo" } }, - "title": "URL is a custom URL type that allows validation at configuration load time.", + "title": "A URL represents a parsed URL (technically, a URI reference).", "type": "object" }, "UpdateRuleGroupResponse": { @@ -4770,6 +4771,7 @@ "type": "object" }, "alertGroups": { + "description": "AlertGroups alert groups", "items": { "$ref": "#/definitions/alertGroup", "type": "object" @@ -5056,6 +5058,7 @@ "type": "object" }, "gettableSilences": { + "description": "GettableSilences gettable silences", "items": { "$ref": "#/definitions/gettableSilence", "type": "object" @@ -6398,6 +6401,253 @@ ] } }, + "/convert/api/prom/rules": { + "get": { + "operationId": "RouteConvertPrometheusCortexGetRules", + "produces": [ + "application/yaml" + ], + "responses": { + "200": { + "description": "PrometheusNamespace", + "schema": { + "$ref": "#/definitions/PrometheusNamespace" + } + }, + "403": { + "description": "ForbiddenError", + "schema": { + "$ref": "#/definitions/ForbiddenError" + } + }, + "404": { + "description": "NotFound", + "schema": { + "$ref": "#/definitions/NotFound" + } + } + }, + "summary": "Gets all Grafana-managed alert rules that were imported from Prometheus-compatible sources, grouped by namespace.", + "tags": [ + "convert_prometheus" + ] + } + }, + "/convert/api/prom/rules/{NamespaceTitle}": { + "delete": { + "operationId": "RouteConvertPrometheusCortexDeleteNamespace", + "parameters": [ + { + "in": "path", + "name": "NamespaceTitle", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json" + ], + "responses": { + "202": { + "description": "ConvertPrometheusResponse", + "schema": { + "$ref": "#/definitions/ConvertPrometheusResponse" + } + }, + "403": { + "description": "ForbiddenError", + "schema": { + "$ref": "#/definitions/ForbiddenError" + } + } + }, + "summary": "Deletes all rule groups that were imported from Prometheus-compatible sources within the specified namespace.", + "tags": [ + "convert_prometheus" + ] + }, + "get": { + "operationId": "RouteConvertPrometheusCortexGetNamespace", + "parameters": [ + { + "in": "path", + "name": "NamespaceTitle", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/yaml" + ], + "responses": { + "200": { + "description": "PrometheusNamespace", + "schema": { + "$ref": "#/definitions/PrometheusNamespace" + } + }, + "403": { + "description": "ForbiddenError", + "schema": { + "$ref": "#/definitions/ForbiddenError" + } + }, + "404": { + "description": "NotFound", + "schema": { + "$ref": "#/definitions/NotFound" + } + } + }, + "summary": "Gets Grafana-managed alert rules that were imported from Prometheus-compatible sources for a specified namespace (folder).", + "tags": [ + "convert_prometheus" + ] + }, + "post": { + "consumes": [ + "application/yaml" + ], + "description": "If the group already exists and was not imported from a Prometheus-compatible source initially,\nit will not be replaced and an error will be returned.", + "operationId": "RouteConvertPrometheusCortexPostRuleGroup", + "parameters": [ + { + "in": "path", + "name": "NamespaceTitle", + "required": true, + "type": "string" + }, + { + "in": "header", + "name": "x-grafana-alerting-datasource-uid", + "type": "string" + }, + { + "in": "header", + "name": "x-grafana-alerting-recording-rules-paused", + "type": "boolean" + }, + { + "in": "header", + "name": "x-grafana-alerting-alert-rules-paused", + "type": "boolean" + }, + { + "in": "body", + "name": "Body", + "schema": { + "$ref": "#/definitions/PrometheusRuleGroup" + } + } + ], + "produces": [ + "application/json" + ], + "responses": { + "202": { + "description": "ConvertPrometheusResponse", + "schema": { + "$ref": "#/definitions/ConvertPrometheusResponse" + } + }, + "403": { + "description": "ForbiddenError", + "schema": { + "$ref": "#/definitions/ForbiddenError" + } + } + }, + "summary": "Converts a Prometheus rule group into a Grafana rule group and creates or updates it within the specified namespace.", + "tags": [ + "convert_prometheus" + ], + "x-raw-request": "true" + } + }, + "/convert/api/prom/rules/{NamespaceTitle}/{Group}": { + "delete": { + "operationId": "RouteConvertPrometheusCortexDeleteRuleGroup", + "parameters": [ + { + "in": "path", + "name": "NamespaceTitle", + "required": true, + "type": "string" + }, + { + "in": "path", + "name": "Group", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json" + ], + "responses": { + "202": { + "description": "ConvertPrometheusResponse", + "schema": { + "$ref": "#/definitions/ConvertPrometheusResponse" + } + }, + "403": { + "description": "ForbiddenError", + "schema": { + "$ref": "#/definitions/ForbiddenError" + } + } + }, + "summary": "Deletes a specific rule group if it was imported from a Prometheus-compatible source.", + "tags": [ + "convert_prometheus" + ] + }, + "get": { + "operationId": "RouteConvertPrometheusCortexGetRuleGroup", + "parameters": [ + { + "in": "path", + "name": "NamespaceTitle", + "required": true, + "type": "string" + }, + { + "in": "path", + "name": "Group", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/yaml" + ], + "responses": { + "200": { + "description": "PrometheusRuleGroup", + "schema": { + "$ref": "#/definitions/PrometheusRuleGroup" + } + }, + "403": { + "description": "ForbiddenError", + "schema": { + "$ref": "#/definitions/ForbiddenError" + } + }, + "404": { + "description": "NotFound", + "schema": { + "$ref": "#/definitions/NotFound" + } + } + }, + "summary": "Gets a single rule group in Prometheus-compatible format if it was imported from a Prometheus-compatible source.", + "tags": [ + "convert_prometheus" + ] + } + }, "/convert/prometheus/config/v1/rules": { "get": { "operationId": "RouteConvertPrometheusGetRules", diff --git a/pkg/services/ngalert/api/tooling/spec.json b/pkg/services/ngalert/api/tooling/spec.json index 571a497cc6b..1b251c81f29 100644 --- a/pkg/services/ngalert/api/tooling/spec.json +++ b/pkg/services/ngalert/api/tooling/spec.json @@ -1102,6 +1102,253 @@ } } }, + "/convert/api/prom/rules": { + "get": { + "produces": [ + "application/yaml" + ], + "tags": [ + "convert_prometheus" + ], + "summary": "Gets all Grafana-managed alert rules that were imported from Prometheus-compatible sources, grouped by namespace.", + "operationId": "RouteConvertPrometheusCortexGetRules", + "responses": { + "200": { + "description": "PrometheusNamespace", + "schema": { + "$ref": "#/definitions/PrometheusNamespace" + } + }, + "403": { + "description": "ForbiddenError", + "schema": { + "$ref": "#/definitions/ForbiddenError" + } + }, + "404": { + "description": "NotFound", + "schema": { + "$ref": "#/definitions/NotFound" + } + } + } + } + }, + "/convert/api/prom/rules/{NamespaceTitle}": { + "get": { + "produces": [ + "application/yaml" + ], + "tags": [ + "convert_prometheus" + ], + "summary": "Gets Grafana-managed alert rules that were imported from Prometheus-compatible sources for a specified namespace (folder).", + "operationId": "RouteConvertPrometheusCortexGetNamespace", + "parameters": [ + { + "type": "string", + "name": "NamespaceTitle", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "PrometheusNamespace", + "schema": { + "$ref": "#/definitions/PrometheusNamespace" + } + }, + "403": { + "description": "ForbiddenError", + "schema": { + "$ref": "#/definitions/ForbiddenError" + } + }, + "404": { + "description": "NotFound", + "schema": { + "$ref": "#/definitions/NotFound" + } + } + } + }, + "post": { + "description": "If the group already exists and was not imported from a Prometheus-compatible source initially,\nit will not be replaced and an error will be returned.", + "consumes": [ + "application/yaml" + ], + "produces": [ + "application/json" + ], + "tags": [ + "convert_prometheus" + ], + "summary": "Converts a Prometheus rule group into a Grafana rule group and creates or updates it within the specified namespace.", + "operationId": "RouteConvertPrometheusCortexPostRuleGroup", + "parameters": [ + { + "type": "string", + "name": "NamespaceTitle", + "in": "path", + "required": true + }, + { + "type": "string", + "name": "x-grafana-alerting-datasource-uid", + "in": "header" + }, + { + "type": "boolean", + "name": "x-grafana-alerting-recording-rules-paused", + "in": "header" + }, + { + "type": "boolean", + "name": "x-grafana-alerting-alert-rules-paused", + "in": "header" + }, + { + "name": "Body", + "in": "body", + "schema": { + "$ref": "#/definitions/PrometheusRuleGroup" + } + } + ], + "responses": { + "202": { + "description": "ConvertPrometheusResponse", + "schema": { + "$ref": "#/definitions/ConvertPrometheusResponse" + } + }, + "403": { + "description": "ForbiddenError", + "schema": { + "$ref": "#/definitions/ForbiddenError" + } + } + }, + "x-raw-request": "true" + }, + "delete": { + "produces": [ + "application/json" + ], + "tags": [ + "convert_prometheus" + ], + "summary": "Deletes all rule groups that were imported from Prometheus-compatible sources within the specified namespace.", + "operationId": "RouteConvertPrometheusCortexDeleteNamespace", + "parameters": [ + { + "type": "string", + "name": "NamespaceTitle", + "in": "path", + "required": true + } + ], + "responses": { + "202": { + "description": "ConvertPrometheusResponse", + "schema": { + "$ref": "#/definitions/ConvertPrometheusResponse" + } + }, + "403": { + "description": "ForbiddenError", + "schema": { + "$ref": "#/definitions/ForbiddenError" + } + } + } + } + }, + "/convert/api/prom/rules/{NamespaceTitle}/{Group}": { + "get": { + "produces": [ + "application/yaml" + ], + "tags": [ + "convert_prometheus" + ], + "summary": "Gets a single rule group in Prometheus-compatible format if it was imported from a Prometheus-compatible source.", + "operationId": "RouteConvertPrometheusCortexGetRuleGroup", + "parameters": [ + { + "type": "string", + "name": "NamespaceTitle", + "in": "path", + "required": true + }, + { + "type": "string", + "name": "Group", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "PrometheusRuleGroup", + "schema": { + "$ref": "#/definitions/PrometheusRuleGroup" + } + }, + "403": { + "description": "ForbiddenError", + "schema": { + "$ref": "#/definitions/ForbiddenError" + } + }, + "404": { + "description": "NotFound", + "schema": { + "$ref": "#/definitions/NotFound" + } + } + } + }, + "delete": { + "produces": [ + "application/json" + ], + "tags": [ + "convert_prometheus" + ], + "summary": "Deletes a specific rule group if it was imported from a Prometheus-compatible source.", + "operationId": "RouteConvertPrometheusCortexDeleteRuleGroup", + "parameters": [ + { + "type": "string", + "name": "NamespaceTitle", + "in": "path", + "required": true + }, + { + "type": "string", + "name": "Group", + "in": "path", + "required": true + } + ], + "responses": { + "202": { + "description": "ConvertPrometheusResponse", + "schema": { + "$ref": "#/definitions/ConvertPrometheusResponse" + } + }, + "403": { + "description": "ForbiddenError", + "schema": { + "$ref": "#/definitions/ForbiddenError" + } + } + } + } + }, "/convert/prometheus/config/v1/rules": { "get": { "produces": [ @@ -8434,8 +8681,9 @@ } }, "URL": { + "description": "The general form represented is:\n\n[scheme:][//[userinfo@]host][/]path[?query][#fragment]\n\nURLs that do not start with a slash after the scheme are interpreted as:\n\nscheme:opaque[?query][#fragment]\n\nThe Host field contains the host and port subcomponents of the URL.\nWhen the port is present, it is separated from the host with a colon.\nWhen the host is an IPv6 address, it must be enclosed in square brackets:\n\"[fe80::1]:80\". The [net.JoinHostPort] function combines a host and port\ninto a string suitable for the Host field, adding square brackets to\nthe host when necessary.\n\nNote that the Path field is stored in decoded form: /%47%6f%2f becomes /Go/.\nA consequence is that it is impossible to tell which slashes in the Path were\nslashes in the raw URL and which were %2f. This distinction is rarely important,\nbut when it is, the code should use the [URL.EscapedPath] method, which preserves\nthe original encoding of Path.\n\nThe RawPath field is an optional field which is only set when the default\nencoding of Path is different from the escaped path. See the EscapedPath method\nfor more details.\n\nURL's String method uses the EscapedPath method to obtain the path.", "type": "object", - "title": "URL is a custom URL type that allows validation at configuration load time.", + "title": "A URL represents a parsed URL (technically, a URI reference).", "properties": { "ForceQuery": { "type": "boolean" @@ -8711,6 +8959,7 @@ } }, "alertGroups": { + "description": "AlertGroups alert groups", "type": "array", "items": { "type": "object", @@ -8997,6 +9246,7 @@ } }, "gettableSilences": { + "description": "GettableSilences gettable silences", "type": "array", "items": { "type": "object", diff --git a/pkg/tests/api/alerting/api_convert_prometheus_test.go b/pkg/tests/api/alerting/api_convert_prometheus_test.go index c92d5f8e428..cb248fba7f3 100644 --- a/pkg/tests/api/alerting/api_convert_prometheus_test.go +++ b/pkg/tests/api/alerting/api_convert_prometheus_test.go @@ -99,283 +99,315 @@ var ( ) func TestIntegrationConvertPrometheusEndpoints(t *testing.T) { - testinfra.SQLiteIntegrationTest(t) + runTest := func(t *testing.T, enableLokiPaths bool) { + testinfra.SQLiteIntegrationTest(t) - // Setup Grafana and its Database - dir, gpath := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{ - DisableLegacyAlerting: true, - EnableUnifiedAlerting: true, - DisableAnonymous: true, - AppModeProduction: true, - EnableFeatureToggles: []string{"alertingConversionAPI"}, + // Setup Grafana and its Database + dir, gpath := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{ + DisableLegacyAlerting: true, + EnableUnifiedAlerting: true, + DisableAnonymous: true, + AppModeProduction: true, + EnableFeatureToggles: []string{"alertingConversionAPI"}, + }) + + grafanaListedAddr, env := testinfra.StartGrafanaEnv(t, dir, gpath) + + // Create users to make authenticated requests + createUser(t, env.SQLStore, env.Cfg, user.CreateUserCommand{ + DefaultOrgRole: string(org.RoleAdmin), + Password: "password", + Login: "admin", + }) + apiClient := newAlertingApiClient(grafanaListedAddr, "admin", "password") + apiClient.prometheusConversionUseLokiPaths = enableLokiPaths + + createUser(t, env.SQLStore, env.Cfg, user.CreateUserCommand{ + DefaultOrgRole: string(org.RoleViewer), + Password: "password", + Login: "viewer", + }) + viewerClient := newAlertingApiClient(grafanaListedAddr, "viewer", "password") + + namespace1 := "test-namespace-1" + namespace2 := "test-namespace-2" + + ds := apiClient.CreateDatasource(t, datasources.DS_PROMETHEUS) + + t.Run("create rule groups and get them back", func(t *testing.T) { + _, status, body := apiClient.ConvertPrometheusPostRuleGroup(t, namespace1, ds.Body.Datasource.UID, promGroup1, nil) + requireStatusCode(t, http.StatusAccepted, status, body) + _, status, body = apiClient.ConvertPrometheusPostRuleGroup(t, namespace1, ds.Body.Datasource.UID, promGroup2, nil) + requireStatusCode(t, http.StatusAccepted, status, body) + + // create a third group in a different namespace + _, status, body = apiClient.ConvertPrometheusPostRuleGroup(t, namespace2, ds.Body.Datasource.UID, promGroup3, nil) + requireStatusCode(t, http.StatusAccepted, status, body) + + // And a non-provisioned rule in another namespace + namespace3UID := util.GenerateShortUID() + apiClient.CreateFolder(t, namespace3UID, "folder") + createRule(t, apiClient, namespace3UID) + + // Now get the first group + group1 := apiClient.ConvertPrometheusGetRuleGroupRules(t, namespace1, promGroup1.Name) + require.Equal(t, promGroup1, group1) + + // Get namespace1 + ns1 := apiClient.ConvertPrometheusGetNamespaceRules(t, namespace1) + expectedNs1 := map[string][]apimodels.PrometheusRuleGroup{ + namespace1: {promGroup1, promGroup2}, + } + require.Equal(t, expectedNs1, ns1) + + // Get all namespaces + namespaces := apiClient.ConvertPrometheusGetAllRules(t) + expectedNamespaces := map[string][]apimodels.PrometheusRuleGroup{ + namespace1: {promGroup1, promGroup2}, + namespace2: {promGroup3}, + } + require.Equal(t, expectedNamespaces, namespaces) + }) + + t.Run("without permissions to create folders cannot create rule groups either", func(t *testing.T) { + _, status, raw := viewerClient.ConvertPrometheusPostRuleGroup(t, namespace1, ds.Body.Datasource.UID, promGroup1, nil) + requireStatusCode(t, http.StatusForbidden, status, raw) + }) + + t.Run("delete one rule group", func(t *testing.T) { + _, status, body := apiClient.ConvertPrometheusPostRuleGroup(t, namespace1, ds.Body.Datasource.UID, promGroup1, nil) + requireStatusCode(t, http.StatusAccepted, status, body) + _, status, body = apiClient.ConvertPrometheusPostRuleGroup(t, namespace1, ds.Body.Datasource.UID, promGroup2, nil) + requireStatusCode(t, http.StatusAccepted, status, body) + _, status, body = apiClient.ConvertPrometheusPostRuleGroup(t, namespace2, ds.Body.Datasource.UID, promGroup3, nil) + requireStatusCode(t, http.StatusAccepted, status, body) + + apiClient.ConvertPrometheusDeleteRuleGroup(t, namespace1, promGroup1.Name) + + // Check that the promGroup2 and promGroup3 are still there + namespaces := apiClient.ConvertPrometheusGetAllRules(t) + expectedNamespaces := map[string][]apimodels.PrometheusRuleGroup{ + namespace1: {promGroup2}, + namespace2: {promGroup3}, + } + require.Equal(t, expectedNamespaces, namespaces) + + // Delete the second namespace + apiClient.ConvertPrometheusDeleteNamespace(t, namespace2) + + // Check that only the first namespace is left + namespaces = apiClient.ConvertPrometheusGetAllRules(t) + expectedNamespaces = map[string][]apimodels.PrometheusRuleGroup{ + namespace1: {promGroup2}, + } + require.Equal(t, expectedNamespaces, namespaces) + }) + } + + t.Run("with the mimirtool paths", func(t *testing.T) { + runTest(t, false) }) - grafanaListedAddr, env := testinfra.StartGrafanaEnv(t, dir, gpath) - - // Create users to make authenticated requests - createUser(t, env.SQLStore, env.Cfg, user.CreateUserCommand{ - DefaultOrgRole: string(org.RoleAdmin), - Password: "password", - Login: "admin", - }) - apiClient := newAlertingApiClient(grafanaListedAddr, "admin", "password") - - createUser(t, env.SQLStore, env.Cfg, user.CreateUserCommand{ - DefaultOrgRole: string(org.RoleViewer), - Password: "password", - Login: "viewer", - }) - viewerClient := newAlertingApiClient(grafanaListedAddr, "viewer", "password") - - namespace1 := "test-namespace-1" - namespace2 := "test-namespace-2" - - ds := apiClient.CreateDatasource(t, datasources.DS_PROMETHEUS) - - t.Run("create rule groups and get them back", func(t *testing.T) { - _, status, body := apiClient.ConvertPrometheusPostRuleGroup(t, namespace1, ds.Body.Datasource.UID, promGroup1, nil) - requireStatusCode(t, http.StatusAccepted, status, body) - _, status, body = apiClient.ConvertPrometheusPostRuleGroup(t, namespace1, ds.Body.Datasource.UID, promGroup2, nil) - requireStatusCode(t, http.StatusAccepted, status, body) - - // create a third group in a different namespace - _, status, body = apiClient.ConvertPrometheusPostRuleGroup(t, namespace2, ds.Body.Datasource.UID, promGroup3, nil) - requireStatusCode(t, http.StatusAccepted, status, body) - - // And a non-provisioned rule in another namespace - namespace3UID := util.GenerateShortUID() - apiClient.CreateFolder(t, namespace3UID, "folder") - createRule(t, apiClient, namespace3UID) - - // Now get the first group - group1 := apiClient.ConvertPrometheusGetRuleGroupRules(t, namespace1, promGroup1.Name) - require.Equal(t, promGroup1, group1) - - // Get namespace1 - ns1 := apiClient.ConvertPrometheusGetNamespaceRules(t, namespace1) - expectedNs1 := map[string][]apimodels.PrometheusRuleGroup{ - namespace1: {promGroup1, promGroup2}, - } - require.Equal(t, expectedNs1, ns1) - - // Get all namespaces - namespaces := apiClient.ConvertPrometheusGetAllRules(t) - expectedNamespaces := map[string][]apimodels.PrometheusRuleGroup{ - namespace1: {promGroup1, promGroup2}, - namespace2: {promGroup3}, - } - require.Equal(t, expectedNamespaces, namespaces) - }) - - t.Run("without permissions to create folders cannot create rule groups either", func(t *testing.T) { - _, status, raw := viewerClient.ConvertPrometheusPostRuleGroup(t, namespace1, ds.Body.Datasource.UID, promGroup1, nil) - requireStatusCode(t, http.StatusForbidden, status, raw) - }) - - t.Run("delete one rule group", func(t *testing.T) { - _, status, body := apiClient.ConvertPrometheusPostRuleGroup(t, namespace1, ds.Body.Datasource.UID, promGroup1, nil) - requireStatusCode(t, http.StatusAccepted, status, body) - _, status, body = apiClient.ConvertPrometheusPostRuleGroup(t, namespace1, ds.Body.Datasource.UID, promGroup2, nil) - requireStatusCode(t, http.StatusAccepted, status, body) - _, status, body = apiClient.ConvertPrometheusPostRuleGroup(t, namespace2, ds.Body.Datasource.UID, promGroup3, nil) - requireStatusCode(t, http.StatusAccepted, status, body) - - apiClient.ConvertPrometheusDeleteRuleGroup(t, namespace1, promGroup1.Name) - - // Check that the promGroup2 and promGroup3 are still there - namespaces := apiClient.ConvertPrometheusGetAllRules(t) - expectedNamespaces := map[string][]apimodels.PrometheusRuleGroup{ - namespace1: {promGroup2}, - namespace2: {promGroup3}, - } - require.Equal(t, expectedNamespaces, namespaces) - - // Delete the second namespace - apiClient.ConvertPrometheusDeleteNamespace(t, namespace2) - - // Check that only the first namespace is left - namespaces = apiClient.ConvertPrometheusGetAllRules(t) - expectedNamespaces = map[string][]apimodels.PrometheusRuleGroup{ - namespace1: {promGroup2}, - } - require.Equal(t, expectedNamespaces, namespaces) + t.Run("with the cortextool Loki paths", func(t *testing.T) { + runTest(t, true) }) } func TestIntegrationConvertPrometheusEndpoints_Conflict(t *testing.T) { - testinfra.SQLiteIntegrationTest(t) + runTest := func(t *testing.T, enableLokiPaths bool) { + testinfra.SQLiteIntegrationTest(t) - // Setup Grafana and its Database - dir, gpath := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{ - DisableLegacyAlerting: true, - EnableUnifiedAlerting: true, - DisableAnonymous: true, - AppModeProduction: true, - EnableFeatureToggles: []string{"alertingConversionAPI"}, - }) + // Setup Grafana and its Database + dir, gpath := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{ + DisableLegacyAlerting: true, + EnableUnifiedAlerting: true, + DisableAnonymous: true, + AppModeProduction: true, + EnableFeatureToggles: []string{"alertingConversionAPI"}, + }) - grafanaListedAddr, env := testinfra.StartGrafanaEnv(t, dir, gpath) + grafanaListedAddr, env := testinfra.StartGrafanaEnv(t, dir, gpath) - // Create users to make authenticated requests - createUser(t, env.SQLStore, env.Cfg, user.CreateUserCommand{ - DefaultOrgRole: string(org.RoleAdmin), - Password: "password", - Login: "admin", - }) - apiClient := newAlertingApiClient(grafanaListedAddr, "admin", "password") + // Create users to make authenticated requests + createUser(t, env.SQLStore, env.Cfg, user.CreateUserCommand{ + DefaultOrgRole: string(org.RoleAdmin), + Password: "password", + Login: "admin", + }) + apiClient := newAlertingApiClient(grafanaListedAddr, "admin", "password") + apiClient.prometheusConversionUseLokiPaths = enableLokiPaths - createUser(t, env.SQLStore, env.Cfg, user.CreateUserCommand{ - DefaultOrgRole: string(org.RoleViewer), - Password: "password", - Login: "viewer", - }) + createUser(t, env.SQLStore, env.Cfg, user.CreateUserCommand{ + DefaultOrgRole: string(org.RoleViewer), + Password: "password", + Login: "viewer", + }) - namespace1 := "test-namespace-1" + namespace1 := "test-namespace-1" - ds := apiClient.CreateDatasource(t, datasources.DS_PROMETHEUS) + ds := apiClient.CreateDatasource(t, datasources.DS_PROMETHEUS) - t.Run("cannot overwrite a rule group with different provenance", func(t *testing.T) { - // Create a rule group using the provisioning API and then try to overwrite it - // using the Prometheus Conversion API. It should fail because the provenance - // we set for rules in these two APIs is different and we check that when updating. - provisionedRuleGroup := apimodels.AlertRuleGroup{ - Title: promGroup1.Name, - Interval: 60, - FolderUID: namespace1, - Rules: []apimodels.ProvisionedAlertRule{ - { - Title: "Rule1", - OrgID: 1, - RuleGroup: promGroup1.Name, - Condition: "A", - NoDataState: apimodels.Alerting, - ExecErrState: apimodels.AlertingErrState, - For: prommodel.Duration(time.Duration(60) * time.Second), - Data: []apimodels.AlertQuery{ - { - RefID: "A", - RelativeTimeRange: apimodels.RelativeTimeRange{ - From: apimodels.Duration(time.Duration(5) * time.Hour), - To: apimodels.Duration(time.Duration(3) * time.Hour), + t.Run("cannot overwrite a rule group with different provenance", func(t *testing.T) { + // Create a rule group using the provisioning API and then try to overwrite it + // using the Prometheus Conversion API. It should fail because the provenance + // we set for rules in these two APIs is different and we check that when updating. + provisionedRuleGroup := apimodels.AlertRuleGroup{ + Title: promGroup1.Name, + Interval: 60, + FolderUID: namespace1, + Rules: []apimodels.ProvisionedAlertRule{ + { + Title: "Rule1", + OrgID: 1, + RuleGroup: promGroup1.Name, + Condition: "A", + NoDataState: apimodels.Alerting, + ExecErrState: apimodels.AlertingErrState, + For: prommodel.Duration(time.Duration(60) * time.Second), + Data: []apimodels.AlertQuery{ + { + RefID: "A", + RelativeTimeRange: apimodels.RelativeTimeRange{ + From: apimodels.Duration(time.Duration(5) * time.Hour), + To: apimodels.Duration(time.Duration(3) * time.Hour), + }, + DatasourceUID: expr.DatasourceUID, + Model: json.RawMessage([]byte(`{"type":"math","expression":"2 + 3 \u003e 1"}`)), }, - DatasourceUID: expr.DatasourceUID, - Model: json.RawMessage([]byte(`{"type":"math","expression":"2 + 3 \u003e 1"}`)), }, }, }, - }, - } + } - // Create the folder - apiClient.CreateFolder(t, namespace1, namespace1) - // Create rule in the root folder using another API - _, status, response := apiClient.CreateOrUpdateRuleGroupProvisioning(t, provisionedRuleGroup) - require.Equalf(t, http.StatusOK, status, response) + // Create the folder + apiClient.CreateFolder(t, namespace1, namespace1) + // Create rule in the root folder using another API + _, status, response := apiClient.CreateOrUpdateRuleGroupProvisioning(t, provisionedRuleGroup) + require.Equalf(t, http.StatusOK, status, response) - // Should fail to post the group - _, status, body := apiClient.ConvertPrometheusPostRuleGroup(t, namespace1, ds.Body.Datasource.UID, promGroup1, nil) - requireStatusCode(t, http.StatusConflict, status, body) + // Should fail to post the group + _, status, body := apiClient.ConvertPrometheusPostRuleGroup(t, namespace1, ds.Body.Datasource.UID, promGroup1, nil) + requireStatusCode(t, http.StatusConflict, status, body) + }) + } + + t.Run("with the mimirtool paths", func(t *testing.T) { + runTest(t, false) + }) + + t.Run("with the cortextool Loki paths", func(t *testing.T) { + runTest(t, true) }) } func TestIntegrationConvertPrometheusEndpoints_CreatePausedRules(t *testing.T) { - testinfra.SQLiteIntegrationTest(t) + runTest := func(t *testing.T, enableLokiPaths bool) { + testinfra.SQLiteIntegrationTest(t) - // Setup Grafana and its Database - dir, path := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{ - DisableLegacyAlerting: true, - EnableUnifiedAlerting: true, - DisableAnonymous: true, - AppModeProduction: true, - EnableFeatureToggles: []string{"alertingConversionAPI"}, - }) + // Setup Grafana and its Database + dir, path := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{ + DisableLegacyAlerting: true, + EnableUnifiedAlerting: true, + DisableAnonymous: true, + AppModeProduction: true, + EnableFeatureToggles: []string{"alertingConversionAPI"}, + }) - grafanaListedAddr, env := testinfra.StartGrafanaEnv(t, dir, path) + grafanaListedAddr, env := testinfra.StartGrafanaEnv(t, dir, path) - // Create users to make authenticated requests - createUser(t, env.SQLStore, env.Cfg, user.CreateUserCommand{ - DefaultOrgRole: string(org.RoleAdmin), - Password: "password", - Login: "admin", - }) - apiClient := newAlertingApiClient(grafanaListedAddr, "admin", "password") + // Create users to make authenticated requests + createUser(t, env.SQLStore, env.Cfg, user.CreateUserCommand{ + DefaultOrgRole: string(org.RoleAdmin), + Password: "password", + Login: "admin", + }) + apiClient := newAlertingApiClient(grafanaListedAddr, "admin", "password") - ds := apiClient.CreateDatasource(t, datasources.DS_PROMETHEUS) + ds := apiClient.CreateDatasource(t, datasources.DS_PROMETHEUS) - namespace1 := "test-namespace-1" + namespace1 := "test-namespace-1" - namespace1UID := util.GenerateShortUID() - apiClient.CreateFolder(t, namespace1UID, namespace1) + namespace1UID := util.GenerateShortUID() + apiClient.CreateFolder(t, namespace1UID, namespace1) - t.Run("when pausing header is set, rules should be paused", func(t *testing.T) { - tests := []struct { - name string - recordingPaused bool - alertPaused bool - }{ - { - name: "do not pause rules", - recordingPaused: false, - alertPaused: false, - }, - { - name: "pause recording rules", - recordingPaused: true, - alertPaused: false, - }, - { - name: "pause alert rules", - recordingPaused: false, - alertPaused: true, - }, - { - name: "pause both recording and alert rules", - recordingPaused: true, - alertPaused: true, - }, - } + t.Run("when pausing header is set, rules should be paused", func(t *testing.T) { + tests := []struct { + name string + recordingPaused bool + alertPaused bool + }{ + { + name: "do not pause rules", + recordingPaused: false, + alertPaused: false, + }, + { + name: "pause recording rules", + recordingPaused: true, + alertPaused: false, + }, + { + name: "pause alert rules", + recordingPaused: false, + alertPaused: true, + }, + { + name: "pause both recording and alert rules", + recordingPaused: true, + alertPaused: true, + }, + } - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - headers := map[string]string{} - if tc.recordingPaused { - headers["X-Grafana-Alerting-Recording-Rules-Paused"] = "true" - } - if tc.alertPaused { - headers["X-Grafana-Alerting-Alert-Rules-Paused"] = "true" - } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + headers := map[string]string{} + if tc.recordingPaused { + headers["X-Grafana-Alerting-Recording-Rules-Paused"] = "true" + } + if tc.alertPaused { + headers["X-Grafana-Alerting-Alert-Rules-Paused"] = "true" + } - apiClient.ConvertPrometheusPostRuleGroup(t, namespace1, ds.Body.Datasource.UID, promGroup1, headers) + apiClient.ConvertPrometheusPostRuleGroup(t, namespace1, ds.Body.Datasource.UID, promGroup1, headers) - gr, _, _ := apiClient.GetRulesGroupWithStatus(t, namespace1UID, promGroup1.Name) + gr, _, _ := apiClient.GetRulesGroupWithStatus(t, namespace1UID, promGroup1.Name) - require.Len(t, gr.Rules, 3) + require.Len(t, gr.Rules, 3) - pausedRecordingRules := 0 - pausedAlertRules := 0 + pausedRecordingRules := 0 + pausedAlertRules := 0 - for _, rule := range gr.Rules { - if rule.GrafanaManagedAlert.IsPaused { - if rule.GrafanaManagedAlert.Record != nil { - pausedRecordingRules++ - } else { - pausedAlertRules++ + for _, rule := range gr.Rules { + if rule.GrafanaManagedAlert.IsPaused { + if rule.GrafanaManagedAlert.Record != nil { + pausedRecordingRules++ + } else { + pausedAlertRules++ + } } } - } - if tc.recordingPaused { - require.Equal(t, 1, pausedRecordingRules) - } else { - require.Equal(t, 0, pausedRecordingRules) - } + if tc.recordingPaused { + require.Equal(t, 1, pausedRecordingRules) + } else { + require.Equal(t, 0, pausedRecordingRules) + } - if tc.alertPaused { - require.Equal(t, 2, pausedAlertRules) - } else { - require.Equal(t, 0, pausedAlertRules) - } - }) - } + if tc.alertPaused { + require.Equal(t, 2, pausedAlertRules) + } else { + require.Equal(t, 0, pausedAlertRules) + } + }) + } + }) + } + + t.Run("with the mimirtool paths", func(t *testing.T) { + runTest(t, false) + }) + + t.Run("with the cortextool Loki paths", func(t *testing.T) { + runTest(t, true) }) } diff --git a/pkg/tests/api/alerting/testing.go b/pkg/tests/api/alerting/testing.go index 6290339dd24..5e1e34f63bf 100644 --- a/pkg/tests/api/alerting/testing.go +++ b/pkg/tests/api/alerting/testing.go @@ -254,7 +254,8 @@ func convertGettableGrafanaRuleToPostable(gettable *apimodels.GettableGrafanaRul } type apiClient struct { - url string + url string + prometheusConversionUseLokiPaths bool } type LegacyApiClient struct { @@ -1121,7 +1122,13 @@ func (a apiClient) ConvertPrometheusPostRuleGroup(t *testing.T, namespaceTitle, func (a apiClient) ConvertPrometheusGetRuleGroupRules(t *testing.T, namespaceTitle, groupName string) apimodels.PrometheusRuleGroup { t.Helper() - req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("%s/api/convert/prometheus/config/v1/rules/%s/%s", a.url, namespaceTitle, groupName), nil) + + path := "%s/api/convert/prometheus/config/v1/rules/%s/%s" + if a.prometheusConversionUseLokiPaths { + path = "%s/api/convert/api/prom/rules/%s/%s" + } + + req, err := http.NewRequest(http.MethodGet, fmt.Sprintf(path, a.url, namespaceTitle, groupName), nil) require.NoError(t, err) rule, status, raw := sendRequestYAML[apimodels.PrometheusRuleGroup](t, req, http.StatusOK) requireStatusCode(t, http.StatusOK, status, raw) @@ -1130,7 +1137,13 @@ func (a apiClient) ConvertPrometheusGetRuleGroupRules(t *testing.T, namespaceTit func (a apiClient) ConvertPrometheusGetNamespaceRules(t *testing.T, namespaceTitle string) map[string][]apimodels.PrometheusRuleGroup { t.Helper() - req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("%s/api/convert/prometheus/config/v1/rules/%s", a.url, namespaceTitle), nil) + + path := "%s/api/convert/prometheus/config/v1/rules/%s" + if a.prometheusConversionUseLokiPaths { + path = "%s/api/convert/api/prom/rules/%s" + } + + req, err := http.NewRequest(http.MethodGet, fmt.Sprintf(path, a.url, namespaceTitle), nil) require.NoError(t, err) ns, status, raw := sendRequestYAML[map[string][]apimodels.PrometheusRuleGroup](t, req, http.StatusOK) requireStatusCode(t, http.StatusOK, status, raw) @@ -1139,7 +1152,13 @@ func (a apiClient) ConvertPrometheusGetNamespaceRules(t *testing.T, namespaceTit func (a apiClient) ConvertPrometheusGetAllRules(t *testing.T) map[string][]apimodels.PrometheusRuleGroup { t.Helper() - req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("%s/api/convert/prometheus/config/v1/rules", a.url), nil) + + path := "%s/api/convert/prometheus/config/v1/rules" + if a.prometheusConversionUseLokiPaths { + path = "%s/api/convert/api/prom/rules" + } + + req, err := http.NewRequest(http.MethodGet, fmt.Sprintf(path, a.url), nil) require.NoError(t, err) result, status, raw := sendRequestYAML[map[string][]apimodels.PrometheusRuleGroup](t, req, http.StatusOK) requireStatusCode(t, http.StatusOK, status, raw) @@ -1148,7 +1167,13 @@ func (a apiClient) ConvertPrometheusGetAllRules(t *testing.T) map[string][]apimo func (a apiClient) ConvertPrometheusDeleteRuleGroup(t *testing.T, namespaceTitle, groupName string) { t.Helper() - req, err := http.NewRequest(http.MethodDelete, fmt.Sprintf("%s/api/convert/prometheus/config/v1/rules/%s/%s", a.url, namespaceTitle, groupName), nil) + + path := "%s/api/convert/prometheus/config/v1/rules/%s/%s" + if a.prometheusConversionUseLokiPaths { + path = "%s/api/convert/api/prom/rules/%s/%s" + } + + req, err := http.NewRequest(http.MethodDelete, fmt.Sprintf(path, a.url, namespaceTitle, groupName), nil) require.NoError(t, err) _, status, raw := sendRequestJSON[apimodels.ConvertPrometheusResponse](t, req, http.StatusAccepted) requireStatusCode(t, http.StatusAccepted, status, raw) @@ -1156,7 +1181,13 @@ func (a apiClient) ConvertPrometheusDeleteRuleGroup(t *testing.T, namespaceTitle func (a apiClient) ConvertPrometheusDeleteNamespace(t *testing.T, namespaceTitle string) { t.Helper() - req, err := http.NewRequest(http.MethodDelete, fmt.Sprintf("%s/api/convert/prometheus/config/v1/rules/%s", a.url, namespaceTitle), nil) + + path := "%s/api/convert/prometheus/config/v1/rules/%s" + if a.prometheusConversionUseLokiPaths { + path = "%s/api/convert/api/prom/rules/%s" + } + + req, err := http.NewRequest(http.MethodDelete, fmt.Sprintf(path, a.url, namespaceTitle), nil) require.NoError(t, err) _, status, raw := sendRequestJSON[apimodels.ConvertPrometheusResponse](t, req, http.StatusAccepted) requireStatusCode(t, http.StatusAccepted, status, raw) diff --git a/public/api-merged.json b/public/api-merged.json index dc455b6a36f..9ddc287f6f6 100644 --- a/public/api-merged.json +++ b/public/api-merged.json @@ -22771,6 +22771,7 @@ } }, "gettableAlerts": { + "description": "GettableAlerts gettable alerts", "type": "array", "items": { "type": "object", diff --git a/public/openapi3.json b/public/openapi3.json index fabc90f92cf..8f339a39b59 100644 --- a/public/openapi3.json +++ b/public/openapi3.json @@ -12838,6 +12838,7 @@ "type": "object" }, "gettableAlerts": { + "description": "GettableAlerts gettable alerts", "items": { "$ref": "#/components/schemas/gettableAlert" }, From d78c646f93bb140a0b7790074c16aa7d76aa4940 Mon Sep 17 00:00:00 2001 From: Matias Chomicki Date: Thu, 27 Feb 2025 16:34:02 +0000 Subject: [PATCH 30/32] New Logs Panel: Log line menu (#101060) * Create LogLineMenu component * Fine tune icon width * LogLineMenu: Add placeholder options * utils: create reusable handleOpenLogsContextClick * LogLineMenu: add callbacks to menu items * LogListContext: create component * LogList: use log list context to connect menu callbacks * LogLine: add pinned style * Remove unused imports * LogLine: add unit test * LogLine: add menu test case * LogLineMenu: add unit test * LogLineMessage: add unit test * LogListContext: add unit test * Remove unused code * Extract translations * Fix handleOpenLogsContextClick * Chore: memoize styles * Virtualization: update node used for underflow detection * Use useStyles2 instead of manually memoizing * Virtualization: export getter instead of variable * Open context: move stopPropagation to the old panel code * Logs: add new container class --- public/app/features/explore/Logs/Logs.tsx | 13 ++- .../logs/components/LogRowMenuCell.tsx | 32 +----- .../logs/components/__mocks__/logRow.ts | 17 ++- .../logs/components/panel/InfiniteScroll.tsx | 5 +- .../logs/components/panel/LogLine.test.tsx | 105 ++++++++++++++++++ .../logs/components/panel/LogLine.tsx | 28 ++++- .../components/panel/LogLineMenu.test.tsx | 94 ++++++++++++++++ .../logs/components/panel/LogLineMenu.tsx | 102 +++++++++++++++++ .../components/panel/LogLineMessage.test.tsx | 32 ++++++ .../logs/components/panel/LogList.tsx | 83 ++++++++------ .../components/panel/LogListContext.test.tsx | 46 ++++++++ .../logs/components/panel/LogListContext.tsx | 33 ++++++ .../logs/components/panel/processing.ts | 2 +- .../logs/components/panel/virtualization.ts | 7 +- public/app/features/logs/utils.ts | 34 ++++++ public/locales/en-US/grafana.json | 8 ++ public/locales/pseudo-LOCALE/grafana.json | 8 ++ 17 files changed, 575 insertions(+), 74 deletions(-) create mode 100644 public/app/features/logs/components/panel/LogLine.test.tsx create mode 100644 public/app/features/logs/components/panel/LogLineMenu.test.tsx create mode 100644 public/app/features/logs/components/panel/LogLineMenu.tsx create mode 100644 public/app/features/logs/components/panel/LogLineMessage.test.tsx create mode 100644 public/app/features/logs/components/panel/LogListContext.test.tsx create mode 100644 public/app/features/logs/components/panel/LogListContext.tsx diff --git a/public/app/features/explore/Logs/Logs.tsx b/public/app/features/explore/Logs/Logs.tsx index 24bc46790a5..f5c6912b76f 100644 --- a/public/app/features/explore/Logs/Logs.tsx +++ b/public/app/features/explore/Logs/Logs.tsx @@ -1065,17 +1065,25 @@ const UnthemedLogs: React.FunctionComponent = (props: Props) => { )} {visualisationType === 'logs' && hasData && config.featureToggles.newLogsPanel && ( <> -
+
{logsContainerRef.current && ( void; - onPinToContentOutlineClick?: (row: LogRowModel, onOpenContext: (row: LogRowModel) => void) => void; addonBefore?: ReactNode[]; addonAfter?: ReactNode[]; } @@ -66,31 +66,9 @@ export const LogRowMenuCell = memo( e.stopPropagation(); }, []); const onShowContextClick = useCallback( - async (event: MouseEvent) => { + async (event: MouseEvent) => { event.stopPropagation(); - // if ctrl or meta key is pressed, open query in new Explore tab - if ( - getRowContextQuery && - (event.nativeEvent.ctrlKey || event.nativeEvent.metaKey || event.nativeEvent.shiftKey) - ) { - const win = window.open('about:blank'); - // for this request we don't want to use the cached filters from a context provider, but always want to refetch and clear - const query = await getRowContextQuery(row, undefined, false); - if (query && win) { - const url = urlUtil.renderUrl(locationUtil.assureBaseUrl(`${getConfig().appSubUrl}explore`), { - left: JSON.stringify({ - datasource: query.datasource, - queries: [query], - range: getDefaultTimeRange(), - }), - }); - win.location = url; - - return; - } - win?.close(); - } - onOpenContext(row); + handleOpenLogsContextClick(event, row, getRowContextQuery, onOpenContext); }, [onOpenContext, getRowContextQuery, row] ); diff --git a/public/app/features/logs/components/__mocks__/logRow.ts b/public/app/features/logs/components/__mocks__/logRow.ts index 0968f242a84..d7777eed731 100644 --- a/public/app/features/logs/components/__mocks__/logRow.ts +++ b/public/app/features/logs/components/__mocks__/logRow.ts @@ -1,4 +1,6 @@ -import { FieldType, LogLevel, LogRowModel, toDataFrame } from '@grafana/data'; +import { FieldType, LogLevel, LogRowModel, LogsSortOrder, toDataFrame } from '@grafana/data'; + +import { LogListModel, preProcessLogs, PreProcessOptions } from '../panel/processing'; export const createLogRow = (overrides?: Partial): LogRowModel => { const uid = overrides?.uid || '1'; @@ -36,3 +38,16 @@ export const createLogRow = (overrides?: Partial): LogRowModel => { ...overrides, }; }; + +export const createLogLine = ( + overrides?: Partial, + processOptions: PreProcessOptions = { + escape: false, + order: LogsSortOrder.Descending, + timeZone: 'browser', + wrap: false, + } +): LogListModel => { + const logs = preProcessLogs([createLogRow(overrides)], processOptions); + return logs[0]; +}; diff --git a/public/app/features/logs/components/panel/InfiniteScroll.tsx b/public/app/features/logs/components/panel/InfiniteScroll.tsx index 3790f5f0b0f..e09d67577e4 100644 --- a/public/app/features/logs/components/panel/InfiniteScroll.tsx +++ b/public/app/features/logs/components/panel/InfiniteScroll.tsx @@ -4,7 +4,7 @@ import { ListChildComponentProps, ListOnItemsRenderedProps } from 'react-window' import { AbsoluteTimeRange, LogsSortOrder, TimeRange } from '@grafana/data'; import { config, reportInteraction } from '@grafana/runtime'; -import { Spinner, useTheme2 } from '@grafana/ui'; +import { Spinner, useStyles2 } from '@grafana/ui'; import { t } from 'app/core/internationalization'; import { canScrollBottom, getVisibleRange, ScrollDirection, shouldLoadMore } from '../InfiniteScroll'; @@ -59,8 +59,7 @@ export const InfiniteScroll = ({ const lastEvent = useRef(null); const countRef = useRef(0); const lastLogOfPage = useRef([]); - const theme = useTheme2(); - const styles = getStyles(theme); + const styles = useStyles2(getStyles); useEffect(() => { // Logs have not changed, ignore effect diff --git a/public/app/features/logs/components/panel/LogLine.test.tsx b/public/app/features/logs/components/panel/LogLine.test.tsx new file mode 100644 index 00000000000..15640f13ea8 --- /dev/null +++ b/public/app/features/logs/components/panel/LogLine.test.tsx @@ -0,0 +1,105 @@ +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; + +import { createTheme } from '@grafana/data'; + +import { LOG_LINE_BODY_FIELD_NAME } from '../LogDetailsBody'; +import { createLogLine } from '../__mocks__/logRow'; + +import { getStyles, LogLine } from './LogLine'; +import { LogListModel } from './processing'; + +const theme = createTheme(); +const styles = getStyles(theme); + +describe('LogLine', () => { + let log: LogListModel; + beforeEach(() => { + log = createLogLine({ labels: { place: 'luna' } }); + }); + + test('Renders a log line', () => { + render( + + ); + expect(screen.getByText(log.timestamp)).toBeInTheDocument(); + expect(screen.getByText(log.body)).toBeInTheDocument(); + }); + + test('Renders a log line with no timestamp', () => { + render( + + ); + expect(screen.queryByText(log.timestamp)).not.toBeInTheDocument(); + expect(screen.getByText(log.body)).toBeInTheDocument(); + }); + + test('Renders a log line with displayed fields', () => { + render( + + ); + expect(screen.getByText(log.timestamp)).toBeInTheDocument(); + expect(screen.queryByText(log.body)).not.toBeInTheDocument(); + expect(screen.getByText('luna')).toBeInTheDocument(); + }); + + test('Renders a log line with body displayed fields', () => { + render( + + ); + expect(screen.getByText(log.timestamp)).toBeInTheDocument(); + expect(screen.getByText(log.body)).toBeInTheDocument(); + expect(screen.getByText('luna')).toBeInTheDocument(); + }); + + describe('Log line menu', () => { + test('Renders a log line menu', async () => { + render( + + ); + expect(screen.queryByText('Copy log line')).not.toBeInTheDocument(); + await userEvent.click(screen.getByLabelText('Log menu')); + expect(screen.getByText('Copy log line')).toBeInTheDocument(); + }); + }); +}); diff --git a/public/app/features/logs/components/panel/LogLine.tsx b/public/app/features/logs/components/panel/LogLine.tsx index d484675d424..58004ccc456 100644 --- a/public/app/features/logs/components/panel/LogLine.tsx +++ b/public/app/features/logs/components/panel/LogLine.tsx @@ -1,12 +1,15 @@ import { css } from '@emotion/css'; import { CSSProperties, useEffect, useRef } from 'react'; +import tinycolor from 'tinycolor2'; import { GrafanaTheme2 } from '@grafana/data'; import { LOG_LINE_BODY_FIELD_NAME } from '../LogDetailsBody'; +import { LogLineMenu } from './LogLineMenu'; +import { useLogIsPinned } from './LogListContext'; import { LogFieldDimension, LogListModel } from './processing'; -import { FIELD_GAP_MULTIPLIER, hasUnderOrOverflow } from './virtualization'; +import { FIELD_GAP_MULTIPLIER, hasUnderOrOverflow, getLineHeight } from './virtualization'; interface Props { displayedFields: string[]; @@ -32,6 +35,7 @@ export const LogLine = ({ wrapLogMessage, }: Props) => { const logLineRef = useRef(null); + const pinned = useLogIsPinned(log); useEffect(() => { if (!onOverflow || !logLineRef.current) { @@ -45,7 +49,12 @@ export const LogLine = ({ }, [index, log.uid, onOverflow, style.height]); return ( -
+
+
@@ -57,7 +66,7 @@ interface LogProps { displayedFields: string[]; log: LogListModel; showTime: boolean; - styles: ReturnType; + styles: LogLineStyles; } const Log = ({ displayedFields, log, showTime, styles }: LogProps) => { @@ -67,7 +76,7 @@ const Log = ({ displayedFields, log, showTime, styles }: LogProps) => { {log.displayLevel} {displayedFields.length > 0 ? ( displayedFields.map((field) => ( - + {getDisplayedFieldValue(field, log)} )) @@ -111,6 +120,9 @@ export const getStyles = (theme: GrafanaTheme2) => { return { logLine: css({ color: theme.colors.text.primary, + display: 'flex', + gap: theme.spacing(0.5), + flexDirection: 'row', fontFamily: theme.typography.fontFamilyMonospace, fontSize: theme.typography.fontSize, wordBreak: 'break-all', @@ -129,6 +141,14 @@ export const getStyles = (theme: GrafanaTheme2) => { }, }, }), + pinnedLogLine: css({ + backgroundColor: tinycolor(theme.colors.info.transparent).setAlpha(0.25).toString(), + }), + menuIcon: css({ + height: getLineHeight(), + margin: 0, + padding: theme.spacing(0, 0, 0, 0.5), + }), logLineMessage: css({ fontFamily: theme.typography.fontFamily, textAlign: 'center', diff --git a/public/app/features/logs/components/panel/LogLineMenu.test.tsx b/public/app/features/logs/components/panel/LogLineMenu.test.tsx new file mode 100644 index 00000000000..27c8b78f9b9 --- /dev/null +++ b/public/app/features/logs/components/panel/LogLineMenu.test.tsx @@ -0,0 +1,94 @@ +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; + +import { createTheme } from '@grafana/data'; + +import { createLogLine } from '../__mocks__/logRow'; + +import { getStyles } from './LogLine'; +import { LogLineMenu } from './LogLineMenu'; +import { LogListContext } from './LogListContext'; +import { LogListModel } from './processing'; + +const theme = createTheme(); +const styles = getStyles(theme); + +describe('LogLineMenu', () => { + let log: LogListModel; + beforeEach(() => { + log = createLogLine({ labels: { place: 'luna' }, rowId: '1' }); + }); + + test('Renders the component', async () => { + render(); + expect(screen.queryByText('Copy log line')).not.toBeInTheDocument(); + await userEvent.click(screen.getByLabelText('Log menu')); + expect(screen.getByText('Copy log line')).toBeInTheDocument(); + }); + + describe('Options', () => { + test('Allows to copy a permalink', async () => { + const onPermalinkClick = jest.fn(); + render( + + + + ); + await userEvent.click(screen.getByLabelText('Log menu')); + await userEvent.click(screen.getByText('Copy link to log line')); + expect(onPermalinkClick).toHaveBeenCalledTimes(1); + }); + + test('Allows to open show context', async () => { + const onOpenContext = jest.fn(); + const logSupportsContext = jest.fn().mockReturnValue(true); + const getRowContextQuery = jest.fn(); + render( + + + + ); + await userEvent.click(screen.getByLabelText('Log menu')); + await userEvent.click(screen.getByText('Show context')); + expect(onOpenContext).toHaveBeenCalledTimes(1); + }); + + test('Uses logSupportsContext to control the display of show context', async () => { + const onOpenContext = jest.fn(); + const logSupportsContext = jest.fn().mockReturnValue(false); + const getRowContextQuery = jest.fn(); + render( + + + + ); + await userEvent.click(screen.getByLabelText('Log menu')); + expect(screen.queryByText('Show context')).not.toBeInTheDocument(); + }); + + test('Allows to pin log line', async () => { + const onPinLine = jest.fn(); + render( + + + + ); + await userEvent.click(screen.getByLabelText('Log menu')); + await userEvent.click(screen.getByText('Pin log')); + expect(onPinLine).toHaveBeenCalledTimes(1); + }); + + test('Allows to unpin log line', async () => { + const onUnpinLine = jest.fn(); + render( + + + + ); + await userEvent.click(screen.getByLabelText('Log menu')); + expect(screen.queryByText('Pin log')).not.toBeInTheDocument(); + await userEvent.click(screen.getByText('Unpin log')); + expect(onUnpinLine).toHaveBeenCalledTimes(1); + }); + }); +}); diff --git a/public/app/features/logs/components/panel/LogLineMenu.tsx b/public/app/features/logs/components/panel/LogLineMenu.tsx new file mode 100644 index 00000000000..b10c2f17ec6 --- /dev/null +++ b/public/app/features/logs/components/panel/LogLineMenu.tsx @@ -0,0 +1,102 @@ +import { useCallback, useMemo, useRef, MouseEvent } from 'react'; + +import { LogRowContextOptions, LogRowModel } from '@grafana/data'; +import { DataQuery } from '@grafana/schema'; +import { Dropdown, IconButton, Menu } from '@grafana/ui'; +import { t } from 'app/core/internationalization'; + +import { copyText, handleOpenLogsContextClick } from '../../utils'; + +import { LogLineStyles } from './LogLine'; +import { useLogIsPinned, useLogListContext } from './LogListContext'; +import { LogListModel } from './processing'; + +export type GetRowContextQueryFn = ( + row: LogRowModel, + options?: LogRowContextOptions, + cacheFilters?: boolean +) => Promise; + +interface Props { + log: LogListModel; + styles: LogLineStyles; +} + +export const LogLineMenu = ({ log, styles }: Props) => { + const { getRowContextQuery, onOpenContext, onPermalinkClick, onPinLine, onUnpinLine, logSupportsContext } = + useLogListContext(); + const pinned = useLogIsPinned(log); + const menuRef = useRef(null); + + const copyLogLine = useCallback(() => { + copyText(log.entry, menuRef); + }, [log.entry]); + + const copyLinkToLogLine = useCallback(() => { + onPermalinkClick?.(log); + }, [log, onPermalinkClick]); + + const shouldlogSupportsContext = useMemo( + () => (logSupportsContext ? logSupportsContext(log) : false), + [log, logSupportsContext] + ); + + const showContext = useCallback( + async (event: MouseEvent) => { + handleOpenLogsContextClick(event, log, getRowContextQuery, (log: LogRowModel) => onOpenContext?.(log, () => {})); + }, + [onOpenContext, getRowContextQuery, log] + ); + + const togglePinning = useCallback(() => { + if (pinned) { + onUnpinLine?.(log); + } else { + onPinLine?.(log); + } + }, [log, onPinLine, onUnpinLine, pinned]); + + const menu = useCallback( + () => ( + + + {onPermalinkClick && log.rowId !== undefined && log.uid && ( + + )} + {(shouldlogSupportsContext || onPinLine || onUnpinLine) && } + {shouldlogSupportsContext && ( + + )} + {!pinned && onPinLine && ( + + )} + {pinned && onUnpinLine && ( + + )} + + ), + [ + copyLinkToLogLine, + copyLogLine, + log.rowId, + log.uid, + onPermalinkClick, + onPinLine, + onUnpinLine, + pinned, + shouldlogSupportsContext, + showContext, + togglePinning, + ] + ); + + return ( + + + + ); +}; diff --git a/public/app/features/logs/components/panel/LogLineMessage.test.tsx b/public/app/features/logs/components/panel/LogLineMessage.test.tsx new file mode 100644 index 00000000000..0034dac14aa --- /dev/null +++ b/public/app/features/logs/components/panel/LogLineMessage.test.tsx @@ -0,0 +1,32 @@ +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; + +import { createTheme } from '@grafana/data'; + +import { getStyles } from './LogLine'; +import { LogLineMessage } from './LogLineMessage'; + +const theme = createTheme(); +const styles = getStyles(theme); + +describe('LogLineMessage', () => { + test('Renders a log line message', () => { + render( + + Message + + ); + expect(screen.getByText('Message')).toBeInTheDocument(); + }); + + test('Renders a button with the message', async () => { + const handleClick = jest.fn(); + render( + + Message + + ); + await userEvent.click(screen.getByText('Message')); + expect(handleClick).toHaveBeenCalledTimes(1); + }); +}); diff --git a/public/app/features/logs/components/panel/LogList.tsx b/public/app/features/logs/components/panel/LogList.tsx index abc48cbe9ce..ae8234bd060 100644 --- a/public/app/features/logs/components/panel/LogList.tsx +++ b/public/app/features/logs/components/panel/LogList.tsx @@ -14,10 +14,12 @@ import { LogsSortOrder, TimeRange, } from '@grafana/data'; -import { useTheme2 } from '@grafana/ui'; +import { PopoverContent, useTheme2 } from '@grafana/ui'; import { InfiniteScroll } from './InfiniteScroll'; import { getGridTemplateColumns } from './LogLine'; +import { GetRowContextQueryFn } from './LogLineMenu'; +import { LogListContext } from './LogListContext'; import { preProcessLogs, LogListModel, calculateFieldDimensions, LogFieldDimension } from './processing'; import { getLogLineSize, @@ -36,9 +38,17 @@ interface Props { eventBus: EventBus; forceEscape?: boolean; getFieldLinks?: GetFieldLinksFn; + getRowContextQuery?: GetRowContextQueryFn; initialScrollPosition?: 'top' | 'bottom'; loadMore?: (range: AbsoluteTimeRange) => void; logs: LogRowModel[]; + logSupportsContext?: (row: LogRowModel) => boolean; + onPermalinkClick?: (row: LogRowModel) => Promise; + onPinLine?: (row: LogRowModel) => void; + onOpenContext?: (row: LogRowModel, onClose: () => void) => void; + onUnpinLine?: (row: LogRowModel) => void; + pinLineButtonTooltipTitle?: PopoverContent; + pinnedLogs?: string[]; showTime: boolean; sortOrder: LogsSortOrder; timeRange: TimeRange; @@ -61,6 +71,7 @@ export const LogList = ({ timeRange, timeZone, wrapLogMessage, + ...logListContext }: Props) => { const [processedLogs, setProcessedLogs] = useState([]); const [listHeight, setListHeight] = useState( @@ -134,40 +145,42 @@ export const LogList = ({ } return ( - - {({ getItemKey, itemCount, onItemsRendered, Renderer }) => ( - - {Renderer} - - )} - + + + {({ getItemKey, itemCount, onItemsRendered, Renderer }) => ( + + {Renderer} + + )} + + ); }; diff --git a/public/app/features/logs/components/panel/LogListContext.test.tsx b/public/app/features/logs/components/panel/LogListContext.test.tsx new file mode 100644 index 00000000000..3505c554797 --- /dev/null +++ b/public/app/features/logs/components/panel/LogListContext.test.tsx @@ -0,0 +1,46 @@ +import { renderHook } from '@testing-library/react'; +import { ReactNode } from 'react'; + +import { createLogLine } from '../__mocks__/logRow'; + +import { useLogListContextData, useLogListContext, useLogIsPinned, LogListContext } from './LogListContext'; + +const log = createLogLine({ rowId: 'yep' }); +const value = { + getRowContextQuery: jest.fn(), + logSupportsContext: jest.fn(), + onPermalinkClick: jest.fn(), + onPinLine: jest.fn(), + onOpenContext: jest.fn(), + onUnpinLine: jest.fn(), + pinLineButtonTooltipTitle: 'test', + pinnedLogs: ['yep'], +}; +const wrapper = ({ children }: { children: ReactNode }) => ( + {children} +); + +test('Provides the Log List Context data', () => { + const { result } = renderHook(() => useLogListContext(), { wrapper }); + + expect(result.current).toEqual(value); +}); + +test('Allows to access context attributes', () => { + const { result } = renderHook(() => useLogListContextData('pinnedLogs'), { wrapper }); + + expect(result.current).toEqual(value.pinnedLogs); +}); + +test('Allows to tell if a log is pinned', () => { + const { result } = renderHook(() => useLogIsPinned(log), { wrapper }); + + expect(result.current).toBe(true); +}); + +test('Allows to tell if a log is pinned', () => { + const otherLog = createLogLine({ rowId: 'nope' }); + const { result } = renderHook(() => useLogIsPinned(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 new file mode 100644 index 00000000000..9f654cf4dce --- /dev/null +++ b/public/app/features/logs/components/panel/LogListContext.tsx @@ -0,0 +1,33 @@ +import { createContext, useContext } from 'react'; + +import { LogRowModel } from '@grafana/data'; +import { PopoverContent } from '@grafana/ui'; + +import { GetRowContextQueryFn } from './LogLineMenu'; + +export interface LogListContextData { + getRowContextQuery?: GetRowContextQueryFn; + logSupportsContext?: (row: LogRowModel) => boolean; + onPermalinkClick?: (row: LogRowModel) => Promise; + onPinLine?: (row: LogRowModel) => void; + onOpenContext?: (row: LogRowModel, onClose: () => void) => void; + onUnpinLine?: (row: LogRowModel) => void; + pinLineButtonTooltipTitle?: PopoverContent; + pinnedLogs?: string[]; +} + +export const LogListContext = createContext({}); + +export const useLogListContextData = (key: keyof LogListContextData) => { + const data: LogListContextData = useContext(LogListContext); + return data[key]; +}; + +export const useLogListContext = (): LogListContextData => { + return useContext(LogListContext); +}; + +export const useLogIsPinned = (log: LogRowModel) => { + const { pinnedLogs } = useContext(LogListContext); + return pinnedLogs?.some((logId) => logId === log.rowId); +}; diff --git a/public/app/features/logs/components/panel/processing.ts b/public/app/features/logs/components/panel/processing.ts index 2d62db31370..ef2c2c81b12 100644 --- a/public/app/features/logs/components/panel/processing.ts +++ b/public/app/features/logs/components/panel/processing.ts @@ -20,7 +20,7 @@ export interface LogFieldDimension { width: number; } -interface PreProcessOptions { +export interface PreProcessOptions { escape: boolean; getFieldLinks?: GetFieldLinksFn; order: LogsSortOrder; diff --git a/public/app/features/logs/components/panel/virtualization.ts b/public/app/features/logs/components/panel/virtualization.ts index a367b41c050..873e7555c58 100644 --- a/public/app/features/logs/components/panel/virtualization.ts +++ b/public/app/features/logs/components/panel/virtualization.ts @@ -8,10 +8,13 @@ let gridSize = 8; let paddingBottom = gridSize * 0.75; let lineHeight = 22; let measurementMode: 'canvas' | 'dom' = 'canvas'; +const iconWidth = 24; // Controls the space between fields in the log line, timestamp, level, displayed fields, and log line body export const FIELD_GAP_MULTIPLIER = 1.5; +export const getLineHeight = () => lineHeight; + export function init(theme: GrafanaTheme2) { const font = `${theme.typography.fontSize}px ${theme.typography.fontFamilyMonospace}`; const letterSpacing = theme.typography.body.letterSpacing; @@ -193,7 +196,7 @@ export function hasUnderOrOverflow(element: HTMLDivElement, calculatedHeight?: n if (element.scrollHeight > height) { return element.scrollHeight; } - const child = element.firstChild; + const child = element.children[1]; if (child instanceof HTMLDivElement && child.clientHeight < height) { return child.clientHeight; } @@ -203,7 +206,7 @@ export function hasUnderOrOverflow(element: HTMLDivElement, calculatedHeight?: n const scrollBarWidth = getScrollbarWidth(); export function getLogContainerWidth(container: HTMLDivElement) { - return container.clientWidth - scrollBarWidth; + return container.clientWidth - scrollBarWidth - iconWidth; } export function getScrollbarWidth() { diff --git a/public/app/features/logs/utils.ts b/public/app/features/logs/utils.ts index c4a19c0aa6c..b55e2f7a3a2 100644 --- a/public/app/features/logs/utils.ts +++ b/public/app/features/logs/utils.ts @@ -1,4 +1,5 @@ import { countBy, chain } from 'lodash'; +import { MouseEvent } from 'react'; import { LogLevel, @@ -15,9 +16,14 @@ import { LogsVolumeType, NumericLogLevel, getFieldDisplayName, + getDefaultTimeRange, + locationUtil, + urlUtil, } from '@grafana/data'; +import { getConfig } from 'app/core/config'; import { getDataframeFields } from './components/logParser'; +import { GetRowContextQueryFn } from './components/panel/LogLineMenu'; /** * Returns the log level of a log line. @@ -303,6 +309,34 @@ export const copyText = async (text: string, buttonRef: React.MutableRefObject, + row: LogRowModel, + getRowContextQuery: GetRowContextQueryFn | undefined, + onOpenContext: (row: LogRowModel) => void +) { + // if ctrl or meta key is pressed, open query in new Explore tab + if (getRowContextQuery && (event.nativeEvent.ctrlKey || event.nativeEvent.metaKey || event.nativeEvent.shiftKey)) { + const win = window.open('about:blank'); + // for this request we don't want to use the cached filters from a context provider, but always want to refetch and clear + const query = await getRowContextQuery(row, undefined, false); + if (query && win) { + const url = urlUtil.renderUrl(locationUtil.assureBaseUrl(`${getConfig().appSubUrl}explore`), { + left: JSON.stringify({ + datasource: query.datasource, + queries: [query], + range: getDefaultTimeRange(), + }), + }); + win.location = url; + + return; + } + win?.close(); + } + onOpenContext(row); +} + export function getLogLevelInfo(dataFrame: DataFrame, allDataFrames: DataFrame[]) { const fieldCache = new FieldCache(dataFrame); const timeField = fieldCache.getFirstFieldOfType(FieldType.time); diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index a064ce993a6..fe2fea8fc02 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -2257,6 +2257,14 @@ "log-line": "Log line", "no-details": "No details available" }, + "log-line-menu": { + "copy-link": "Copy link to log line", + "copy-log": "Copy log line", + "icon-label": "Log menu", + "pin-to-outline": "Pin log", + "show-context": "Show context", + "unpin-from-outline": "Unpin log" + }, "log-row-message": { "ellipsis": "… ", "more": "more", diff --git a/public/locales/pseudo-LOCALE/grafana.json b/public/locales/pseudo-LOCALE/grafana.json index 317a107f796..d6af162cbf2 100644 --- a/public/locales/pseudo-LOCALE/grafana.json +++ b/public/locales/pseudo-LOCALE/grafana.json @@ -2257,6 +2257,14 @@ "log-line": "Ŀőģ ľįʼnę", "no-details": "Ńő đęŧäįľş äväįľäþľę" }, + "log-line-menu": { + "copy-link": "Cőpy ľįʼnĸ ŧő ľőģ ľįʼnę", + "copy-log": "Cőpy ľőģ ľįʼnę", + "icon-label": "Ŀőģ męʼnū", + "pin-to-outline": "Pįʼn ľőģ", + "show-context": "Ŝĥőŵ čőʼnŧęχŧ", + "unpin-from-outline": "Ůʼnpįʼn ľőģ" + }, "log-row-message": { "ellipsis": "… ", "more": "mőřę", From bc4be187afc29594887a0a6ba75e599fef40a2e4 Mon Sep 17 00:00:00 2001 From: Moustafa Baiou Date: Thu, 27 Feb 2025 15:29:51 -0500 Subject: [PATCH 31/32] Alerting: Fix evaluation of rules with no-op math expressions When you use a math expression with out any operators, the dataFrame pointer is identical between the expression result and the input query/expression. This was resulting in the values returned from an evaluation overshadowing each other, depending on the order of the processing of the result map. For example: ``` A: some_metric B: reduce of A C: math expression -> "${B}" D: Threshold evaluation of C -> "C > 0" ``` With a value of 1 for `some_metric`, might result in a evaluation result of one of the following (somewhat at random): 1. { B: 1, D: 1 } 2. { C: 1, D: 1} While you would expect to see: { B: 1, C: 1, D: 1 } --- pkg/services/ngalert/eval/eval.go | 2 +- pkg/services/ngalert/eval/eval_test.go | 513 ++++++++++++++----------- 2 files changed, 297 insertions(+), 218 deletions(-) diff --git a/pkg/services/ngalert/eval/eval.go b/pkg/services/ngalert/eval/eval.go index 683ce31bb1a..f5e968fda0b 100644 --- a/pkg/services/ngalert/eval/eval.go +++ b/pkg/services/ngalert/eval/eval.go @@ -501,7 +501,7 @@ func queryDataResponseToExecutionResults(c models.Condition, execResp *backend.Q if frame.Fields[0].Len() == 1 { v = frame.At(0, 0).(*float64) // type checked above } - captureFn(frame.RefID, frame.Fields[0].Labels, v) + captureFn(refID, frame.Fields[0].Labels, v) } if refID == c.Condition { diff --git a/pkg/services/ngalert/eval/eval_test.go b/pkg/services/ngalert/eval/eval_test.go index 6710737891b..bde445c732c 100644 --- a/pkg/services/ngalert/eval/eval_test.go +++ b/pkg/services/ngalert/eval/eval_test.go @@ -739,243 +739,322 @@ func TestEvaluate(t *testing.T) { resp backend.QueryDataResponse expected Results error string - }{{ - name: "is no data with no frames", - cond: models.Condition{ - Data: []models.AlertQuery{{ - RefID: "A", - DatasourceUID: "test", - }, { - RefID: "B", - DatasourceUID: expr.DatasourceUID, - }, { - RefID: "C", - DatasourceUID: expr.OldDatasourceUID, - }, { - RefID: "D", - DatasourceUID: expr.MLDatasourceUID, + }{ + { + name: "is no data with no frames", + cond: models.Condition{ + Data: []models.AlertQuery{{ + RefID: "A", + DatasourceUID: "test", + }, { + RefID: "B", + DatasourceUID: expr.DatasourceUID, + }, { + RefID: "C", + DatasourceUID: expr.OldDatasourceUID, + }, { + RefID: "D", + DatasourceUID: expr.MLDatasourceUID, + }}, + }, + resp: backend.QueryDataResponse{ + Responses: backend.Responses{ + "A": {Frames: nil}, + "B": {Frames: []*data.Frame{{Fields: nil}}}, + "C": {Frames: nil}, + "D": {Frames: []*data.Frame{{Fields: nil}}}, + }, + }, + expected: Results{{ + State: NoData, + Instance: data.Labels{ + "datasource_uid": "test", + "ref_id": "A", + }, }}, }, - resp: backend.QueryDataResponse{ - Responses: backend.Responses{ - "A": {Frames: nil}, - "B": {Frames: []*data.Frame{{Fields: nil}}}, - "C": {Frames: nil}, - "D": {Frames: []*data.Frame{{Fields: nil}}}, + { + name: "is no data for one frame with no fields", + cond: models.Condition{ + Data: []models.AlertQuery{{ + RefID: "A", + DatasourceUID: "test", + }}, }, - }, - expected: Results{{ - State: NoData, - Instance: data.Labels{ - "datasource_uid": "test", - "ref_id": "A", + resp: backend.QueryDataResponse{ + Responses: backend.Responses{ + "A": {Frames: []*data.Frame{{Fields: nil}}}, + }, }, - }}, - }, { - name: "is no data for one frame with no fields", - cond: models.Condition{ - Data: []models.AlertQuery{{ - RefID: "A", - DatasourceUID: "test", + expected: Results{{ + State: NoData, + Instance: data.Labels{ + "datasource_uid": "test", + "ref_id": "A", + }, }}, }, - resp: backend.QueryDataResponse{ - Responses: backend.Responses{ - "A": {Frames: []*data.Frame{{Fields: nil}}}, + { + name: "results contains captured values for exact label matches", + cond: models.Condition{ + Condition: "B", }, + resp: backend.QueryDataResponse{ + Responses: backend.Responses{ + "A": { + Frames: []*data.Frame{{ + RefID: "A", + Fields: []*data.Field{ + data.NewField( + "Value", + data.Labels{"foo": "bar"}, + []*float64{util.Pointer(10.0)}, + ), + }, + }}, + }, + "B": { + Frames: []*data.Frame{{ + RefID: "B", + Fields: []*data.Field{ + data.NewField( + "Value", + data.Labels{"foo": "bar"}, + []*float64{util.Pointer(1.0)}, + ), + }, + }}, + }, + }, + }, + expected: Results{{ + State: Alerting, + Instance: data.Labels{ + "foo": "bar", + }, + Values: map[string]NumberValueCapture{ + "A": { + Var: "A", + Labels: data.Labels{"foo": "bar"}, + Value: util.Pointer(10.0), + }, + "B": { + Var: "B", + Labels: data.Labels{"foo": "bar"}, + Value: util.Pointer(1.0), + }, + }, + EvaluationString: "[ var='A' labels={foo=bar} value=10 ], [ var='B' labels={foo=bar} value=1 ]", + }}, }, - expected: Results{{ - State: NoData, - Instance: data.Labels{ - "datasource_uid": "test", - "ref_id": "A", + { + name: "results contains captured values for subset of labels", + cond: models.Condition{ + Condition: "B", }, - }}, - }, { - name: "results contains captured values for exact label matches", - cond: models.Condition{ - Condition: "B", + resp: backend.QueryDataResponse{ + Responses: backend.Responses{ + "A": { + Frames: []*data.Frame{{ + RefID: "A", + Fields: []*data.Field{ + data.NewField( + "Value", + data.Labels{"foo": "bar"}, + []*float64{util.Pointer(10.0)}, + ), + }, + }}, + }, + "B": { + Frames: []*data.Frame{{ + RefID: "B", + Fields: []*data.Field{ + data.NewField( + "Value", + data.Labels{"foo": "bar", "bar": "baz"}, + []*float64{util.Pointer(1.0)}, + ), + }, + }}, + }, + }, + }, + expected: Results{{ + State: Alerting, + Instance: data.Labels{ + "foo": "bar", + "bar": "baz", + }, + Values: map[string]NumberValueCapture{ + "A": { + Var: "A", + Labels: data.Labels{"foo": "bar"}, + Value: util.Pointer(10.0), + }, + "B": { + Var: "B", + Labels: data.Labels{"foo": "bar", "bar": "baz"}, + Value: util.Pointer(1.0), + }, + }, + EvaluationString: "[ var='A' labels={foo=bar} value=10 ], [ var='B' labels={bar=baz, foo=bar} value=1 ]", + }}, }, - resp: backend.QueryDataResponse{ - Responses: backend.Responses{ - "A": { - Frames: []*data.Frame{{ - RefID: "A", - Fields: []*data.Field{ - data.NewField( - "Value", - data.Labels{"foo": "bar"}, - []*float64{util.Pointer(10.0)}, - ), - }, - }}, - }, - "B": { - Frames: []*data.Frame{{ - RefID: "B", - Fields: []*data.Field{ - data.NewField( - "Value", - data.Labels{"foo": "bar"}, - []*float64{util.Pointer(1.0)}, - ), - }, - }}, + { + name: "results contains error if condition frame has error", + cond: models.Condition{ + Condition: "B", + }, + resp: backend.QueryDataResponse{ + Responses: backend.Responses{ + "A": { + Frames: []*data.Frame{{ + RefID: "A", + Fields: []*data.Field{ + data.NewField( + "Value", + data.Labels{"foo": "bar"}, + []*float64{util.Pointer(10.0)}, + ), + }, + }}, + }, + "B": { + Frames: []*data.Frame{{ + RefID: "B", + Fields: []*data.Field{ + data.NewField( + "Value", + data.Labels{"foo": "bar", "bar": "baz"}, + []*float64{util.Pointer(1.0)}, + ), + }, + }}, + Error: errors.New("some frame error"), + }, }, }, + expected: Results{{ + State: Error, + Error: errors.New("some frame error"), + EvaluationString: "", + }}, }, - expected: Results{{ - State: Alerting, - Instance: data.Labels{ - "foo": "bar", + { + name: "results contain underlying error if condition frame has error that depends on another node", + cond: models.Condition{ + Condition: "B", }, - Values: map[string]NumberValueCapture{ - "A": { - Var: "A", - Labels: data.Labels{"foo": "bar"}, - Value: util.Pointer(10.0), - }, - "B": { - Var: "B", - Labels: data.Labels{"foo": "bar"}, - Value: util.Pointer(1.0), + resp: backend.QueryDataResponse{ + Responses: backend.Responses{ + "A": { + Frames: []*data.Frame{{ + RefID: "A", + Fields: []*data.Field{ + data.NewField( + "Value", + data.Labels{"foo": "bar"}, + []*float64{util.Pointer(10.0)}, + ), + }, + }}, + Error: errors.New("another error depends on me"), + }, + "B": { + Frames: []*data.Frame{{ + RefID: "B", + Fields: []*data.Field{ + data.NewField( + "Value", + data.Labels{"foo": "bar", "bar": "baz"}, + []*float64{util.Pointer(1.0)}, + ), + }, + }}, + Error: expr.MakeDependencyError("B", "A"), + }, }, }, - EvaluationString: "[ var='A' labels={foo=bar} value=10 ], [ var='B' labels={foo=bar} value=1 ]", - }}, - }, { - name: "results contains captured values for subset of labels", - cond: models.Condition{ - Condition: "B", + expected: Results{{ + State: Error, + Error: errors.New("another error depends on me"), + EvaluationString: "", + }}, }, - resp: backend.QueryDataResponse{ - Responses: backend.Responses{ - "A": { - Frames: []*data.Frame{{ - RefID: "A", - Fields: []*data.Field{ - data.NewField( - "Value", - data.Labels{"foo": "bar"}, - []*float64{util.Pointer(10.0)}, - ), - }, - }}, - }, - "B": { - Frames: []*data.Frame{{ - RefID: "B", - Fields: []*data.Field{ - data.NewField( - "Value", - data.Labels{"foo": "bar", "bar": "baz"}, - []*float64{util.Pointer(1.0)}, - ), - }, - }}, + { + name: "result values for all refIDs when a math no-op expression is used and two results share the same frame pointer", + cond: models.Condition{ + Condition: "C", + }, + resp: backend.QueryDataResponse{ + Responses: backend.Responses{ + "A": { + Frames: []*data.Frame{{ + RefID: "A", + Fields: []*data.Field{ + data.NewField( + "Value", + data.Labels{"foo": "bar"}, + []*float64{util.Pointer(10.0)}, + ), + }, + }}, + }, + "B": { + // in a math no-op expression the frame for B is the same as A + // e.g. A = some_query, B = `${A}` + Frames: []*data.Frame{{ + RefID: "A", + Fields: []*data.Field{ + data.NewField( + "Value", + data.Labels{"foo": "bar"}, + []*float64{util.Pointer(10.0)}, + ), + }, + }}, + }, + "C": { + Frames: []*data.Frame{{ + RefID: "C", + Fields: []*data.Field{ + data.NewField( + "Value", + data.Labels{"foo": "bar"}, + []*float64{util.Pointer(1.0)}, + ), + }, + }}, + }, }, }, + expected: Results{{ + State: Alerting, + Instance: data.Labels{ + "foo": "bar", + }, + Values: map[string]NumberValueCapture{ + "A": { + Var: "A", + Labels: data.Labels{"foo": "bar"}, + Value: util.Pointer(10.0), + }, + "B": { + Var: "B", + Labels: data.Labels{"foo": "bar"}, + Value: util.Pointer(10.0), + }, + "C": { + Var: "C", + Labels: data.Labels{"foo": "bar"}, + Value: util.Pointer(1.0), + }, + }, + EvaluationString: "[ var='A' labels={foo=bar} value=10 ], [ var='B' labels={foo=bar} value=10 ], [ var='C' labels={foo=bar} value=1 ]", + }}, }, - expected: Results{{ - State: Alerting, - Instance: data.Labels{ - "foo": "bar", - "bar": "baz", - }, - Values: map[string]NumberValueCapture{ - "A": { - Var: "A", - Labels: data.Labels{"foo": "bar"}, - Value: util.Pointer(10.0), - }, - "B": { - Var: "B", - Labels: data.Labels{"foo": "bar", "bar": "baz"}, - Value: util.Pointer(1.0), - }, - }, - EvaluationString: "[ var='A' labels={foo=bar} value=10 ], [ var='B' labels={bar=baz, foo=bar} value=1 ]", - }}, - }, { - name: "results contains error if condition frame has error", - cond: models.Condition{ - Condition: "B", - }, - resp: backend.QueryDataResponse{ - Responses: backend.Responses{ - "A": { - Frames: []*data.Frame{{ - RefID: "A", - Fields: []*data.Field{ - data.NewField( - "Value", - data.Labels{"foo": "bar"}, - []*float64{util.Pointer(10.0)}, - ), - }, - }}, - }, - "B": { - Frames: []*data.Frame{{ - RefID: "B", - Fields: []*data.Field{ - data.NewField( - "Value", - data.Labels{"foo": "bar", "bar": "baz"}, - []*float64{util.Pointer(1.0)}, - ), - }, - }}, - Error: errors.New("some frame error"), - }, - }, - }, - expected: Results{{ - State: Error, - Error: errors.New("some frame error"), - EvaluationString: "", - }}, - }, { - name: "results contain underlying error if condition frame has error that depends on another node", - cond: models.Condition{ - Condition: "B", - }, - resp: backend.QueryDataResponse{ - Responses: backend.Responses{ - "A": { - Frames: []*data.Frame{{ - RefID: "A", - Fields: []*data.Field{ - data.NewField( - "Value", - data.Labels{"foo": "bar"}, - []*float64{util.Pointer(10.0)}, - ), - }, - }}, - Error: errors.New("another error depends on me"), - }, - "B": { - Frames: []*data.Frame{{ - RefID: "B", - Fields: []*data.Field{ - data.NewField( - "Value", - data.Labels{"foo": "bar", "bar": "baz"}, - []*float64{util.Pointer(1.0)}, - ), - }, - }}, - Error: expr.MakeDependencyError("B", "A"), - }, - }, - }, - expected: Results{{ - State: Error, - Error: errors.New("another error depends on me"), - EvaluationString: "", - }}, - }} + } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { From 1d2f271c9550a07fac68b7f176c06e48ed2f0b7d Mon Sep 17 00:00:00 2001 From: aishyandapalli Date: Thu, 27 Feb 2025 15:24:35 -0800 Subject: [PATCH 32/32] VizTooltip: Pass `maxHeight` setting to Exemplar tooltips (#100478) Co-authored-by: Kristina Durivage Co-authored-by: Leon Sorokin --- .../visualization/data-hover/DataHoverView.tsx | 14 ++++++++++++-- .../visualization/data-hover/ExemplarHoverView.tsx | 9 ++++++--- .../app/plugins/panel/heatmap/HeatmapTooltip.tsx | 1 + public/app/plugins/panel/heatmap/module.tsx | 8 ++++++-- .../plugins/panel/timeseries/TimeSeriesPanel.tsx | 1 + .../panel/timeseries/plugins/ExemplarMarker.tsx | 5 ++++- .../panel/timeseries/plugins/ExemplarsPlugin.tsx | 6 ++++-- 7 files changed, 34 insertions(+), 10 deletions(-) diff --git a/public/app/features/visualization/data-hover/DataHoverView.tsx b/public/app/features/visualization/data-hover/DataHoverView.tsx index adc593a9a14..79785c94eb2 100644 --- a/public/app/features/visualization/data-hover/DataHoverView.tsx +++ b/public/app/features/visualization/data-hover/DataHoverView.tsx @@ -23,6 +23,7 @@ export interface Props { mode?: TooltipDisplayMode | null; header?: string; padding?: number; + maxHeight?: number; } export interface DisplayValue { @@ -92,7 +93,16 @@ export function getDisplayValuesAndLinks( return { displayValues, links }; } -export const DataHoverView = ({ data, rowIndex, columnIndex, sortOrder, mode, header, padding = 0 }: Props) => { +export const DataHoverView = ({ + data, + rowIndex, + columnIndex, + sortOrder, + mode, + header, + padding = 0, + maxHeight, +}: Props) => { const styles = useStyles2(getStyles, padding); if (!data || rowIndex == null) { @@ -108,7 +118,7 @@ export const DataHoverView = ({ data, rowIndex, columnIndex, sortOrder, mode, he const { displayValues, links } = dispValuesAndLinks; if (header === 'Exemplar') { - return ; + return ; } return ( diff --git a/public/app/features/visualization/data-hover/ExemplarHoverView.tsx b/public/app/features/visualization/data-hover/ExemplarHoverView.tsx index b353b08abba..b2f17d17333 100644 --- a/public/app/features/visualization/data-hover/ExemplarHoverView.tsx +++ b/public/app/features/visualization/data-hover/ExemplarHoverView.tsx @@ -11,10 +11,11 @@ export interface Props { displayValues: DisplayValue[]; links?: LinkModel[]; header?: string; + maxHeight?: number; } -export const ExemplarHoverView = ({ displayValues, links, header = 'Exemplar' }: Props) => { - const styles = useStyles2(getStyles); +export const ExemplarHoverView = ({ displayValues, links, header = 'Exemplar', maxHeight }: Props) => { + const styles = useStyles2(getStyles, 0, maxHeight); const time = displayValues.find((val) => val.name === 'Time'); displayValues = displayValues.filter((val) => val.name !== 'Time'); // time? @@ -49,7 +50,7 @@ export const ExemplarHoverView = ({ displayValues, links, header = 'Exemplar' }: ); }; -const getStyles = (theme: GrafanaTheme2, padding = 0) => { +const getStyles = (theme: GrafanaTheme2, padding = 0, maxHeight?: number) => { return { exemplarWrapper: css({ display: 'flex', @@ -79,6 +80,8 @@ const getStyles = (theme: GrafanaTheme2, padding = 0) => { gap: 4, borderTop: `1px solid ${theme.colors.border.medium}`, padding: theme.spacing(1), + overflowY: 'auto', + maxHeight: maxHeight, }), exemplarFooter: css({ display: 'flex', diff --git a/public/app/plugins/panel/heatmap/HeatmapTooltip.tsx b/public/app/plugins/panel/heatmap/HeatmapTooltip.tsx index 896dbeb1feb..d15b13cd50c 100644 --- a/public/app/plugins/panel/heatmap/HeatmapTooltip.tsx +++ b/public/app/plugins/panel/heatmap/HeatmapTooltip.tsx @@ -56,6 +56,7 @@ export const HeatmapTooltip = (props: HeatmapTooltipProps) => { rowIndex={props.dataIdxs[2]} header={'Exemplar'} padding={8} + maxHeight={props.maxHeight} /> ); } diff --git a/public/app/plugins/panel/heatmap/module.tsx b/public/app/plugins/panel/heatmap/module.tsx index 6ec45e27d9a..ff3dfb38214 100644 --- a/public/app/plugins/panel/heatmap/module.tsx +++ b/public/app/plugins/panel/heatmap/module.tsx @@ -1,4 +1,4 @@ -import { FieldConfigProperty, FieldType, identityOverrideProcessor, PanelPlugin } from '@grafana/data'; +import { DataFrame, FieldConfigProperty, FieldType, identityOverrideProcessor, PanelPlugin } from '@grafana/data'; import { config } from '@grafana/runtime'; import { AxisPlacement, @@ -442,7 +442,9 @@ export const plugin = new PanelPlugin(HeatmapPanel) settings: { integer: true, }, - showIf: (options) => options.tooltip?.mode === TooltipDisplayMode.Multi, + showIf: (options: Options, data: DataFrame[] | undefined, annotations: DataFrame[] | undefined) => + options.tooltip?.mode === TooltipDisplayMode.Multi || + annotations?.some((df) => df.meta?.custom?.resultType === 'exemplar'), }); category = ['Legend']; @@ -459,6 +461,8 @@ export const plugin = new PanelPlugin(HeatmapPanel) name: 'Color', defaultValue: defaultOptions.exemplars.color, category, + showIf: (options: Options, data: DataFrame[] | undefined, annotations: DataFrame[] | undefined) => + annotations?.some((df) => df.meta?.custom?.resultType === 'exemplar'), }); }) .setSuggestionsSupplier(new HeatmapSuggestionsSupplier()) diff --git a/public/app/plugins/panel/timeseries/TimeSeriesPanel.tsx b/public/app/plugins/panel/timeseries/TimeSeriesPanel.tsx index f8866b705f3..e4124886ce2 100644 --- a/public/app/plugins/panel/timeseries/TimeSeriesPanel.tsx +++ b/public/app/plugins/panel/timeseries/TimeSeriesPanel.tsx @@ -159,6 +159,7 @@ export const TimeSeriesPanel = ({ config={uplotConfig} exemplars={data.annotations} timeZone={timeZone} + maxHeight={options.tooltip.maxHeight} /> )} {((canEditThresholds && onThresholdsChange) || showThresholds) && ( diff --git a/public/app/plugins/panel/timeseries/plugins/ExemplarMarker.tsx b/public/app/plugins/panel/timeseries/plugins/ExemplarMarker.tsx index 54e79e06743..f41e5e3af7a 100644 --- a/public/app/plugins/panel/timeseries/plugins/ExemplarMarker.tsx +++ b/public/app/plugins/panel/timeseries/plugins/ExemplarMarker.tsx @@ -29,6 +29,7 @@ interface ExemplarMarkerProps { exemplarColor?: string; clickedExemplarFieldIndex: DataFrameFieldIndex | undefined; setClickedExemplarFieldIndex: React.Dispatch; + maxHeight?: number; } export const ExemplarMarker = ({ @@ -39,6 +40,7 @@ export const ExemplarMarker = ({ exemplarColor, clickedExemplarFieldIndex, setClickedExemplarFieldIndex, + maxHeight, }: ExemplarMarkerProps) => { const styles = useStyles2(getExemplarMarkerStyles); const [isOpen, setIsOpen] = useState(false); @@ -163,7 +165,7 @@ export const ExemplarMarker = ({ return (
{isLocked && } - +
); }, [ @@ -175,6 +177,7 @@ export const ExemplarMarker = ({ floatingStyles, getFloatingProps, refs.setFloating, + maxHeight, ]); const seriesColor = config diff --git a/public/app/plugins/panel/timeseries/plugins/ExemplarsPlugin.tsx b/public/app/plugins/panel/timeseries/plugins/ExemplarsPlugin.tsx index c741f821f88..673f2288dc7 100644 --- a/public/app/plugins/panel/timeseries/plugins/ExemplarsPlugin.tsx +++ b/public/app/plugins/panel/timeseries/plugins/ExemplarsPlugin.tsx @@ -18,9 +18,10 @@ interface ExemplarsPluginProps { exemplars: DataFrame[]; timeZone: TimeZone; visibleSeries?: VisibleExemplarLabels; + maxHeight?: number; } -export const ExemplarsPlugin = ({ exemplars, timeZone, config, visibleSeries }: ExemplarsPluginProps) => { +export const ExemplarsPlugin = ({ exemplars, timeZone, config, visibleSeries, maxHeight }: ExemplarsPluginProps) => { const plotInstance = useRef(); const [lockedExemplarFieldIndex, setLockedExemplarFieldIndex] = useState(); @@ -83,10 +84,11 @@ export const ExemplarsPlugin = ({ exemplars, timeZone, config, visibleSeries }: dataFrameFieldIndex={dataFrameFieldIndex} config={config} exemplarColor={markerColor} + maxHeight={maxHeight} /> ); }, - [config, timeZone, visibleSeries, setLockedExemplarFieldIndex, lockedExemplarFieldIndex] + [config, timeZone, visibleSeries, setLockedExemplarFieldIndex, lockedExemplarFieldIndex, maxHeight] ); return (