From 3badf73b45037b46ef6fdbd3382b9bf940735883 Mon Sep 17 00:00:00 2001 From: Tom Ratcliffe Date: Tue, 18 Jun 2024 14:24:23 +0100 Subject: [PATCH 01/13] Alerting: Fix setting of existing Telegram Chat ID value (#89287) --- .../alerting/unified/Receivers.test.tsx | 65 +++++++++++++------ .../provisioned/config/api/v1/alerts.json | 26 ++++++++ .../components/settings/__mocks__/server.ts | 17 ++++- .../cloud-alertmanager-notifier-types.ts | 2 +- 4 files changed, 85 insertions(+), 25 deletions(-) create mode 100644 public/app/features/alerting/unified/components/settings/__mocks__/api/alertmanager/provisioned/config/api/v1/alerts.json diff --git a/public/app/features/alerting/unified/Receivers.test.tsx b/public/app/features/alerting/unified/Receivers.test.tsx index ac966805830..982b4488eec 100644 --- a/public/app/features/alerting/unified/Receivers.test.tsx +++ b/public/app/features/alerting/unified/Receivers.test.tsx @@ -3,14 +3,13 @@ import { selectOptionInTest } from 'test/helpers/selectOptionInTest'; import { render, screen, waitFor, userEvent } from 'test/test-utils'; import { - EXTERNAL_VANILLA_ALERTMANAGER_UID, + PROVISIONED_MIMIR_ALERTMANAGER_UID, + mockDataSources, setupVanillaAlertmanagerServer, } from 'app/features/alerting/unified/components/settings/__mocks__/server'; import { setupMswServer } from 'app/features/alerting/unified/mockApi'; -import { grantUserPermissions, mockDataSource } from 'app/features/alerting/unified/mocks'; +import { grantUserPermissions } from 'app/features/alerting/unified/mocks'; import { setupDataSources } from 'app/features/alerting/unified/testSetup/datasources'; -import { DataSourceType } from 'app/features/alerting/unified/utils/datasource'; -import { AlertManagerDataSourceJsonData, AlertManagerImplementation } from 'app/plugins/datasource/alertmanager/types'; import { AccessControlAction } from 'app/types'; import ContactPoints from './Receivers'; @@ -19,15 +18,15 @@ import 'core-js/stable/structured-clone'; const server = setupMswServer(); -const mockDataSources = { - [EXTERNAL_VANILLA_ALERTMANAGER_UID]: mockDataSource({ - uid: EXTERNAL_VANILLA_ALERTMANAGER_UID, - name: EXTERNAL_VANILLA_ALERTMANAGER_UID, - type: DataSourceType.Alertmanager, - jsonData: { - implementation: AlertManagerImplementation.prometheus, - }, - }), +const assertSaveWasSuccessful = async () => { + // TODO: Have a better way to assert that the contact point was saved. This is instead asserting on some + // text that's present on the list page, as there's a lot of overlap in text between the form and the list page + return waitFor(() => expect(screen.getByText(/search by name or type/i)).toBeInTheDocument(), { timeout: 2000 }); +}; + +const saveContactPoint = async () => { + const user = userEvent.setup(); + return user.click(await screen.findByRole('button', { name: /save contact point/i })); }; beforeEach(() => { @@ -37,17 +36,22 @@ beforeEach(() => { AccessControlAction.AlertingNotificationsExternalRead, AccessControlAction.AlertingNotificationsExternalWrite, ]); + + setupVanillaAlertmanagerServer(server); + setupDataSources(mockDataSources[PROVISIONED_MIMIR_ALERTMANAGER_UID]); }); it('can save a contact point with a select dropdown', async () => { - setupVanillaAlertmanagerServer(server); - setupDataSources(mockDataSources[EXTERNAL_VANILLA_ALERTMANAGER_UID]); - const user = userEvent.setup(); render(, { historyOptions: { - initialEntries: [`/alerting/notifications/receivers/new?alertmanager=${EXTERNAL_VANILLA_ALERTMANAGER_UID}`], + initialEntries: [ + { + pathname: `/alerting/notifications/receivers/new`, + search: `?alertmanager=${PROVISIONED_MIMIR_ALERTMANAGER_UID}`, + }, + ], }, }); @@ -66,9 +70,28 @@ it('can save a contact point with a select dropdown', async () => { await user.type(botToken, 'sometoken'); await user.type(chatId, '-123'); - await user.click(await screen.findByRole('button', { name: /save contact point/i })); + await saveContactPoint(); - // TODO: Have a better way to assert that the contact point was saved. This is instead asserting on some - // text that's present on the list page, as there's a lot of overlap in text between the form and the list page - await waitFor(() => expect(screen.getByText(/search by name or type/i)).toBeInTheDocument(), { timeout: 2000 }); + await assertSaveWasSuccessful(); +}); + +it('can save existing Telegram contact point', async () => { + render(, { + historyOptions: { + initialEntries: [ + { + pathname: `/alerting/notifications/receivers/Telegram/edit`, + search: `?alertmanager=${PROVISIONED_MIMIR_ALERTMANAGER_UID}`, + }, + ], + }, + }); + + // Here, we're implicitly testing that our parsing of an existing Telegram integration works correctly + // Our mock server will reject a request if we've sent the Chat ID as `0`, + // so opening and trying to save an existing Telegram integration should + // trigger this error if it regresses + await saveContactPoint(); + + await assertSaveWasSuccessful(); }); diff --git a/public/app/features/alerting/unified/components/settings/__mocks__/api/alertmanager/provisioned/config/api/v1/alerts.json b/public/app/features/alerting/unified/components/settings/__mocks__/api/alertmanager/provisioned/config/api/v1/alerts.json new file mode 100644 index 00000000000..7d3a346d87d --- /dev/null +++ b/public/app/features/alerting/unified/components/settings/__mocks__/api/alertmanager/provisioned/config/api/v1/alerts.json @@ -0,0 +1,26 @@ +{ + "template_files": {}, + "alertmanager_config": { + "global": {}, + "receivers": [ + { + "name": "default" + }, + { + "name": "Telegram", + "telegram_configs": [ + { + "bot_token": "abc", + "chat_id": -123, + "disable_notifications": false, + "parse_mode": "MarkdownV2", + "send_resolved": true + } + ] + } + ], + "route": { + "receiver": "default" + } + } +} diff --git a/public/app/features/alerting/unified/components/settings/__mocks__/server.ts b/public/app/features/alerting/unified/components/settings/__mocks__/server.ts index 41ead147008..bb34baac74b 100644 --- a/public/app/features/alerting/unified/components/settings/__mocks__/server.ts +++ b/public/app/features/alerting/unified/components/settings/__mocks__/server.ts @@ -16,6 +16,7 @@ import { DataSourceType } from '../../../utils/datasource'; import internalAlertmanagerConfig from './api/alertmanager/grafana/config/api/v1/alerts.json'; import history from './api/alertmanager/grafana/config/history.json'; +import cloudAlertmanagerConfig from './api/alertmanager/provisioned/config/api/v1/alerts.json'; import vanillaAlertmanagerConfig from './api/alertmanager/vanilla prometheus/api/v2/status.json'; import datasources from './api/datasources.json'; import admin_config from './api/v1/ngalert/admin_config.json'; @@ -37,7 +38,7 @@ const mocks = { getAllDataSources: jest.mocked(config.getAllDataSources), }; -const mockDataSources = { +export const mockDataSources = { [EXTERNAL_VANILLA_ALERTMANAGER_UID]: mockDataSource({ uid: EXTERNAL_VANILLA_ALERTMANAGER_UID, name: EXTERNAL_VANILLA_ALERTMANAGER_UID, @@ -95,7 +96,12 @@ const createAlertmanagerConfigurationHandlers = () => { }; return [ - http.get(`/api/alertmanager/:name/config/api/v1/alerts`, () => HttpResponse.json(internalAlertmanagerConfig)), + http.get<{ name: string }>(`/api/alertmanager/:name/config/api/v1/alerts`, ({ params }) => { + if (params.name === 'grafana') { + return HttpResponse.json(internalAlertmanagerConfig); + } + return HttpResponse.json(cloudAlertmanagerConfig); + }), http.post(`/api/alertmanager/:name/config/api/v1/alerts`, async ({ request }) => { await delay(1000); // simulate some time @@ -108,7 +114,12 @@ const createAlertmanagerConfigurationHandlers = () => { return false; } - return (receiver.telegram_configs || []).some((config) => typeof config.parse_mode === 'object'); + const invalidParseMode = (receiver.telegram_configs || []).some( + (config) => typeof config.parse_mode === 'object' + ); + const invalidChatId = (receiver.telegram_configs || []).some((config) => Number(config.chat_id) >= 0); + + return invalidParseMode || invalidChatId; }); if (invalidConfig) { diff --git a/public/app/features/alerting/unified/utils/cloud-alertmanager-notifier-types.ts b/public/app/features/alerting/unified/utils/cloud-alertmanager-notifier-types.ts index 06ee6893057..0ff45bb14e9 100644 --- a/public/app/features/alerting/unified/utils/cloud-alertmanager-notifier-types.ts +++ b/public/app/features/alerting/unified/utils/cloud-alertmanager-notifier-types.ts @@ -410,7 +410,7 @@ export const cloudNotifierTypes: Array> = [ }), option('chat_id', 'Chat ID', 'ID of the chat where to send the messages', { required: true, - setValueAs: (value) => (typeof value === 'string' ? parseInt(value, 10) : 0), + setValueAs: (value) => (typeof value === 'string' ? parseInt(value, 10) : value), }), option('message', 'Message', 'Message template', { placeholder: '{{ template "webex.default.message" .}}', From 50dd95c09b537400b27f8ba4e2d300ca60ba9ac2 Mon Sep 17 00:00:00 2001 From: Ashley Harrison Date: Tue, 18 Jun 2024 14:33:08 +0100 Subject: [PATCH 02/13] Chore: Remove `betterPageScrolling` toggle (#89339) * remove betterPageScrolling toggle * add scrollTo test shim --- .../feature-toggles/index.md | 1 - .../src/types/featureToggles.gen.ts | 1 - pkg/services/featuremgmt/registry.go | 8 --- pkg/services/featuremgmt/toggles_gen.csv | 1 - pkg/services/featuremgmt/toggles_gen.go | 4 -- pkg/services/featuremgmt/toggles_gen.json | 3 +- public/app/app.ts | 11 --- ...laggedScroller.tsx => NativeScrollbar.tsx} | 13 +--- public/app/core/components/Page/Page.tsx | 10 +-- public/sass/_grafana.scss | 1 - public/sass/components/_scrollbar.scss | 72 ------------------- public/test/jest-setup.ts | 2 + 12 files changed, 11 insertions(+), 116 deletions(-) rename public/app/core/components/{FlaggedScroller.tsx => NativeScrollbar.tsx} (72%) delete mode 100644 public/sass/components/_scrollbar.scss diff --git a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md index 99c345d9eb0..f58f4a964c5 100644 --- a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md +++ b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md @@ -62,7 +62,6 @@ Most [generally available](https://grafana.com/docs/release-life-cycle/#general- | `logRowsPopoverMenu` | Enable filtering menu displayed when text of a log line is selected | Yes | | `lokiQueryHints` | Enables query hints for Loki | Yes | | `alertingQueryOptimization` | Optimizes eligible queries in order to reduce load on datasources | | -| `betterPageScrolling` | Removes CustomScrollbar from the UI, relying on native browser scrollbars | Yes | | `cloudWatchNewLabelParsing` | Updates CloudWatch label parsing to be more accurate | Yes | | `pluginProxyPreserveTrailingSlash` | Preserve plugin proxy trailing slash. | | diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index ca52ce578c1..ecf1ee58d56 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -169,7 +169,6 @@ export interface FeatureToggles { kubernetesAggregator?: boolean; expressionParser?: boolean; groupByVariable?: boolean; - betterPageScrolling?: boolean; authAPIAccessTokenAuth?: boolean; scopeFilters?: boolean; ssoSettingsSAML?: boolean; diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index de3a47a560c..c51cd816c69 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -1126,14 +1126,6 @@ var ( HideFromDocs: true, HideFromAdminPage: true, }, - { - Name: "betterPageScrolling", - Description: "Removes CustomScrollbar from the UI, relying on native browser scrollbars", - Stage: FeatureStageGeneralAvailability, - FrontendOnly: true, - Owner: grafanaFrontendPlatformSquad, - Expression: "true", // enabled by default - }, { Name: "authAPIAccessTokenAuth", Description: "Enables the use of Auth API access tokens for authentication", diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv index c920feb800e..28ceeea27f6 100644 --- a/pkg/services/featuremgmt/toggles_gen.csv +++ b/pkg/services/featuremgmt/toggles_gen.csv @@ -150,7 +150,6 @@ tlsMemcached,experimental,@grafana/grafana-operator-experience-squad,false,false kubernetesAggregator,experimental,@grafana/grafana-app-platform-squad,false,true,false expressionParser,experimental,@grafana/grafana-app-platform-squad,false,true,false groupByVariable,experimental,@grafana/dashboards-squad,false,false,false -betterPageScrolling,GA,@grafana/grafana-frontend-platform,false,false,true authAPIAccessTokenAuth,experimental,@grafana/identity-access-team,false,false,false scopeFilters,experimental,@grafana/dashboards-squad,false,false,false ssoSettingsSAML,preview,@grafana/identity-access-team,false,false,false diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go index 0cd93f4753f..6ff8c3ec65c 100644 --- a/pkg/services/featuremgmt/toggles_gen.go +++ b/pkg/services/featuremgmt/toggles_gen.go @@ -611,10 +611,6 @@ const ( // Enable groupBy variable support in scenes dashboards FlagGroupByVariable = "groupByVariable" - // FlagBetterPageScrolling - // Removes CustomScrollbar from the UI, relying on native browser scrollbars - FlagBetterPageScrolling = "betterPageScrolling" - // FlagAuthAPIAccessTokenAuth // Enables the use of Auth API access tokens for authentication FlagAuthAPIAccessTokenAuth = "authAPIAccessTokenAuth" diff --git a/pkg/services/featuremgmt/toggles_gen.json b/pkg/services/featuremgmt/toggles_gen.json index 731316282d3..4d8099eb846 100644 --- a/pkg/services/featuremgmt/toggles_gen.json +++ b/pkg/services/featuremgmt/toggles_gen.json @@ -463,7 +463,8 @@ "metadata": { "name": "betterPageScrolling", "resourceVersion": "1717578796182", - "creationTimestamp": "2024-03-06T15:06:47Z" + "creationTimestamp": "2024-03-06T15:06:47Z", + "deletionTimestamp": "2024-06-18T09:25:56Z" }, "spec": { "description": "Removes CustomScrollbar from the UI, relying on native browser scrollbars", diff --git a/public/app/app.ts b/public/app/app.ts index 770653fe974..f48ad85af7a 100644 --- a/public/app/app.ts +++ b/public/app/app.ts @@ -42,7 +42,6 @@ import { import { setPanelDataErrorView } from '@grafana/runtime/src/components/PanelDataErrorView'; import { setPanelRenderer } from '@grafana/runtime/src/components/PanelRenderer'; import { setPluginPage } from '@grafana/runtime/src/components/PluginPage'; -import { getScrollbarWidth } from '@grafana/ui'; import config, { updateConfig } from 'app/core/config'; import { arrayMove } from 'app/core/utils/arrayMove'; import { getStandardTransformers } from 'app/features/transformers/standardTransformers'; @@ -136,10 +135,6 @@ export class GrafanaApp { // This needs to be done after the `initEchoSrv` since it is being used under the hood. startMeasure('frontend_app_init'); - if (!config.featureToggles.betterPageScrolling) { - addClassIfNoOverlayScrollbar(); - } - setLocale(config.bootData.user.locale); setWeekStart(config.bootData.user.weekStart); setPanelRenderer(PanelRenderer); @@ -367,12 +362,6 @@ function initEchoSrv() { } } -function addClassIfNoOverlayScrollbar() { - if (getScrollbarWidth() > 0) { - document.body.classList.add('no-overlay-scrollbar'); - } -} - /** * Report when a metric of a given name was marked during the document lifecycle. Works for markers with no duration, * like PerformanceMark or PerformancePaintTiming (e.g. created with performance.mark, or first-contentful-paint) diff --git a/public/app/core/components/FlaggedScroller.tsx b/public/app/core/components/NativeScrollbar.tsx similarity index 72% rename from public/app/core/components/FlaggedScroller.tsx rename to public/app/core/components/NativeScrollbar.tsx index 9d9c3cc5043..76d447f4311 100644 --- a/public/app/core/components/FlaggedScroller.tsx +++ b/public/app/core/components/NativeScrollbar.tsx @@ -1,21 +1,12 @@ import { css, cx } from '@emotion/css'; import React, { useEffect, useRef } from 'react'; -import { config } from '@grafana/runtime'; import { CustomScrollbar, useStyles2 } from '@grafana/ui'; -type FlaggedScrollerProps = Parameters[0]; - -export default function FlaggedScrollbar(props: FlaggedScrollerProps) { - if (config.featureToggles.betterPageScrolling) { - return {props.children}; - } - - return ; -} +type Props = Parameters[0]; // Shim to provide API-compatibility for Page's scroll-related props -function NativeScrollbar({ children, scrollRefCallback, scrollTop, divId }: FlaggedScrollerProps) { +export default function NativeScrollbar({ children, scrollRefCallback, scrollTop, divId }: Props) { const styles = useStyles2(getStyles); const ref = useRef(null); diff --git a/public/app/core/components/Page/Page.tsx b/public/app/core/components/Page/Page.tsx index f88d0dd011b..c588d17be1e 100644 --- a/public/app/core/components/Page/Page.tsx +++ b/public/app/core/components/Page/Page.tsx @@ -5,7 +5,7 @@ import { GrafanaTheme2, PageLayoutType } from '@grafana/data'; import { useStyles2 } from '@grafana/ui'; import { useGrafana } from 'app/core/context/GrafanaContext'; -import FlaggedScrollbar from '../FlaggedScroller'; +import NativeScrollbar from '../NativeScrollbar'; import { PageContents } from './PageContents'; import { PageHeader } from './PageHeader'; @@ -53,7 +53,7 @@ export const Page: PageType = ({ return (
{layout === PageLayoutType.Standard && ( - }
{children}
- + )} {layout === PageLayoutType.Canvas && ( -
{children}
-
+ )} {layout === PageLayoutType.Custom && children} diff --git a/public/sass/_grafana.scss b/public/sass/_grafana.scss index f19901dc014..4137058f724 100644 --- a/public/sass/_grafana.scss +++ b/public/sass/_grafana.scss @@ -20,7 +20,6 @@ @import 'utils/widths'; // COMPONENTS -@import 'components/scrollbar'; @import 'components/buttons'; @import 'components/alerts'; @import 'components/tags'; diff --git a/public/sass/components/_scrollbar.scss b/public/sass/components/_scrollbar.scss deleted file mode 100644 index 1d2f90f130f..00000000000 --- a/public/sass/components/_scrollbar.scss +++ /dev/null @@ -1,72 +0,0 @@ -// Scrollbars -// Note, this is not applied by default if the `betterPageScrolling` feature flag is applied -.no-overlay-scrollbar { - ::-webkit-scrollbar { - width: 8px; - height: 8px; - } - - ::-webkit-scrollbar:hover { - height: 8px; - } - - ::-webkit-scrollbar-button:start:decrement, - ::-webkit-scrollbar-button:end:increment { - display: none; - } - ::-webkit-scrollbar-button:horizontal:decrement { - display: none; - } - ::-webkit-scrollbar-button:horizontal:increment { - display: none; - } - ::-webkit-scrollbar-button:vertical:decrement { - display: none; - } - ::-webkit-scrollbar-button:vertical:increment { - display: none; - } - ::-webkit-scrollbar-button:horizontal:decrement:active { - background-image: none; - } - ::-webkit-scrollbar-button:horizontal:increment:active { - background-image: none; - } - ::-webkit-scrollbar-button:vertical:decrement:active { - background-image: none; - } - ::-webkit-scrollbar-button:vertical:increment:active { - background-image: none; - } - ::-webkit-scrollbar-track-piece { - background-color: transparent; - } - - ::-webkit-scrollbar-thumb:vertical { - height: 50px; - background: -webkit-gradient( - linear, - left top, - right top, - color-stop(0%, $scrollbarBackground), - color-stop(100%, $scrollbarBackground2) - ); - border: 1px solid $scrollbarBorder; - border-top: 1px solid $scrollbarBorder; - border-left: 1px solid $scrollbarBorder; - } - - ::-webkit-scrollbar-thumb:horizontal { - width: 50px; - background: -webkit-gradient( - linear, - left top, - left bottom, - color-stop(0%, $scrollbarBackground), - color-stop(100%, $scrollbarBackground2) - ); - border: 1px solid $scrollbarBorder; - border-top: 1px solid $scrollbarBorder; - border-left: 1px solid $scrollbarBorder; - } -} diff --git a/public/test/jest-setup.ts b/public/test/jest-setup.ts index 4d34bb4a7fd..640db119ccd 100644 --- a/public/test/jest-setup.ts +++ b/public/test/jest-setup.ts @@ -67,6 +67,8 @@ global.IntersectionObserver = mockIntersectionObserver; global.TextEncoder = TextEncoder; global.TextDecoder = TextDecoder; +// add scrollTo interface since it's not implemented in jsdom +Element.prototype.scrollTo = () => {}; jest.mock('../app/core/core', () => ({ ...jest.requireActual('../app/core/core'), From ae04580e5f16335b5867252a18bc33c3ccbb95f4 Mon Sep 17 00:00:00 2001 From: Dominik Prokop Date: Tue, 18 Jun 2024 16:08:16 +0200 Subject: [PATCH 03/13] DashboardScene: Make Grafana usable when custom home dashboard is invalid (#89305) * DashboardScene: Make Grafana usable when custom home dashboard is invalid * Tests * Remove console.error --- .../pages/DashboardScenePage.tsx | 2 + .../DashboardScenePageStateManager.test.ts | 47 +++++++++++----- .../pages/DashboardScenePageStateManager.ts | 55 ++++++++++++++++++- 3 files changed, 87 insertions(+), 17 deletions(-) diff --git a/public/app/features/dashboard-scene/pages/DashboardScenePage.tsx b/public/app/features/dashboard-scene/pages/DashboardScenePage.tsx index a1b4f0f46f3..0df1d89260b 100644 --- a/public/app/features/dashboard-scene/pages/DashboardScenePage.tsx +++ b/public/app/features/dashboard-scene/pages/DashboardScenePage.tsx @@ -19,7 +19,9 @@ export interface Props extends GrafanaRouteComponentProps { const loader = new DashboardScenePageStateManager({}); await loader.loadDashboard({ uid: 'fake-dash', route: DashboardRoutes.Normal }); - expect(loader.state.dashboard).toBeUndefined(); + expect(loader.state.dashboard).toBeDefined(); expect(loader.state.isLoading).toBe(false); - expect(loader.state.loadError).toBe('Error: Dashboard not found'); - }); - - it('should handle home dashboard redirect', async () => { - setBackendSrv({ - get: () => Promise.resolve({ redirectUri: '/d/asd' }), - } as unknown as BackendSrv); - - const loader = new DashboardScenePageStateManager({}); - await loader.loadDashboard({ uid: '', route: DashboardRoutes.Home }); - - expect(loader.state.dashboard).toBeUndefined(); - expect(loader.state.loadError).toBeUndefined(); + expect(loader.state.loadError).toBe('Dashboard not found'); }); it('shoud fetch dashboard from local storage and remove it after if it exists', async () => { @@ -94,6 +82,37 @@ describe('DashboardScenePageStateManager', () => { expect(loader.state.isLoading).toBe(false); }); + describe('Home dashboard', () => { + it('should handle home dashboard redirect', async () => { + setBackendSrv({ + get: () => Promise.resolve({ redirectUri: '/d/asd' }), + } as unknown as BackendSrv); + + const loader = new DashboardScenePageStateManager({}); + await loader.loadDashboard({ uid: '', route: DashboardRoutes.Home }); + + expect(loader.state.dashboard).toBeUndefined(); + expect(loader.state.loadError).toBeUndefined(); + }); + + it('should handle invalid home dashboard request', async () => { + setBackendSrv({ + get: () => + Promise.reject({ + status: 500, + data: { message: 'Failed to load home dashboard' }, + }), + } as unknown as BackendSrv); + + const loader = new DashboardScenePageStateManager({}); + await loader.loadDashboard({ uid: '', route: DashboardRoutes.Home }); + + expect(loader.state.dashboard).toBeDefined(); + expect(loader.state.dashboard?.state.title).toEqual('Failed to load home dashboard'); + expect(loader.state.loadError).toEqual('Failed to load home dashboard'); + }); + }); + describe('New dashboards', () => { it('Should have new empty model with meta.isNew and should not be cached', async () => { const loader = new DashboardScenePageStateManager({}); diff --git a/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.ts b/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.ts index 30e7064eb2a..551d4208308 100644 --- a/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.ts +++ b/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.ts @@ -1,10 +1,13 @@ import { locationUtil } from '@grafana/data'; import { config, getBackendSrv, isFetchError, locationService } from '@grafana/runtime'; +import { defaultDashboard } from '@grafana/schema'; import { StateManagerBase } from 'app/core/services/StateManagerBase'; import { default as localStorageStore } from 'app/core/store'; +import { getMessageFromError } from 'app/core/utils/errors'; import { startMeasure, stopMeasure } from 'app/core/utils/metrics'; import { dashboardLoaderSrv } from 'app/features/dashboard/services/DashboardLoaderSrv'; import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv'; +import { DashboardModel } from 'app/features/dashboard/state'; import { emitDashboardViewEvent } from 'app/features/dashboard/state/analyticsProcessor'; import { DASHBOARD_FROM_LS_KEY, @@ -16,7 +19,10 @@ import { DashboardDTO, DashboardRoutes } from 'app/types'; import { PanelEditor } from '../panel-edit/PanelEditor'; import { DashboardScene } from '../scene/DashboardScene'; import { buildNewDashboardSaveModel } from '../serialization/buildNewDashboardSaveModel'; -import { transformSaveModelToScene } from '../serialization/transformSaveModelToScene'; +import { + createDashboardSceneFromDashboardModel, + transformSaveModelToScene, +} from '../serialization/transformSaveModelToScene'; import { restoreDashboardStateFromLocalStorage } from '../utils/dashboardSessionState'; import { updateNavModel } from './utils'; @@ -143,7 +149,6 @@ export class DashboardScenePageStateManager extends StateManagerBase

${msg}

`, + mode: 'html', + }, + title: '', + transparent: true, + type: 'text', + }, + ], + }, + { canSave: false, canEdit: false } + ) + ); +} From f0e63c6fd50616e17d3bea6d1dc9560f96f94420 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Agn=C3=A8s=20Toulet?= <35176601+AgnesToulet@users.noreply.github.com> Date: Tue, 18 Jun 2024 16:11:59 +0200 Subject: [PATCH 04/13] Doc: Update image rendering with HTTPS configuration (#88505) * Doc: Update image rendering with HTTPS configuration * add version * Update docs/sources/setup-grafana/image-rendering/_index.md Co-authored-by: Christopher Moyer <35463610+chri2547@users.noreply.github.com> --------- Co-authored-by: Christopher Moyer <35463610+chri2547@users.noreply.github.com> --- .../setup-grafana/image-rendering/_index.md | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/docs/sources/setup-grafana/image-rendering/_index.md b/docs/sources/setup-grafana/image-rendering/_index.md index d9438fa0142..ed04d5cc64e 100644 --- a/docs/sources/setup-grafana/image-rendering/_index.md +++ b/docs/sources/setup-grafana/image-rendering/_index.md @@ -207,6 +207,47 @@ HTTP_PORT=0 } ``` +#### HTTP protocol + +{{% admonition type="note" %}} +HTTPS protocol is supported in the image renderer v3.11.0 and later. +{{% /admonition %}} + +Change the protocol of the server, it can be `http` or `https`. Default is `http`. + +```json +{ + "service": { + "protocol": "http" + } +} +``` + +#### HTTPS certificate and key file + +Path to the image renderer certificate and key file used to start an HTTPS server. + +```json +{ + "service": { + "certFile": "./path/to/cert", + "certKey": "./path/to/key" + } +} +``` + +#### HTTPS min TLS version + +Minimum TLS version allowed. Accepted values are: `TLSv1.2`, `TLSv1.3`. Default is `TLSv1.2`. + +```json +{ + "service": { + "minTLSVersion": "TLSv1.2" + } +} +``` + #### Enable Prometheus metrics You can enable [Prometheus](https://prometheus.io/) metrics endpoint `/metrics` using the environment variable `ENABLE_METRICS`. Node.js and render request duration metrics are included, see [Enable Prometheus metrics endpoint]({{< relref "./monitoring#enable-prometheus-metrics-endpoint" >}}) for details. From 3fdc66d284ed4d6dbdbfca2a76d9b30abb27cd85 Mon Sep 17 00:00:00 2001 From: Ivan Ortega Alba Date: Tue, 18 Jun 2024 16:17:59 +0200 Subject: [PATCH 05/13] Scenes: Setting default_home_dashboard_path returns blank page and no controls (#89304) --- .../pages/DashboardScenePage.test.tsx | 49 +++++++++++++++++-- .../pages/DashboardScenePage.tsx | 7 ++- .../scene/DashboardControls.test.tsx | 22 +++++++-- .../scene/DashboardControls.tsx | 11 +++-- .../scene/DashboardSceneRenderer.tsx | 2 +- 5 files changed, 76 insertions(+), 15 deletions(-) diff --git a/public/app/features/dashboard-scene/pages/DashboardScenePage.test.tsx b/public/app/features/dashboard-scene/pages/DashboardScenePage.test.tsx index 3fe7ba89db6..51777c2fd7a 100644 --- a/public/app/features/dashboard-scene/pages/DashboardScenePage.test.tsx +++ b/public/app/features/dashboard-scene/pages/DashboardScenePage.test.tsx @@ -11,9 +11,11 @@ import { config, getPluginLinkExtensions, locationService, setPluginImportUtils import { VizPanel } from '@grafana/scenes'; import { Dashboard } from '@grafana/schema'; import { getRouteComponentProps } from 'app/core/navigation/__mocks__/routeProps'; +import { GrafanaRouteComponentProps } from 'app/core/navigation/types'; import store from 'app/core/store'; import { DashboardLoaderSrv, setDashboardLoaderSrv } from 'app/features/dashboard/services/DashboardLoaderSrv'; import { DASHBOARD_FROM_LS_KEY } from 'app/features/dashboard/state/initDashboard'; +import { DashboardRoutes } from 'app/types'; import { dashboardSceneGraph } from '../utils/dashboardSceneGraph'; @@ -24,6 +26,11 @@ jest.mock('@grafana/runtime', () => ({ ...jest.requireActual('@grafana/runtime'), setPluginExtensionGetter: jest.fn(), getPluginLinkExtensions: jest.fn(), + getBackendSrv: () => { + return { + get: jest.fn().mockResolvedValue({ dashboard: simpleDashboard, meta: { url: '' } }), + }; + }, getDataSourceSrv: () => { return { get: jest.fn().mockResolvedValue({}), @@ -37,12 +44,19 @@ jest.mock('@grafana/runtime', () => ({ const getPluginLinkExtensionsMock = jest.mocked(getPluginLinkExtensions); -function setup() { +function setup({ routeProps }: { routeProps?: Partial } = {}) { const context = getGrafanaContextMock(); + const defaultRouteProps = getRouteComponentProps(); const props: Props = { - ...getRouteComponentProps(), + ...defaultRouteProps, + match: { + ...defaultRouteProps.match, + params: { + uid: 'my-dash-uid', + }, + }, + ...routeProps, }; - props.match.params.uid = 'my-dash-uid'; const renderResult = render( @@ -258,14 +272,39 @@ describe('DashboardScenePage', () => { }); describe('home page', () => { - it('should not show controls', async () => { + it('should render the dashboard when the route is home', async () => { + setup({ + routeProps: { + route: { + ...getRouteComponentProps().route, + routeName: DashboardRoutes.Home, + }, + match: { + ...getRouteComponentProps().match, + path: '/', + params: {}, + }, + }, + }); + + await waitForDashbordToRender(); + + expect(await screen.findByTitle('Panel A')).toBeInTheDocument(); + expect(await screen.findByText('Content A')).toBeInTheDocument(); + + expect(await screen.findByTitle('Panel B')).toBeInTheDocument(); + expect(await screen.findByText('Content B')).toBeInTheDocument(); + }); + + it('should show controls', async () => { getDashboardScenePageStateManager().clearDashboardCache(); loadDashboardMock.mockClear(); loadDashboardMock.mockResolvedValue({ dashboard: { panels: [] }, meta: {} }); setup(); - await waitFor(() => expect(screen.queryByText('Refresh')).not.toBeInTheDocument()); + await waitFor(() => expect(screen.queryByText('Refresh')).toBeInTheDocument()); + await waitFor(() => expect(screen.queryByText('Last 6 hours')).toBeInTheDocument()); }); }); }); diff --git a/public/app/features/dashboard-scene/pages/DashboardScenePage.tsx b/public/app/features/dashboard-scene/pages/DashboardScenePage.tsx index 0df1d89260b..7159235f8c1 100644 --- a/public/app/features/dashboard-scene/pages/DashboardScenePage.tsx +++ b/public/app/features/dashboard-scene/pages/DashboardScenePage.tsx @@ -76,7 +76,12 @@ export function DashboardScenePage({ match, route, queryParams, history }: Props } // Do not render anything when transitioning from one dashboard to another - if (match.params.type !== 'snapshot' && dashboard.state.uid && dashboard.state.uid !== match.params.uid) { + if ( + match.params.type !== 'snapshot' && + dashboard.state.uid && + dashboard.state.uid !== match.params.uid && + route.routeName !== DashboardRoutes.Home + ) { return null; } diff --git a/public/app/features/dashboard-scene/scene/DashboardControls.test.tsx b/public/app/features/dashboard-scene/scene/DashboardControls.test.tsx index 52b46d49c99..30b391e0aba 100644 --- a/public/app/features/dashboard-scene/scene/DashboardControls.test.tsx +++ b/public/app/features/dashboard-scene/scene/DashboardControls.test.tsx @@ -104,17 +104,29 @@ describe('DashboardControls', () => { expect(scene.state.hideTimeControls).toBeTruthy(); expect(scene.state.hideVariableControls).toBeTruthy(); expect(scene.state.hideLinksControls).toBeTruthy(); + }); + + it('should not override state if no new state comes from url', () => { + const scene = buildTestScene({ hideTimeControls: true, hideVariableControls: true, hideLinksControls: true }); scene.updateFromUrl({}); - expect(scene.state.hideTimeControls).toBeFalsy(); - expect(scene.state.hideVariableControls).toBeFalsy(); - expect(scene.state.hideLinksControls).toBeFalsy(); + expect(scene.state.hideTimeControls).toBeTruthy(); + expect(scene.state.hideVariableControls).toBeTruthy(); + expect(scene.state.hideLinksControls).toBeTruthy(); }); it('should not call setState if no changes', () => { const scene = buildTestScene(); const setState = jest.spyOn(scene, 'setState'); - scene.updateFromUrl({}); - scene.updateFromUrl({}); + scene.updateFromUrl({ + '_dash.hideTimePicker': 'true', + '_dash.hideVariables': 'true', + '_dash.hideLinks': 'true', + }); + scene.updateFromUrl({ + '_dash.hideTimePicker': 'true', + '_dash.hideVariables': 'true', + '_dash.hideLinks': 'true', + }); expect(setState).toHaveBeenCalledTimes(1); }); }); diff --git a/public/app/features/dashboard-scene/scene/DashboardControls.tsx b/public/app/features/dashboard-scene/scene/DashboardControls.tsx index 39db075bc44..5614c57bfd4 100644 --- a/public/app/features/dashboard-scene/scene/DashboardControls.tsx +++ b/public/app/features/dashboard-scene/scene/DashboardControls.tsx @@ -54,9 +54,14 @@ export class DashboardControls extends SceneObjectBase { updateFromUrl(values: SceneObjectUrlValues) { const update: Partial = {}; - update.hideTimeControls = values['_dash.hideTimePicker'] === 'true' || values['_dash.hideTimePicker'] === ''; - update.hideVariableControls = values['_dash.hideVariables'] === 'true' || values['_dash.hideVariables'] === ''; - update.hideLinksControls = values['_dash.hideLinks'] === 'true' || values['_dash.hideLinks'] === ''; + update.hideTimeControls = + values['_dash.hideTimePicker'] === 'true' || values['_dash.hideTimePicker'] === '' || this.state.hideTimeControls; + update.hideVariableControls = + values['_dash.hideVariables'] === 'true' || + values['_dash.hideVariables'] === '' || + this.state.hideVariableControls; + update.hideLinksControls = + values['_dash.hideLinks'] === 'true' || values['_dash.hideLinks'] === '' || this.state.hideLinksControls; if (Object.entries(update).some(([k, v]) => v !== this.state[k as keyof DashboardControlsState])) { this.setState(update); diff --git a/public/app/features/dashboard-scene/scene/DashboardSceneRenderer.tsx b/public/app/features/dashboard-scene/scene/DashboardSceneRenderer.tsx index f8e77d21856..553710923cf 100644 --- a/public/app/features/dashboard-scene/scene/DashboardSceneRenderer.tsx +++ b/public/app/features/dashboard-scene/scene/DashboardSceneRenderer.tsx @@ -57,7 +57,7 @@ export function DashboardSceneRenderer({ model }: SceneComponentProps {scopes && } - {!isHomePage && controls && hasControls && ( + {controls && hasControls && (
From 791bcd93dfa55a9e20a1052288afaad062d70488 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 18 Jun 2024 17:05:43 +0200 Subject: [PATCH 06/13] Bump ws from 7.5.6 to 7.5.10 (#89362) Bumps [ws](https://github.com/websockets/ws) from 7.5.6 to 7.5.10. - [Release notes](https://github.com/websockets/ws/releases) - [Commits](https://github.com/websockets/ws/compare/7.5.6...7.5.10) --- updated-dependencies: - dependency-name: ws dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 9fe63d38e1b..be5bd5342fc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -30951,8 +30951,8 @@ __metadata: linkType: hard "ws@npm:^7.2.0, ws@npm:^7.3.1": - version: 7.5.6 - resolution: "ws@npm:7.5.6" + version: 7.5.10 + resolution: "ws@npm:7.5.10" peerDependencies: bufferutil: ^4.0.1 utf-8-validate: ^5.0.2 @@ -30961,7 +30961,7 @@ __metadata: optional: true utf-8-validate: optional: true - checksum: 10/745fc1a1cdac7e91f2c7340f7006aea06454b6ca4f24615a1e81124f46d99a7200839895c088a30c1d8d92dd1a9d349046bd4bc3475447aaf13b1f5cb48a18b7 + checksum: 10/9c796b84ba80ffc2c2adcdfc9c8e9a219ba99caa435c9a8d45f9ac593bba325563b3f83edc5eb067cc6d21b9a6bf2c930adf76dd40af5f58a5ca6859e81858f0 languageName: node linkType: hard From 50244ed4a1435cbf3e3c87d4af34fd7937f7c259 Mon Sep 17 00:00:00 2001 From: Kristin Laemmert Date: Tue, 18 Jun 2024 11:07:15 -0400 Subject: [PATCH 07/13] Experimental Feature Toggle: databaseReadReplica (#89232) This adds a version of the SQLStore that includes a ReadReplica. The primary DB can be accessed directly - from the caller's standpoint, there is no difference between the SQLStore and ReplStore unless they wish to explicitly call the ReadReplica() and use that for the DB sessions. Currently only the stats service GetSystemStats and GetAdminStats are using the ReadReplica(); if it's misconfigured or if the databaseReadReplica feature flag is not turned on, it will fall back to the usual (SQLStore) behavior. Testing requires a database and read replica - the replication should already be configured. I have been testing this locally with a docker mysql setup (https://medium.com/@vbabak/docker-mysql-master-slave-replication-setup-2ff553fceef2) and the following config: [feature_toggles] databaseReadReplica = true [database] type = mysql name = grafana user = grafana password = password host = 127.0.0.1:3306 [database_replica] type = mysql name = grafana user = grafana password = password host = 127.0.0.1:3307 --- .../feature-toggles/index.md | 1 + go.work.sum | 1 + .../src/types/featureToggles.gen.ts | 1 + pkg/infra/db/db.go | 9 + pkg/infra/db/dbrepl.go | 9 + .../statscollector/concurrent_users_test.go | 4 +- pkg/server/test_env.go | 3 + pkg/server/wire.go | 6 +- pkg/services/featuremgmt/registry.go | 7 + pkg/services/featuremgmt/toggles_gen.csv | 1 + pkg/services/featuremgmt/toggles_gen.go | 4 + pkg/services/featuremgmt/toggles_gen.json | 12 ++ pkg/services/sqlstore/database_config.go | 18 +- pkg/services/sqlstore/replstore.go | 194 ++++++++++++++++++ pkg/services/stats/statsimpl/stats.go | 13 +- pkg/services/stats/statsimpl/stats_test.go | 12 +- 16 files changed, 272 insertions(+), 23 deletions(-) create mode 100644 pkg/infra/db/dbrepl.go create mode 100644 pkg/services/sqlstore/replstore.go diff --git a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md index f58f4a964c5..53578a6dfb7 100644 --- a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md +++ b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md @@ -190,6 +190,7 @@ Experimental features might be changed or removed without prior notice. | `alertingCentralAlertHistory` | Enables the new central alert history. | | `azureMonitorPrometheusExemplars` | Allows configuration of Azure Monitor as a data source that can provide Prometheus exemplars | | `pinNavItems` | Enables pinning of nav items | +| `databaseReadReplica` | Use a read replica for some database queries. | ## Development feature toggles diff --git a/go.work.sum b/go.work.sum index ba490777376..917fda449c8 100644 --- a/go.work.sum +++ b/go.work.sum @@ -1286,6 +1286,7 @@ github.com/rogpeppe/go-charset v0.0.0-20180617210344-2471d30d28b4 h1:BN/Nyn2nWMo github.com/rs/xid v1.5.0 h1:mKX4bl4iPYJtEIxp6CYiUuLQ/8DYMoz0PUdtGgMFRVc= github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= github.com/rs/zerolog v1.15.0 h1:uPRuwkWF4J6fGsJ2R0Gn2jB1EQiav9k3S6CSdygQJXY= +github.com/russross/blackfriday v1.5.2 h1:HyvC0ARfnZBqnXwABFeSZHpKvJHJJfPz81GNueLj0oo= github.com/russross/blackfriday v1.6.0 h1:KqfZb0pUVN2lYqZUYRddxF4OR8ZMURnJIG5Y3VRLtww= github.com/russross/blackfriday v1.6.0/go.mod h1:ti0ldHuxg49ri4ksnFxlkCfN+hvslNlmVHqNRXXJNAY= github.com/ruudk/golang-pdf417 v0.0.0-20201230142125-a7e3863a1245 h1:K1Xf3bKttbF+koVGaX5xngRIZ5bVjbmPnaxE/dR08uY= diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index ecf1ee58d56..7f5da38828d 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -195,4 +195,5 @@ export interface FeatureToggles { authZGRPCServer?: boolean; openSearchBackendFlowEnabled?: boolean; ssoSettingsLDAP?: boolean; + databaseReadReplica?: boolean; } diff --git a/pkg/infra/db/db.go b/pkg/infra/db/db.go index 574354fa605..48581f7d65d 100644 --- a/pkg/infra/db/db.go +++ b/pkg/infra/db/db.go @@ -62,6 +62,15 @@ func InitTestDB(t sqlutil.ITestDB, opts ...InitTestDBOpt) *sqlstore.SQLStore { return db } +func InitTestReplDBWithCfg(t sqlutil.ITestDB, opts ...InitTestDBOpt) (*sqlstore.ReplStore, *setting.Cfg) { + return sqlstore.InitTestReplDB(t, opts...) +} + +func InitTestReplDB(t sqlutil.ITestDB, opts ...InitTestDBOpt) *sqlstore.ReplStore { + db, _ := InitTestReplDBWithCfg(t, opts...) + return db +} + func InitTestDBWithCfg(t sqlutil.ITestDB, opts ...InitTestDBOpt) (*sqlstore.SQLStore, *setting.Cfg) { return sqlstore.InitTestDB(t, opts...) } diff --git a/pkg/infra/db/dbrepl.go b/pkg/infra/db/dbrepl.go new file mode 100644 index 00000000000..4b3bac0bb1d --- /dev/null +++ b/pkg/infra/db/dbrepl.go @@ -0,0 +1,9 @@ +package db + +import "github.com/grafana/grafana/pkg/services/sqlstore" + +type ReplDB interface { + // DB is the primary database connection. + DB() *sqlstore.SQLStore + ReadReplica() *sqlstore.SQLStore +} diff --git a/pkg/infra/usagestats/statscollector/concurrent_users_test.go b/pkg/infra/usagestats/statscollector/concurrent_users_test.go index decb1642657..919a74fa22e 100644 --- a/pkg/infra/usagestats/statscollector/concurrent_users_test.go +++ b/pkg/infra/usagestats/statscollector/concurrent_users_test.go @@ -24,7 +24,7 @@ func TestMain(m *testing.M) { } func TestConcurrentUsersMetrics(t *testing.T) { - sqlStore, cfg := db.InitTestDBWithCfg(t) + sqlStore, cfg := db.InitTestReplDBWithCfg(t) statsService := statsimpl.ProvideService(&setting.Cfg{}, sqlStore) s := createService(t, cfg, sqlStore, statsService) @@ -42,7 +42,7 @@ func TestConcurrentUsersMetrics(t *testing.T) { } func TestConcurrentUsersStats(t *testing.T) { - sqlStore, cfg := db.InitTestDBWithCfg(t) + sqlStore, cfg := db.InitTestReplDBWithCfg(t) statsService := statsimpl.ProvideService(&setting.Cfg{}, sqlStore) s := createService(t, cfg, sqlStore, statsService) diff --git a/pkg/server/test_env.go b/pkg/server/test_env.go index 7e4c39fa8c4..a2098c0a8b6 100644 --- a/pkg/server/test_env.go +++ b/pkg/server/test_env.go @@ -15,6 +15,7 @@ import ( func ProvideTestEnv( server *Server, db db.DB, + repldb db.ReplDB, cfg *setting.Cfg, ns *notifications.NotificationServiceMock, grpcServer grpcserver.Provider, @@ -26,6 +27,7 @@ func ProvideTestEnv( return &TestEnv{ Server: server, SQLStore: db, + ReadReplStore: repldb, Cfg: cfg, NotificationService: ns, GRPCServer: grpcServer, @@ -39,6 +41,7 @@ func ProvideTestEnv( type TestEnv struct { Server *Server SQLStore db.DB + ReadReplStore db.ReplDB Cfg *setting.Cfg NotificationService *notifications.NotificationServiceMock GRPCServer grpcserver.Provider diff --git a/pkg/server/wire.go b/pkg/server/wire.go index ccf410105ab..a8bf87483eb 100644 --- a/pkg/server/wire.go +++ b/pkg/server/wire.go @@ -390,6 +390,7 @@ var wireSet = wire.NewSet( wireBasicSet, metrics.WireSet, sqlstore.ProvideService, + sqlstore.ProvideServiceWithReadReplica, ngmetrics.ProvideService, wire.Bind(new(notifications.Service), new(*notifications.NotificationService)), wire.Bind(new(notifications.WebhookSender), new(*notifications.NotificationService)), @@ -405,6 +406,7 @@ var wireCLISet = wire.NewSet( wireBasicSet, metrics.WireSet, sqlstore.ProvideService, + sqlstore.ProvideServiceWithReadReplica, ngmetrics.ProvideService, wire.Bind(new(notifications.Service), new(*notifications.NotificationService)), wire.Bind(new(notifications.WebhookSender), new(*notifications.NotificationService)), @@ -420,12 +422,14 @@ var wireTestSet = wire.NewSet( ProvideTestEnv, metrics.WireSetForTest, sqlstore.ProvideServiceForTests, + sqlstore.ProvideServiceWithReadReplicaForTests, ngmetrics.ProvideServiceForTest, notifications.MockNotificationService, wire.Bind(new(notifications.Service), new(*notifications.NotificationServiceMock)), wire.Bind(new(notifications.WebhookSender), new(*notifications.NotificationServiceMock)), wire.Bind(new(notifications.EmailSender), new(*notifications.NotificationServiceMock)), wire.Bind(new(db.DB), new(*sqlstore.SQLStore)), + wire.Bind(new(db.ReplDB), new(*sqlstore.ReplStore)), prefimpl.ProvideService, oauthtoken.ProvideService, oauthtokentest.ProvideService, @@ -439,7 +443,7 @@ func Initialize(cfg *setting.Cfg, opts Options, apiOpts api.ServerOptions) (*Ser func InitializeForTest(t sqlutil.ITestDB, cfg *setting.Cfg, opts Options, apiOpts api.ServerOptions) (*TestEnv, error) { wire.Build(wireExtsTestSet) - return &TestEnv{Server: &Server{}, SQLStore: &sqlstore.SQLStore{}, Cfg: &setting.Cfg{}}, nil + return &TestEnv{Server: &Server{}, SQLStore: &sqlstore.SQLStore{}, ReadReplStore: &sqlstore.ReplStore{}, Cfg: &setting.Cfg{}}, nil } func InitializeForCLI(cfg *setting.Cfg) (Runner, error) { diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index c51cd816c69..51bf6ea8828 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -1323,6 +1323,13 @@ var ( HideFromDocs: true, HideFromAdminPage: true, }, + { + Name: "databaseReadReplica", + Description: "Use a read replica for some database queries.", + Stage: FeatureStageExperimental, + Owner: grafanaBackendServicesSquad, + Expression: "false", // enabled by default + }, } ) diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv index 28ceeea27f6..c0b5e76d4c1 100644 --- a/pkg/services/featuremgmt/toggles_gen.csv +++ b/pkg/services/featuremgmt/toggles_gen.csv @@ -176,3 +176,4 @@ pinNavItems,experimental,@grafana/grafana-frontend-platform,false,false,false authZGRPCServer,experimental,@grafana/identity-access-team,false,false,false openSearchBackendFlowEnabled,preview,@grafana/aws-datasources,false,false,false ssoSettingsLDAP,experimental,@grafana/identity-access-team,false,false,false +databaseReadReplica,experimental,@grafana/grafana-backend-services-squad,false,false,false diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go index 6ff8c3ec65c..d7f10257ddf 100644 --- a/pkg/services/featuremgmt/toggles_gen.go +++ b/pkg/services/featuremgmt/toggles_gen.go @@ -714,4 +714,8 @@ const ( // FlagSsoSettingsLDAP // Use the new SSO Settings API to configure LDAP FlagSsoSettingsLDAP = "ssoSettingsLDAP" + + // FlagDatabaseReadReplica + // Use a read replica for some database queries. + FlagDatabaseReadReplica = "databaseReadReplica" ) diff --git a/pkg/services/featuremgmt/toggles_gen.json b/pkg/services/featuremgmt/toggles_gen.json index 4d8099eb846..02df39301fd 100644 --- a/pkg/services/featuremgmt/toggles_gen.json +++ b/pkg/services/featuremgmt/toggles_gen.json @@ -655,6 +655,18 @@ "frontend": true } }, + { + "metadata": { + "name": "databaseReadReplica", + "resourceVersion": "1718308641844", + "creationTimestamp": "2024-06-13T19:57:21Z" + }, + "spec": { + "description": "Use a read replica for some database queries.", + "stage": "experimental", + "codeowner": "@grafana/grafana-backend-services-squad" + } + }, { "metadata": { "name": "dataplaneFrontendFallback", diff --git a/pkg/services/sqlstore/database_config.go b/pkg/services/sqlstore/database_config.go index 171ff4972b9..8ffc69fcf66 100644 --- a/pkg/services/sqlstore/database_config.go +++ b/pkg/services/sqlstore/database_config.go @@ -65,9 +65,11 @@ func NewDatabaseConfig(cfg *setting.Cfg, features featuremgmt.FeatureToggles) (* return dbCfg, nil } -func (dbCfg *DatabaseConfig) readConfig(cfg *setting.Cfg) error { - sec := cfg.Raw.Section("database") - +// readConfigSection reads the database configuration from the given block of +// the configuration file. This method allows us to add a "database_replica" +// section to the configuration file while using the same cfg struct. +func (dbCfg *DatabaseConfig) readConfigSection(cfg *setting.Cfg, section string) error { + sec := cfg.Raw.Section(section) cfgURL := sec.Key("url").String() if len(cfgURL) != 0 { dbURL, err := url.Parse(cfgURL) @@ -101,7 +103,6 @@ func (dbCfg *DatabaseConfig) readConfig(cfg *setting.Cfg) error { dbCfg.MaxOpenConn = sec.Key("max_open_conn").MustInt(0) dbCfg.MaxIdleConn = sec.Key("max_idle_conn").MustInt(2) dbCfg.ConnMaxLifetime = sec.Key("conn_max_lifetime").MustInt(14400) - dbCfg.SslMode = sec.Key("ssl_mode").String() dbCfg.SSLSNI = sec.Key("ssl_sni").String() dbCfg.CaCertPath = sec.Key("ca_cert_path").String() @@ -110,21 +111,22 @@ func (dbCfg *DatabaseConfig) readConfig(cfg *setting.Cfg) error { dbCfg.ServerCertName = sec.Key("server_cert_name").String() dbCfg.Path = sec.Key("path").MustString("data/grafana.db") dbCfg.IsolationLevel = sec.Key("isolation_level").String() - dbCfg.CacheMode = sec.Key("cache_mode").MustString("private") dbCfg.WALEnabled = sec.Key("wal").MustBool(false) dbCfg.SkipMigrations = sec.Key("skip_migrations").MustBool() dbCfg.MigrationLock = sec.Key("migration_locking").MustBool(true) dbCfg.MigrationLockAttemptTimeout = sec.Key("locking_attempt_timeout_sec").MustInt() - dbCfg.QueryRetries = sec.Key("query_retries").MustInt() dbCfg.TransactionRetries = sec.Key("transaction_retries").MustInt(5) - dbCfg.LogQueries = sec.Key("log_queries").MustBool(false) - return nil } +// readConfig is a wrapper around readConfigSection that read the "database" configuration block. +func (dbCfg *DatabaseConfig) readConfig(cfg *setting.Cfg) error { + return dbCfg.readConfigSection(cfg, "database") +} + func (dbCfg *DatabaseConfig) buildConnectionString(cfg *setting.Cfg, features featuremgmt.FeatureToggles) error { if dbCfg.ConnectionString != "" { return nil diff --git a/pkg/services/sqlstore/replstore.go b/pkg/services/sqlstore/replstore.go new file mode 100644 index 00000000000..c3540d79c4f --- /dev/null +++ b/pkg/services/sqlstore/replstore.go @@ -0,0 +1,194 @@ +package sqlstore + +import ( + "errors" + "time" + + "github.com/dlmiddlecote/sqlstats" + "github.com/prometheus/client_golang/prometheus" + "xorm.io/xorm" + + "github.com/grafana/grafana/pkg/bus" + "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/infra/tracing" + "github.com/grafana/grafana/pkg/registry" + "github.com/grafana/grafana/pkg/services/featuremgmt" + "github.com/grafana/grafana/pkg/services/sqlstore/migrations" + "github.com/grafana/grafana/pkg/services/sqlstore/migrator" + "github.com/grafana/grafana/pkg/services/sqlstore/sqlutil" + "github.com/grafana/grafana/pkg/setting" +) + +// ReplStore is a wrapper around a main SQLStore and a read-only SQLStore. The +// main SQLStore is anonymous, so the ReplStore may be used directly as a +// SQLStore. +type ReplStore struct { + *SQLStore + repl *SQLStore +} + +// DB returns the main SQLStore. +func (rs ReplStore) DB() *SQLStore { + return rs.SQLStore +} + +// ReadReplica returns the read-only SQLStore. If no read replica is configured, +// it returns the main SQLStore. +func (rs ReplStore) ReadReplica() *SQLStore { + if rs.repl == nil { + rs.log.Debug("ReadReplica not configured, using main SQLStore") + return rs.SQLStore + } + rs.log.Debug("Using ReadReplica") + return rs.repl +} + +// ProvideServiceWithReadReplica creates a new *SQLStore connection intended for +// use as a ReadReplica of the main SQLStore. The primary SQLStore must already +// be initialized. +func ProvideServiceWithReadReplica(primary *SQLStore, cfg *setting.Cfg, + features featuremgmt.FeatureToggles, migrations registry.DatabaseMigrator, + bus bus.Bus, tracer tracing.Tracer) (*ReplStore, error) { + // start with the initialized SQLStore + replStore := &ReplStore{primary, nil} + + // FeatureToggle fallback: If the FlagDatabaseReadReplica feature flag is not enabled, return a single SQLStore. + if !features.IsEnabledGlobally(featuremgmt.FlagDatabaseReadReplica) { + primary.log.Debug("ReadReplica feature flag not enabled, using main SQLStore") + return replStore, nil + } + + // This change will make xorm use an empty default schema for postgres and + // by that mimic the functionality of how it was functioning before + // xorm's changes above. + xorm.DefaultPostgresSchema = "" + s, err := newReadOnlySQLStore(cfg, features, bus, tracer) + if err != nil { + return nil, err + } + s.features = features + s.tracer = tracer + + // initialize and register metrics wrapper around the *sql.DB + db := s.engine.DB().DB + + // register the go_sql_stats_connections_* metrics + if err := prometheus.Register(sqlstats.NewStatsCollector("grafana_repl", db)); err != nil { + s.log.Warn("Failed to register sqlstore stats collector", "error", err) + } + + replStore.repl = s + return replStore, nil +} + +// newReadOnlySQLStore creates a new *SQLStore intended for use with a +// fully-populated read replica of the main Grafana Database. It provides no +// write capabilities and does not run migrations, but other tracing and logging +// features are enabled. +func newReadOnlySQLStore(cfg *setting.Cfg, features featuremgmt.FeatureToggles, bus bus.Bus, tracer tracing.Tracer) (*SQLStore, error) { + s := &SQLStore{ + cfg: cfg, + log: log.New("replstore"), + bus: bus, + tracer: tracer, + } + + s.features = features + s.tracer = tracer + + err := s.initReadOnlyEngine(s.engine) + if err != nil { + return nil, err + } + s.dialect = migrator.NewDialect(s.engine.DriverName()) + return s, nil +} + +// initReadOnlyEngine initializes ss.engine for read-only operations. The database must be a fully-populated read replica. +func (ss *SQLStore) initReadOnlyEngine(engine *xorm.Engine) error { + if ss.engine != nil { + ss.log.Debug("Already connected to database replica") + return nil + } + + dbCfg, err := NewRODatabaseConfig(ss.cfg, ss.features) + if err != nil { + return err + } + ss.dbCfg = dbCfg + + if ss.cfg.DatabaseInstrumentQueries { + ss.dbCfg.Type = WrapDatabaseDriverWithHooks(ss.dbCfg.Type, ss.tracer) + } + + if engine == nil { + var err error + engine, err = xorm.NewEngine(ss.dbCfg.Type, ss.dbCfg.ConnectionString) + if err != nil { + ss.log.Error("failed to connect to database replica", "error", err) + return err + } + // Only for MySQL or MariaDB, verify we can connect with the current connection string's system var for transaction isolation. + // If not, create a new engine with a compatible connection string. + if ss.dbCfg.Type == migrator.MySQL { + engine, err = ss.ensureTransactionIsolationCompatibility(engine, ss.dbCfg.ConnectionString) + if err != nil { + return err + } + } + } + + engine.SetMaxOpenConns(ss.dbCfg.MaxOpenConn) + engine.SetMaxIdleConns(ss.dbCfg.MaxIdleConn) + engine.SetConnMaxLifetime(time.Second * time.Duration(ss.dbCfg.ConnMaxLifetime)) + + // configure sql logging + debugSQL := ss.cfg.Raw.Section("database_replica").Key("log_queries").MustBool(false) + if !debugSQL { + engine.SetLogger(&xorm.DiscardLogger{}) + } else { + // add stack to database calls to be able to see what repository initiated queries. Top 7 items from the stack as they are likely in the xorm library. + engine.SetLogger(NewXormLogger(log.LvlInfo, log.WithSuffix(log.New("replsstore.xorm"), log.CallerContextKey, log.StackCaller(log.DefaultCallerDepth)))) + engine.ShowSQL(true) + engine.ShowExecTime(true) + } + + ss.engine = engine + return nil +} + +// NewRODatabaseConfig creates a new read-only database configuration. +func NewRODatabaseConfig(cfg *setting.Cfg, features featuremgmt.FeatureToggles) (*DatabaseConfig, error) { + if cfg == nil { + return nil, errors.New("cfg cannot be nil") + } + + dbCfg := &DatabaseConfig{} + if err := dbCfg.readConfigSection(cfg, "database_replica"); err != nil { + return nil, err + } + + if err := dbCfg.buildConnectionString(cfg, features); err != nil { + return nil, err + } + + return dbCfg, nil +} + +// ProvideServiceWithReadReplicaForTests wraps the SQLStore in a ReplStore, with the main sqlstore as both the primary and read replica. +// TODO: eventually this should be replaced with a more robust test setup which in +func ProvideServiceWithReadReplicaForTests(testDB *SQLStore, t sqlutil.ITestDB, cfg *setting.Cfg, features featuremgmt.FeatureToggles, migrations registry.DatabaseMigrator) (*ReplStore, error) { + return &ReplStore{testDB, testDB}, nil +} + +// InitTestReplDB initializes a test DB and returns it wrapped in a ReplStore with the main SQLStore as both the primary and read replica. +func InitTestReplDB(t sqlutil.ITestDB, opts ...InitTestDBOpt) (*ReplStore, *setting.Cfg) { + t.Helper() + features := getFeaturesForTesting(opts...) + cfg := getCfgForTesting(opts...) + ss, err := initTestDB(t, cfg, features, migrations.ProvideOSSMigrations(features), opts...) + if err != nil { + t.Fatalf("failed to initialize sql repl store: %s", err) + } + return &ReplStore{ss, ss}, cfg +} diff --git a/pkg/services/stats/statsimpl/stats.go b/pkg/services/stats/statsimpl/stats.go index e2139c80b57..7f580471212 100644 --- a/pkg/services/stats/statsimpl/stats.go +++ b/pkg/services/stats/statsimpl/stats.go @@ -9,6 +9,7 @@ import ( "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/services/libraryelements/model" "github.com/grafana/grafana/pkg/services/org" + "github.com/grafana/grafana/pkg/services/sqlstore" "github.com/grafana/grafana/pkg/services/sqlstore/migrator" "github.com/grafana/grafana/pkg/services/stats" "github.com/grafana/grafana/pkg/setting" @@ -17,12 +18,12 @@ import ( const activeUserTimeLimit = time.Hour * 24 * 30 const dailyActiveUserTimeLimit = time.Hour * 24 -func ProvideService(cfg *setting.Cfg, db db.DB) stats.Service { +func ProvideService(cfg *setting.Cfg, db *sqlstore.ReplStore) stats.Service { return &sqlStatsService{cfg: cfg, db: db} } type sqlStatsService struct { - db db.DB + db *sqlstore.ReplStore cfg *setting.Cfg } @@ -62,8 +63,8 @@ func notServiceAccount(dialect migrator.Dialect) string { } func (ss *sqlStatsService) GetSystemStats(ctx context.Context, query *stats.GetSystemStatsQuery) (result *stats.SystemStats, err error) { - dialect := ss.db.GetDialect() - err = ss.db.WithDbSession(ctx, func(dbSession *db.Session) error { + dialect := ss.db.ReadReplica().GetDialect() + err = ss.db.ReadReplica().WithDbSession(ctx, func(dbSession *db.Session) error { sb := &db.SQLBuilder{} sb.Write("SELECT ") sb.Write(`(SELECT COUNT(*) FROM ` + dialect.Quote("user") + ` WHERE ` + notServiceAccount(dialect) + `) AS users,`) @@ -148,8 +149,8 @@ func (ss *sqlStatsService) roleCounterSQL(ctx context.Context) string { } func (ss *sqlStatsService) GetAdminStats(ctx context.Context, query *stats.GetAdminStatsQuery) (result *stats.AdminStats, err error) { - err = ss.db.WithDbSession(ctx, func(dbSession *db.Session) error { - dialect := ss.db.GetDialect() + err = ss.db.ReadReplica().WithDbSession(ctx, func(dbSession *db.Session) error { + dialect := ss.db.ReadReplica().GetDialect() now := time.Now() activeEndDate := now.Add(-activeUserTimeLimit) dailyActiveEndDate := now.Add(-dailyActiveUserTimeLimit) diff --git a/pkg/services/stats/statsimpl/stats_test.go b/pkg/services/stats/statsimpl/stats_test.go index e63d51fddc0..15ec1c50334 100644 --- a/pkg/services/stats/statsimpl/stats_test.go +++ b/pkg/services/stats/statsimpl/stats_test.go @@ -32,9 +32,9 @@ func TestIntegrationStatsDataAccess(t *testing.T) { if testing.Short() { t.Skip("skipping integration test") } - db, cfg := db.InitTestDBWithCfg(t) - statsService := &sqlStatsService{db: db} - populateDB(t, db, cfg) + store, cfg := db.InitTestReplDBWithCfg(t) + statsService := &sqlStatsService{db: store} + populateDB(t, store, cfg) t.Run("Get system stats should not results in error", func(t *testing.T) { query := stats.GetSystemStatsQuery{} @@ -49,7 +49,7 @@ func TestIntegrationStatsDataAccess(t *testing.T) { assert.Equal(t, int64(0), result.APIKeys) assert.Equal(t, int64(2), result.Correlations) assert.NotNil(t, result.DatabaseCreatedTime) - assert.Equal(t, db.GetDialect().DriverName(), result.DatabaseDriver) + assert.Equal(t, store.GetDialect().DriverName(), result.DatabaseDriver) }) t.Run("Get system user count stats should not results in error", func(t *testing.T) { @@ -157,8 +157,8 @@ func TestIntegration_GetAdminStats(t *testing.T) { if testing.Short() { t.Skip("skipping integration test") } - db, cfg := db.InitTestDBWithCfg(t) - statsService := ProvideService(cfg, db) + store, cfg := db.InitTestReplDBWithCfg(t) + statsService := ProvideService(cfg, store) query := stats.GetAdminStatsQuery{} _, err := statsService.GetAdminStats(context.Background(), &query) From 34b3dbdbf3fa46bd9f49d25d3df1e8f935fcc016 Mon Sep 17 00:00:00 2001 From: Laura Benz <48948963+L-M-K-B@users.noreply.github.com> Date: Tue, 18 Jun 2024 17:24:48 +0200 Subject: [PATCH 08/13] RestoreDashboards: Adjust path (#89233) * refactor: change path * fix: page headline * refactor: remove condition --- pkg/api/api.go | 4 ++++ pkg/services/navtree/navtreeimpl/navtree.go | 4 ++-- public/app/core/utils/navBarItem-translations.ts | 8 ++++---- .../features/browse-dashboards/RecentlyDeletedPage.tsx | 2 +- public/app/routes/routes.tsx | 2 +- public/locales/en-US/grafana.json | 2 +- public/locales/pseudo-LOCALE/grafana.json | 2 +- 7 files changed, 14 insertions(+), 10 deletions(-) diff --git a/pkg/api/api.go b/pkg/api/api.go index 76ff1a24c70..c6c1ee022cd 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -165,6 +165,10 @@ func (hs *HTTPServer) registerRoutes() { ) } + if hs.Features.IsEnabledGlobally(featuremgmt.FlagDashboardRestore) { + r.Get("/dashboard/recently-deleted", reqSignedIn, hs.Index) + } + r.Get("/explore", authorize(ac.EvalPermission(ac.ActionDatasourcesExplore)), hs.Index) r.Get("/playlists/", reqSignedIn, hs.Index) diff --git a/pkg/services/navtree/navtreeimpl/navtree.go b/pkg/services/navtree/navtreeimpl/navtree.go index 7ed6f2164c0..a9247c16877 100644 --- a/pkg/services/navtree/navtreeimpl/navtree.go +++ b/pkg/services/navtree/navtreeimpl/navtree.go @@ -358,8 +358,8 @@ func (s *ServiceImpl) buildDashboardNavLinks(c *contextmodel.ReqContext) []*navt dashboardChildNavs = append(dashboardChildNavs, &navtree.NavLink{ Text: "Recently Deleted", SubTitle: "Any items listed here for more than 30 days will be automatically deleted.", - Id: "dashboards/recentlyDeleted", - Url: s.cfg.AppSubURL + "/dashboard/recentlyDeleted", + Id: "dashboards/recently-deleted", + Url: s.cfg.AppSubURL + "/dashboard/recently-deleted", }) } } diff --git a/public/app/core/utils/navBarItem-translations.ts b/public/app/core/utils/navBarItem-translations.ts index 9297cb4816c..6dcaa0ebf74 100644 --- a/public/app/core/utils/navBarItem-translations.ts +++ b/public/app/core/utils/navBarItem-translations.ts @@ -40,8 +40,8 @@ export function getNavTitle(navId: string | undefined) { return t('nav.reporting.title', 'Reporting'); case 'dashboards/public': return t('nav.public.title', 'Public dashboards'); - case 'dashboards/recentlyDeleted': - return t('nav.recentlyDeleted.title', 'Recently Deleted'); + case 'dashboards/recently-deleted': + return t('nav.recently-deleted.title', 'Recently Deleted'); case 'dashboards/new': return t('nav.new-dashboard.title', 'New dashboard'); case 'dashboards/folder/new': @@ -208,9 +208,9 @@ export function getNavSubTitle(navId: string | undefined) { ); case 'dashboards/library-panels': return t('nav.library-panels.subtitle', 'Reusable panels that can be added to multiple dashboards'); - case 'dashboards/recentlyDeleted': + case 'dashboards/recently-deleted': return t( - 'nav.recentlyDeleted.subtitle', + 'nav.recently-deleted.subtitle', 'Any items listed here for more than 30 days will be automatically deleted.' ); case 'alerting': diff --git a/public/app/features/browse-dashboards/RecentlyDeletedPage.tsx b/public/app/features/browse-dashboards/RecentlyDeletedPage.tsx index 149ca72a03b..ee0579fbead 100644 --- a/public/app/features/browse-dashboards/RecentlyDeletedPage.tsx +++ b/public/app/features/browse-dashboards/RecentlyDeletedPage.tsx @@ -34,7 +34,7 @@ const RecentlyDeletedPage = memo(() => { }, [dispatch, stateManager]); return ( - + contextSrv.evaluatePermission([AccessControlAction.DashboardsDelete]), component: SafeDynamicImport( () => import(/* webpackChunkName: "RecentlyDeletedPage" */ 'app/features/browse-dashboards/RecentlyDeletedPage') diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index 36fb784122a..0f00b256579 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -1184,7 +1184,7 @@ "public": { "title": "Public dashboards" }, - "recentlyDeleted": { + "recently-deleted": { "subtitle": "Any items listed here for more than 30 days will be automatically deleted.", "title": "Recently Deleted" }, diff --git a/public/locales/pseudo-LOCALE/grafana.json b/public/locales/pseudo-LOCALE/grafana.json index bfe2a139e08..065ea9ab9b5 100644 --- a/public/locales/pseudo-LOCALE/grafana.json +++ b/public/locales/pseudo-LOCALE/grafana.json @@ -1184,7 +1184,7 @@ "public": { "title": "Pūþľįč đäşĥþőäřđş" }, - "recentlyDeleted": { + "recently-deleted": { "subtitle": "Åʼny įŧęmş ľįşŧęđ ĥęřę ƒőř mőřę ŧĥäʼn 30 đäyş ŵįľľ þę äūŧőmäŧįčäľľy đęľęŧęđ.", "title": "Ŗęčęʼnŧľy Đęľęŧęđ" }, From f0270d8e312006bdf38387c7909742e91a1262ed Mon Sep 17 00:00:00 2001 From: Fabrizio <135109076+fabrizio-grafana@users.noreply.github.com> Date: Tue, 18 Jun 2024 18:05:55 +0200 Subject: [PATCH 09/13] Update code owners for O11Y package (#89334) --- .github/CODEOWNERS | 36 +++++++++++++++++++++++------------- 1 file changed, 23 insertions(+), 13 deletions(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 6136bd07605..f5da47a35b3 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -313,37 +313,47 @@ /e2e/ @grafana/grafana-frontend-platform /e2e/cloud-plugins-suite/ @grafana/partner-datasources /e2e/plugin-e2e/plugin-e2e-api-tests/ @grafana/plugins-platform-frontend + +# Packages /packages/ @grafana/grafana-frontend-platform @grafana/plugins-platform-frontend +/packages/grafana-data/src/**/*logs* @grafana/observability-logs +/packages/grafana-data/src/transformations/ @grafana/dataviz-squad /packages/grafana-e2e-selectors/ @grafana/grafana-frontend-platform +/packages/grafana-flamegraph/ @grafana/observability-traces-and-profiling +/packages/grafana-o11y-ds-frontend/ @grafana/observability-logs +/packages/grafana-o11y-ds-frontend/src/IntervalInput/ @grafana/observability-traces-and-profiling +/packages/grafana-o11y-ds-frontend/src/NodeGraph/ @grafana/observability-traces-and-profiling +/packages/grafana-o11y-ds-frontend/src/pyroscope/ @grafana/observability-traces-and-profiling +/packages/grafana-o11y-ds-frontend/src/SpanBar/ @grafana/observability-traces-and-profiling +/packages/grafana-o11y-ds-frontend/src/TraceToLogs/ @grafana/observability-traces-and-profiling +/packages/grafana-o11y-ds-frontend/src/TraceToMetrics/ @grafana/observability-traces-and-profiling +/packages/grafana-o11y-ds-frontend/src/TraceToProfiles/ @grafana/observability-traces-and-profiling +/packages/grafana-plugin-configs/ @grafana/plugins-platform-frontend +/packages/grafana-prometheus/ @grafana/observability-metrics +/packages/grafana-schema/src/**/*canvas* @grafana/dataviz-squad +/packages/grafana-schema/src/**/*tempo* @grafana/observability-traces-and-profiling +/packages/grafana-sql/ @grafana/partner-datasources @grafana/oss-big-tent /packages/grafana-ui/.storybook/ @grafana/plugins-platform-frontend /packages/grafana-ui/src/components/ @grafana/grafana-frontend-platform +/packages/grafana-ui/src/components/BarGauge/ @grafana/dataviz-squad +/packages/grafana-ui/src/components/DataLinks/ @grafana/dataviz-squad /packages/grafana-ui/src/components/DateTimePickers/ @grafana/grafana-frontend-platform +/packages/grafana-ui/src/components/Gauge/ @grafana/dataviz-squad +/packages/grafana-ui/src/components/Sparkline/ @grafana/grafana-frontend-platform @grafana/app-o11y-visualizations /packages/grafana-ui/src/components/Table/ @grafana/dataviz-squad /packages/grafana-ui/src/components/Table/SparklineCell.tsx @grafana/dataviz-squad @grafana/app-o11y-visualizations -/packages/grafana-ui/src/components/Gauge/ @grafana/dataviz-squad -/packages/grafana-ui/src/components/BarGauge/ @grafana/dataviz-squad /packages/grafana-ui/src/components/uPlot/ @grafana/dataviz-squad -/packages/grafana-ui/src/components/DataLinks/ @grafana/dataviz-squad /packages/grafana-ui/src/components/ValuePicker/ @grafana/dataviz-squad /packages/grafana-ui/src/components/VizLayout/ @grafana/dataviz-squad /packages/grafana-ui/src/components/VizLegend/ @grafana/dataviz-squad /packages/grafana-ui/src/components/VizRepeater/ @grafana/dataviz-squad /packages/grafana-ui/src/components/VizTooltip/ @grafana/dataviz-squad -/packages/grafana-ui/src/components/Sparkline/ @grafana/grafana-frontend-platform @grafana/app-o11y-visualizations /packages/grafana-ui/src/graveyard/Graph/ @grafana/dataviz-squad /packages/grafana-ui/src/graveyard/GraphNG/ @grafana/dataviz-squad /packages/grafana-ui/src/graveyard/TimeSeries/ @grafana/dataviz-squad /packages/grafana-ui/src/utils/storybook/ @grafana/plugins-platform-frontend -/packages/grafana-data/src/transformations/ @grafana/dataviz-squad -/packages/grafana-data/src/**/*logs* @grafana/observability-logs -/packages/grafana-schema/src/**/*tempo* @grafana/observability-traces-and-profiling -/packages/grafana-schema/src/**/*canvas* @grafana/dataviz-squad -/packages/grafana-flamegraph/ @grafana/observability-traces-and-profiling + /plugins-bundled/ @grafana/plugins-platform-frontend -/packages/grafana-plugin-configs/ @grafana/plugins-platform-frontend -/packages/grafana-prometheus/ @grafana/observability-metrics -/packages/grafana-o11y-ds-frontend/ @grafana/observability-logs @grafana/observability-traces-and-profiling -/packages/grafana-sql/ @grafana/partner-datasources @grafana/oss-big-tent # root files, mostly frontend /.browserslistrc @grafana/frontend-ops From 6597bed9f78581ec8dab59c6d8ca9f907f35c71b Mon Sep 17 00:00:00 2001 From: Josh Hunt Date: Tue, 18 Jun 2024 17:26:02 +0100 Subject: [PATCH 10/13] Chore: Bump ws (#89371) --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index be5bd5342fc..4529834161a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -30966,8 +30966,8 @@ __metadata: linkType: hard "ws@npm:^8.16.0, ws@npm:^8.2.3, ws@npm:^8.9.0": - version: 8.16.0 - resolution: "ws@npm:8.16.0" + version: 8.17.1 + resolution: "ws@npm:8.17.1" peerDependencies: bufferutil: ^4.0.1 utf-8-validate: ">=5.0.2" @@ -30976,7 +30976,7 @@ __metadata: optional: true utf-8-validate: optional: true - checksum: 10/7c511c59e979bd37b63c3aea4a8e4d4163204f00bd5633c053b05ed67835481995f61a523b0ad2b603566f9a89b34cb4965cb9fab9649fbfebd8f740cea57f17 + checksum: 10/4264ae92c0b3e59c7e309001e93079b26937aab181835fb7af79f906b22cd33b6196d96556dafb4e985742dd401e99139572242e9847661fdbc96556b9e6902d languageName: node linkType: hard From 966cee864a227cc5d2a060e1d2797b317328165a Mon Sep 17 00:00:00 2001 From: Matias Chomicki Date: Tue, 18 Jun 2024 18:34:39 +0200 Subject: [PATCH 11/13] LogRows: add missing call to close the popover (#89370) --- public/app/features/logs/components/LogRows.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/public/app/features/logs/components/LogRows.tsx b/public/app/features/logs/components/LogRows.tsx index 87306b91e2a..20556b9f232 100644 --- a/public/app/features/logs/components/LogRows.tsx +++ b/public/app/features/logs/components/LogRows.tsx @@ -147,6 +147,7 @@ class UnThemedLogRows extends PureComponent { if (document.getSelection()?.toString()) { return; } + this.closePopoverMenu(); }; closePopoverMenu = () => { From 59f255bb7fbe48818b4e0346e321b66ddb39bf6c Mon Sep 17 00:00:00 2001 From: Travis Patterson Date: Tue, 18 Jun 2024 10:54:14 -0600 Subject: [PATCH 12/13] Add info tooltips to logs that have been sampled by adaptive logs (#89320) * Add info tooltips to logs that have been sampled by adaptive logs * review commends and linter changes * make warnings match info * fix betterer results --- .betterer.results | 3 +-- public/app/features/logs/components/LogRow.tsx | 14 ++++++++++++-- .../features/logs/components/getLogRowStyles.ts | 13 ++++++++++--- public/app/features/logs/utils.ts | 16 ++++++++++++++++ 4 files changed, 39 insertions(+), 7 deletions(-) diff --git a/.betterer.results b/.betterer.results index d266b08b826..87b8a31de53 100644 --- a/.betterer.results +++ b/.betterer.results @@ -4702,8 +4702,7 @@ exports[`better eslint`] = { [0, 0, 0, "Styles should be written using objects.", "31"], [0, 0, 0, "Styles should be written using objects.", "32"], [0, 0, 0, "Styles should be written using objects.", "33"], - [0, 0, 0, "Styles should be written using objects.", "34"], - [0, 0, 0, "Styles should be written using objects.", "35"] + [0, 0, 0, "Styles should be written using objects.", "34"] ], "public/app/features/logs/components/log-context/LogContextButtons.tsx:5381": [ [0, 0, 0, "No untranslated strings. Wrap text with ", "0"] diff --git a/public/app/features/logs/components/LogRow.tsx b/public/app/features/logs/components/LogRow.tsx index fe161e3111e..a06edb4bdf1 100644 --- a/public/app/features/logs/components/LogRow.tsx +++ b/public/app/features/logs/components/LogRow.tsx @@ -17,7 +17,7 @@ import { reportInteraction } from '@grafana/runtime'; import { DataQuery, TimeZone } from '@grafana/schema'; import { withTheme2, Themeable2, Icon, Tooltip, PopoverContent } from '@grafana/ui'; -import { checkLogsError, escapeUnescapedString } from '../utils'; +import { checkLogsError, escapeUnescapedString, checkLogsSampled } from '../utils'; import { LogDetails } from './LogDetails'; import { LogLabels } from './LogLabels'; @@ -222,6 +222,7 @@ class UnThemedLogRow extends PureComponent { const { showDetails, showingContext, permalinked } = this.state; const levelStyles = getLogLevelStyles(theme, row.logLevel); const { errorMessage, hasError } = checkLogsError(row); + const { sampleMessage, isSampled } = checkLogsSampled(row); const logRowBackground = cx(styles.logsRow, { [styles.errorLogRow]: hasError, [styles.highlightBackground]: showingContext || permalinked, @@ -255,13 +256,22 @@ class UnThemedLogRow extends PureComponent { )} {hasError && ( )} + {isSampled && ( + + + + )} { width: 4em; cursor: default; `, - logIconError: css` - color: ${theme.colors.warning.main}; - `, + logIconError: css({ + color: theme.colors.warning.main, + position: 'relative', + top: '-2px', + }), + logIconInfo: css({ + color: theme.colors.info.main, + position: 'relative', + top: '-2px', + }), logsRowToggleDetails: css` label: logs-row-toggle-details__level; font-size: 9px; diff --git a/public/app/features/logs/utils.ts b/public/app/features/logs/utils.ts index 150caee3740..4d8f88c5641 100644 --- a/public/app/features/logs/utils.ts +++ b/public/app/features/logs/utils.ts @@ -154,6 +154,22 @@ export const checkLogsError = (logRow: LogRowModel): { hasError: boolean; errorM }; }; +export const checkLogsSampled = (logRow: LogRowModel): { isSampled: boolean; sampleMessage?: string } => { + if (logRow.labels.__adaptive_logs_sampled__) { + let msg = + logRow.labels.__adaptive_logs_sampled__ === 'true' + ? 'Logs like this one have been dropped by Adaptive Logs' + : `${logRow.labels.__adaptive_logs_sampled__}% of logs like this one have been dropped by Adaptive Logs`; + return { + isSampled: true, + sampleMessage: msg, + }; + } + return { + isSampled: false, + }; +}; + export const escapeUnescapedString = (string: string) => string.replace(/\\r\\n|\\n|\\t|\\r/g, (match: string) => (match.slice(1) === 't' ? '\t' : '\n')); From b0c043de5fe0779c89ed178995b864c07f935f39 Mon Sep 17 00:00:00 2001 From: Joao Silva <100691367+JoaoSilvaGrafana@users.noreply.github.com> Date: Tue, 18 Jun 2024 18:06:35 +0100 Subject: [PATCH 13/13] Fix: Portuguese Brazilian wasn't loading translations (#89302) --- public/app/core/internationalization/constants.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/app/core/internationalization/constants.ts b/public/app/core/internationalization/constants.ts index a241519fed4..f4ab99631cc 100644 --- a/public/app/core/internationalization/constants.ts +++ b/public/app/core/internationalization/constants.ts @@ -5,7 +5,7 @@ export const ENGLISH_US = 'en-US'; export const FRENCH_FRANCE = 'fr-FR'; export const SPANISH_SPAIN = 'es-ES'; export const GERMAN_GERMANY = 'de-DE'; -export const BRAZILIAN_PORTUGUESE = 'pt-br'; +export const BRAZILIAN_PORTUGUESE = 'pt-BR'; export const CHINESE_SIMPLIFIED = 'zh-Hans'; export const PSEUDO_LOCALE = 'pseudo-LOCALE';