From 18c1c4cf9565bd7002e5b2f0ca1f624cbc19b5ca Mon Sep 17 00:00:00 2001 From: "grafana-pr-automation[bot]" <140550294+grafana-pr-automation[bot]@users.noreply.github.com> Date: Thu, 12 Dec 2024 17:30:48 +0200 Subject: [PATCH 01/28] I18n: Download translations from Crowdin (#97849) 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 | 4 ++++ public/locales/es-ES/grafana.json | 4 ++++ public/locales/fr-FR/grafana.json | 4 ++++ public/locales/pt-BR/grafana.json | 4 ++++ public/locales/zh-Hans/grafana.json | 4 ++++ 5 files changed, 20 insertions(+) diff --git a/public/locales/de-DE/grafana.json b/public/locales/de-DE/grafana.json index 97de1f1933a..0668470abd2 100644 --- a/public/locales/de-DE/grafana.json +++ b/public/locales/de-DE/grafana.json @@ -479,6 +479,10 @@ }, "rule-list": { "configure-datasource": "", + "filter-view": { + "no-more-results": "", + "no-rules-found": "" + }, "new-alert-rule": "" }, "rule-state": { diff --git a/public/locales/es-ES/grafana.json b/public/locales/es-ES/grafana.json index 9c2dd653fd2..07cadcb979e 100644 --- a/public/locales/es-ES/grafana.json +++ b/public/locales/es-ES/grafana.json @@ -479,6 +479,10 @@ }, "rule-list": { "configure-datasource": "", + "filter-view": { + "no-more-results": "", + "no-rules-found": "" + }, "new-alert-rule": "" }, "rule-state": { diff --git a/public/locales/fr-FR/grafana.json b/public/locales/fr-FR/grafana.json index 9bf42b88871..306d0d048ca 100644 --- a/public/locales/fr-FR/grafana.json +++ b/public/locales/fr-FR/grafana.json @@ -479,6 +479,10 @@ }, "rule-list": { "configure-datasource": "", + "filter-view": { + "no-more-results": "", + "no-rules-found": "" + }, "new-alert-rule": "" }, "rule-state": { diff --git a/public/locales/pt-BR/grafana.json b/public/locales/pt-BR/grafana.json index 88ba779bff8..18d926ed4b0 100644 --- a/public/locales/pt-BR/grafana.json +++ b/public/locales/pt-BR/grafana.json @@ -479,6 +479,10 @@ }, "rule-list": { "configure-datasource": "", + "filter-view": { + "no-more-results": "", + "no-rules-found": "" + }, "new-alert-rule": "" }, "rule-state": { diff --git a/public/locales/zh-Hans/grafana.json b/public/locales/zh-Hans/grafana.json index 7fa6ccf0c88..830dc6074b4 100644 --- a/public/locales/zh-Hans/grafana.json +++ b/public/locales/zh-Hans/grafana.json @@ -475,6 +475,10 @@ }, "rule-list": { "configure-datasource": "", + "filter-view": { + "no-more-results": "", + "no-rules-found": "" + }, "new-alert-rule": "" }, "rule-state": { From 3fdf0ea1f40469d1fcb60439c24e8349c885e486 Mon Sep 17 00:00:00 2001 From: Matias Chomicki Date: Thu, 12 Dec 2024 15:44:06 +0000 Subject: [PATCH 02/28] Logs: Added option to show the log line body when displayed fields are used (#97209) * LogDetailsBody: create and integrate component * Chore: sort prop names * Add tests * Address lint issues * Update betterer * Update public/app/features/logs/components/LogDetailsBody.tsx Co-authored-by: Galen Kistler <109082771+gtk-grafana@users.noreply.github.com> * Update public/app/features/logs/components/LogDetailsBody.tsx Co-authored-by: Galen Kistler <109082771+gtk-grafana@users.noreply.github.com> * Update tests --------- Co-authored-by: Galen Kistler <109082771+gtk-grafana@users.noreply.github.com> --- .betterer.results | 5 -- .../logs/components/LogDetails.test.tsx | 28 +++++++ .../features/logs/components/LogDetails.tsx | 26 +++++- .../logs/components/LogDetailsBody.tsx | 84 +++++++++++++++++++ .../logs/components/LogLabels.test.tsx | 4 +- .../features/logs/components/LogLabels.tsx | 4 +- .../LogRowMessageDisplayedFields.test.tsx | 10 ++- .../LogRowMessageDisplayedFields.tsx | 12 ++- public/locales/en-US/grafana.json | 6 ++ public/locales/pseudo-LOCALE/grafana.json | 6 ++ 10 files changed, 171 insertions(+), 14 deletions(-) create mode 100644 public/app/features/logs/components/LogDetailsBody.tsx diff --git a/.betterer.results b/.betterer.results index 0dcf32bb34a..d4168a6aaa1 100644 --- a/.betterer.results +++ b/.betterer.results @@ -3303,11 +3303,6 @@ exports[`better eslint`] = { "public/app/features/logs/components/InfiniteScroll.tsx:5381": [ [0, 0, 0, "No untranslated strings. Wrap text with ", "0"] ], - "public/app/features/logs/components/LogDetails.tsx:5381": [ - [0, 0, 0, "No untranslated strings. Wrap text with ", "0"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "1"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "2"] - ], "public/app/features/logs/components/LogLabelStats.tsx:5381": [ [0, 0, 0, "No untranslated strings. Wrap text with ", "0"], [0, 0, 0, "No untranslated strings. Wrap text with ", "1"], diff --git a/public/app/features/logs/components/LogDetails.test.tsx b/public/app/features/logs/components/LogDetails.test.tsx index a98dbd1a9f9..c69d1a6e932 100644 --- a/public/app/features/logs/components/LogDetails.test.tsx +++ b/public/app/features/logs/components/LogDetails.test.tsx @@ -14,6 +14,7 @@ import { } from '@grafana/data'; import { LogDetails, Props } from './LogDetails'; +import { LOG_LINE_BODY_FIELD_NAME } from './LogDetailsBody'; import { createLogRow } from './__mocks__/logRow'; import { getLogRowStyles } from './getLogRowStyles'; @@ -58,6 +59,33 @@ describe('LogDetails', () => { expect(screen.getByRole('cell', { name: 'key2' })).toBeInTheDocument(); expect(screen.getByRole('cell', { name: 'label2' })).toBeInTheDocument(); }); + it('should show an option to display the log line when displayed fields are used', async () => { + const onClickShowField = jest.fn(); + + setup({ displayedFields: ['key1'], onClickShowField }, { labels: { key1: 'label1' } }); + expect(screen.getByRole('cell', { name: 'key1' })).toBeInTheDocument(); + expect(screen.getByLabelText('Show log line')).toBeInTheDocument(); + + await userEvent.click(screen.getByLabelText('Show log line')); + + expect(onClickShowField).toHaveBeenCalledTimes(1); + }); + it('should show an active option to display the log line when displayed fields are used', async () => { + const onClickHideField = jest.fn(); + + setup({ displayedFields: ['key1', LOG_LINE_BODY_FIELD_NAME], onClickHideField }, { labels: { key1: 'label1' } }); + expect(screen.getByRole('cell', { name: 'key1' })).toBeInTheDocument(); + expect(screen.getByLabelText('Hide log line')).toBeInTheDocument(); + + await userEvent.click(screen.getByLabelText('Hide log line')); + + expect(onClickHideField).toHaveBeenCalledTimes(1); + }); + it('should not show an option to display the log line when displayed fields are not used', () => { + setup({ displayedFields: undefined }, { labels: { key1: 'label1' } }); + expect(screen.getByRole('cell', { name: 'key1' })).toBeInTheDocument(); + expect(screen.queryByLabelText('Show log line')).not.toBeInTheDocument(); + }); it('should render filter controls when the callbacks are provided', () => { setup( { diff --git a/public/app/features/logs/components/LogDetails.tsx b/public/app/features/logs/components/LogDetails.tsx index 4d99054d1d9..807320d5829 100644 --- a/public/app/features/logs/components/LogDetails.tsx +++ b/public/app/features/logs/components/LogDetails.tsx @@ -3,9 +3,11 @@ import { PureComponent } from 'react'; import { CoreApp, DataFrame, DataFrameType, Field, LinkModel, LogRowModel } from '@grafana/data'; import { PopoverContent, Themeable2, withTheme2 } from '@grafana/ui'; +import { Trans } from 'app/core/internationalization'; import { calculateLogsLabelStats, calculateStats } from '../utils'; +import { LogDetailsBody } from './LogDetailsBody'; import { LogDetailsRow } from './LogDetailsRow'; import { getLogLevelStyles, LogRowStyles } from './getLogRowStyles'; import { getAllFields, createLogLineLinks } from './logParser'; @@ -86,10 +88,28 @@ class UnThemedLogDetails extends PureComponent {
+ {displayedFields && displayedFields.length > 0 && ( + <> + + + + + + )} {(labelsAvailable || fieldsAvailable) && ( )} @@ -142,7 +162,7 @@ class UnThemedLogDetails extends PureComponent { {fieldsWithLinksAvailable && ( )} @@ -192,7 +212,7 @@ class UnThemedLogDetails extends PureComponent { {!fieldsAvailable && !labelsAvailable && !fieldsWithLinksAvailable && ( )} diff --git a/public/app/features/logs/components/LogDetailsBody.tsx b/public/app/features/logs/components/LogDetailsBody.tsx new file mode 100644 index 00000000000..0cc844e246d --- /dev/null +++ b/public/app/features/logs/components/LogDetailsBody.tsx @@ -0,0 +1,84 @@ +import { css } from '@emotion/css'; +import memoizeOne from 'memoize-one'; + +import { CoreApp, GrafanaTheme2, LogRowModel } from '@grafana/data'; +import { reportInteraction } from '@grafana/runtime'; +import { IconButton, Themeable2 } from '@grafana/ui'; + +import { getLogRowStyles } from './getLogRowStyles'; + +export interface Props extends Themeable2 { + app?: CoreApp; + disableActions: boolean; + displayedFields?: string[]; + onClickShowField?: (key: string) => void; + onClickHideField?: (key: string) => void; + row: LogRowModel; + theme: GrafanaTheme2; +} + +const getStyles = memoizeOne((theme: GrafanaTheme2) => { + return { + buttonRow: css({ + display: 'flex', + flexDirection: 'row', + gap: theme.spacing(0.5), + marginLeft: theme.spacing(0.5), + }), + }; +}); + +export const LOG_LINE_BODY_FIELD_NAME = '___LOG_LINE_BODY___'; + +export const LogDetailsBody = (props: Props) => { + const showField = () => { + const { onClickShowField, row } = props; + if (onClickShowField) { + onClickShowField(LOG_LINE_BODY_FIELD_NAME); + } + + reportInteraction('grafana_explore_logs_log_details_show_body_clicked', { + datasourceType: row.datasourceType, + logRowUid: row.uid, + type: 'enable', + app: props.app, + }); + }; + + const hideField = () => { + const { onClickHideField, row } = props; + if (onClickHideField) { + onClickHideField(LOG_LINE_BODY_FIELD_NAME); + } + + reportInteraction('grafana_explore_logs_log_details_show_body_clicked', { + datasourceType: row.datasourceType, + logRowUid: row.uid, + type: 'disable', + app: props.app, + }); + }; + + const { theme, displayedFields, disableActions, row } = props; + const styles = getStyles(theme); + const rowStyles = getLogRowStyles(theme); + + const toggleFieldButton = + displayedFields != null && displayedFields.includes(LOG_LINE_BODY_FIELD_NAME) ? ( + + ) : ( + + ); + + return ( + + + + + + ); +}; diff --git a/public/app/features/logs/components/LogLabels.test.tsx b/public/app/features/logs/components/LogLabels.test.tsx index af5217d110f..f736b0a595c 100644 --- a/public/app/features/logs/components/LogLabels.test.tsx +++ b/public/app/features/logs/components/LogLabels.test.tsx @@ -1,6 +1,7 @@ import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; +import { LOG_LINE_BODY_FIELD_NAME } from './LogDetailsBody'; import { LogLabels, LogLabelsList } from './LogLabels'; describe('', () => { @@ -38,8 +39,9 @@ describe('', () => { describe('', () => { it('renders labels', () => { - render(); + render(); expect(screen.queryByText('bar')).toBeInTheDocument(); expect(screen.queryByText('42')).toBeInTheDocument(); + expect(screen.queryByText('log line')).toBeInTheDocument(); }); }); diff --git a/public/app/features/logs/components/LogLabels.tsx b/public/app/features/logs/components/LogLabels.tsx index 951597c887f..fa675287a72 100644 --- a/public/app/features/logs/components/LogLabels.tsx +++ b/public/app/features/logs/components/LogLabels.tsx @@ -4,6 +4,8 @@ import { memo, forwardRef, useMemo } from 'react'; import { GrafanaTheme2, Labels } from '@grafana/data'; import { Tooltip, useStyles2 } from '@grafana/ui'; +import { LOG_LINE_BODY_FIELD_NAME } from './LogDetailsBody'; + // Levels are already encoded in color, filename is a Loki-ism const HIDDEN_LABELS = ['detected_level', 'level', 'lvl', 'filename']; @@ -59,7 +61,7 @@ export const LogLabelsList = memo(({ labels }: LogLabelsArrayProps) => { {labels.map((label) => ( - {label} + {label === LOG_LINE_BODY_FIELD_NAME ? 'log line' : label} ))} diff --git a/public/app/features/logs/components/LogRowMessageDisplayedFields.test.tsx b/public/app/features/logs/components/LogRowMessageDisplayedFields.test.tsx index 05b48eabff1..4e0bdfccf4a 100644 --- a/public/app/features/logs/components/LogRowMessageDisplayedFields.test.tsx +++ b/public/app/features/logs/components/LogRowMessageDisplayedFields.test.tsx @@ -4,6 +4,7 @@ import userEvent from '@testing-library/user-event'; import { createTheme, LogLevel } from '@grafana/data'; import { IconButton } from '@grafana/ui'; +import { LOG_LINE_BODY_FIELD_NAME } from './LogDetailsBody'; import { LogRowMessageDisplayedFields, Props } from './LogRowMessageDisplayedFields'; import { createLogRow } from './__mocks__/logRow'; import { getLogRowStyles } from './getLogRowStyles'; @@ -42,7 +43,14 @@ const setup = (propOverrides: Partial = {}, detectedFields = ['place', 'p describe('LogRowMessageDisplayedFields', () => { it('renders diplayed fields from a log row', () => { setup(); - expect(screen.queryByText('Logs are wonderful')).not.toBeInTheDocument(); + expect(screen.queryByText(/Logs are wonderful/)).not.toBeInTheDocument(); + expect(screen.getByText(/place=Earth/)).toBeInTheDocument(); + expect(screen.getByText(/planet=Mars/)).toBeInTheDocument(); + }); + + it('renders diplayed fields and body from a log row', () => { + setup({}, ['place', 'planet', LOG_LINE_BODY_FIELD_NAME]); + expect(screen.queryByText(/Logs are wonderful/)).toBeInTheDocument(); expect(screen.getByText(/place=Earth/)).toBeInTheDocument(); expect(screen.getByText(/planet=Mars/)).toBeInTheDocument(); }); diff --git a/public/app/features/logs/components/LogRowMessageDisplayedFields.tsx b/public/app/features/logs/components/LogRowMessageDisplayedFields.tsx index 363fb97bb17..2cc495148a1 100644 --- a/public/app/features/logs/components/LogRowMessageDisplayedFields.tsx +++ b/public/app/features/logs/components/LogRowMessageDisplayedFields.tsx @@ -3,6 +3,7 @@ import { memo, ReactNode, useMemo } from 'react'; import { LogRowModel, Field, LinkModel, DataFrame } from '@grafana/data'; +import { LOG_LINE_BODY_FIELD_NAME } from './LogDetailsBody'; import { LogRowMenuCell } from './LogRowMenuCell'; import { LogRowStyles } from './getLogRowStyles'; import { getAllFields } from './logParser'; @@ -45,21 +46,26 @@ export const LogRowMessageDisplayedFields = memo((props: Props) => { let line = ''; for (let i = 0; i < detectedFields.length; i++) { const parsedKey = detectedFields[i]; + + if (parsedKey === LOG_LINE_BODY_FIELD_NAME) { + line += ` ${row.entry}`; + } + const field = fields.find((field) => { const { keys } = field; return keys[0] === parsedKey; }); - if (field) { + if (field != null) { line += ` ${parsedKey}=${field.values}`; } - if (row.labels[parsedKey] !== undefined && row.labels[parsedKey] !== null) { + if (row.labels[parsedKey] != null && row.labels[parsedKey] != null) { line += ` ${parsedKey}=${row.labels[parsedKey]}`; } } return line.trimStart(); - }, [detectedFields, fields, row.labels]); + }, [detectedFields, fields, row.entry, row.labels]); const shouldShowMenu = useMemo(() => mouseIsOver || pinned, [mouseIsOver, pinned]); diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index d1b9458d42c..8482a186a35 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -1758,6 +1758,12 @@ "infinite-scroll": { "older-logs": "Older logs" }, + "log-details": { + "fields": "Fields", + "links": "Links", + "log-line": "Log line", + "no-details": "No details available" + }, "log-row-message": { "ellipsis": "… ", "more": "more" diff --git a/public/locales/pseudo-LOCALE/grafana.json b/public/locales/pseudo-LOCALE/grafana.json index aeb77531dbb..ea4bf022248 100644 --- a/public/locales/pseudo-LOCALE/grafana.json +++ b/public/locales/pseudo-LOCALE/grafana.json @@ -1758,6 +1758,12 @@ "infinite-scroll": { "older-logs": "Øľđęř ľőģş" }, + "log-details": { + "fields": "Fįęľđş", + "links": "Ŀįʼnĸş", + "log-line": "Ŀőģ ľįʼnę", + "no-details": "Ńő đęŧäįľş äväįľäþľę" + }, "log-row-message": { "ellipsis": "… ", "more": "mőřę" From dbe2d7ac47fbf8add76fbfea14cf9dd0ac6e90b4 Mon Sep 17 00:00:00 2001 From: Andreas Christou Date: Thu, 12 Dec 2024 16:38:26 +0000 Subject: [PATCH 03/28] Azure: Add documentation for Basic Logs (#97193) * Add documentation for Basic Logs * Lint * Update docs/sources/datasources/azure-monitor/query-editor/index.md Co-authored-by: Larissa Wandzura <126723338+lwandz13@users.noreply.github.com> --------- Co-authored-by: Larissa Wandzura <126723338+lwandz13@users.noreply.github.com> --- .../sources/datasources/azure-monitor/_index.md | 1 + .../azure-monitor/query-editor/index.md | 17 +++++++++++++++++ 2 files changed, 18 insertions(+) diff --git a/docs/sources/datasources/azure-monitor/_index.md b/docs/sources/datasources/azure-monitor/_index.md index 9b30cd66e21..7474ba3165d 100644 --- a/docs/sources/datasources/azure-monitor/_index.md +++ b/docs/sources/datasources/azure-monitor/_index.md @@ -116,6 +116,7 @@ For details, refer to [Configuring using Workload Identity](#configuring-using-w | **Application (client) ID** | Sets the application/client ID for the Azure AD app registration to use for authentication. | | **Client secret** | Sets the application client secret for the Azure AD app registration to use for authentication. For details, see the [Azure application secret docs](https://docs.microsoft.com/en-us/azure/active-directory/develop/howto-create-service-principal-portal#option-2-create-a-new-application-secret). | | **Default subscription** | _(Optional)_ Sets a default subscription for template variables to use. | +| **Enable Basic Logs** | Allows this data source to execute queries against [Basic Logs tables](https://learn.microsoft.com/en-us/azure/azure-monitor/logs/basic-logs-query?tabs=portal-1) in supported Log Analytics Workspaces. These queries may incur additional costs. | ### Provision the data source diff --git a/docs/sources/datasources/azure-monitor/query-editor/index.md b/docs/sources/datasources/azure-monitor/query-editor/index.md index ccfc12a81bb..28256b66000 100644 --- a/docs/sources/datasources/azure-monitor/query-editor/index.md +++ b/docs/sources/datasources/azure-monitor/query-editor/index.md @@ -126,6 +126,8 @@ Azure Monitor Logs collects and organises log and performance data from [support While Azure Monitor Metrics stores only simplified numerical data, Logs can store different data types, each with their own structure. You can also perform complex analysis of Logs data by using KQL. +The Azure Monitor data source also supports querying of [Basic Logs](https://learn.microsoft.com/en-us/azure/azure-monitor/logs/basic-logs-query?tabs=portal-1) tables (if they exist in your Log Analytics workspace). This feature must be enabled in the data source configuration. + {{< figure src="/static/img/docs/azure-monitor/query-editor-logs.png" max-width="800px" class="docs-image--no-shadow" caption="Azure Monitor Logs sample query comparing successful requests to failed requests" >}} ### Create a Logs query @@ -146,6 +148,21 @@ You can also perform complex analysis of Logs data by using KQL. You can also augment queries by using [template variables]({{< relref "./template-variables" >}}). +**To create a Basic Logs query:** + +1. Ensure that the data source has the `Enable Basic Logs` toggle enabled. +1. In a Grafana panel, select the **Azure Monitor** data source. +1. Select the **Logs** service. +1. Select a resource to query. Multiple resources can be selected as long as they are of the same type. +1. Switch the `Logs` toggle from `Analytics` to `Basic`. A modal will display to notify users of potential additional costs. + {{% admonition type="note" %}} + Basic Logs queries do not support time-ranges specified in the query. The time-range will be hardcoded to the dashboard time-range. There are also other query limitations. See the + [documentation for details.](https://learn.microsoft.com/en-us/azure/azure-monitor/logs/basic-logs-query?tabs=portal-1#limitations) + {{% /admonition %}} +1. Enter your KQL query. + +You can also augment queries by using [template variables](https://grafana.com/docs/grafana//datasources/azure-monitor/template-variables/). + ### Logs query examples Azure Monitor Logs queries are written using the Kusto Query Language (KQL), a rich language similar to SQL. From 08bb66ba72b5567db6665714bf6e042b35077d50 Mon Sep 17 00:00:00 2001 From: Ashley Harrison Date: Thu, 12 Dec 2024 16:39:52 +0000 Subject: [PATCH 04/28] Chore: Update to node 22 (#97779) * update to node 22 * downgrade to 22.11.0 * switch to using node-version-file * update @types/node * revert back to using node-version --- .drone.yml | 102 +++++++++--------- .../detect-breaking-changes-levitate.yml | 6 +- .nvmrc | 2 +- Dockerfile | 2 +- contribute/developer-guide.md | 2 +- .../grafana-extensionstest-app/package.json | 2 +- .../grafana-test-datasource/package.json | 2 +- package.json | 4 +- packages/grafana-data/package.json | 2 +- packages/grafana-e2e-selectors/package.json | 2 +- packages/grafana-flamegraph/package.json | 2 +- packages/grafana-icons/package.json | 2 +- .../grafana-o11y-ds-frontend/package.json | 2 +- packages/grafana-prometheus/package.json | 2 +- packages/grafana-sql/package.json | 2 +- packages/grafana-ui/package.json | 2 +- .../datasource/azuremonitor/package.json | 2 +- .../datasource/cloud-monitoring/package.json | 2 +- .../package.json | 2 +- .../grafana-pyroscope-datasource/package.json | 2 +- .../grafana-testdata-datasource/package.json | 2 +- .../plugins/datasource/jaeger/package.json | 2 +- .../app/plugins/datasource/mssql/package.json | 2 +- .../app/plugins/datasource/mysql/package.json | 2 +- .../app/plugins/datasource/parca/package.json | 2 +- .../app/plugins/datasource/tempo/package.json | 2 +- .../plugins/datasource/zipkin/package.json | 2 +- scripts/drone/variables.star | 2 +- yarn.lock | 91 ++++++++-------- 29 files changed, 123 insertions(+), 130 deletions(-) diff --git a/.drone.yml b/.drone.yml index 07831c07af2..e61e5e26c01 100644 --- a/.drone.yml +++ b/.drone.yml @@ -117,14 +117,14 @@ steps: - commands: - yarn install --immutable || yarn install --immutable depends_on: [] - image: node:20.9.0-alpine + image: node:22.11.0-alpine name: yarn-install - commands: - yarn storybook --quiet depends_on: - yarn-install detach: true - image: node:20.9.0-alpine + image: node:22.11.0-alpine name: start-storybook - commands: - npx wait-on@7.2.0 -t 1m http://$HOST:$PORT @@ -175,14 +175,14 @@ steps: - commands: - yarn install --immutable || yarn install --immutable depends_on: [] - image: node:20.9.0-alpine + image: node:22.11.0-alpine name: yarn-install - commands: - apk add --update git bash - yarn betterer ci depends_on: - yarn-install - image: node:20.9.0-alpine + image: node:22.11.0-alpine name: betterer-frontend - commands: - echo $(/usr/bin/github-app-external-token) > /github-app/token @@ -229,7 +229,7 @@ steps: - yarn-install environment: TEST_MAX_WORKERS: 50% - image: node:20.9.0-alpine + image: node:22.11.0-alpine name: test-frontend trigger: event: @@ -314,7 +314,7 @@ steps: - commands: - yarn install --immutable || yarn install --immutable depends_on: [] - image: node:20.9.0-alpine + image: node:22.11.0-alpine name: yarn-install - commands: - yarn run prettier:check @@ -324,7 +324,7 @@ steps: - yarn-install environment: TEST_MAX_WORKERS: 50% - image: node:20.9.0-alpine + image: node:22.11.0-alpine name: lint-frontend - commands: - |- @@ -337,7 +337,7 @@ steps: \ " depends_on: - yarn-install - image: node:20-bookworm + image: node:22-bookworm name: verify-i18n trigger: event: @@ -667,7 +667,7 @@ steps: - commands: - yarn install --immutable || yarn install --immutable depends_on: [] - image: node:20.9.0-alpine + image: node:22.11.0-alpine name: yarn-install - commands: - apk add --update jq bash @@ -678,7 +678,7 @@ steps: - yarn-install environment: NODE_OPTIONS: --max_old_space_size=8192 - image: node:20.9.0-alpine + image: node:22.11.0-alpine name: build-frontend-packages - failure: ignore image: grafana/drone-downstream @@ -742,7 +742,7 @@ steps: - yarn-install environment: NODE_OPTIONS: --max_old_space_size=8192 - image: node:20.9.0-alpine + image: node:22.11.0-alpine name: build-test-plugins - commands: - apk add --update tar bash @@ -870,7 +870,7 @@ steps: HOST: grafana-server PORT: "3001" PROV_DIR: /grafana/scripts/grafana-server/tmp/conf/provisioning - image: node:20-bookworm + image: node:22-bookworm name: playwright-plugin-e2e - commands: - apt-get update @@ -960,7 +960,7 @@ steps: - build-frontend-packages environment: NODE_OPTIONS: --max_old_space_size=4096 - image: node:20.9.0-alpine + image: node:22.11.0-alpine name: build-storybook when: paths: @@ -1307,7 +1307,7 @@ steps: - commands: - yarn install --immutable || yarn install --immutable depends_on: [] - image: node:20.9.0-alpine + image: node:22.11.0-alpine name: yarn-install - commands: - pip3 install codespell @@ -1320,7 +1320,7 @@ steps: - yarn-install environment: NODE_OPTIONS: --max_old_space_size=8192 - image: node:20.9.0-alpine + image: node:22.11.0-alpine name: lint-docs - commands: - mkdir -p /hugo/content/docs/grafana/latest @@ -1695,7 +1695,7 @@ steps: - commands: - yarn install --immutable || yarn install --immutable depends_on: [] - image: node:20.9.0-alpine + image: node:22.11.0-alpine name: yarn-install - commands: - pip3 install codespell @@ -1708,7 +1708,7 @@ steps: - yarn-install environment: NODE_OPTIONS: --max_old_space_size=8192 - image: node:20.9.0-alpine + image: node:22.11.0-alpine name: lint-docs - commands: - mkdir -p /hugo/content/docs/grafana/latest @@ -1771,14 +1771,14 @@ steps: - commands: - yarn install --immutable || yarn install --immutable depends_on: [] - image: node:20.9.0-alpine + image: node:22.11.0-alpine name: yarn-install - commands: - apk add --update git bash - yarn betterer ci depends_on: - yarn-install - image: node:20.9.0-alpine + image: node:22.11.0-alpine name: betterer-frontend - commands: - yarn run ci:test-frontend @@ -1786,7 +1786,7 @@ steps: - yarn-install environment: TEST_MAX_WORKERS: 50% - image: node:20.9.0-alpine + image: node:22.11.0-alpine name: test-frontend trigger: branch: main @@ -1829,7 +1829,7 @@ steps: - commands: - yarn install --immutable || yarn install --immutable depends_on: [] - image: node:20.9.0-alpine + image: node:22.11.0-alpine name: yarn-install - commands: - yarn run prettier:check @@ -1839,7 +1839,7 @@ steps: - yarn-install environment: TEST_MAX_WORKERS: 50% - image: node:20.9.0-alpine + image: node:22.11.0-alpine name: lint-frontend - commands: - |- @@ -1852,7 +1852,7 @@ steps: \ " depends_on: - yarn-install - image: node:20-bookworm + image: node:22-bookworm name: verify-i18n trigger: branch: main @@ -2041,14 +2041,14 @@ steps: - commands: - yarn install --immutable || yarn install --immutable depends_on: [] - image: node:20.9.0-alpine + image: node:22.11.0-alpine name: yarn-install - commands: - yarn storybook --quiet depends_on: - yarn-install detach: true - image: node:20.9.0-alpine + image: node:22.11.0-alpine name: start-storybook - commands: - npx wait-on@7.2.0 -t 1m http://$HOST:$PORT @@ -2148,7 +2148,7 @@ steps: - commands: - yarn install --immutable || yarn install --immutable depends_on: [] - image: node:20.9.0-alpine + image: node:22.11.0-alpine name: yarn-install - commands: - apk add --update jq @@ -2159,7 +2159,7 @@ steps: - yarn install --mode=update-lockfile depends_on: - yarn-install - image: node:20.9.0-alpine + image: node:22.11.0-alpine name: update-package-json-version - commands: - apk add --update jq bash @@ -2171,7 +2171,7 @@ steps: - update-package-json-version environment: NODE_OPTIONS: --max_old_space_size=8192 - image: node:20.9.0-alpine + image: node:22.11.0-alpine name: build-frontend-packages - commands: - docker run --privileged --rm tonistiigi/binfmt --install all @@ -2226,7 +2226,7 @@ steps: - yarn-install environment: NODE_OPTIONS: --max_old_space_size=8192 - image: node:20.9.0-alpine + image: node:22.11.0-alpine name: build-test-plugins - commands: - apk add --update tar bash @@ -2354,7 +2354,7 @@ steps: HOST: grafana-server PORT: "3001" PROV_DIR: /grafana/scripts/grafana-server/tmp/conf/provisioning - image: node:20-bookworm + image: node:22-bookworm name: playwright-plugin-e2e - commands: - apt-get update @@ -2444,7 +2444,7 @@ steps: - build-frontend-packages environment: NODE_OPTIONS: --max_old_space_size=4096 - image: node:20.9.0-alpine + image: node:22.11.0-alpine name: build-storybook when: paths: @@ -2494,7 +2494,7 @@ steps: GRAFANA_MISC_STATS_API_KEY: from_secret: grafana_misc_stats_api_key failure: ignore - image: node:20.9.0-alpine + image: node:22.11.0-alpine name: publish-frontend-metrics when: repo: @@ -2536,7 +2536,7 @@ steps: environment: NPM_TOKEN: from_secret: npm_token - image: node:20.9.0-alpine + image: node:22.11.0-alpine name: release-canary-npm-packages when: paths: @@ -2943,14 +2943,14 @@ steps: - commands: - yarn install --immutable || yarn install --immutable depends_on: [] - image: node:20.9.0-alpine + image: node:22.11.0-alpine name: yarn-install - commands: - apk add --update git bash - yarn betterer ci depends_on: - yarn-install - image: node:20.9.0-alpine + image: node:22.11.0-alpine name: betterer-frontend - commands: - yarn run ci:test-frontend @@ -2958,7 +2958,7 @@ steps: - yarn-install environment: TEST_MAX_WORKERS: 50% - image: node:20.9.0-alpine + image: node:22.11.0-alpine name: test-frontend trigger: branch: @@ -2999,7 +2999,7 @@ steps: - commands: - yarn install --immutable || yarn install --immutable depends_on: [] - image: node:20.9.0-alpine + image: node:22.11.0-alpine name: yarn-install - commands: - yarn run prettier:check @@ -3009,7 +3009,7 @@ steps: - yarn-install environment: TEST_MAX_WORKERS: 50% - image: node:20.9.0-alpine + image: node:22.11.0-alpine name: lint-frontend - commands: - |- @@ -3022,7 +3022,7 @@ steps: \ " depends_on: - yarn-install - image: node:20-bookworm + image: node:22-bookworm name: verify-i18n trigger: branch: @@ -3863,7 +3863,7 @@ steps: - commands: - yarn install --immutable || yarn install --immutable depends_on: [] - image: node:20.9.0-alpine + image: node:22.11.0-alpine name: yarn-install - commands: - ./bin/build artifacts npm retrieve --tag ${DRONE_TAG} @@ -3887,7 +3887,7 @@ steps: NPM_TOKEN: from_secret: npm_token failure: ignore - image: node:20.9.0-alpine + image: node:22.11.0-alpine name: release-npm-packages trigger: event: @@ -3922,7 +3922,7 @@ steps: 1\n else\n sleep 60\n fi\n done\n \ " depends_on: [] - image: node:20.9.0-alpine + image: node:22.11.0-alpine name: verify-grafanacom trigger: event: @@ -4022,7 +4022,7 @@ steps: \ " depends_on: - publish-grafanacom - image: node:20.9.0-alpine + image: node:22.11.0-alpine name: verify-grafanacom trigger: event: @@ -4446,14 +4446,14 @@ steps: - commands: - yarn install --immutable || yarn install --immutable depends_on: [] - image: node:20.9.0-alpine + image: node:22.11.0-alpine name: yarn-install - commands: - apk add --update git bash - yarn betterer ci depends_on: - yarn-install - image: node:20.9.0-alpine + image: node:22.11.0-alpine name: betterer-frontend - commands: - yarn run ci:test-frontend @@ -4461,7 +4461,7 @@ steps: - yarn-install environment: TEST_MAX_WORKERS: 50% - image: node:20.9.0-alpine + image: node:22.11.0-alpine name: test-frontend trigger: cron: @@ -5443,8 +5443,8 @@ steps: - trivy --exit-code 0 --severity UNKNOWN,LOW,MEDIUM docker:27-cli - trivy --exit-code 0 --severity UNKNOWN,LOW,MEDIUM alpine/git:2.40.1 - trivy --exit-code 0 --severity UNKNOWN,LOW,MEDIUM golang:1.23.1-alpine - - trivy --exit-code 0 --severity UNKNOWN,LOW,MEDIUM node:20.9.0-alpine - - trivy --exit-code 0 --severity UNKNOWN,LOW,MEDIUM node:20-bookworm + - trivy --exit-code 0 --severity UNKNOWN,LOW,MEDIUM node:22.11.0-alpine + - trivy --exit-code 0 --severity UNKNOWN,LOW,MEDIUM node:22-bookworm - trivy --exit-code 0 --severity UNKNOWN,LOW,MEDIUM google/cloud-sdk:431.0.0 - trivy --exit-code 0 --severity UNKNOWN,LOW,MEDIUM grafana/grafana-ci-deploy:1.3.3 - trivy --exit-code 0 --severity UNKNOWN,LOW,MEDIUM alpine:3.20.3 @@ -5482,8 +5482,8 @@ steps: - trivy --exit-code 1 --severity HIGH,CRITICAL docker:27-cli - trivy --exit-code 1 --severity HIGH,CRITICAL alpine/git:2.40.1 - trivy --exit-code 1 --severity HIGH,CRITICAL golang:1.23.1-alpine - - trivy --exit-code 1 --severity HIGH,CRITICAL node:20.9.0-alpine - - trivy --exit-code 1 --severity HIGH,CRITICAL node:20-bookworm + - trivy --exit-code 1 --severity HIGH,CRITICAL node:22.11.0-alpine + - trivy --exit-code 1 --severity HIGH,CRITICAL node:22-bookworm - trivy --exit-code 1 --severity HIGH,CRITICAL google/cloud-sdk:431.0.0 - trivy --exit-code 1 --severity HIGH,CRITICAL grafana/grafana-ci-deploy:1.3.3 - trivy --exit-code 1 --severity HIGH,CRITICAL alpine:3.20.3 @@ -5751,6 +5751,6 @@ kind: secret name: gcr_credentials --- kind: signature -hmac: 04ba0c9b8e69705a28a24ba03de14ece0b15c4b44f6262fcbc6a9ee874b5a9db +hmac: 08edb24b8ec53e0ce09f525cf6fdf4ea5dc67710fe64c3ea93ceaf3af42b8305 ... diff --git a/.github/workflows/detect-breaking-changes-levitate.yml b/.github/workflows/detect-breaking-changes-levitate.yml index 408c71e6455..70c823483d9 100644 --- a/.github/workflows/detect-breaking-changes-levitate.yml +++ b/.github/workflows/detect-breaking-changes-levitate.yml @@ -31,7 +31,7 @@ jobs: path: './pr' - uses: actions/setup-node@v4 with: - node-version: 20.9.0 + node-version: 22.11.0 - name: Get yarn cache directory path id: yarn-cache-dir-path @@ -79,7 +79,7 @@ jobs: - uses: actions/setup-node@v4 with: - node-version: 20.9.0 + node-version: 22.11.0 - name: Get yarn cache directory path id: yarn-cache-dir-path @@ -126,7 +126,7 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: - node-version: 20.9.0 + node-version: 22.11.0 - name: Get built packages from pr uses: actions/download-artifact@v4 diff --git a/.nvmrc b/.nvmrc index 805b5a4e00b..bb8c76c68e2 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -v20.9.0 +v22.11.0 diff --git a/Dockerfile b/Dockerfile index dc326221eb0..324597b0254 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,6 @@ ARG BASE_IMAGE=alpine:3.20 -ARG JS_IMAGE=node:20-alpine +ARG JS_IMAGE=node:22-alpine ARG JS_PLATFORM=linux/amd64 ARG GO_IMAGE=golang:1.23.1-alpine diff --git a/contribute/developer-guide.md b/contribute/developer-guide.md index 804daa903e0..9a4d98d8003 100644 --- a/contribute/developer-guide.md +++ b/contribute/developer-guide.md @@ -18,7 +18,7 @@ We recommend using [Homebrew](https://brew.sh/) for installing any missing depen ``` brew install git brew install go -brew install node@20 +brew install node@22 brew install corepack corepack enable ``` diff --git a/e2e/test-plugins/grafana-extensionstest-app/package.json b/e2e/test-plugins/grafana-extensionstest-app/package.json index fe2cf427950..e071c25cf66 100644 --- a/e2e/test-plugins/grafana-extensionstest-app/package.json +++ b/e2e/test-plugins/grafana-extensionstest-app/package.json @@ -13,7 +13,7 @@ "devDependencies": { "@grafana/plugin-configs": "workspace:*", "@types/lodash": "4.17.7", - "@types/node": "20.14.14", + "@types/node": "22.10.2", "@types/prismjs": "1.26.4", "@types/react": "18.3.3", "@types/react-dom": "18.2.25", diff --git a/e2e/test-plugins/grafana-test-datasource/package.json b/e2e/test-plugins/grafana-test-datasource/package.json index 98f5bab46cf..519ba9f2731 100644 --- a/e2e/test-plugins/grafana-test-datasource/package.json +++ b/e2e/test-plugins/grafana-test-datasource/package.json @@ -13,7 +13,7 @@ "devDependencies": { "@grafana/plugin-configs": "11.5.0-pre", "@types/lodash": "4.17.7", - "@types/node": "20.14.14", + "@types/node": "22.10.2", "@types/prismjs": "1.26.4", "@types/react": "18.3.3", "@types/react-dom": "18.2.25", diff --git a/package.json b/package.json index 3d0dc685fd4..031790f1409 100644 --- a/package.json +++ b/package.json @@ -125,7 +125,7 @@ "@types/lodash": "4.17.13", "@types/logfmt": "^1.2.3", "@types/lucene": "^2", - "@types/node": "20.16.15", + "@types/node": "22.10.2", "@types/node-forge": "^1", "@types/ol-ext": "npm:@siedlerchr/types-ol-ext@3.2.4", "@types/pluralize": "^0.0.33", @@ -448,7 +448,7 @@ ] }, "engines": { - "node": ">= 20" + "node": ">= 22" }, "packageManager": "yarn@4.5.3", "dependenciesMeta": { diff --git a/packages/grafana-data/package.json b/packages/grafana-data/package.json index af55235aa81..bf294d4cf70 100644 --- a/packages/grafana-data/package.json +++ b/packages/grafana-data/package.json @@ -66,7 +66,7 @@ "@types/dompurify": "^3.0.0", "@types/history": "4.7.11", "@types/lodash": "4.17.13", - "@types/node": "20.16.15", + "@types/node": "22.10.2", "@types/papaparse": "5.3.15", "@types/react": "18.3.3", "@types/react-dom": "18.2.25", diff --git a/packages/grafana-e2e-selectors/package.json b/packages/grafana-e2e-selectors/package.json index 10d4d6d62df..67ed12e44c8 100644 --- a/packages/grafana-e2e-selectors/package.json +++ b/packages/grafana-e2e-selectors/package.json @@ -40,7 +40,7 @@ }, "devDependencies": { "@rollup/plugin-node-resolve": "15.3.0", - "@types/node": "20.16.15", + "@types/node": "22.10.2", "@types/semver": "7.5.8", "esbuild": "0.24.0", "rimraf": "6.0.1", diff --git a/packages/grafana-flamegraph/package.json b/packages/grafana-flamegraph/package.json index bd89eddf7f9..fb19a78a9b4 100644 --- a/packages/grafana-flamegraph/package.json +++ b/packages/grafana-flamegraph/package.json @@ -68,7 +68,7 @@ "@types/d3": "^7", "@types/jest": "^29.5.4", "@types/lodash": "4.17.13", - "@types/node": "20.16.15", + "@types/node": "22.10.2", "@types/react": "18.3.3", "@types/react-virtualized-auto-sizer": "1.0.4", "@types/tinycolor2": "1.4.6", diff --git a/packages/grafana-icons/package.json b/packages/grafana-icons/package.json index 3da9b39aeed..4b1f5bc29cb 100644 --- a/packages/grafana-icons/package.json +++ b/packages/grafana-icons/package.json @@ -45,7 +45,7 @@ "@svgr/plugin-prettier": "^8.1.0", "@svgr/plugin-svgo": "^8.1.0", "@types/babel__core": "^7", - "@types/node": "20.16.15", + "@types/node": "22.10.2", "@types/react": "18.3.3", "@types/react-dom": "18.2.25", "esbuild": "0.24.0", diff --git a/packages/grafana-o11y-ds-frontend/package.json b/packages/grafana-o11y-ds-frontend/package.json index eef5c567570..93a0668ecdb 100644 --- a/packages/grafana-o11y-ds-frontend/package.json +++ b/packages/grafana-o11y-ds-frontend/package.json @@ -36,7 +36,7 @@ "@testing-library/react": "16.0.1", "@testing-library/user-event": "14.5.2", "@types/jest": "^29.5.4", - "@types/node": "20.16.15", + "@types/node": "22.10.2", "@types/react": "18.3.3", "@types/systemjs": "6.15.1", "jest": "^29.6.4", diff --git a/packages/grafana-prometheus/package.json b/packages/grafana-prometheus/package.json index 5980575d5f2..f8ea06e3423 100644 --- a/packages/grafana-prometheus/package.json +++ b/packages/grafana-prometheus/package.json @@ -92,7 +92,7 @@ "@types/jest": "29.5.14", "@types/jquery": "3.5.32", "@types/lodash": "4.17.13", - "@types/node": "20.16.15", + "@types/node": "22.10.2", "@types/pluralize": "^0.0.33", "@types/prismjs": "1.26.5", "@types/react": "18.3.3", diff --git a/packages/grafana-sql/package.json b/packages/grafana-sql/package.json index c5b2888db46..d1d539a4a06 100644 --- a/packages/grafana-sql/package.json +++ b/packages/grafana-sql/package.json @@ -42,7 +42,7 @@ "@testing-library/user-event": "14.5.2", "@types/jest": "^29.5.4", "@types/lodash": "4.17.13", - "@types/node": "20.16.15", + "@types/node": "22.10.2", "@types/react": "18.3.3", "@types/react-dom": "18.2.25", "@types/react-virtualized-auto-sizer": "1.0.4", diff --git a/packages/grafana-ui/package.json b/packages/grafana-ui/package.json index 665db5f4d5f..d67a177f0d4 100644 --- a/packages/grafana-ui/package.json +++ b/packages/grafana-ui/package.json @@ -144,7 +144,7 @@ "@types/is-hotkey": "0.1.10", "@types/jest": "29.5.14", "@types/mock-raf": "1.0.6", - "@types/node": "20.16.15", + "@types/node": "22.10.2", "@types/prismjs": "1.26.5", "@types/react": "18.3.3", "@types/react-color": "3.0.12", diff --git a/public/app/plugins/datasource/azuremonitor/package.json b/public/app/plugins/datasource/azuremonitor/package.json index 76e07e77b51..d9b89b0c373 100644 --- a/public/app/plugins/datasource/azuremonitor/package.json +++ b/public/app/plugins/datasource/azuremonitor/package.json @@ -33,7 +33,7 @@ "@testing-library/user-event": "14.5.2", "@types/jest": "29.5.14", "@types/lodash": "4.17.13", - "@types/node": "20.16.15", + "@types/node": "22.10.2", "@types/prismjs": "1.26.5", "@types/react": "18.3.3", "@types/react-dom": "18.2.25", diff --git a/public/app/plugins/datasource/cloud-monitoring/package.json b/public/app/plugins/datasource/cloud-monitoring/package.json index 871b77b7cbe..fae06f0ce64 100644 --- a/public/app/plugins/datasource/cloud-monitoring/package.json +++ b/public/app/plugins/datasource/cloud-monitoring/package.json @@ -35,7 +35,7 @@ "@types/debounce-promise": "3.1.9", "@types/jest": "29.5.14", "@types/lodash": "4.17.13", - "@types/node": "20.16.15", + "@types/node": "22.10.2", "@types/prismjs": "1.26.5", "@types/react": "18.3.3", "@types/react-dom": "18.2.25", diff --git a/public/app/plugins/datasource/grafana-postgresql-datasource/package.json b/public/app/plugins/datasource/grafana-postgresql-datasource/package.json index 1cbbf04b7a9..c8dd82e5398 100644 --- a/public/app/plugins/datasource/grafana-postgresql-datasource/package.json +++ b/public/app/plugins/datasource/grafana-postgresql-datasource/package.json @@ -23,7 +23,7 @@ "@testing-library/user-event": "14.5.2", "@types/jest": "29.5.14", "@types/lodash": "4.17.13", - "@types/node": "20.16.15", + "@types/node": "22.10.2", "@types/react": "18.3.3", "ts-node": "10.9.2", "typescript": "5.5.4", diff --git a/public/app/plugins/datasource/grafana-pyroscope-datasource/package.json b/public/app/plugins/datasource/grafana-pyroscope-datasource/package.json index abea7a88eed..46381b60009 100644 --- a/public/app/plugins/datasource/grafana-pyroscope-datasource/package.json +++ b/public/app/plugins/datasource/grafana-pyroscope-datasource/package.json @@ -27,7 +27,7 @@ "@testing-library/user-event": "14.5.2", "@types/jest": "29.5.14", "@types/lodash": "4.17.13", - "@types/node": "20.16.15", + "@types/node": "22.10.2", "@types/prismjs": "1.26.5", "@types/react": "18.3.3", "@types/react-dom": "18.2.25", diff --git a/public/app/plugins/datasource/grafana-testdata-datasource/package.json b/public/app/plugins/datasource/grafana-testdata-datasource/package.json index 74696b23884..98d66817aeb 100644 --- a/public/app/plugins/datasource/grafana-testdata-datasource/package.json +++ b/public/app/plugins/datasource/grafana-testdata-datasource/package.json @@ -30,7 +30,7 @@ "@types/d3-random": "^3.0.2", "@types/jest": "29.5.14", "@types/lodash": "4.17.13", - "@types/node": "20.16.15", + "@types/node": "22.10.2", "@types/react": "18.3.3", "@types/react-dom": "18.2.25", "@types/uuid": "9.0.8", diff --git a/public/app/plugins/datasource/jaeger/package.json b/public/app/plugins/datasource/jaeger/package.json index ea8ce4d9a84..69c34d37e15 100644 --- a/public/app/plugins/datasource/jaeger/package.json +++ b/public/app/plugins/datasource/jaeger/package.json @@ -31,7 +31,7 @@ "@types/jest": "29.5.14", "@types/lodash": "4.17.13", "@types/logfmt": "^1.2.3", - "@types/node": "20.16.15", + "@types/node": "22.10.2", "@types/react": "18.3.3", "@types/react-dom": "18.2.25", "@types/react-window": "1.8.8", diff --git a/public/app/plugins/datasource/mssql/package.json b/public/app/plugins/datasource/mssql/package.json index c4ea78cfbbc..3fa83058650 100644 --- a/public/app/plugins/datasource/mssql/package.json +++ b/public/app/plugins/datasource/mssql/package.json @@ -23,7 +23,7 @@ "@testing-library/user-event": "14.5.2", "@types/jest": "29.5.14", "@types/lodash": "4.17.13", - "@types/node": "20.16.15", + "@types/node": "22.10.2", "@types/react": "18.3.3", "ts-node": "10.9.2", "typescript": "5.5.4", diff --git a/public/app/plugins/datasource/mysql/package.json b/public/app/plugins/datasource/mysql/package.json index b3bd70432d5..f4eba056126 100644 --- a/public/app/plugins/datasource/mysql/package.json +++ b/public/app/plugins/datasource/mysql/package.json @@ -23,7 +23,7 @@ "@testing-library/user-event": "14.5.2", "@types/jest": "29.5.14", "@types/lodash": "4.17.13", - "@types/node": "20.16.15", + "@types/node": "22.10.2", "@types/react": "18.3.3", "ts-node": "10.9.2", "typescript": "5.5.4", diff --git a/public/app/plugins/datasource/parca/package.json b/public/app/plugins/datasource/parca/package.json index 3657e81b193..2db8b77cd13 100644 --- a/public/app/plugins/datasource/parca/package.json +++ b/public/app/plugins/datasource/parca/package.json @@ -23,7 +23,7 @@ "@testing-library/react": "16.0.1", "@testing-library/user-event": "14.5.2", "@types/lodash": "4.17.13", - "@types/node": "20.16.15", + "@types/node": "22.10.2", "@types/react": "18.3.3", "@types/react-dom": "18.2.25", "ts-node": "10.9.2", diff --git a/public/app/plugins/datasource/tempo/package.json b/public/app/plugins/datasource/tempo/package.json index 68f3ebae996..bd64af43cc8 100644 --- a/public/app/plugins/datasource/tempo/package.json +++ b/public/app/plugins/datasource/tempo/package.json @@ -46,7 +46,7 @@ "@testing-library/user-event": "14.5.2", "@types/jest": "29.5.14", "@types/lodash": "4.17.13", - "@types/node": "20.16.15", + "@types/node": "22.10.2", "@types/prismjs": "1.26.5", "@types/react": "18.3.3", "@types/react-dom": "18.2.25", diff --git a/public/app/plugins/datasource/zipkin/package.json b/public/app/plugins/datasource/zipkin/package.json index 14cc700877a..27bb1b61f48 100644 --- a/public/app/plugins/datasource/zipkin/package.json +++ b/public/app/plugins/datasource/zipkin/package.json @@ -26,7 +26,7 @@ "@testing-library/react": "16.0.1", "@types/jest": "29.5.14", "@types/lodash": "4.17.13", - "@types/node": "20.16.15", + "@types/node": "22.10.2", "@types/react": "18.3.3", "@types/react-dom": "18.2.25", "ts-node": "10.9.2", diff --git a/scripts/drone/variables.star b/scripts/drone/variables.star index 1ab4fc7590d..bd9536ee040 100644 --- a/scripts/drone/variables.star +++ b/scripts/drone/variables.star @@ -6,4 +6,4 @@ grabpl_version = "v3.1.1" golang_version = "1.23.1" # nodejs_version should match what's in ".nvmrc", but without the v prefix. -nodejs_version = "20.9.0" +nodejs_version = "22.11.0" diff --git a/yarn.lock b/yarn.lock index 75f2246a467..6416fc3773a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2800,7 +2800,7 @@ __metadata: "@testing-library/user-event": "npm:14.5.2" "@types/jest": "npm:29.5.14" "@types/lodash": "npm:4.17.13" - "@types/node": "npm:20.16.15" + "@types/node": "npm:22.10.2" "@types/prismjs": "npm:1.26.5" "@types/react": "npm:18.3.3" "@types/react-dom": "npm:18.2.25" @@ -2842,7 +2842,7 @@ __metadata: "@testing-library/user-event": "npm:14.5.2" "@types/jest": "npm:29.5.14" "@types/lodash": "npm:4.17.13" - "@types/node": "npm:20.16.15" + "@types/node": "npm:22.10.2" "@types/react": "npm:18.3.3" lodash: "npm:4.17.21" react: "npm:18.2.0" @@ -2872,7 +2872,7 @@ __metadata: "@testing-library/user-event": "npm:14.5.2" "@types/jest": "npm:29.5.14" "@types/lodash": "npm:4.17.13" - "@types/node": "npm:20.16.15" + "@types/node": "npm:22.10.2" "@types/prismjs": "npm:1.26.5" "@types/react": "npm:18.3.3" "@types/react-dom": "npm:18.2.25" @@ -2914,7 +2914,7 @@ __metadata: "@types/d3-random": "npm:^3.0.2" "@types/jest": "npm:29.5.14" "@types/lodash": "npm:4.17.13" - "@types/node": "npm:20.16.15" + "@types/node": "npm:22.10.2" "@types/react": "npm:18.3.3" "@types/react-dom": "npm:18.2.25" "@types/uuid": "npm:9.0.8" @@ -2955,7 +2955,7 @@ __metadata: "@types/jest": "npm:29.5.14" "@types/lodash": "npm:4.17.13" "@types/logfmt": "npm:^1.2.3" - "@types/node": "npm:20.16.15" + "@types/node": "npm:22.10.2" "@types/react": "npm:18.3.3" "@types/react-dom": "npm:18.2.25" "@types/react-window": "npm:1.8.8" @@ -2995,7 +2995,7 @@ __metadata: "@testing-library/user-event": "npm:14.5.2" "@types/jest": "npm:29.5.14" "@types/lodash": "npm:4.17.13" - "@types/node": "npm:20.16.15" + "@types/node": "npm:22.10.2" "@types/react": "npm:18.3.3" lodash: "npm:4.17.21" react: "npm:18.2.0" @@ -3026,7 +3026,7 @@ __metadata: "@testing-library/user-event": "npm:14.5.2" "@types/jest": "npm:29.5.14" "@types/lodash": "npm:4.17.13" - "@types/node": "npm:20.16.15" + "@types/node": "npm:22.10.2" "@types/react": "npm:18.3.3" lodash: "npm:4.17.21" react: "npm:18.2.0" @@ -3054,7 +3054,7 @@ __metadata: "@testing-library/react": "npm:16.0.1" "@testing-library/user-event": "npm:14.5.2" "@types/lodash": "npm:4.17.13" - "@types/node": "npm:20.16.15" + "@types/node": "npm:22.10.2" "@types/react": "npm:18.3.3" "@types/react-dom": "npm:18.2.25" lodash: "npm:4.17.21" @@ -3092,7 +3092,7 @@ __metadata: "@types/debounce-promise": "npm:3.1.9" "@types/jest": "npm:29.5.14" "@types/lodash": "npm:4.17.13" - "@types/node": "npm:20.16.15" + "@types/node": "npm:22.10.2" "@types/prismjs": "npm:1.26.5" "@types/react": "npm:18.3.3" "@types/react-dom": "npm:18.2.25" @@ -3147,7 +3147,7 @@ __metadata: "@testing-library/user-event": "npm:14.5.2" "@types/jest": "npm:29.5.14" "@types/lodash": "npm:4.17.13" - "@types/node": "npm:20.16.15" + "@types/node": "npm:22.10.2" "@types/prismjs": "npm:1.26.5" "@types/react": "npm:18.3.3" "@types/react-dom": "npm:18.2.25" @@ -3197,7 +3197,7 @@ __metadata: "@testing-library/react": "npm:16.0.1" "@types/jest": "npm:29.5.14" "@types/lodash": "npm:4.17.13" - "@types/node": "npm:20.16.15" + "@types/node": "npm:22.10.2" "@types/react": "npm:18.3.3" "@types/react-dom": "npm:18.2.25" lodash: "npm:4.17.21" @@ -3256,7 +3256,7 @@ __metadata: "@types/dompurify": "npm:^3.0.0" "@types/history": "npm:4.7.11" "@types/lodash": "npm:4.17.13" - "@types/node": "npm:20.16.15" + "@types/node": "npm:22.10.2" "@types/papaparse": "npm:5.3.15" "@types/react": "npm:18.3.3" "@types/react-dom": "npm:18.2.25" @@ -3303,7 +3303,7 @@ __metadata: dependencies: "@grafana/tsconfig": "npm:^2.0.0" "@rollup/plugin-node-resolve": "npm:15.3.0" - "@types/node": "npm:20.16.15" + "@types/node": "npm:22.10.2" "@types/semver": "npm:7.5.8" esbuild: "npm:0.24.0" rimraf: "npm:6.0.1" @@ -3464,7 +3464,7 @@ __metadata: "@types/d3": "npm:^7" "@types/jest": "npm:^29.5.4" "@types/lodash": "npm:4.17.13" - "@types/node": "npm:20.16.15" + "@types/node": "npm:22.10.2" "@types/react": "npm:18.3.3" "@types/react-virtualized-auto-sizer": "npm:1.0.4" "@types/tinycolor2": "npm:1.4.6" @@ -3548,7 +3548,7 @@ __metadata: "@testing-library/react": "npm:16.0.1" "@testing-library/user-event": "npm:14.5.2" "@types/jest": "npm:^29.5.4" - "@types/node": "npm:20.16.15" + "@types/node": "npm:22.10.2" "@types/react": "npm:18.3.3" "@types/systemjs": "npm:6.15.1" jest: "npm:^29.6.4" @@ -3638,7 +3638,7 @@ __metadata: "@types/jest": "npm:29.5.14" "@types/jquery": "npm:3.5.32" "@types/lodash": "npm:4.17.13" - "@types/node": "npm:20.16.15" + "@types/node": "npm:22.10.2" "@types/pluralize": "npm:^0.0.33" "@types/prismjs": "npm:1.26.5" "@types/react": "npm:18.3.3" @@ -3770,7 +3770,7 @@ __metadata: "@svgr/plugin-prettier": "npm:^8.1.0" "@svgr/plugin-svgo": "npm:^8.1.0" "@types/babel__core": "npm:^7" - "@types/node": "npm:20.16.15" + "@types/node": "npm:22.10.2" "@types/react": "npm:18.3.3" "@types/react-dom": "npm:18.2.25" esbuild: "npm:0.24.0" @@ -3869,7 +3869,7 @@ __metadata: "@testing-library/user-event": "npm:14.5.2" "@types/jest": "npm:^29.5.4" "@types/lodash": "npm:4.17.13" - "@types/node": "npm:20.16.15" + "@types/node": "npm:22.10.2" "@types/react": "npm:18.3.3" "@types/react-dom": "npm:18.2.25" "@types/react-virtualized-auto-sizer": "npm:1.0.4" @@ -3956,7 +3956,7 @@ __metadata: "@types/jquery": "npm:3.5.32" "@types/lodash": "npm:4.17.13" "@types/mock-raf": "npm:1.0.6" - "@types/node": "npm:20.16.15" + "@types/node": "npm:22.10.2" "@types/prismjs": "npm:1.26.5" "@types/react": "npm:18.3.3" "@types/react-color": "npm:3.0.12" @@ -8409,7 +8409,7 @@ __metadata: "@grafana/schema": "workspace:*" "@grafana/ui": "workspace:*" "@types/lodash": "npm:4.17.7" - "@types/node": "npm:20.14.14" + "@types/node": "npm:22.10.2" "@types/prismjs": "npm:1.26.4" "@types/react": "npm:18.3.3" "@types/react-dom": "npm:18.2.25" @@ -8441,7 +8441,7 @@ __metadata: "@grafana/schema": "workspace:*" "@grafana/ui": "workspace:*" "@types/lodash": "npm:4.17.7" - "@types/node": "npm:20.14.14" + "@types/node": "npm:22.10.2" "@types/prismjs": "npm:1.26.4" "@types/react": "npm:18.3.3" "@types/react-dom": "npm:18.2.25" @@ -9511,7 +9511,25 @@ __metadata: languageName: node linkType: hard -"@types/node@npm:*, @types/node@npm:>=10.0.0, @types/node@npm:>=13.7.0, @types/node@npm:>=13.7.4, @types/node@npm:^22.0.0": +"@types/node@npm:*, @types/node@npm:>=13.7.0, @types/node@npm:^22.0.0": + version: 22.9.0 + resolution: "@types/node@npm:22.9.0" + dependencies: + undici-types: "npm:~6.19.8" + checksum: 10/a7df3426891868b0f5fb03e46aeddd8446178233521c624a44531c92a040cf08a82d8235f7e1e02af731fd16984665d4d71f3418caf9c2788313b10f040d615d + languageName: node + linkType: hard + +"@types/node@npm:22.10.2": + version: 22.10.2 + resolution: "@types/node@npm:22.10.2" + dependencies: + undici-types: "npm:~6.20.0" + checksum: 10/451adfefed4add58b069407173e616220fd4aaa3307cdde1bb701aa053b65b54ced8483db2f870dcedec7a58cb3b06101fbc19d85852716672ec1fd3660947fa + languageName: node + linkType: hard + +"@types/node@npm:>=10.0.0, @types/node@npm:>=13.7.4": version: 22.10.1 resolution: "@types/node@npm:22.10.1" dependencies: @@ -9520,24 +9538,6 @@ __metadata: languageName: node linkType: hard -"@types/node@npm:20.14.14": - version: 20.14.14 - resolution: "@types/node@npm:20.14.14" - dependencies: - undici-types: "npm:~5.26.4" - checksum: 10/035bc347e3de04888d537801e23eb4b4f99522975ca002dbfef978edd853710031b7cd43bf022670d6aba4ed5d4ac75ea1b5ff77ff8f80998bffd943b7bcef48 - languageName: node - linkType: hard - -"@types/node@npm:20.16.15": - version: 20.16.15 - resolution: "@types/node@npm:20.16.15" - dependencies: - undici-types: "npm:~6.19.2" - checksum: 10/f4706676fdff05c7fb188399da578bbcce9260f889cfd767f1c3ee4fb33c369eb1ffa24161651ea3ef5b6a33e2e9b66f393949cab2be77d019277a1e16a7af36 - languageName: node - linkType: hard - "@types/nodemailer@npm:*": version: 6.4.15 resolution: "@types/nodemailer@npm:6.4.15" @@ -17646,7 +17646,7 @@ __metadata: "@types/lodash": "npm:4.17.13" "@types/logfmt": "npm:^1.2.3" "@types/lucene": "npm:^2" - "@types/node": "npm:20.16.15" + "@types/node": "npm:22.10.2" "@types/node-forge": "npm:^1" "@types/ol-ext": "npm:@siedlerchr/types-ol-ext@3.2.4" "@types/pluralize": "npm:^0.0.33" @@ -29808,14 +29808,7 @@ __metadata: languageName: node linkType: hard -"undici-types@npm:~5.26.4": - version: 5.26.5 - resolution: "undici-types@npm:5.26.5" - checksum: 10/0097779d94bc0fd26f0418b3a05472410408877279141ded2bd449167be1aed7ea5b76f756562cb3586a07f251b90799bab22d9019ceba49c037c76445f7cddd - languageName: node - linkType: hard - -"undici-types@npm:~6.19.2": +"undici-types@npm:~6.19.8": version: 6.19.8 resolution: "undici-types@npm:6.19.8" checksum: 10/cf0b48ed4fc99baf56584afa91aaffa5010c268b8842f62e02f752df209e3dea138b372a60a963b3b2576ed932f32329ce7ddb9cb5f27a6c83040d8cd74b7a70 From ee661ac54b32d754a9aa163487f98528fa303029 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 12 Dec 2024 16:43:28 +0000 Subject: [PATCH 05/28] Update React Aria (#96960) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package.json | 8 +- packages/grafana-ui/package.json | 8 +- yarn.lock | 244 +++++++++++++++---------------- 3 files changed, 130 insertions(+), 130 deletions(-) diff --git a/package.json b/package.json index 031790f1409..5d15a5d7c14 100644 --- a/package.json +++ b/package.json @@ -295,10 +295,10 @@ "@opentelemetry/exporter-collector": "0.25.0", "@opentelemetry/semantic-conventions": "1.27.0", "@popperjs/core": "2.11.8", - "@react-aria/dialog": "3.5.19", - "@react-aria/focus": "3.18.4", - "@react-aria/overlays": "3.23.4", - "@react-aria/utils": "3.25.3", + "@react-aria/dialog": "3.5.20", + "@react-aria/focus": "3.19.0", + "@react-aria/overlays": "3.24.0", + "@react-aria/utils": "3.26.0", "@react-awesome-query-builder/ui": "6.6.4", "@reduxjs/toolkit": "2.3.0", "@testing-library/react-hooks": "^8.0.1", diff --git a/packages/grafana-ui/package.json b/packages/grafana-ui/package.json index d67a177f0d4..bd49d1cfd9a 100644 --- a/packages/grafana-ui/package.json +++ b/packages/grafana-ui/package.json @@ -58,10 +58,10 @@ "@leeoniya/ufuzzy": "1.0.17", "@monaco-editor/react": "4.6.0", "@popperjs/core": "2.11.8", - "@react-aria/dialog": "3.5.19", - "@react-aria/focus": "3.18.4", - "@react-aria/overlays": "3.23.4", - "@react-aria/utils": "3.25.3", + "@react-aria/dialog": "3.5.20", + "@react-aria/focus": "3.19.0", + "@react-aria/overlays": "3.24.0", + "@react-aria/utils": "3.26.0", "@tanstack/react-virtual": "^3.5.1", "@types/jquery": "3.5.32", "@types/lodash": "4.17.13", diff --git a/yarn.lock b/yarn.lock index 6416fc3773a..a31260c3029 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3921,10 +3921,10 @@ __metadata: "@leeoniya/ufuzzy": "npm:1.0.17" "@monaco-editor/react": "npm:4.6.0" "@popperjs/core": "npm:2.11.8" - "@react-aria/dialog": "npm:3.5.19" - "@react-aria/focus": "npm:3.18.4" - "@react-aria/overlays": "npm:3.23.4" - "@react-aria/utils": "npm:3.25.3" + "@react-aria/dialog": "npm:3.5.20" + "@react-aria/focus": "npm:3.19.0" + "@react-aria/overlays": "npm:3.24.0" + "@react-aria/utils": "npm:3.26.0" "@rollup/plugin-node-resolve": "npm:15.3.0" "@storybook/addon-a11y": "npm:^8.4.2" "@storybook/addon-actions": "npm:^8.4.2" @@ -4184,40 +4184,40 @@ __metadata: languageName: node linkType: hard -"@internationalized/date@npm:^3.5.6": - version: 3.5.6 - resolution: "@internationalized/date@npm:3.5.6" +"@internationalized/date@npm:^3.6.0": + version: 3.6.0 + resolution: "@internationalized/date@npm:3.6.0" dependencies: "@swc/helpers": "npm:^0.5.0" - checksum: 10/54734b53ca74a32aae368a8f963324352b1fd5b13029b6e82555307b8f2ff355658c90e82a4f38f154a3edf874387d1efd26fc80f2edd068ce04f48f6467f26c + checksum: 10/862a34376ff1e36da0afa4a667b62183d7a480eb7c5b919b0a6e1450f4162bc8a6c8941e073a4fb0c6fce6d9420894051235f9e604ee1162fae7b59972a64d88 languageName: node linkType: hard -"@internationalized/message@npm:^3.1.5": - version: 3.1.5 - resolution: "@internationalized/message@npm:3.1.5" +"@internationalized/message@npm:^3.1.6": + version: 3.1.6 + resolution: "@internationalized/message@npm:3.1.6" dependencies: "@swc/helpers": "npm:^0.5.0" intl-messageformat: "npm:^10.1.0" - checksum: 10/210951fd8055af4db70d465e49bcbbdf2545ed223b936af9c1f18b745a51689ecb0ca49cbd5ee2dbfeccce2447808b7fe309bd12ee81f7e09283f20bf04200e9 + checksum: 10/31611a81bba7c0b18fad02ccb363632cbef3a0cc6e6e564578dfc7ae42cfde48b0ec18b89bff24390bb748df63d40cd2028483068e7d1492aee0533648a71e04 languageName: node linkType: hard -"@internationalized/number@npm:^3.5.4": - version: 3.5.4 - resolution: "@internationalized/number@npm:3.5.4" +"@internationalized/number@npm:^3.6.0": + version: 3.6.0 + resolution: "@internationalized/number@npm:3.6.0" dependencies: "@swc/helpers": "npm:^0.5.0" - checksum: 10/16641aecb58c075a6322dc6b36a2c6e521845296f81b86a128d015f072d1af998289b71b4d8b9521e7576bdeabfaf8067a3e741b0116c8595d82a4461c1ae03b + checksum: 10/76428f75fd289008580108beef02816f94aaca37d48c9e26c2274fdbb81e8f32c4247ff624b8f854384b18ce2556cc8d817ccf1bc39b0fd87b0da1f3d911209b languageName: node linkType: hard -"@internationalized/string@npm:^3.2.4": - version: 3.2.4 - resolution: "@internationalized/string@npm:3.2.4" +"@internationalized/string@npm:^3.2.5": + version: 3.2.5 + resolution: "@internationalized/string@npm:3.2.5" dependencies: "@swc/helpers": "npm:^0.5.0" - checksum: 10/5fdb7f0bf7fa7055cdf62ded4efd6849d3db9cf0e6d53f349889e2ec9517b9135ad38a6bb8dcf25142c69c381618c0dd1a6a072117dd7cf2867ce17374f0f835 + checksum: 10/7a20359796545cd5724aa546018554b81ea168c58738e98884c54ef0de2fba7b802504c79678d690709b0a435478912aa771c426feed72aae1d93ee782b7ac5d languageName: node linkType: hard @@ -6270,129 +6270,129 @@ __metadata: languageName: node linkType: hard -"@react-aria/dialog@npm:3.5.19": - version: 3.5.19 - resolution: "@react-aria/dialog@npm:3.5.19" +"@react-aria/dialog@npm:3.5.20": + version: 3.5.20 + resolution: "@react-aria/dialog@npm:3.5.20" dependencies: - "@react-aria/focus": "npm:^3.18.4" - "@react-aria/overlays": "npm:^3.23.4" - "@react-aria/utils": "npm:^3.25.3" - "@react-types/dialog": "npm:^3.5.13" - "@react-types/shared": "npm:^3.25.0" + "@react-aria/focus": "npm:^3.19.0" + "@react-aria/overlays": "npm:^3.24.0" + "@react-aria/utils": "npm:^3.26.0" + "@react-types/dialog": "npm:^3.5.14" + "@react-types/shared": "npm:^3.26.0" "@swc/helpers": "npm:^0.5.0" peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0 - react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0 - checksum: 10/44eba70f927cbdb003da37021ee6dde363abfa3c179437d83e19c72931c0fab12db20dbd603578d372df534ddce119e22b6908935544ce7a696523aa71b72e2e + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + checksum: 10/a3e18df7495fc659d46c0ae61ae6addcd55fc6e512f0562c3e31d9e43cb5a95f7f49f6e840894b594839533681701606af804e70ff4c4b22ac4c7f0224f3c817 languageName: node linkType: hard -"@react-aria/focus@npm:3.18.4, @react-aria/focus@npm:^3.18.4": - version: 3.18.4 - resolution: "@react-aria/focus@npm:3.18.4" +"@react-aria/focus@npm:3.19.0, @react-aria/focus@npm:^3.19.0": + version: 3.19.0 + resolution: "@react-aria/focus@npm:3.19.0" dependencies: - "@react-aria/interactions": "npm:^3.22.4" - "@react-aria/utils": "npm:^3.25.3" - "@react-types/shared": "npm:^3.25.0" + "@react-aria/interactions": "npm:^3.22.5" + "@react-aria/utils": "npm:^3.26.0" + "@react-types/shared": "npm:^3.26.0" "@swc/helpers": "npm:^0.5.0" clsx: "npm:^2.0.0" peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0 - checksum: 10/912cd8a98cbe978240991bec8077c7956ca03ee78cb10152c7a1131a53fb622a5c9b87a4047909f032a7550c37ed9ec50488437a17c761c5c852b721cbaa0bd2 + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + checksum: 10/4b507c382ce83b698637464094257af23abdfa1a096486cdf898d0e8db120dcd87035b59dab8855180cc1671e89d410b8812936100a1502ead01f80b52dfa4e9 languageName: node linkType: hard -"@react-aria/i18n@npm:^3.12.3": - version: 3.12.3 - resolution: "@react-aria/i18n@npm:3.12.3" +"@react-aria/i18n@npm:^3.12.4": + version: 3.12.4 + resolution: "@react-aria/i18n@npm:3.12.4" dependencies: - "@internationalized/date": "npm:^3.5.6" - "@internationalized/message": "npm:^3.1.5" - "@internationalized/number": "npm:^3.5.4" - "@internationalized/string": "npm:^3.2.4" - "@react-aria/ssr": "npm:^3.9.6" - "@react-aria/utils": "npm:^3.25.3" - "@react-types/shared": "npm:^3.25.0" + "@internationalized/date": "npm:^3.6.0" + "@internationalized/message": "npm:^3.1.6" + "@internationalized/number": "npm:^3.6.0" + "@internationalized/string": "npm:^3.2.5" + "@react-aria/ssr": "npm:^3.9.7" + "@react-aria/utils": "npm:^3.26.0" + "@react-types/shared": "npm:^3.26.0" "@swc/helpers": "npm:^0.5.0" peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0 - checksum: 10/54f111d9a9da68edcb8b821f7c8ead92f0c4d85307dbabee78bc5c89f5a19cdfa406b1e40b7c6f9dc26f7cedce4c9c5a10f8dcdae289e5a404c07b6fdda98aba + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + checksum: 10/3a62d641ed23b96378d82f252cecf17071ca842a2d964e35c9764a7ad1f7f4815472e8f9bf54fe094d15e81f3fb4b982b2b2949298f79ef1a7e2e6b538e1a12a languageName: node linkType: hard -"@react-aria/interactions@npm:^3.22.4": - version: 3.22.4 - resolution: "@react-aria/interactions@npm:3.22.4" +"@react-aria/interactions@npm:^3.22.5": + version: 3.22.5 + resolution: "@react-aria/interactions@npm:3.22.5" dependencies: - "@react-aria/ssr": "npm:^3.9.6" - "@react-aria/utils": "npm:^3.25.3" - "@react-types/shared": "npm:^3.25.0" + "@react-aria/ssr": "npm:^3.9.7" + "@react-aria/utils": "npm:^3.26.0" + "@react-types/shared": "npm:^3.26.0" "@swc/helpers": "npm:^0.5.0" peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0 - checksum: 10/095d084bd642b47a5cc2a846fa50e0953682ddcad694cc78df344b1f235e292945746692f84d27f465f7ff0117b485c3f5b69f050be196df0c3e7343d3239551 + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + checksum: 10/058fbbfd295471bde94cbe6501d5ebb64d1371c455e43c8c55daf6edd02983f73eb1eb716dd03b96c275aed26fa16a355a35f3daffefc54904576d5d26b4458e languageName: node linkType: hard -"@react-aria/overlays@npm:3.23.4, @react-aria/overlays@npm:^3.23.4": - version: 3.23.4 - resolution: "@react-aria/overlays@npm:3.23.4" +"@react-aria/overlays@npm:3.24.0, @react-aria/overlays@npm:^3.24.0": + version: 3.24.0 + resolution: "@react-aria/overlays@npm:3.24.0" dependencies: - "@react-aria/focus": "npm:^3.18.4" - "@react-aria/i18n": "npm:^3.12.3" - "@react-aria/interactions": "npm:^3.22.4" - "@react-aria/ssr": "npm:^3.9.6" - "@react-aria/utils": "npm:^3.25.3" - "@react-aria/visually-hidden": "npm:^3.8.17" - "@react-stately/overlays": "npm:^3.6.11" - "@react-types/button": "npm:^3.10.0" - "@react-types/overlays": "npm:^3.8.10" - "@react-types/shared": "npm:^3.25.0" + "@react-aria/focus": "npm:^3.19.0" + "@react-aria/i18n": "npm:^3.12.4" + "@react-aria/interactions": "npm:^3.22.5" + "@react-aria/ssr": "npm:^3.9.7" + "@react-aria/utils": "npm:^3.26.0" + "@react-aria/visually-hidden": "npm:^3.8.18" + "@react-stately/overlays": "npm:^3.6.12" + "@react-types/button": "npm:^3.10.1" + "@react-types/overlays": "npm:^3.8.11" + "@react-types/shared": "npm:^3.26.0" "@swc/helpers": "npm:^0.5.0" peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0 - react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0 - checksum: 10/6f641a1f7c1976758dd062c4345d2e8882575e2b645a4e4693f051699bae48c48979db3539e77c387aee64600d7a555b94687cd01449094cf875ce80ec6aa2ed + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + checksum: 10/32bb00a435af82dfb57561737082fb0ac2361954fe217be25c84011bdaf23e683201b0142ba3d1137c90be713894b94c68ae8db4dc269cb09f449673ffa7dcb6 languageName: node linkType: hard -"@react-aria/ssr@npm:^3.9.6": - version: 3.9.6 - resolution: "@react-aria/ssr@npm:3.9.6" +"@react-aria/ssr@npm:^3.9.7": + version: 3.9.7 + resolution: "@react-aria/ssr@npm:3.9.7" dependencies: "@swc/helpers": "npm:^0.5.0" peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0 - checksum: 10/ea6b290346ce1e119ed9233fc0e34693d52ab9dc2509f07ab10710409b89484a544b7f26c1438802e97f3fb634844ae54638850cdd95caca0d1f5571781bf982 + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + checksum: 10/a5c8e9ffee1dfd3c5b9f66051a7faab11d53ba001ac7f476b61fa4b38fd8b4835c1a85ff2157ec25fb5b63beb88fbae9e80610fa065a30cbe30875fcbca3114b languageName: node linkType: hard -"@react-aria/utils@npm:3.25.3, @react-aria/utils@npm:^3.25.3": - version: 3.25.3 - resolution: "@react-aria/utils@npm:3.25.3" +"@react-aria/utils@npm:3.26.0, @react-aria/utils@npm:^3.26.0": + version: 3.26.0 + resolution: "@react-aria/utils@npm:3.26.0" dependencies: - "@react-aria/ssr": "npm:^3.9.6" - "@react-stately/utils": "npm:^3.10.4" - "@react-types/shared": "npm:^3.25.0" + "@react-aria/ssr": "npm:^3.9.7" + "@react-stately/utils": "npm:^3.10.5" + "@react-types/shared": "npm:^3.26.0" "@swc/helpers": "npm:^0.5.0" clsx: "npm:^2.0.0" peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0 - checksum: 10/86aed35da5cb0d48d949e40bf8226d5a6d6c92a8cdc60e3e12d524d1f3cc91ab6b54c5e1642823773cbb889fb61af7da22e89488b704b56fc5f4d8d59da7519b + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + checksum: 10/484cfbc553b3313d291a78bbfd3fcc435ce36b7fc5408935be027f22f41e73b69306d67aa0bcf5e010f0059d4a6776217110f95012001699fc9d37da54963c7b languageName: node linkType: hard -"@react-aria/visually-hidden@npm:^3.8.17": - version: 3.8.17 - resolution: "@react-aria/visually-hidden@npm:3.8.17" +"@react-aria/visually-hidden@npm:^3.8.18": + version: 3.8.18 + resolution: "@react-aria/visually-hidden@npm:3.8.18" dependencies: - "@react-aria/interactions": "npm:^3.22.4" - "@react-aria/utils": "npm:^3.25.3" - "@react-types/shared": "npm:^3.25.0" + "@react-aria/interactions": "npm:^3.22.5" + "@react-aria/utils": "npm:^3.26.0" + "@react-types/shared": "npm:^3.26.0" "@swc/helpers": "npm:^0.5.0" peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0 - checksum: 10/d9cdc97d80e750f4582cc3f2bcc0bde12b6c70fbd408ca9ebba2ef654ab27b0181cdd072a8b30ae36f14d9ec16a79c2d38d7cbe834c96fad693daebc7be41616 + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + checksum: 10/4f068dbeeaf3751c526bf7d1467d9eeca43a6e665d5e9245b3556e9c5a43f28eb3c14ff56fffeb0eecbf6282eae0cc17824249bfba9199d97d23374ee733e82e languageName: node linkType: hard @@ -6430,31 +6430,31 @@ __metadata: languageName: node linkType: hard -"@react-stately/overlays@npm:^3.6.11": - version: 3.6.11 - resolution: "@react-stately/overlays@npm:3.6.11" +"@react-stately/overlays@npm:^3.6.12": + version: 3.6.12 + resolution: "@react-stately/overlays@npm:3.6.12" dependencies: - "@react-stately/utils": "npm:^3.10.4" - "@react-types/overlays": "npm:^3.8.10" + "@react-stately/utils": "npm:^3.10.5" + "@react-types/overlays": "npm:^3.8.11" "@swc/helpers": "npm:^0.5.0" peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0 - checksum: 10/98190b4b0ced5c94d924cf97b5d43a6e28f68aa44de7bb789c20354f30f00309c86089fb6948b5ec9d09f01605b5a412fb246545b7ee9bc34e3183e7261a2805 + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + checksum: 10/78c448964fea85d148a6d45c723d26df765ca049445077b85a318b1a10f1b888d543318a251ff80f28e9109da2c722160be4657eae5dae0c2865f24039fecb11 languageName: node linkType: hard -"@react-stately/utils@npm:^3.10.4": - version: 3.10.4 - resolution: "@react-stately/utils@npm:3.10.4" +"@react-stately/utils@npm:^3.10.5": + version: 3.10.5 + resolution: "@react-stately/utils@npm:3.10.5" dependencies: "@swc/helpers": "npm:^0.5.0" peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0 - checksum: 10/8a56b4d0cf8d5a7a692d6f94ffff63feac2d7078fbc5642b94b0afcaaf7c8f7f4682cfe546f98265034c52576c198be5502cff3f9b145137884e50eb9ffb96d5 + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + checksum: 10/76133eb64fa945216e51d8a81a0ebd06eeb78aa2d9c91d79eeb80ff44c70a6b0d6d940618b31b499fee8216640b3bf6183391151dc1769e756b56ff6b5e167ec languageName: node linkType: hard -"@react-types/button@npm:3.10.1, @react-types/button@npm:^3.10.0": +"@react-types/button@npm:3.10.1, @react-types/button@npm:^3.10.1": version: 3.10.1 resolution: "@react-types/button@npm:3.10.1" dependencies: @@ -6465,15 +6465,15 @@ __metadata: languageName: node linkType: hard -"@react-types/dialog@npm:^3.5.13": - version: 3.5.13 - resolution: "@react-types/dialog@npm:3.5.13" +"@react-types/dialog@npm:^3.5.14": + version: 3.5.14 + resolution: "@react-types/dialog@npm:3.5.14" dependencies: - "@react-types/overlays": "npm:^3.8.10" - "@react-types/shared": "npm:^3.25.0" + "@react-types/overlays": "npm:^3.8.11" + "@react-types/shared": "npm:^3.26.0" peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0 - checksum: 10/c7ee923c326b7377660400ec794ef98330388250aa32df23f5c22a63e483bcad221283de26181bf2a5cab2c20d517f528bf351c9786ac9fece3e3726e4ae07c3 + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + checksum: 10/4339a86c862832860eeb9be102a77cd1119d1ef968bb51603d8d789ee26404a28fde047203c4b579ee0ba2975cf36a8991cfc750449222c0fa150eed84464499 languageName: node linkType: hard @@ -6489,7 +6489,7 @@ __metadata: languageName: node linkType: hard -"@react-types/overlays@npm:3.8.11, @react-types/overlays@npm:^3.8.10, @react-types/overlays@npm:^3.8.11": +"@react-types/overlays@npm:3.8.11, @react-types/overlays@npm:^3.8.11": version: 3.8.11 resolution: "@react-types/overlays@npm:3.8.11" dependencies: @@ -6509,7 +6509,7 @@ __metadata: languageName: node linkType: hard -"@react-types/shared@npm:^3.25.0, @react-types/shared@npm:^3.26.0": +"@react-types/shared@npm:^3.26.0": version: 3.26.0 resolution: "@react-types/shared@npm:3.26.0" peerDependencies: @@ -17599,10 +17599,10 @@ __metadata: "@playwright/test": "npm:1.48.2" "@pmmmwh/react-refresh-webpack-plugin": "npm:0.5.15" "@popperjs/core": "npm:2.11.8" - "@react-aria/dialog": "npm:3.5.19" - "@react-aria/focus": "npm:3.18.4" - "@react-aria/overlays": "npm:3.23.4" - "@react-aria/utils": "npm:3.25.3" + "@react-aria/dialog": "npm:3.5.20" + "@react-aria/focus": "npm:3.19.0" + "@react-aria/overlays": "npm:3.24.0" + "@react-aria/utils": "npm:3.26.0" "@react-awesome-query-builder/ui": "npm:6.6.4" "@react-types/button": "npm:3.10.1" "@react-types/menu": "npm:3.9.13" From 4521cfb28c251acfc665c3d7bc790e853758e62d Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 12 Dec 2024 16:44:13 +0000 Subject: [PATCH 06/28] Update dependency @babel/preset-react to v7.26.3 (#97677) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- packages/grafana-flamegraph/package.json | 2 +- yarn.lock | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/grafana-flamegraph/package.json b/packages/grafana-flamegraph/package.json index fb19a78a9b4..d6e113243e8 100644 --- a/packages/grafana-flamegraph/package.json +++ b/packages/grafana-flamegraph/package.json @@ -58,7 +58,7 @@ "devDependencies": { "@babel/core": "7.26.0", "@babel/preset-env": "7.26.0", - "@babel/preset-react": "7.25.9", + "@babel/preset-react": "7.26.3", "@grafana/tsconfig": "^2.0.0", "@rollup/plugin-node-resolve": "15.3.0", "@testing-library/dom": "10.4.0", diff --git a/yarn.lock b/yarn.lock index a31260c3029..5d9c624f9ef 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1412,9 +1412,9 @@ __metadata: languageName: node linkType: hard -"@babel/preset-react@npm:7.25.9": - version: 7.25.9 - resolution: "@babel/preset-react@npm:7.25.9" +"@babel/preset-react@npm:7.26.3": + version: 7.26.3 + resolution: "@babel/preset-react@npm:7.26.3" dependencies: "@babel/helper-plugin-utils": "npm:^7.25.9" "@babel/helper-validator-option": "npm:^7.25.9" @@ -1424,7 +1424,7 @@ __metadata: "@babel/plugin-transform-react-pure-annotations": "npm:^7.25.9" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10/3c9daf47cf51568d96984d21b9f83992590c0e91f16a333f999100bb3c2c200730cde6806ed37fd2c999e0a63becefc881740b8f765b5a4aff4efc674e3e4197 + checksum: 10/88cb78c402b79f32389ee06451da51698d5b1da7641d9a47482883f537fe5441a138bd4c077d8533fd6d557406b08911c47b94402cea843db598e020bdd9a373 languageName: node linkType: hard @@ -3450,7 +3450,7 @@ __metadata: dependencies: "@babel/core": "npm:7.26.0" "@babel/preset-env": "npm:7.26.0" - "@babel/preset-react": "npm:7.25.9" + "@babel/preset-react": "npm:7.26.3" "@emotion/css": "npm:11.13.5" "@grafana/data": "npm:11.5.0-pre" "@grafana/tsconfig": "npm:^2.0.0" From c05547ba7e36e99161259dd6207986905318f6f1 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 12 Dec 2024 16:44:48 +0000 Subject: [PATCH 07/28] Update dependency @emotion/react to v11.14.0 (#97752) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package.json | 2 +- packages/grafana-ui/package.json | 2 +- yarn.lock | 32 ++++++++++++++++---------------- 3 files changed, 18 insertions(+), 18 deletions(-) diff --git a/package.json b/package.json index 5d15a5d7c14..a94dcf98b91 100644 --- a/package.json +++ b/package.json @@ -254,7 +254,7 @@ "dependencies": { "@bsull/augurs": "^0.7.0", "@emotion/css": "11.13.5", - "@emotion/react": "11.13.5", + "@emotion/react": "11.14.0", "@fingerprintjs/fingerprintjs": "^3.4.2", "@floating-ui/react": "0.26.28", "@formatjs/intl-durationformat": "^0.6.0", diff --git a/packages/grafana-ui/package.json b/packages/grafana-ui/package.json index bd49d1cfd9a..bbf9e7513d2 100644 --- a/packages/grafana-ui/package.json +++ b/packages/grafana-ui/package.json @@ -47,7 +47,7 @@ ], "dependencies": { "@emotion/css": "11.13.5", - "@emotion/react": "11.13.5", + "@emotion/react": "11.14.0", "@emotion/serialize": "1.3.3", "@floating-ui/react": "0.26.28", "@grafana/data": "11.5.0-pre", diff --git a/yarn.lock b/yarn.lock index 5d9c624f9ef..f5dc7e17c99 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1858,16 +1858,16 @@ __metadata: languageName: node linkType: hard -"@emotion/cache@npm:^11.11.0, @emotion/cache@npm:^11.13.5, @emotion/cache@npm:^11.4.0": - version: 11.13.5 - resolution: "@emotion/cache@npm:11.13.5" +"@emotion/cache@npm:^11.11.0, @emotion/cache@npm:^11.13.5, @emotion/cache@npm:^11.14.0, @emotion/cache@npm:^11.4.0": + version: 11.14.0 + resolution: "@emotion/cache@npm:11.14.0" dependencies: "@emotion/memoize": "npm:^0.9.0" "@emotion/sheet": "npm:^1.4.0" "@emotion/utils": "npm:^1.4.2" "@emotion/weak-memoize": "npm:^0.4.0" stylis: "npm:4.2.0" - checksum: 10/d91139453d279cfd6f6f38180d3af2fdcee8c0fc6d9a6faa2cdce9a1211b294a8019ef45365bf1171e0687d1744a70ff760637b88ed46f7a9fe74db9dc36f4df + checksum: 10/52336b28a27b07dde8fcdfd80851cbd1487672bbd4db1e24cca1440c95d8a6a968c57b0453c2b7c88d9b432b717f99554dbecc05b5cdef27933299827e69fd8e languageName: node linkType: hard @@ -1938,15 +1938,15 @@ __metadata: languageName: node linkType: hard -"@emotion/react@npm:11.13.5, @emotion/react@npm:^11.8.1": - version: 11.13.5 - resolution: "@emotion/react@npm:11.13.5" +"@emotion/react@npm:11.14.0, @emotion/react@npm:^11.8.1": + version: 11.14.0 + resolution: "@emotion/react@npm:11.14.0" dependencies: "@babel/runtime": "npm:^7.18.3" "@emotion/babel-plugin": "npm:^11.13.5" - "@emotion/cache": "npm:^11.13.5" + "@emotion/cache": "npm:^11.14.0" "@emotion/serialize": "npm:^1.3.3" - "@emotion/use-insertion-effect-with-fallbacks": "npm:^1.1.0" + "@emotion/use-insertion-effect-with-fallbacks": "npm:^1.2.0" "@emotion/utils": "npm:^1.4.2" "@emotion/weak-memoize": "npm:^0.4.0" hoist-non-react-statics: "npm:^3.3.1" @@ -1955,7 +1955,7 @@ __metadata: peerDependenciesMeta: "@types/react": optional: true - checksum: 10/c85bde9c6144be6f243e44d6834b16df8151b91c5136ea28d9d917155d64d1bbfba000ae98866fd9f5089546f0b0655c20b9e4280cdc5ccf39acc4a2fde37a39 + checksum: 10/3356c1d66f37f4e7abf88a2be843f6023b794b286c9c99a0aaf1cd1b2b7c50f8d80a2ef77183da737de70150f638e698ff4a2a38ab2d922f868615f1d5761c37 languageName: node linkType: hard @@ -1986,12 +1986,12 @@ __metadata: languageName: node linkType: hard -"@emotion/use-insertion-effect-with-fallbacks@npm:^1.1.0": - version: 1.1.0 - resolution: "@emotion/use-insertion-effect-with-fallbacks@npm:1.1.0" +"@emotion/use-insertion-effect-with-fallbacks@npm:^1.2.0": + version: 1.2.0 + resolution: "@emotion/use-insertion-effect-with-fallbacks@npm:1.2.0" peerDependencies: react: ">=16.8.0" - checksum: 10/33a10f44a873b3f5ccd2a1a3d13c2f34ed628f5a2be1ccf28540a86535a14d3a930afcbef209d48346a22ec60ff48f43c86ee9c846b9480d23a55a17145da66c + checksum: 10/2374999db8d53ef661d61ed1026c42a849632e4f03826f7eba0314c1d92ae342161d737f5045453aa46dd4008e13ccefeba68d3165b667dfad8e5784fcb0c643 languageName: node linkType: hard @@ -3908,7 +3908,7 @@ __metadata: dependencies: "@babel/core": "npm:7.26.0" "@emotion/css": "npm:11.13.5" - "@emotion/react": "npm:11.13.5" + "@emotion/react": "npm:11.14.0" "@emotion/serialize": "npm:1.3.3" "@faker-js/faker": "npm:^9.0.0" "@floating-ui/react": "npm:0.26.28" @@ -17551,7 +17551,7 @@ __metadata: "@cypress/webpack-preprocessor": "npm:6.0.2" "@emotion/css": "npm:11.13.5" "@emotion/eslint-plugin": "npm:11.12.0" - "@emotion/react": "npm:11.13.5" + "@emotion/react": "npm:11.14.0" "@fingerprintjs/fingerprintjs": "npm:^3.4.2" "@floating-ui/react": "npm:0.26.28" "@formatjs/intl-durationformat": "npm:^0.6.0" From 7b934f0d2ba46f1446be6908dc34fe57d0241108 Mon Sep 17 00:00:00 2001 From: "grafana-pr-automation[bot]" <140550294+grafana-pr-automation[bot]@users.noreply.github.com> Date: Thu, 12 Dec 2024 16:48:47 +0000 Subject: [PATCH 08/28] I18n: Download translations from Crowdin (#97868) 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 | 6 ++++++ public/locales/es-ES/grafana.json | 6 ++++++ public/locales/fr-FR/grafana.json | 6 ++++++ public/locales/pt-BR/grafana.json | 6 ++++++ public/locales/zh-Hans/grafana.json | 6 ++++++ 5 files changed, 30 insertions(+) diff --git a/public/locales/de-DE/grafana.json b/public/locales/de-DE/grafana.json index 0668470abd2..08f14d2cebe 100644 --- a/public/locales/de-DE/grafana.json +++ b/public/locales/de-DE/grafana.json @@ -1758,6 +1758,12 @@ "infinite-scroll": { "older-logs": "" }, + "log-details": { + "fields": "", + "links": "", + "log-line": "", + "no-details": "" + }, "log-row-message": { "ellipsis": "", "more": "" diff --git a/public/locales/es-ES/grafana.json b/public/locales/es-ES/grafana.json index 07cadcb979e..3fea3cd61d0 100644 --- a/public/locales/es-ES/grafana.json +++ b/public/locales/es-ES/grafana.json @@ -1758,6 +1758,12 @@ "infinite-scroll": { "older-logs": "" }, + "log-details": { + "fields": "", + "links": "", + "log-line": "", + "no-details": "" + }, "log-row-message": { "ellipsis": "", "more": "" diff --git a/public/locales/fr-FR/grafana.json b/public/locales/fr-FR/grafana.json index 306d0d048ca..4c336a2a73e 100644 --- a/public/locales/fr-FR/grafana.json +++ b/public/locales/fr-FR/grafana.json @@ -1758,6 +1758,12 @@ "infinite-scroll": { "older-logs": "" }, + "log-details": { + "fields": "", + "links": "", + "log-line": "", + "no-details": "" + }, "log-row-message": { "ellipsis": "", "more": "" diff --git a/public/locales/pt-BR/grafana.json b/public/locales/pt-BR/grafana.json index 18d926ed4b0..61c86964bf3 100644 --- a/public/locales/pt-BR/grafana.json +++ b/public/locales/pt-BR/grafana.json @@ -1758,6 +1758,12 @@ "infinite-scroll": { "older-logs": "" }, + "log-details": { + "fields": "", + "links": "", + "log-line": "", + "no-details": "" + }, "log-row-message": { "ellipsis": "", "more": "" diff --git a/public/locales/zh-Hans/grafana.json b/public/locales/zh-Hans/grafana.json index 830dc6074b4..410e4cdfae4 100644 --- a/public/locales/zh-Hans/grafana.json +++ b/public/locales/zh-Hans/grafana.json @@ -1748,6 +1748,12 @@ "infinite-scroll": { "older-logs": "" }, + "log-details": { + "fields": "", + "links": "", + "log-line": "", + "no-details": "" + }, "log-row-message": { "ellipsis": "", "more": "" From ebfa06e0409d69a807ce855b0eba58c52ee276d7 Mon Sep 17 00:00:00 2001 From: Matias Chomicki Date: Thu, 12 Dec 2024 16:49:59 +0000 Subject: [PATCH 09/28] Loki: Added support to show label types in Log Details (#97284) * Log Details: add support to resolve label types if supported * Update public/app/features/logs/utils.ts Co-authored-by: Galen Kistler <109082771+gtk-grafana@users.noreply.github.com> * LogDetailsRow: create component to display label type * Remove log * Add missing index resolution for label type values * Make type icon less prominent * Update new icon styles * Formatting * Update public/app/features/logs/utils.ts Co-authored-by: Sven Grossmann * Update public/app/features/logs/utils.ts Co-authored-by: Sven Grossmann * LogDetails: update test * Formatting --------- Co-authored-by: Galen Kistler <109082771+gtk-grafana@users.noreply.github.com> Co-authored-by: Sven Grossmann --- .../logs/components/LogDetails.test.tsx | 85 +++++++++++++++++++ .../logs/components/LogDetailsRow.tsx | 38 ++++++++- public/app/features/logs/utils.ts | 45 ++++++++++ 3 files changed, 166 insertions(+), 2 deletions(-) diff --git a/public/app/features/logs/components/LogDetails.test.tsx b/public/app/features/logs/components/LogDetails.test.tsx index c69d1a6e932..89b6e6793e6 100644 --- a/public/app/features/logs/components/LogDetails.test.tsx +++ b/public/app/features/logs/components/LogDetails.test.tsx @@ -295,4 +295,89 @@ describe('LogDetails', () => { expect(screen.getByText('shouldShowLinkName')).toBeInTheDocument(); expect(screen.getByText('shouldShowLinkValue')).toBeInTheDocument(); }); + + describe('Label types', () => { + const entry = 'test'; + const labels = { + label1: 'value1', + label2: 'value2', + label3: 'value3', + }; + const dataFrame = createDataFrame({ + fields: [ + { name: 'timestamp', config: {}, type: FieldType.time, values: [1] }, + { name: 'body', type: FieldType.string, values: [entry] }, + { name: 'id', type: FieldType.string, values: ['1'] }, + { + name: 'labels', + type: FieldType.other, + values: [labels], + }, + { + name: 'labelTypes', + type: FieldType.other, + values: [ + { + label1: 'I', + label2: 'S', + label3: 'P', + }, + ], + }, + ], + meta: { + type: DataFrameType.LogLines, + }, + }); + it('should show label types if they are available and supported', () => { + setup( + {}, + { + entry, + dataFrame, + entryFieldIndex: 0, + rowIndex: 0, + labels, + datasourceType: 'loki', + rowId: '1', + } + ); + + // Show labels and links + expect(screen.getByText('label1')).toBeInTheDocument(); + expect(screen.getByText('value1')).toBeInTheDocument(); + expect(screen.getByText('label2')).toBeInTheDocument(); + expect(screen.getByText('value2')).toBeInTheDocument(); + expect(screen.getByText('label3')).toBeInTheDocument(); + expect(screen.getByText('value3')).toBeInTheDocument(); + expect(screen.getByText('I')).toBeInTheDocument(); + expect(screen.getByText('S')).toBeInTheDocument(); + expect(screen.getByText('P')).toBeInTheDocument(); + }); + it('should not show label types if they are unavailable or not supported', () => { + setup( + {}, + { + entry, + dataFrame, + entryFieldIndex: 0, + rowIndex: 0, + labels, + datasourceType: 'other datasource', + rowId: '1', + } + ); + + // Show labels and links + expect(screen.getByText('label1')).toBeInTheDocument(); + expect(screen.getByText('value1')).toBeInTheDocument(); + expect(screen.getByText('label2')).toBeInTheDocument(); + expect(screen.getByText('value2')).toBeInTheDocument(); + expect(screen.getByText('label3')).toBeInTheDocument(); + expect(screen.getByText('value3')).toBeInTheDocument(); + expect(screen.queryByText('I')).not.toBeInTheDocument(); + expect(screen.queryByText('S')).not.toBeInTheDocument(); + expect(screen.queryByText('P')).not.toBeInTheDocument(); + }); + }); }); diff --git a/public/app/features/logs/components/LogDetailsRow.tsx b/public/app/features/logs/components/LogDetailsRow.tsx index 0139ddec647..39a7795483e 100644 --- a/public/app/features/logs/components/LogDetailsRow.tsx +++ b/public/app/features/logs/components/LogDetailsRow.tsx @@ -15,9 +15,18 @@ import { LogRowModel, } from '@grafana/data'; import { reportInteraction } from '@grafana/runtime'; -import { ClipboardButton, DataLinkButton, IconButton, PopoverContent, Themeable2, withTheme2 } from '@grafana/ui'; +import { + ClipboardButton, + DataLinkButton, + IconButton, + PopoverContent, + Themeable2, + Tooltip, + withTheme2, +} from '@grafana/ui'; import { logRowToSingleRowDataFrame } from '../logsModel'; +import { getLabelTypeFromRow } from '../utils'; import { LogLabelStats } from './LogLabelStats'; import { getLogRowStyles } from './getLogRowStyles'; @@ -50,6 +59,19 @@ interface State { const getStyles = memoizeOne((theme: GrafanaTheme2) => { return { + labelType: css({ + border: `solid 1px ${theme.colors.text.secondary}`, + color: theme.colors.text.secondary, + borderRadius: theme.shape.radius.circle, + fontSize: theme.spacing(1), + lineHeight: theme.spacing(1.25), + height: theme.spacing(1.5), + width: theme.spacing(1.5), + display: 'flex', + justifyContent: 'center', + verticalAlign: 'middle', + marginLeft: theme.spacing(1), + }), wordBreakAll: css({ label: 'wordBreakAll', wordBreak: 'break-all', @@ -275,6 +297,7 @@ class UnThemedLogDetailsRow extends PureComponent { const singleVal = parsedValues == null ? false : parsedValues.length === 1; const hasFilteringFunctionality = !disableActions && onClickFilterLabel && onClickFilterOutLabel; const refIdTooltip = app === CoreApp.Explore && row.dataFrame?.refId ? ` in query ${row.dataFrame?.refId}` : ''; + const labelType = singleKey ? getLabelTypeFromRow(parsedKeys[0], row) : null; const isMultiParsedValueWithNoContent = !singleVal && parsedValues != null && !parsedValues.every((val) => val === ''); @@ -321,6 +344,7 @@ class UnThemedLogDetailsRow extends PureComponent { + {/* Key - value columns */} {showFieldsStats && singleKey && singleVal && ( -
+ Log line +
- Fields + Fields
- Links + Links
- No details available + No details available
+
{!disableActions && displayedFields && toggleFieldButton}
+
+ {row.entry} +
{labelType && }{singleKey ? parsedKeys[0] : this.generateMultiVal(parsedKeys)} @@ -360,7 +384,7 @@ class UnThemedLogDetailsRow extends PureComponent {
+ { } } +function LabelTypeBadge({ type, styles }: { type: string; styles: ReturnType }) { + return ( + +
+ {type.substring(0, 1)} +
+
+ ); +} + interface AsyncIconButtonProps extends Pick, 'onClick'> { name: IconName; isActive(): Promise; diff --git a/public/app/features/logs/utils.ts b/public/app/features/logs/utils.ts index 4d8f88c5641..e6db5c67038 100644 --- a/public/app/features/logs/utils.ts +++ b/public/app/features/logs/utils.ts @@ -343,3 +343,48 @@ export function createLogRowsMap() { return false; }; } + +function getLabelTypeFromFrame(labelKey: string, frame: DataFrame, index: number): null | string { + const typeField = frame.fields.find((field) => field.name === 'labelTypes')?.values[index]; + if (!typeField) { + return null; + } + return typeField[labelKey] ?? null; +} + +export function getLabelTypeFromRow(label: string, row: LogRowModel) { + if (!row.datasourceType) { + return null; + } + const idField = row.dataFrame.fields.find((field) => field.name === 'id'); + if (!idField) { + return null; + } + const rowIndex = idField.values.findIndex((id) => id === row.rowId); + if (rowIndex < 0) { + return null; + } + const labelType = getLabelTypeFromFrame(label, row.dataFrame, rowIndex); + if (!labelType) { + return null; + } + return getDataSourceLabelType(labelType, row.datasourceType); +} + +function getDataSourceLabelType(labelType: string, datasourceType: string) { + switch (datasourceType) { + case 'loki': + switch (labelType) { + case 'I': + return 'Indexed label'; + case 'S': + return 'Structured metadata'; + case 'P': + return 'Parsed label'; + default: + return null; + } + default: + return null; + } +} From 482af9178296dc8b2f52fa8006a8510ce3200fb2 Mon Sep 17 00:00:00 2001 From: Tom Ratcliffe Date: Thu, 12 Dec 2024 17:11:04 +0000 Subject: [PATCH 10/28] Alerting: Remove manual feature discovery mocking (#96243) * Update mock methods to remove discovery API * Update tests to remove mock discovery API * Remove ref in TESTING.md * Fix other references to 'mimir' --- .../alerting/unified/CloneRuleEditor.test.tsx | 12 ++++++------ .../unified/NotificationPolicies.test.tsx | 5 +++-- .../unified/RuleEditorExisting.test.tsx | 3 ++- .../unified/RuleEditorGrafanaRules.test.tsx | 8 ++++---- .../app/features/alerting/unified/TESTING.md | 2 -- .../RuleEditorCloudRules.test.tsx.snap | 2 +- .../RuleEditorRecordingRule.test.tsx.snap | 2 +- .../contact-points/ContactPoints.test.tsx | 3 ++- .../__mocks__/mimirFlavoredServer.ts | 3 ++- .../SimplifiedRuleEditor.test.tsx | 9 ++++----- .../rules/RuleActionsButtons.test.tsx | 11 ++++------- .../useUpdateRuleGroup.test.tsx.snap | 4 ++-- .../unified/hooks/useAbilities.test.tsx | 3 ++- .../unified/hooks/useIsRuleEditable.test.tsx | 10 ++++------ .../app/features/alerting/unified/mockApi.ts | 18 ------------------ .../alerting/unified/mocks/server/configure.ts | 5 +---- .../alerting/unified/mocks/server/constants.ts | 2 ++ 17 files changed, 40 insertions(+), 62 deletions(-) diff --git a/public/app/features/alerting/unified/CloneRuleEditor.test.tsx b/public/app/features/alerting/unified/CloneRuleEditor.test.tsx index 3eff21b9b26..7399bcae26b 100644 --- a/public/app/features/alerting/unified/CloneRuleEditor.test.tsx +++ b/public/app/features/alerting/unified/CloneRuleEditor.test.tsx @@ -3,6 +3,7 @@ import { FormProvider, useForm } from 'react-hook-form'; import { getWrapper, render, waitFor, waitForElementToBeRemoved, within } from 'test/test-utils'; import { byRole, byTestId, byText } from 'testing-library-selector'; +import { MIMIR_DATASOURCE_UID } from 'app/features/alerting/unified/mocks/server/constants'; import { RuleWithLocation } from 'app/types/unified-alerting'; import { AccessControlAction } from '../../../types'; @@ -15,7 +16,7 @@ import { import { CloneRuleEditor, cloneRuleDefinition } from './CloneRuleEditor'; import { ExpressionEditorProps } from './components/rule-editor/ExpressionEditor'; -import { mockFeatureDiscoveryApi, setupMswServer } from './mockApi'; +import { setupMswServer } from './mockApi'; import { grantUserPermissions, mockDataSource, @@ -27,7 +28,6 @@ import { grafanaRulerRule } from './mocks/grafanaRulerApi'; import { mockRulerRulesApiResponse, mockRulerRulesGroupApiResponse } from './mocks/rulerApi'; import { AlertingQueryRunner } from './state/AlertingQueryRunner'; import { setupDataSources } from './testSetup/datasources'; -import { buildInfoResponse } from './testSetup/featureDiscovery'; import { RuleFormValues } from './types/rule-form'; import { Annotation } from './utils/constants'; import { getDefaultFormValues } from './utils/rule-form'; @@ -107,10 +107,10 @@ describe('CloneRuleEditor', function () { it('should populate form values from the existing alert rule', async function () { const dsSettings = mockDataSource({ name: 'my-prom-ds', - uid: 'my-prom-ds', + uid: MIMIR_DATASOURCE_UID, }); setupDataSources(dsSettings); - mockFeatureDiscoveryApi(server).discoverDsFeatures(dsSettings, buildInfoResponse.mimir); + const originRule = mockRulerAlertingRule({ for: '1m', alert: 'First Ruler Rule', @@ -119,11 +119,11 @@ describe('CloneRuleEditor', function () { annotations: { [Annotation.summary]: 'This is a very important alert rule' }, }); - mockRulerRulesApiResponse(server, 'my-prom-ds', { + mockRulerRulesApiResponse(server, MIMIR_DATASOURCE_UID, { 'namespace-one': [{ name: 'group1', interval: '20s', rules: [originRule] }], }); - mockRulerRulesGroupApiResponse(server, 'my-prom-ds', 'namespace-one', 'group1', { + mockRulerRulesGroupApiResponse(server, MIMIR_DATASOURCE_UID, 'namespace-one', 'group1', { name: 'group1', interval: '20s', rules: [originRule], diff --git a/public/app/features/alerting/unified/NotificationPolicies.test.tsx b/public/app/features/alerting/unified/NotificationPolicies.test.tsx index 82da6db5e59..b034266ac1f 100644 --- a/public/app/features/alerting/unified/NotificationPolicies.test.tsx +++ b/public/app/features/alerting/unified/NotificationPolicies.test.tsx @@ -11,6 +11,7 @@ import { makeAllAlertmanagerConfigFetchFail, makeAllK8sGetEndpointsFail, } from 'app/features/alerting/unified/mocks/server/configure'; +import { MIMIR_DATASOURCE_UID } from 'app/features/alerting/unified/mocks/server/constants'; import { getAlertmanagerConfig, setAlertmanagerConfig, @@ -97,9 +98,9 @@ const dataSources = { }, }), mimir: mockDataSource({ - name: 'mimir', + name: 'MimirAlertmanager', type: DataSourceType.Alertmanager, - uid: 'mimir', + uid: MIMIR_DATASOURCE_UID, jsonData: { implementation: AlertManagerImplementation.mimir, }, diff --git a/public/app/features/alerting/unified/RuleEditorExisting.test.tsx b/public/app/features/alerting/unified/RuleEditorExisting.test.tsx index d885a9c5688..15ecae4e3ec 100644 --- a/public/app/features/alerting/unified/RuleEditorExisting.test.tsx +++ b/public/app/features/alerting/unified/RuleEditorExisting.test.tsx @@ -4,6 +4,7 @@ import { render, screen } from 'test/test-utils'; import { contextSrv } from 'app/core/services/context_srv'; import { setFolderResponse } from 'app/features/alerting/unified/mocks/server/configure'; +import { MIMIR_DATASOURCE_UID } from 'app/features/alerting/unified/mocks/server/constants'; import { captureRequests } from 'app/features/alerting/unified/mocks/server/events'; import { DashboardSearchItemType } from 'app/features/search/types'; @@ -75,7 +76,7 @@ describe('RuleEditor grafana managed rules', () => { const dataSources = { default: mockDataSource({ - uid: 'mimir', + uid: MIMIR_DATASOURCE_UID, type: 'prometheus', name: 'Mimir', isDefault: true, diff --git a/public/app/features/alerting/unified/RuleEditorGrafanaRules.test.tsx b/public/app/features/alerting/unified/RuleEditorGrafanaRules.test.tsx index 4f7776594fd..41a35d2ff60 100644 --- a/public/app/features/alerting/unified/RuleEditorGrafanaRules.test.tsx +++ b/public/app/features/alerting/unified/RuleEditorGrafanaRules.test.tsx @@ -5,13 +5,13 @@ import { screen } from 'test/test-utils'; import { byRole } from 'testing-library-selector'; import { contextSrv } from 'app/core/services/context_srv'; -import { mockFeatureDiscoveryApi, setupMswServer } from 'app/features/alerting/unified/mockApi'; +import { setupMswServer } from 'app/features/alerting/unified/mockApi'; +import { PROMETHEUS_DATASOURCE_UID } from 'app/features/alerting/unified/mocks/server/constants'; import { AccessControlAction } from 'app/types'; import { grantUserPermissions, mockDataSource } from './mocks'; import { grafanaRulerGroup } from './mocks/grafanaRulerApi'; import { setupDataSources } from './testSetup/datasources'; -import { buildInfoResponse } from './testSetup/featureDiscovery'; jest.mock('app/core/components/AppChrome/AppChromeUpdate', () => ({ AppChromeUpdate: ({ actions }: { actions: React.ReactNode }) =>
{actions}
, @@ -19,7 +19,7 @@ jest.mock('app/core/components/AppChrome/AppChromeUpdate', () => ({ jest.setTimeout(60 * 1000); -const server = setupMswServer(); +setupMswServer(); describe('RuleEditor grafana managed rules', () => { beforeEach(() => { @@ -47,6 +47,7 @@ describe('RuleEditor grafana managed rules', () => { { type: 'prometheus', name: 'Prom', + uid: PROMETHEUS_DATASOURCE_UID, isDefault: true, }, { alerting: false } @@ -54,7 +55,6 @@ describe('RuleEditor grafana managed rules', () => { }; setupDataSources(dataSources.default); - mockFeatureDiscoveryApi(server).discoverDsFeatures(dataSources.default, buildInfoResponse.mimir); const { user } = renderRuleEditor(); diff --git a/public/app/features/alerting/unified/TESTING.md b/public/app/features/alerting/unified/TESTING.md index d09526860db..6962222a470 100644 --- a/public/app/features/alerting/unified/TESTING.md +++ b/public/app/features/alerting/unified/TESTING.md @@ -13,8 +13,6 @@ If you don't find a helper for an endpoint you're looking for, please add it. ### Common API requests -- `/buildinfo` - Use `mockFeatureDiscoveryApi` and `buildInfoResponse` object to mock the endpoint response according to your needs - `api/v1/eval` used by AlertingQueryRunner Use `mockApi.eval` Usually an empty response should do the trick diff --git a/public/app/features/alerting/unified/__snapshots__/RuleEditorCloudRules.test.tsx.snap b/public/app/features/alerting/unified/__snapshots__/RuleEditorCloudRules.test.tsx.snap index 61ee13594c8..0d27075abcc 100644 --- a/public/app/features/alerting/unified/__snapshots__/RuleEditorCloudRules.test.tsx.snap +++ b/public/app/features/alerting/unified/__snapshots__/RuleEditorCloudRules.test.tsx.snap @@ -11,7 +11,7 @@ exports[`RuleEditor cloud can create a new cloud alert 1`] = ` ], ], "method": "GET", - "url": "https://mimir.local:9000/api/v1/status/buildinfo", + "url": "http://localhost/api/datasources/proxy/uid/mimir/api/v1/status/buildinfo", }, { "body": "", diff --git a/public/app/features/alerting/unified/__snapshots__/RuleEditorRecordingRule.test.tsx.snap b/public/app/features/alerting/unified/__snapshots__/RuleEditorRecordingRule.test.tsx.snap index d95652d615c..467e9383927 100644 --- a/public/app/features/alerting/unified/__snapshots__/RuleEditorRecordingRule.test.tsx.snap +++ b/public/app/features/alerting/unified/__snapshots__/RuleEditorRecordingRule.test.tsx.snap @@ -11,7 +11,7 @@ exports[`RuleEditor recording rules can create a new cloud recording rule 1`] = ], ], "method": "GET", - "url": "https://mimir.local:9000/api/v1/status/buildinfo", + "url": "http://localhost/api/datasources/proxy/uid/mimir/api/v1/status/buildinfo", }, { "body": "", diff --git a/public/app/features/alerting/unified/components/contact-points/ContactPoints.test.tsx b/public/app/features/alerting/unified/components/contact-points/ContactPoints.test.tsx index 83b4d539675..633e3d30248 100644 --- a/public/app/features/alerting/unified/components/contact-points/ContactPoints.test.tsx +++ b/public/app/features/alerting/unified/components/contact-points/ContactPoints.test.tsx @@ -3,6 +3,7 @@ import { ComponentProps, ReactNode } from 'react'; import { render, screen, userEvent, waitFor, waitForElementToBeRemoved, within } from 'test/test-utils'; import { selectors } from '@grafana/e2e-selectors'; +import { MIMIR_DATASOURCE_UID } from 'app/features/alerting/unified/mocks/server/constants'; import { flushMicrotasks, testWithFeatureToggles } from 'app/features/alerting/unified/test/test-utils'; import { K8sAnnotations } from 'app/features/alerting/unified/utils/k8s/constants'; import { AlertManagerDataSourceJsonData, AlertManagerImplementation } from 'app/plugins/datasource/alertmanager/types'; @@ -16,7 +17,7 @@ import { DataSourceType, GRAFANA_RULES_SOURCE_NAME } from '../../utils/datasourc import { ContactPoint } from './ContactPoint'; import { ContactPointsPageContents } from './ContactPoints'; -import setupMimirFlavoredServer, { MIMIR_DATASOURCE_UID } from './__mocks__/mimirFlavoredServer'; +import setupMimirFlavoredServer from './__mocks__/mimirFlavoredServer'; import setupVanillaAlertmanagerFlavoredServer, { VANILLA_ALERTMANAGER_DATASOURCE_UID, } from './__mocks__/vanillaAlertmanagerServer'; diff --git a/public/app/features/alerting/unified/components/contact-points/__mocks__/mimirFlavoredServer.ts b/public/app/features/alerting/unified/components/contact-points/__mocks__/mimirFlavoredServer.ts index 8318e5fd2bf..7cb339d0851 100644 --- a/public/app/features/alerting/unified/components/contact-points/__mocks__/mimirFlavoredServer.ts +++ b/public/app/features/alerting/unified/components/contact-points/__mocks__/mimirFlavoredServer.ts @@ -1,13 +1,14 @@ import { HttpResponse, http } from 'msw'; import { SetupServer } from 'msw/node'; +import { MIMIR_DATASOURCE_UID } from 'app/features/alerting/unified/mocks/server/constants'; import { AlertManagerCortexConfig } from 'app/plugins/datasource/alertmanager/types'; import mimirAlertmanagerMock from './alertmanager.mimir.config.mock.json'; // this one emulates a mimir server setup -export const MIMIR_DATASOURCE_UID = 'mimir'; +/** @deprecated Move to main alerting MSW server instead */ export default (server: SetupServer) => { server.use( http.get(`/api/alertmanager/${MIMIR_DATASOURCE_UID}/config/api/v1/alerts`, () => diff --git a/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/simplifiedRouting/SimplifiedRuleEditor.test.tsx b/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/simplifiedRouting/SimplifiedRuleEditor.test.tsx index 79f7d011c35..bdd5fced16b 100644 --- a/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/simplifiedRouting/SimplifiedRuleEditor.test.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/simplifiedRouting/SimplifiedRuleEditor.test.tsx @@ -8,14 +8,14 @@ import { byRole } from 'testing-library-selector'; import { config } from '@grafana/runtime'; import { contextSrv } from 'app/core/services/context_srv'; import RuleEditor from 'app/features/alerting/unified/RuleEditor'; -import { mockFeatureDiscoveryApi, setupMswServer } from 'app/features/alerting/unified/mockApi'; +import { setupMswServer } from 'app/features/alerting/unified/mockApi'; import { grantUserPermissions, mockDataSource } from 'app/features/alerting/unified/mocks'; import { setAlertmanagerChoices } from 'app/features/alerting/unified/mocks/server/configure'; +import { PROMETHEUS_DATASOURCE_UID } from 'app/features/alerting/unified/mocks/server/constants'; import { captureRequests, serializeRequests } from 'app/features/alerting/unified/mocks/server/events'; import { FOLDER_TITLE_HAPPY_PATH } from 'app/features/alerting/unified/mocks/server/handlers/search'; import { AlertmanagerProvider } from 'app/features/alerting/unified/state/AlertmanagerContext'; import { testWithFeatureToggles } from 'app/features/alerting/unified/test/test-utils'; -import { buildInfoResponse } from 'app/features/alerting/unified/testSetup/featureDiscovery'; import { DataSourceType, GRAFANA_DATASOURCE_NAME } from 'app/features/alerting/unified/utils/datasource'; import { AlertmanagerChoice } from 'app/plugins/datasource/alertmanager/types'; import { AccessControlAction } from 'app/types'; @@ -29,13 +29,14 @@ jest.mock('app/core/components/AppChrome/AppChromeUpdate', () => ({ jest.setTimeout(60 * 1000); -const server = setupMswServer(); +setupMswServer(); const dataSources = { default: mockDataSource( { type: 'prometheus', name: 'Prom', + uid: PROMETHEUS_DATASOURCE_UID, isDefault: true, }, { alerting: false } @@ -78,8 +79,6 @@ describe('Can create a new grafana managed alert using simplified routing', () = AccessControlAction.AlertingNotificationsRead, AccessControlAction.AlertingNotificationsWrite, ]); - - mockFeatureDiscoveryApi(server).discoverDsFeatures(dataSources.default, buildInfoResponse.mimir); }); it('cannot create new grafana managed alert when using simplified routing and not selecting a contact point', async () => { diff --git a/public/app/features/alerting/unified/components/rules/RuleActionsButtons.test.tsx b/public/app/features/alerting/unified/components/rules/RuleActionsButtons.test.tsx index 2f105fffe5f..57725b1975b 100644 --- a/public/app/features/alerting/unified/components/rules/RuleActionsButtons.test.tsx +++ b/public/app/features/alerting/unified/components/rules/RuleActionsButtons.test.tsx @@ -5,7 +5,7 @@ import { config, locationService, setPluginLinksHook } from '@grafana/runtime'; import { interceptLinkClicks } from 'app/core/navigation/patch/interceptLinkClicks'; import { contextSrv } from 'app/core/services/context_srv'; import { RuleActionsButtons } from 'app/features/alerting/unified/components/rules/RuleActionsButtons'; -import { mockFeatureDiscoveryApi, setupMswServer } from 'app/features/alerting/unified/mockApi'; +import { setupMswServer } from 'app/features/alerting/unified/mockApi'; import { getCloudRule, getGrafanaRule, @@ -14,19 +14,17 @@ import { mockGrafanaRulerRule, mockPromAlertingRule, } from 'app/features/alerting/unified/mocks'; +import { MIMIR_DATASOURCE_UID } from 'app/features/alerting/unified/mocks/server/constants'; import { AccessControlAction } from 'app/types'; import { PromAlertingRuleState } from 'app/types/unified-alerting-dto'; import { setupDataSources } from '../../testSetup/datasources'; -import { buildInfoResponse } from '../../testSetup/featureDiscovery'; import { fromCombinedRule, stringifyIdentifier } from '../../utils/rule-id'; -const server = setupMswServer(); +setupMswServer(); jest.mock('app/core/services/context_srv'); const mockContextSrv = jest.mocked(contextSrv); -// const locationPushSpy = jest.spyOn(locationService, 'push'); - const ui = { detailsButton: byRole('link', { name: /View/ }), menu: byRole('menu'), @@ -66,7 +64,7 @@ setPluginLinksHook(() => ({ isLoading: false, })); -const mimirDs = mockDataSource({ uid: 'mimir', name: 'Mimir' }); +const mimirDs = mockDataSource({ uid: MIMIR_DATASOURCE_UID, name: 'Mimir' }); const prometheusDs = mockDataSource({ uid: 'prometheus', name: 'Prometheus' }); setupDataSources(mimirDs, prometheusDs); @@ -106,7 +104,6 @@ describe('RuleActionsButtons', () => { const user = userEvent.setup(); grantAllPermissions(); const mockRule = getCloudRule(undefined, { rulesSource: mimirDs }); - mockFeatureDiscoveryApi(server).discoverDsFeatures(mimirDs, buildInfoResponse.mimir); render(); diff --git a/public/app/features/alerting/unified/hooks/ruleGroup/__snapshots__/useUpdateRuleGroup.test.tsx.snap b/public/app/features/alerting/unified/hooks/ruleGroup/__snapshots__/useUpdateRuleGroup.test.tsx.snap index 5357d0c6a6d..af6e4c58c40 100644 --- a/public/app/features/alerting/unified/hooks/ruleGroup/__snapshots__/useUpdateRuleGroup.test.tsx.snap +++ b/public/app/features/alerting/unified/hooks/ruleGroup/__snapshots__/useUpdateRuleGroup.test.tsx.snap @@ -11,7 +11,7 @@ exports[`reorder rules for rule group should correctly reorder rules 1`] = ` ], ], "method": "GET", - "url": "https://mimir.local:9000/api/v1/status/buildinfo", + "url": "http://localhost/api/datasources/proxy/uid/mimir/api/v1/status/buildinfo", }, { "body": "", @@ -89,7 +89,7 @@ exports[`useUpdateRuleGroupConfiguration should be able to move a Data Source ma ], ], "method": "GET", - "url": "https://mimir.local:9000/api/v1/status/buildinfo", + "url": "http://localhost/api/datasources/proxy/uid/mimir/api/v1/status/buildinfo", }, { "body": "", diff --git a/public/app/features/alerting/unified/hooks/useAbilities.test.tsx b/public/app/features/alerting/unified/hooks/useAbilities.test.tsx index 4f84677370f..a7b550e0b68 100644 --- a/public/app/features/alerting/unified/hooks/useAbilities.test.tsx +++ b/public/app/features/alerting/unified/hooks/useAbilities.test.tsx @@ -3,6 +3,7 @@ import { getWrapper, render, renderHook, screen, waitFor } from 'test/test-utils import { setupMswServer } from 'app/features/alerting/unified/mockApi'; import { setFolderAccessControl } from 'app/features/alerting/unified/mocks/server/configure'; +import { MIMIR_DATASOURCE_UID } from 'app/features/alerting/unified/mocks/server/constants'; import { AlertManagerDataSourceJsonData, AlertManagerImplementation } from 'app/plugins/datasource/alertmanager/types'; import { AccessControlAction } from 'app/types'; import { CombinedRule } from 'app/types/unified-alerting'; @@ -86,7 +87,7 @@ describe('alertmanager abilities', () => { it('should report everything except exporting for Mimir alertmanager', () => { setupDataSources( mockDataSource({ - name: 'mimir', + name: MIMIR_DATASOURCE_UID, type: DataSourceType.Alertmanager, jsonData: { implementation: AlertManagerImplementation.mimir, diff --git a/public/app/features/alerting/unified/hooks/useIsRuleEditable.test.tsx b/public/app/features/alerting/unified/hooks/useIsRuleEditable.test.tsx index beb802cb88e..f8e85ba76d6 100644 --- a/public/app/features/alerting/unified/hooks/useIsRuleEditable.test.tsx +++ b/public/app/features/alerting/unified/hooks/useIsRuleEditable.test.tsx @@ -3,13 +3,13 @@ import * as React from 'react'; import { Provider } from 'react-redux'; import { contextSrv } from 'app/core/services/context_srv'; +import { MIMIR_DATASOURCE_UID } from 'app/features/alerting/unified/mocks/server/constants'; import { configureStore } from 'app/store/configureStore'; import { AccessControlAction, FolderDTO } from 'app/types'; -import { mockFeatureDiscoveryApi, setupMswServer } from '../mockApi'; +import { setupMswServer } from '../mockApi'; import { mockDataSource, mockFolder, mockRulerAlertingRule, mockRulerGrafanaRule } from '../mocks'; import { setupDataSources } from '../testSetup/datasources'; -import { buildInfoResponse } from '../testSetup/featureDiscovery'; import { useFolder } from './useFolder'; import { useIsRuleEditable } from './useIsRuleEditable'; @@ -20,10 +20,10 @@ const mocks = { useFolder: jest.mocked(useFolder), }; -const server = setupMswServer(); +setupMswServer(); const dataSources = { - mimir: mockDataSource({ uid: 'mimir', name: 'Mimir' }), + mimir: mockDataSource({ uid: MIMIR_DATASOURCE_UID, name: 'Mimir' }), }; setupDataSources(dataSources.mimir); @@ -107,8 +107,6 @@ describe('useIsRuleEditable', () => { beforeEach(() => { mocks.useFolder.mockReturnValue({ loading: false }); contextSrv.isEditor = true; - - mockFeatureDiscoveryApi(server).discoverDsFeatures(dataSources.mimir, buildInfoResponse.mimir); }); it('Should allow editing and deleting when the user has alert rule external write permission', async () => { diff --git a/public/app/features/alerting/unified/mockApi.ts b/public/app/features/alerting/unified/mockApi.ts index c4f636a7922..2ff26834dee 100644 --- a/public/app/features/alerting/unified/mockApi.ts +++ b/public/app/features/alerting/unified/mockApi.ts @@ -1,7 +1,6 @@ import { HttpResponse, http } from 'msw'; import { SetupServer, setupServer } from 'msw/node'; -import { DataSourceInstanceSettings } from '@grafana/data'; import { setBackendSrv } from '@grafana/runtime'; import { AlertGroupUpdated } from 'app/features/alerting/unified/api/alertRuleApi'; import allHandlers from 'app/features/alerting/unified/mocks/server/all-handlers'; @@ -12,7 +11,6 @@ import { import { resetRoutingTreeMap } from 'app/features/alerting/unified/mocks/server/entities/k8s/routingtrees'; import { DashboardDTO, FolderDTO, OrgUser } from 'app/types'; import { - PromBuildInfoResponse, PromRulesResponse, RulerGrafanaRuleDTO, RulerRuleGroupDTO, @@ -189,22 +187,6 @@ export function mockAlertRuleApi(server: SetupServer) { }; } -/** - * Used to mock the response from the /api/v1/status/buildinfo endpoint - */ -export function mockFeatureDiscoveryApi(server: SetupServer) { - return { - /** - * - * @param dsSettings Use `mockDataSource` to create a faks data source settings - * @param response Use `buildInfoResponse` to get a pre-defined response for Prometheus and Mimir - */ - discoverDsFeatures: (dsSettings: DataSourceInstanceSettings, response: PromBuildInfoResponse) => { - server.use(http.get(`${dsSettings.url}/api/v1/status/buildinfo`, () => HttpResponse.json(response))); - }, - }; -} - export function mockExportApi(server: SetupServer) { // exportRule, exportRulesGroup, exportRulesFolder use the same API endpoint but with different parameters return { diff --git a/public/app/features/alerting/unified/mocks/server/configure.ts b/public/app/features/alerting/unified/mocks/server/configure.ts index 340081f0262..063a070a10f 100644 --- a/public/app/features/alerting/unified/mocks/server/configure.ts +++ b/public/app/features/alerting/unified/mocks/server/configure.ts @@ -2,7 +2,7 @@ import { HttpResponse, http } from 'msw'; import { DataSourceInstanceSettings } from '@grafana/data'; import { config } from '@grafana/runtime'; -import server, { mockFeatureDiscoveryApi } from 'app/features/alerting/unified/mockApi'; +import server from 'app/features/alerting/unified/mockApi'; import { mockDataSource, mockFolder } from 'app/features/alerting/unified/mocks'; import { getAlertmanagerConfigHandler, @@ -23,7 +23,6 @@ import { FolderDTO } from 'app/types'; import { PromRuleGroupDTO } from 'app/types/unified-alerting-dto'; import { setupDataSources } from '../../testSetup/datasources'; -import { buildInfoResponse } from '../../testSetup/featureDiscovery'; import { DataSourceType } from '../../utils/datasource'; import { ApiMachineryError } from '../../utils/k8s/errors'; @@ -101,7 +100,6 @@ export function mimirDataSource() { type: DataSourceType.Prometheus, name: MIMIR_DATASOURCE_UID, uid: MIMIR_DATASOURCE_UID, - url: 'https://mimir.local:9000', jsonData: { manageAlerts: true, implementation: 'mimir', @@ -111,7 +109,6 @@ export function mimirDataSource() { ); setupDataSources(dataSource); - mockFeatureDiscoveryApi(server).discoverDsFeatures(dataSource, buildInfoResponse.mimir); return { dataSource }; } diff --git a/public/app/features/alerting/unified/mocks/server/constants.ts b/public/app/features/alerting/unified/mocks/server/constants.ts index cc5c79d2415..1727726ce73 100644 --- a/public/app/features/alerting/unified/mocks/server/constants.ts +++ b/public/app/features/alerting/unified/mocks/server/constants.ts @@ -1,3 +1,5 @@ export const MIMIR_DATASOURCE_UID = 'mimir'; export const MIMIR_GROUP_NAME = 'group-1'; export const MIMIR_NAMESPACE_NAME = 'namespace-1'; + +export const PROMETHEUS_DATASOURCE_UID = 'prometheus'; From 3266d62d6d4e7fdd0f25893c202cc95e0b908724 Mon Sep 17 00:00:00 2001 From: Brendan O'Handley Date: Thu, 12 Dec 2024 11:31:55 -0600 Subject: [PATCH 11/28] Explore metrics: OTel breakdown get request header too large with job and instance bug (#97136) * don't duplicate job or instance in match terms for OTel * truncate job and instance list if exceeding 2000 chars for GET request * test for warning when truncating * fix check for duplicated job and instance, make easier to read * add trans and remove console.log * report when a metric has too many labels for job and info * remove console.warn * fix tests for not logging warning * fix tests * remove unused import * make i18n-extract * Update public/app/features/trails/interactions.ts Co-authored-by: ismail simsek * allow to dismiss warning --------- Co-authored-by: ismail simsek --- .../trails/Breakdown/LabelBreakdownScene.tsx | 33 +++++++++++++++++-- public/app/features/trails/DataTrail.tsx | 6 ++++ public/app/features/trails/interactions.ts | 3 ++ public/app/features/trails/otel/api.test.ts | 26 ++++++++++++--- public/app/features/trails/otel/api.ts | 12 ++++--- public/app/features/trails/otel/util.ts | 26 ++++++++++++--- public/app/features/trails/otel/utils.test.ts | 4 ++- public/app/features/trails/shared.ts | 2 ++ public/locales/en-US/grafana.json | 1 + public/locales/pseudo-LOCALE/grafana.json | 1 + 10 files changed, 98 insertions(+), 16 deletions(-) diff --git a/public/app/features/trails/Breakdown/LabelBreakdownScene.tsx b/public/app/features/trails/Breakdown/LabelBreakdownScene.tsx index 0072bbafb85..e655fbdb588 100644 --- a/public/app/features/trails/Breakdown/LabelBreakdownScene.tsx +++ b/public/app/features/trails/Breakdown/LabelBreakdownScene.tsx @@ -1,7 +1,7 @@ import init from '@bsull/augurs/outlier'; import { css } from '@emotion/css'; import { isNumber, max, min, throttle } from 'lodash'; -import { useEffect } from 'react'; +import { useEffect, useState } from 'react'; import { DataFrame, FieldType, GrafanaTheme2, PanelData, SelectableValue } from '@grafana/data'; import { config } from '@grafana/runtime'; @@ -26,7 +26,7 @@ import { VizPanel, } from '@grafana/scenes'; import { DataQuery, SortOrder, TooltipDisplayMode } from '@grafana/schema'; -import { Button, Field, LoadingPlaceholder, useStyles2 } from '@grafana/ui'; +import { Alert, Button, Field, LoadingPlaceholder, useStyles2 } from '@grafana/ui'; import { Trans } from 'app/core/internationalization'; import { getAutoQueriesForMetric } from '../AutomaticMetricQueries/AutoQueryEngine'; @@ -47,6 +47,7 @@ import { VAR_FILTERS, VAR_GROUP_BY, VAR_GROUP_BY_EXP, + VAR_MISSING_OTEL_TARGETS, VAR_OTEL_GROUP_LEFT, } from '../shared'; import { getColorByIndex, getTrailFor } from '../utils'; @@ -338,6 +339,14 @@ export class LabelBreakdownScene extends SceneObjectBase option.value !== ALL_VARIABLE_VALUE).unshift(all); } + const [dismissOtelWarning, updateDismissOtelWarning] = useState(false); + const missingOtelTargets = sceneGraph.lookupVariable(VAR_MISSING_OTEL_TARGETS, trail)?.getValue(); + if (missingOtelTargets && !dismissOtelWarning) { + reportExploreMetrics('missing_otel_labels_by_truncating_job_and_instance', { + metric: trail.state.metric, + }); + } + useEffect(() => { if (useOtelExperience) { // this will update the group left variable @@ -369,6 +378,22 @@ export class LabelBreakdownScene extends SceneObjectBase )} + {missingOtelTargets && !dismissOtelWarning && ( + updateDismissOtelWarning(true)} + className={styles.truncatedOTelResources} + > + + This metric has too many job and instance label values to call the Prometheus label_values endpoint with + the match[] parameter. These label values are used to join the metric with target_info, which contains + the resource attributes. Please include more resource attributes filters. + + + )} +
{body && }
@@ -400,6 +425,10 @@ function getStyles(theme: GrafanaTheme2) { gap: theme.spacing(2), justifyContent: 'space-between', }), + truncatedOTelResources: css({ + minWidth: '30vw', + flexGrow: 0, + }), }; } diff --git a/public/app/features/trails/DataTrail.tsx b/public/app/features/trails/DataTrail.tsx index 0101498a33c..547de8ee9c0 100644 --- a/public/app/features/trails/DataTrail.tsx +++ b/public/app/features/trails/DataTrail.tsx @@ -65,6 +65,7 @@ import { VAR_DATASOURCE, VAR_DATASOURCE_EXPR, VAR_FILTERS, + VAR_MISSING_OTEL_TARGETS, VAR_OTEL_DEPLOYMENT_ENV, VAR_OTEL_GROUP_LEFT, VAR_OTEL_JOIN_QUERY, @@ -732,6 +733,11 @@ function getVariableSet( value: undefined, hide: VariableHide.hideVariable, }), + new ConstantVariable({ + name: VAR_MISSING_OTEL_TARGETS, + hide: VariableHide.hideVariable, + value: false, + }), ], }); } diff --git a/public/app/features/trails/interactions.ts b/public/app/features/trails/interactions.ts index 7abe3ba2ef4..181b321dfc7 100644 --- a/public/app/features/trails/interactions.ts +++ b/public/app/features/trails/interactions.ts @@ -123,6 +123,9 @@ type Interactions = { sortBy: string }; wasm_not_supported: {}, + missing_otel_labels_by_truncating_job_and_instance: { + metric?: string; + }, }; const PREFIX = 'grafana_explore_metrics_'; diff --git a/public/app/features/trails/otel/api.test.ts b/public/app/features/trails/otel/api.test.ts index 104fb4de2cd..b5469478c5a 100644 --- a/public/app/features/trails/otel/api.test.ts +++ b/public/app/features/trails/otel/api.test.ts @@ -9,6 +9,19 @@ import { getFilteredResourceAttributes, } from './api'; +jest.mock('./util', () => ({ + ...jest.requireActual('./util'), + limitOtelMatchTerms: jest.fn().mockImplementation(() => { + return { + jobsRegex: 'job=~"job1|job2"', + instancesRegex: 'instance=~"instance1|instance2"', + // this flag is used when the values exceed 2000 characters + // in this mock we are not including more, just flipping the flag + missingOtelTargets: true, + }; + }), +})); + jest.mock('@grafana/runtime', () => ({ ...jest.requireActual('@grafana/runtime'), config: { @@ -72,7 +85,7 @@ describe('OTEL API', () => { to: 'now', }; - afterAll(() => { + afterEach(() => { jest.clearAllMocks(); }); @@ -117,11 +130,16 @@ describe('OTEL API', () => { describe('getFilteredResourceAttributes', () => { it('should fetch and filter OTEL resources with excluded filters', async () => { - const resources = await getFilteredResourceAttributes(dataSourceUid, timeRange, 'metric', ['job']); + const { attributes } = await getFilteredResourceAttributes(dataSourceUid, timeRange, 'metric', ['job']); // promotedResourceAttribute will be filtered out because even though it is a resource attribute, it is also a metric label and wee prioritize metric labels - expect(resources).not.toEqual(['promotedResourceAttribute', 'resourceAttribute']); + expect(attributes).not.toEqual(['promotedResourceAttribute', 'resourceAttribute']); // the resource attributes returned are the ones only present on target_info - expect(resources).toEqual(['resourceAttribute']); + expect(attributes).toEqual(['resourceAttribute']); + }); + + it('should return a boolean true if the job and instance list for matching is truncated', async () => { + const { missingOtelTargets } = await getFilteredResourceAttributes(dataSourceUid, timeRange, 'metric', ['job']); + expect(missingOtelTargets).toBe(true); }); }); }); diff --git a/public/app/features/trails/otel/api.ts b/public/app/features/trails/otel/api.ts index 6e1dc9cec61..b1c1b8a09f8 100644 --- a/public/app/features/trails/otel/api.ts +++ b/public/app/features/trails/otel/api.ts @@ -5,7 +5,7 @@ import { config, getBackendSrv } from '@grafana/runtime'; import { callSuggestionsApi } from '../utils'; import { OtelResponse, LabelResponse, OtelTargetType } from './types'; -import { sortResources } from './util'; +import { limitOtelMatchTerms, sortResources } from './util'; const OTEL_RESOURCE_EXCLUDED_FILTERS = ['__name__', 'deployment_environment']; // name is handled by metric search metrics bar /** @@ -242,14 +242,16 @@ export async function getFilteredResourceAttributes( // OTel metrics require unique identifies for the resource. Job+instance is the unique identifier. // If there are none, we cannot join on a target_info resource if (metricResources.jobs.length === 0 || metricResources.instances.length === 0) { - return []; + return { attributes: [], missingOtelTargets: false }; } // The URL for the labels endpoint const url = `/api/datasources/uid/${datasourceUid}/resources/api/v1/labels`; // The match param for the metric to get all possible labels for this metric - const metricMatchParam = `${metric}{job=~"${metricResources.jobs.join('|')}",instance=~"${metricResources.instances.join('|')}"}`; + const metricMatchTerms = limitOtelMatchTerms([], metricResources.jobs, metricResources.instances); + + let metricMatchParam = `${metric}{${metricMatchTerms.jobsRegex},${metricMatchTerms.instancesRegex}}`; const start = getPrometheusTime(timeRange.from, false); const end = getPrometheusTime(timeRange.to, true); @@ -272,7 +274,7 @@ export async function getFilteredResourceAttributes( const metricLabels = metricResponse.data ?? []; // only get the resource attributes filtered by job and instance values present on the metric - const targetInfoMatchParam = `target_info{job=~"${metricResources.jobs.join('|')}",instance=~"${metricResources.instances.join('|')}"}`; + let targetInfoMatchParam = `target_info{${metricMatchTerms.jobsRegex},${metricMatchTerms.instancesRegex}}`; const targetInfoParams: Record = { start, @@ -303,5 +305,5 @@ export async function getFilteredResourceAttributes( // return a string array const resourceAttributes = sortedResourceAttributes.map((el) => el.text); - return resourceAttributes; + return { attributes: resourceAttributes, missingOtelTargets: metricMatchTerms.missingOtelTargets }; } diff --git a/public/app/features/trails/otel/util.ts b/public/app/features/trails/otel/util.ts index ef935bc9017..f4cdae14d36 100644 --- a/public/app/features/trails/otel/util.ts +++ b/public/app/features/trails/otel/util.ts @@ -5,6 +5,7 @@ import { DataTrail } from '../DataTrail'; import { VAR_DATASOURCE_EXPR, VAR_FILTERS, + VAR_MISSING_OTEL_TARGETS, VAR_OTEL_DEPLOYMENT_ENV, VAR_OTEL_GROUP_LEFT, VAR_OTEL_JOIN_QUERY, @@ -174,6 +175,8 @@ export function limitOtelMatchTerms( // stop before the total count reaches 2000 // show a warning that there are missing OTel targets and // the user must select more OTel resource attributes + const jobCheck: { [key: string]: boolean } = {}; + const instanceCheck: { [key: string]: boolean } = {}; for (let i = 0; i < jobsList.length; i++) { // use or character for the count const orChars = i === 0 ? 0 : 2; @@ -191,9 +194,12 @@ export function limitOtelMatchTerms( jobsRegex += `${jobsList[i]}`; instancesRegex += `${instancesList[i]}`; } else { - jobsRegex += `|${jobsList[i]}`; - instancesRegex += `|${instancesList[i]}`; + // check to make sure we aren't duplicating job or instance + jobsRegex += jobCheck[jobsList[i]] ? '' : `|${jobsList[i]}`; + instancesRegex += instanceCheck[instancesList[i]] ? '' : `|${instancesList[i]}`; } + jobCheck[jobsList[i]] = true; + instanceCheck[instancesList[i]] = true; } else { missingOtelTargets = true; break; @@ -239,7 +245,12 @@ export async function updateOtelJoinWithGroupLeft(trail: DataTrail, metric: stri } const otelGroupLeft = sceneGraph.lookupVariable(VAR_OTEL_GROUP_LEFT, trail); const otelJoinQueryVariable = sceneGraph.lookupVariable(VAR_OTEL_JOIN_QUERY, trail); - if (!(otelGroupLeft instanceof ConstantVariable) || !(otelJoinQueryVariable instanceof ConstantVariable)) { + const missingOtelTargetsVariable = sceneGraph.lookupVariable(VAR_MISSING_OTEL_TARGETS, trail); + if ( + !(otelGroupLeft instanceof ConstantVariable) || + !(otelJoinQueryVariable instanceof ConstantVariable) || + !(missingOtelTargetsVariable instanceof ConstantVariable) + ) { return; } // Remove the group left @@ -272,7 +283,12 @@ export async function updateOtelJoinWithGroupLeft(trail: DataTrail, metric: stri excludeFilterKeys = excludeFilterKeys.concat(['job', 'instance']); } const datasourceUid = sceneGraph.interpolate(trail, VAR_DATASOURCE_EXPR); - const attributes = await getFilteredResourceAttributes(datasourceUid, timeRange, metric, excludeFilterKeys); + const { attributes, missingOtelTargets } = await getFilteredResourceAttributes( + datasourceUid, + timeRange, + metric, + excludeFilterKeys + ); // here we start to add the attributes to the group left if (attributes.length > 0) { // update the group left variable that contains all the filtered resource attributes @@ -283,6 +299,8 @@ export async function updateOtelJoinWithGroupLeft(trail: DataTrail, metric: stri // update the join query that is interpolated in all queries otelJoinQueryVariable.setState({ value: otelJoinQuery }); } + // used to show a warning in label breakdown that the user must select more OTel resource attributes + missingOtelTargetsVariable.setState({ value: missingOtelTargets }); } /** diff --git a/public/app/features/trails/otel/utils.test.ts b/public/app/features/trails/otel/utils.test.ts index b6edb714bbf..ff54ef6f7ee 100644 --- a/public/app/features/trails/otel/utils.test.ts +++ b/public/app/features/trails/otel/utils.test.ts @@ -21,7 +21,9 @@ jest.mock('./api', () => ({ totalOtelResources: jest.fn(() => ({ job: 'oteldemo', instance: 'instance' })), getDeploymentEnvironments: jest.fn(() => ['production', 'staging']), isOtelStandardization: jest.fn(() => true), - getFilteredResourceAttributes: jest.fn().mockResolvedValue(['resourceAttribute']), + getFilteredResourceAttributes: jest + .fn() + .mockResolvedValue({ attributes: ['resourceAttribute'], missingOtelTargets: false }), })); describe('sortResources', () => { diff --git a/public/app/features/trails/shared.ts b/public/app/features/trails/shared.ts index 5ec6b86f6a9..69af2ab8930 100644 --- a/public/app/features/trails/shared.ts +++ b/public/app/features/trails/shared.ts @@ -34,6 +34,8 @@ export const VAR_OTEL_GROUP_BY = 'otel_groupby'; export const VAR_OTEL_GROUP_BY_EXPR = '${otel_groupby}'; export const VAR_OTEL_GROUP_LEFT = 'otel_group_left'; export const VAR_OTEL_GROUP_LEFT_EXPR = '${otel_group_left}'; +export const VAR_MISSING_OTEL_TARGETS = 'missing_otel_targets'; +export const VAR_MISSING_OTEL_TARGETS_EXPR = '${missing_otel_targets}'; export const LOGS_METRIC = '$__logs__'; export const KEY_SQR_METRIC_VIZ_QUERY = 'sqr-metric-viz-query'; diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index 8482a186a35..5bff044da72 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -1290,6 +1290,7 @@ "breakdown": { "clearFilter": "Clear filter", "labelSelect": "Select", + "missing-otel-labels": "This metric has too many job and instance label values to call the Prometheus label_values endpoint with the match[] parameter. These label values are used to join the metric with target_info, which contains the resource attributes. Please include more resource attributes filters.", "noMatchingValue": "No values found matching; {{filter}}", "sortBy": "Sort by" }, diff --git a/public/locales/pseudo-LOCALE/grafana.json b/public/locales/pseudo-LOCALE/grafana.json index ea4bf022248..fe64f55e4d4 100644 --- a/public/locales/pseudo-LOCALE/grafana.json +++ b/public/locales/pseudo-LOCALE/grafana.json @@ -1290,6 +1290,7 @@ "breakdown": { "clearFilter": "Cľęäř ƒįľŧęř", "labelSelect": "Ŝęľęčŧ", + "missing-otel-labels": "Ŧĥįş męŧřįč ĥäş ŧőő mäʼny ĵőþ äʼnđ įʼnşŧäʼnčę ľäþęľ väľūęş ŧő čäľľ ŧĥę Přőmęŧĥęūş ľäþęľ_väľūęş ęʼnđpőįʼnŧ ŵįŧĥ ŧĥę mäŧčĥ[] päřämęŧęř. Ŧĥęşę ľäþęľ väľūęş äřę ūşęđ ŧő ĵőįʼn ŧĥę męŧřįč ŵįŧĥ ŧäřģęŧ_įʼnƒő, ŵĥįčĥ čőʼnŧäįʼnş ŧĥę řęşőūřčę äŧŧřįþūŧęş. Pľęäşę įʼnčľūđę mőřę řęşőūřčę äŧŧřįþūŧęş ƒįľŧęřş.", "noMatchingValue": "Ńő väľūęş ƒőūʼnđ mäŧčĥįʼnģ; {{filter}}", "sortBy": "Ŝőřŧ þy" }, From 3345f860dc001eda96f51c9d70c61e6050bc0401 Mon Sep 17 00:00:00 2001 From: ismail simsek Date: Thu, 12 Dec 2024 18:40:11 +0100 Subject: [PATCH 12/28] Explore Metrics: Refactor auto query generators (#97815) * move graph builders in one file * rename the folder as auto-query * have a components folder * more refactoring * rename * remove getQueryGeneratorFor * why not renaming again * add tests for baseQuery * remove the need of default query generator * refactor getAutoQueriesForMetric * split the logic * better structure * better structure 2 * clean up * summary tests * unite tests for default.ts * update unit tests * update unit tests * cleaning * one variable for filters --- .../trails/ActionTabs/MetricOverviewScene.tsx | 2 +- .../AutomaticMetricQueries/AutoQueryEngine.ts | 16 --- .../graph-builders/heatmap.ts | 18 ---- .../graph-builders/percentiles.ts | 14 --- .../graph-builders/simple.ts | 14 --- .../graph-builders/types.ts | 4 - .../query-generators/common/baseQuery.ts | 10 -- .../query-generators/default.test.ts | 81 -------------- .../query-generators/default.ts | 82 -------------- .../query-generators/getQueryGeneratorFor.ts | 20 ---- .../query-generators/histogram.ts | 83 -------------- .../query-generators/summary.ts | 39 ------- .../AutomaticMetricQueries/units.test.ts | 18 ---- .../trails/AutomaticMetricQueries/units.ts | 38 ------- .../trails/Breakdown/LabelBreakdownScene.tsx | 4 +- .../app/features/trails/MetricGraphScene.tsx | 2 +- public/app/features/trails/MetricScene.tsx | 4 +- .../trails/MetricSelect/previewPanel.ts | 2 +- .../components}/AutoVizPanel.tsx | 8 +- .../components}/AutoVizPanelQuerySelector.tsx | 5 +- .../getAutoQueriesForMetric.test.ts} | 99 +++++++++++++---- .../autoQuery/getAutoQueriesForMetric.ts | 36 +++++++ .../trails/autoQuery/graphBuilders.ts | 41 +++++++ .../queryGenerators/baseQuery.test.ts | 43 ++++++++ .../autoQuery/queryGenerators/baseQuery.ts | 37 +++++++ .../autoQuery/queryGenerators/common.test.ts | 80 ++++++++++++++ .../queryGenerators/common.ts} | 12 +-- .../autoQuery/queryGenerators/default.test.ts | 41 +++++++ .../autoQuery/queryGenerators/default.ts | 46 ++++++++ .../queryGenerators/histogram.test.ts | 101 ++++++++++++++++++ .../autoQuery/queryGenerators/histogram.ts | 77 +++++++++++++ .../autoQuery/queryGenerators/summary.test.ts | 49 +++++++++ .../autoQuery/queryGenerators/summary.ts | 27 +++++ .../types.ts | 8 ++ .../features/trails/autoQuery/units.test.ts | 94 ++++++++++++++++ public/app/features/trails/autoQuery/units.ts | 35 ++++++ public/app/features/trails/shared.ts | 2 +- 37 files changed, 811 insertions(+), 481 deletions(-) delete mode 100644 public/app/features/trails/AutomaticMetricQueries/AutoQueryEngine.ts delete mode 100644 public/app/features/trails/AutomaticMetricQueries/graph-builders/heatmap.ts delete mode 100644 public/app/features/trails/AutomaticMetricQueries/graph-builders/percentiles.ts delete mode 100644 public/app/features/trails/AutomaticMetricQueries/graph-builders/simple.ts delete mode 100644 public/app/features/trails/AutomaticMetricQueries/graph-builders/types.ts delete mode 100644 public/app/features/trails/AutomaticMetricQueries/query-generators/common/baseQuery.ts delete mode 100644 public/app/features/trails/AutomaticMetricQueries/query-generators/default.test.ts delete mode 100644 public/app/features/trails/AutomaticMetricQueries/query-generators/default.ts delete mode 100644 public/app/features/trails/AutomaticMetricQueries/query-generators/getQueryGeneratorFor.ts delete mode 100644 public/app/features/trails/AutomaticMetricQueries/query-generators/histogram.ts delete mode 100644 public/app/features/trails/AutomaticMetricQueries/query-generators/summary.ts delete mode 100644 public/app/features/trails/AutomaticMetricQueries/units.test.ts delete mode 100644 public/app/features/trails/AutomaticMetricQueries/units.ts rename public/app/features/trails/{AutomaticMetricQueries => autoQuery/components}/AutoVizPanel.tsx (87%) rename public/app/features/trails/{AutomaticMetricQueries => autoQuery/components}/AutoVizPanelQuerySelector.tsx (92%) rename public/app/features/trails/{AutomaticMetricQueries/AutoQueryEngine.test.ts => autoQuery/getAutoQueriesForMetric.test.ts} (83%) create mode 100644 public/app/features/trails/autoQuery/getAutoQueriesForMetric.ts create mode 100644 public/app/features/trails/autoQuery/graphBuilders.ts create mode 100644 public/app/features/trails/autoQuery/queryGenerators/baseQuery.test.ts create mode 100644 public/app/features/trails/autoQuery/queryGenerators/baseQuery.ts create mode 100644 public/app/features/trails/autoQuery/queryGenerators/common.test.ts rename public/app/features/trails/{AutomaticMetricQueries/query-generators/common/generator.ts => autoQuery/queryGenerators/common.ts} (80%) create mode 100644 public/app/features/trails/autoQuery/queryGenerators/default.test.ts create mode 100644 public/app/features/trails/autoQuery/queryGenerators/default.ts create mode 100644 public/app/features/trails/autoQuery/queryGenerators/histogram.test.ts create mode 100644 public/app/features/trails/autoQuery/queryGenerators/histogram.ts create mode 100644 public/app/features/trails/autoQuery/queryGenerators/summary.test.ts create mode 100644 public/app/features/trails/autoQuery/queryGenerators/summary.ts rename public/app/features/trails/{AutomaticMetricQueries => autoQuery}/types.ts (75%) create mode 100644 public/app/features/trails/autoQuery/units.test.ts create mode 100644 public/app/features/trails/autoQuery/units.ts diff --git a/public/app/features/trails/ActionTabs/MetricOverviewScene.tsx b/public/app/features/trails/ActionTabs/MetricOverviewScene.tsx index 5ce78e36621..c82226fc763 100644 --- a/public/app/features/trails/ActionTabs/MetricOverviewScene.tsx +++ b/public/app/features/trails/ActionTabs/MetricOverviewScene.tsx @@ -13,9 +13,9 @@ import { import { Stack, Text, TextLink } from '@grafana/ui'; import { Trans } from 'app/core/internationalization'; -import { getUnitFromMetric } from '../AutomaticMetricQueries/units'; import { MetricScene } from '../MetricScene'; import { StatusWrapper } from '../StatusWrapper'; +import { getUnitFromMetric } from '../autoQuery/units'; import { reportExploreMetrics } from '../interactions'; import { updateOtelJoinWithGroupLeft } from '../otel/util'; import { VAR_DATASOURCE_EXPR, VAR_GROUP_BY, VAR_OTEL_GROUP_LEFT } from '../shared'; diff --git a/public/app/features/trails/AutomaticMetricQueries/AutoQueryEngine.ts b/public/app/features/trails/AutomaticMetricQueries/AutoQueryEngine.ts deleted file mode 100644 index 94d2287e126..00000000000 --- a/public/app/features/trails/AutomaticMetricQueries/AutoQueryEngine.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { getQueryGeneratorFor } from './query-generators/getQueryGeneratorFor'; -import { AutoQueryInfo } from './types'; - -export function getAutoQueriesForMetric(metric: string): AutoQueryInfo { - const metricParts = metric.split('_'); - - const suffix = metricParts.at(-1); - - const generator = getQueryGeneratorFor(suffix); - - if (!generator) { - throw new Error(`Unable to generate queries for metric "${metric}" due to issues with derived suffix "${suffix}"`); - } - - return generator(metricParts); -} diff --git a/public/app/features/trails/AutomaticMetricQueries/graph-builders/heatmap.ts b/public/app/features/trails/AutomaticMetricQueries/graph-builders/heatmap.ts deleted file mode 100644 index 252dba74701..00000000000 --- a/public/app/features/trails/AutomaticMetricQueries/graph-builders/heatmap.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { PanelBuilders } from '@grafana/scenes'; -import { HeatmapColorMode } from 'app/plugins/panel/heatmap/types'; - -import { CommonVizParams } from './types'; - -export function heatmapGraphBuilder({ title, unit }: CommonVizParams) { - return PanelBuilders.heatmap() // - .setTitle(title) - .setUnit(unit) - .setOption('calculate', false) - .setOption('color', { - mode: HeatmapColorMode.Scheme, - exponent: 0.5, - scheme: 'Spectral', - steps: 32, - reverse: false, - }); -} diff --git a/public/app/features/trails/AutomaticMetricQueries/graph-builders/percentiles.ts b/public/app/features/trails/AutomaticMetricQueries/graph-builders/percentiles.ts deleted file mode 100644 index 36578e56ffb..00000000000 --- a/public/app/features/trails/AutomaticMetricQueries/graph-builders/percentiles.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { PanelBuilders } from '@grafana/scenes'; -import { SortOrder } from '@grafana/schema'; -import { TooltipDisplayMode } from '@grafana/ui'; - -import { CommonVizParams } from './types'; - -export function percentilesGraphBuilder({ title, unit }: CommonVizParams) { - return PanelBuilders.timeseries() - .setTitle(title) - .setUnit(unit) - .setCustomFieldConfig('fillOpacity', 9) - .setOption('tooltip', { mode: TooltipDisplayMode.Multi, sort: SortOrder.Descending }) - .setOption('legend', { showLegend: false }); -} diff --git a/public/app/features/trails/AutomaticMetricQueries/graph-builders/simple.ts b/public/app/features/trails/AutomaticMetricQueries/graph-builders/simple.ts deleted file mode 100644 index d04766f23e4..00000000000 --- a/public/app/features/trails/AutomaticMetricQueries/graph-builders/simple.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { PanelBuilders } from '@grafana/scenes'; -import { SortOrder } from '@grafana/schema'; -import { TooltipDisplayMode } from '@grafana/ui'; - -import { CommonVizParams } from './types'; - -export function simpleGraphBuilder({ title, unit }: CommonVizParams) { - return PanelBuilders.timeseries() // - .setTitle(title) - .setUnit(unit) - .setOption('legend', { showLegend: false }) - .setOption('tooltip', { mode: TooltipDisplayMode.Multi, sort: SortOrder.Descending }) - .setCustomFieldConfig('fillOpacity', 9); -} diff --git a/public/app/features/trails/AutomaticMetricQueries/graph-builders/types.ts b/public/app/features/trails/AutomaticMetricQueries/graph-builders/types.ts deleted file mode 100644 index 5167b71f003..00000000000 --- a/public/app/features/trails/AutomaticMetricQueries/graph-builders/types.ts +++ /dev/null @@ -1,4 +0,0 @@ -export type CommonVizParams = { - title: string; - unit: string; -}; diff --git a/public/app/features/trails/AutomaticMetricQueries/query-generators/common/baseQuery.ts b/public/app/features/trails/AutomaticMetricQueries/query-generators/common/baseQuery.ts deleted file mode 100644 index 5d0759e669d..00000000000 --- a/public/app/features/trails/AutomaticMetricQueries/query-generators/common/baseQuery.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { VAR_METRIC_EXPR, VAR_FILTERS_EXPR, VAR_OTEL_JOIN_QUERY_EXPR } from 'app/features/trails/shared'; - -const GENERAL_BASE_QUERY = `${VAR_METRIC_EXPR}${VAR_FILTERS_EXPR}`; -const GENERAL_RATE_BASE_QUERY = `rate(${GENERAL_BASE_QUERY}[$__rate_interval])`; - -export function getGeneralBaseQuery(rate: boolean) { - return rate - ? `${GENERAL_RATE_BASE_QUERY} ${VAR_OTEL_JOIN_QUERY_EXPR}` - : `${GENERAL_BASE_QUERY} ${VAR_OTEL_JOIN_QUERY_EXPR}`; -} diff --git a/public/app/features/trails/AutomaticMetricQueries/query-generators/default.test.ts b/public/app/features/trails/AutomaticMetricQueries/query-generators/default.test.ts deleted file mode 100644 index cddc48925ec..00000000000 --- a/public/app/features/trails/AutomaticMetricQueries/query-generators/default.test.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { VAR_GROUP_BY_EXP } from '../../shared'; -import { AutoQueryDef, AutoQueryInfo } from '../types'; - -import { getGeneralBaseQuery } from './common/baseQuery'; -import { generateQueries } from './default'; - -describe('generateQueries', () => { - const agg = 'sum'; - const unit = 'mockunit'; - - type QueryInfoKey = keyof AutoQueryInfo; - - function testRateIndependentAssertions(queryDef: AutoQueryDef, key: QueryInfoKey) { - describe('regardless of rate', () => { - test(`specified unit must be propagated`, () => expect(queryDef.unit).toBe(unit)); - test(`only one query is expected`, () => expect(queryDef.queries.length).toBe(1)); - const query = queryDef.queries[0]; - test(`specified agg function must be propagated in the query expr`, () => { - const queryAggFunction = query.expr.split('(', 2)[0]; - expect(queryAggFunction).toBe(agg); - }); - if (key === 'breakdown') { - const expectedSuffix = `by(${VAR_GROUP_BY_EXP})`; - test(`breakdown query must end with "${expectedSuffix}"`, () => { - const suffix = query.expr.substring(query.expr.length - expectedSuffix.length); - expect(suffix).toBe(expectedSuffix); - }); - } - }); - } - - function testRateSpecificAssertions(queryDef: AutoQueryDef, rate: boolean, key: QueryInfoKey) { - const query = queryDef.queries[0]; - const firstParen = query.expr.indexOf('('); - const expectedBaseQuery = getGeneralBaseQuery(rate); - const detectedBaseQuery = query.expr.substring(firstParen + 1, firstParen + 1 + expectedBaseQuery.length); - - const inParentheses = rate ? 'overall per-second rate' : 'overall'; - const description = `\${metric} (${inParentheses})`; - - describe(`since rate is ${rate}`, () => { - test(`base query must be "${expectedBaseQuery}"`, () => expect(detectedBaseQuery).toBe(expectedBaseQuery)); - if (key === 'main') { - test(`main panel title contains expected description "${description}"`, () => - expect(queryDef.title).toContain(description)); - } else { - test(`${key} panel title is just "\${metric}"`, () => expect(queryDef.title).toBe('${metric}')); - test(`${key} panel title does not contain description "${description}"`, () => - expect(queryDef.title).not.toContain(description)); - } - - if (key === 'breakdown') { - test(`breakdown query uses "{{\${groupby}}}" as legend`, () => - expect(query.legendFormat).toBe('{{${groupby}}}')); - } else { - test(`preview query uses "${description}" as legend`, () => expect(query.legendFormat).toBe(description)); - } - }); - } - - for (const rate of [true, false]) { - describe(`when rate is ${rate}`, () => { - const queryInfo = generateQueries({ agg, unit, rate }); - - let key: QueryInfoKey; - for (key in queryInfo) { - if (key !== 'variants') { - const queryDef = queryInfo[key]; - describe(`queryInfo.${key}`, () => testRateIndependentAssertions(queryDef, key)); - describe(`queryInfo.${key}`, () => testRateSpecificAssertions(queryDef, rate, key)); - continue; - } - - queryInfo[key].forEach((queryDef, index) => { - describe(`queryInfo.${key}[${index}]`, () => testRateIndependentAssertions(queryDef, key)); - describe(`queryInfo.${key}[${index}]`, () => testRateSpecificAssertions(queryDef, rate, key)); - }); - } - }); - } -}); diff --git a/public/app/features/trails/AutomaticMetricQueries/query-generators/default.ts b/public/app/features/trails/AutomaticMetricQueries/query-generators/default.ts deleted file mode 100644 index b81feb30d62..00000000000 --- a/public/app/features/trails/AutomaticMetricQueries/query-generators/default.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { VAR_GROUP_BY_EXP, VAR_METRIC_EXPR } from 'app/features/trails/shared'; - -import { AutoQueryInfo } from '../types'; -import { getPerSecondRateUnit, getUnit } from '../units'; - -import { getGeneralBaseQuery } from './common/baseQuery'; -import { generateCommonAutoQueryInfo } from './common/generator'; - -/** These suffixes will set rate to true */ -const RATE_SUFFIXES = new Set(['count', 'total']); - -const UNSUPPORTED_SUFFIXES = new Set(['sum', 'bucket']); - -/** Non-default aggregation keyed by suffix */ -const SPECIFIC_AGGREGATIONS_FOR_SUFFIX: Record = { - count: 'sum', - total: 'sum', -}; - -function shouldCheckPreviousSuffixForUnit(suffix: string) { - return suffix === 'total'; -} - -const aggLabels: Record = { - avg: 'average', - sum: 'overall', -}; - -function getAggLabel(agg: string) { - return aggLabels[agg] || agg; -} - -export type AutoQueryParameters = { - agg: string; - unit: string; - rate: boolean; -}; - -export function generateQueries({ agg, rate, unit }: AutoQueryParameters): AutoQueryInfo { - const baseQuery = getGeneralBaseQuery(rate); - - const aggregationDescription = rate ? `${getAggLabel(agg)} per-second rate` : `${getAggLabel(agg)}`; - - const description = `${VAR_METRIC_EXPR} (${aggregationDescription})`; - - const mainQueryExpr = `${agg}(${baseQuery})`; - const breakdownQueryExpr = `${agg}(${baseQuery})by(${VAR_GROUP_BY_EXP})`; - - return generateCommonAutoQueryInfo({ - description, - mainQueryExpr, - breakdownQueryExpr, - unit, - }); -} - -export function createDefaultMetricQueryDefs(metricParts: string[]) { - // Get the last part of the metric name - const suffix = metricParts.at(-1); - - // If the suffix is null or is in the set of unsupported suffixes, throw an error because the metric should be delegated to a different generator (summary or histogram) - if (suffix == null || UNSUPPORTED_SUFFIXES.has(suffix)) { - throw new Error(`This function does not support a metric suffix of "${suffix}"`); - } - - // Check if generating rate query and/or aggregation query - const rate = RATE_SUFFIXES.has(suffix); - const agg = SPECIFIC_AGGREGATIONS_FOR_SUFFIX[suffix] || 'avg'; - - // Try to find the unit in the Prometheus metric name - const unitSuffix = shouldCheckPreviousSuffixForUnit(suffix) ? metricParts.at(-2) : suffix; - - // Get the Grafana unit or Grafana rate unit - const unit = rate ? getPerSecondRateUnit(unitSuffix) : getUnit(unitSuffix); - - const params = { - agg, - unit, - rate, - }; - return generateQueries(params); -} diff --git a/public/app/features/trails/AutomaticMetricQueries/query-generators/getQueryGeneratorFor.ts b/public/app/features/trails/AutomaticMetricQueries/query-generators/getQueryGeneratorFor.ts deleted file mode 100644 index e44bfc31e60..00000000000 --- a/public/app/features/trails/AutomaticMetricQueries/query-generators/getQueryGeneratorFor.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { AutoQueryInfo } from '../types'; - -import { createDefaultMetricQueryDefs } from './default'; -import { createHistogramMetricQueryDefs } from './histogram'; -import { createSummaryMetricQueryDefs } from './summary'; - -// TODO: when we have a known unit parameter, use that rather than having the generator functions infer from suffix -export type MetricQueriesGenerator = (metricParts: string[]) => AutoQueryInfo; - -export function getQueryGeneratorFor(suffix?: string): MetricQueriesGenerator { - if (suffix === 'sum') { - return createSummaryMetricQueryDefs; - } - - if (suffix === 'bucket') { - return createHistogramMetricQueryDefs; - } - - return createDefaultMetricQueryDefs; -} diff --git a/public/app/features/trails/AutomaticMetricQueries/query-generators/histogram.ts b/public/app/features/trails/AutomaticMetricQueries/query-generators/histogram.ts deleted file mode 100644 index 4fd492d5322..00000000000 --- a/public/app/features/trails/AutomaticMetricQueries/query-generators/histogram.ts +++ /dev/null @@ -1,83 +0,0 @@ -import { PromQuery } from '@grafana/prometheus'; - -import { VAR_FILTERS_EXPR, VAR_GROUP_BY_EXP, VAR_METRIC_EXPR, VAR_OTEL_JOIN_QUERY_EXPR } from '../../shared'; -import { heatmapGraphBuilder } from '../graph-builders/heatmap'; -import { percentilesGraphBuilder } from '../graph-builders/percentiles'; -import { simpleGraphBuilder } from '../graph-builders/simple'; -import { AutoQueryDef } from '../types'; -import { getUnit } from '../units'; - -export function createHistogramMetricQueryDefs(metricParts: string[]) { - const title = `${VAR_METRIC_EXPR}`; - - const unitSuffix = metricParts.at(-2); - - const unit = getUnit(unitSuffix); - - const common = { - title, - unit, - }; - - const p50: AutoQueryDef = { - ...common, - variant: 'p50', - queries: [percentileQuery(50)], - vizBuilder: () => simpleGraphBuilder(p50), - }; - - const breakdown: AutoQueryDef = { - ...common, - variant: 'p50', - queries: [percentileQuery(50, [VAR_GROUP_BY_EXP])], - vizBuilder: () => simpleGraphBuilder(breakdown), - }; - - const percentiles: AutoQueryDef = { - ...common, - variant: 'percentiles', - queries: [99, 90, 50].map((p) => percentileQuery(p)), - vizBuilder: () => percentilesGraphBuilder(percentiles), - }; - - const heatmap: AutoQueryDef = { - ...common, - variant: 'heatmap', - queries: [heatMapQuery()], - vizBuilder: () => heatmapGraphBuilder(heatmap), - }; - - return { preview: heatmap, main: heatmap, variants: [percentiles, heatmap], breakdown: breakdown }; -} - -const BASE_QUERY = `rate(${VAR_METRIC_EXPR}${VAR_FILTERS_EXPR}[$__rate_interval])${VAR_OTEL_JOIN_QUERY_EXPR}`; - -function baseQuery(groupings: string[] = []) { - const sumByList = ['le', ...groupings]; - return `sum by(${sumByList.join(', ')}) (${BASE_QUERY})`; -} - -function heatMapQuery(groupings: string[] = []): PromQuery { - return { - refId: 'Heatmap', - expr: baseQuery(groupings), - format: 'heatmap', - }; -} - -function percentileQuery(percentile: number, groupings: string[] = []) { - const percent = percentile / 100; - - let legendFormat = `${percentile}th Percentile`; - - // For the breakdown view, show the label value variable we are grouping by - if (groupings[0]) { - legendFormat = `{{${groupings[0]}}}`; - } - - return { - refId: `Percentile${percentile}`, - expr: `histogram_quantile(${percent}, ${baseQuery(groupings)})`, - legendFormat, - }; -} diff --git a/public/app/features/trails/AutomaticMetricQueries/query-generators/summary.ts b/public/app/features/trails/AutomaticMetricQueries/query-generators/summary.ts deleted file mode 100644 index cd73d93db25..00000000000 --- a/public/app/features/trails/AutomaticMetricQueries/query-generators/summary.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { VAR_GROUP_BY_EXP, VAR_METRIC_EXPR } from '../../shared'; -import { AutoQueryInfo } from '../types'; -import { getUnit } from '../units'; - -import { getGeneralBaseQuery } from './common/baseQuery'; -import { generateCommonAutoQueryInfo } from './common/generator'; - -export function createSummaryMetricQueryDefs(metricParts: string[]): AutoQueryInfo { - const suffix = metricParts.at(-1); - if (suffix !== 'sum') { - throw new Error('createSummaryMetricQueryDefs is only to be used for metrics that end in "_sum"'); - } - - const unitSuffix = metricParts.at(-2); - const unit = getUnit(unitSuffix); - - const rate = true; - const baseQuery = getGeneralBaseQuery(rate); - - const subMetric = metricParts.slice(0, -1).join('_'); - const mainQueryExpr = createMeanExpr(`sum(${baseQuery})`); - const breakdownQueryExpr = createMeanExpr(`sum(${baseQuery})by(${VAR_GROUP_BY_EXP})`); - - const operationDescription = `average`; - const description = `${subMetric} (${operationDescription})`; - - function createMeanExpr(expr: string) { - const numerator = expr.replace(VAR_METRIC_EXPR, `${subMetric}_sum`); - const denominator = expr.replace(VAR_METRIC_EXPR, `${subMetric}_count`); - return `${numerator}/${denominator}`; - } - - return generateCommonAutoQueryInfo({ - description, - mainQueryExpr, - breakdownQueryExpr, - unit, - }); -} diff --git a/public/app/features/trails/AutomaticMetricQueries/units.test.ts b/public/app/features/trails/AutomaticMetricQueries/units.test.ts deleted file mode 100644 index 15f61988690..00000000000 --- a/public/app/features/trails/AutomaticMetricQueries/units.test.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { getUnitFromMetric } from './units'; - -// Tests for units -describe('getUnitFromMetric', () => { - it('should return the last part of the metric if it is a valid unit', () => { - expect(getUnitFromMetric('go_gc_gomemlimit_bytes')).toBe('bytes'); - expect(getUnitFromMetric('go_gc_duration_seconds')).toBe('seconds'); - }); - - it('should return the second to last part of the metric if it is a valid unit', () => { - expect(getUnitFromMetric('go_gc_heap_allocs_by_size_bytes_count')).toBe('bytes'); - expect(getUnitFromMetric('go_cpu_classes_gc_mark_assist_cpu_seconds_total')).toBe('seconds'); - }); - - it('should return null if no valid unit is found', () => { - expect(getUnitFromMetric('ALERTS')).toBe(null); - }); -}); diff --git a/public/app/features/trails/AutomaticMetricQueries/units.ts b/public/app/features/trails/AutomaticMetricQueries/units.ts deleted file mode 100644 index e025d41af2d..00000000000 --- a/public/app/features/trails/AutomaticMetricQueries/units.ts +++ /dev/null @@ -1,38 +0,0 @@ -const DEFAULT_UNIT = 'short'; - -// Get unit from metric name (e.g. "go_gc_duration_seconds" -> "seconds") -export function getUnitFromMetric(metric: string) { - const metricParts = metric.split('_'); - const suffix = metricParts.at(-1) ?? ''; - const secondToLastSuffix = metricParts.at(-2) ?? ''; - if (UNIT_LIST.includes(suffix)) { - return suffix; - } else if (UNIT_LIST.includes(secondToLastSuffix)) { - return secondToLastSuffix; - } else { - return null; - } -} - -// Get Grafana unit for a panel (e.g. "go_gc_duration_seconds" -> "s") -export function getUnit(metricPart: string | undefined) { - return (metricPart && UNIT_MAP[metricPart]) || DEFAULT_UNIT; -} - -const UNIT_MAP: Record = { - bytes: 'bytes', - seconds: 's', -}; - -const UNIT_LIST = ['bytes', 'seconds']; - -const RATE_UNIT_MAP: Record = { - bytes: 'Bps', // bytes per second - seconds: 'short', // seconds per second is unitless -- this may indicate a count of some resource that is active -}; - -const DEFAULT_RATE_UNIT = 'cps'; // Count per second - -export function getPerSecondRateUnit(metricPart: string | undefined) { - return (metricPart && RATE_UNIT_MAP[metricPart]) || DEFAULT_RATE_UNIT; -} diff --git a/public/app/features/trails/Breakdown/LabelBreakdownScene.tsx b/public/app/features/trails/Breakdown/LabelBreakdownScene.tsx index e655fbdb588..1ab14b84aac 100644 --- a/public/app/features/trails/Breakdown/LabelBreakdownScene.tsx +++ b/public/app/features/trails/Breakdown/LabelBreakdownScene.tsx @@ -29,13 +29,13 @@ import { DataQuery, SortOrder, TooltipDisplayMode } from '@grafana/schema'; import { Alert, Button, Field, LoadingPlaceholder, useStyles2 } from '@grafana/ui'; import { Trans } from 'app/core/internationalization'; -import { getAutoQueriesForMetric } from '../AutomaticMetricQueries/AutoQueryEngine'; -import { AutoQueryDef } from '../AutomaticMetricQueries/types'; import { BreakdownLabelSelector } from '../BreakdownLabelSelector'; import { DataTrail } from '../DataTrail'; import { MetricScene } from '../MetricScene'; import { AddToExplorationButton } from '../MetricSelect/AddToExplorationsButton'; import { StatusWrapper } from '../StatusWrapper'; +import { getAutoQueriesForMetric } from '../autoQuery/getAutoQueriesForMetric'; +import { AutoQueryDef } from '../autoQuery/types'; import { reportExploreMetrics } from '../interactions'; import { updateOtelJoinWithGroupLeft } from '../otel/util'; import { getSortByPreference } from '../services/store'; diff --git a/public/app/features/trails/MetricGraphScene.tsx b/public/app/features/trails/MetricGraphScene.tsx index 69003e9edc5..659d1aaf6c5 100644 --- a/public/app/features/trails/MetricGraphScene.tsx +++ b/public/app/features/trails/MetricGraphScene.tsx @@ -13,8 +13,8 @@ import { } from '@grafana/scenes'; import { useStyles2 } from '@grafana/ui'; -import { AutoVizPanel } from './AutomaticMetricQueries/AutoVizPanel'; import { MetricActionBar } from './MetricScene'; +import { AutoVizPanel } from './autoQuery/components/AutoVizPanel'; import { getTrailSettings } from './utils'; export const MAIN_PANEL_MIN_HEIGHT = 280; diff --git a/public/app/features/trails/MetricScene.tsx b/public/app/features/trails/MetricScene.tsx index 13b3c820cf5..0e3cb6b48f7 100644 --- a/public/app/features/trails/MetricScene.tsx +++ b/public/app/features/trails/MetricScene.tsx @@ -18,13 +18,13 @@ import { getExploreUrl } from '../../core/utils/explore'; import { buildMetricOverviewScene } from './ActionTabs/MetricOverviewScene'; import { buildRelatedMetricsScene } from './ActionTabs/RelatedMetricsScene'; -import { getAutoQueriesForMetric } from './AutomaticMetricQueries/AutoQueryEngine'; -import { AutoQueryDef, AutoQueryInfo } from './AutomaticMetricQueries/types'; import { buildLabelBreakdownActionScene } from './Breakdown/LabelBreakdownScene'; import { MAIN_PANEL_MAX_HEIGHT, MAIN_PANEL_MIN_HEIGHT, MetricGraphScene } from './MetricGraphScene'; import { buildRelatedLogsScene } from './RelatedLogs/RelatedLogsScene'; import { ShareTrailButton } from './ShareTrailButton'; import { useBookmarkState } from './TrailStore/useBookmarkState'; +import { getAutoQueriesForMetric } from './autoQuery/getAutoQueriesForMetric'; +import { AutoQueryDef, AutoQueryInfo } from './autoQuery/types'; import { reportExploreMetrics } from './interactions'; import { ActionViewDefinition, diff --git a/public/app/features/trails/MetricSelect/previewPanel.ts b/public/app/features/trails/MetricSelect/previewPanel.ts index 791c57ce949..dd5feb9391b 100644 --- a/public/app/features/trails/MetricSelect/previewPanel.ts +++ b/public/app/features/trails/MetricSelect/previewPanel.ts @@ -1,7 +1,7 @@ import { PromQuery } from '@grafana/prometheus'; import { SceneCSSGridItem, SceneQueryRunner, SceneVariableSet } from '@grafana/scenes'; -import { getAutoQueriesForMetric } from '../AutomaticMetricQueries/AutoQueryEngine'; +import { getAutoQueriesForMetric } from '../autoQuery/getAutoQueriesForMetric'; import { getVariablesWithMetricConstant, MDP_METRIC_PREVIEW, trailDS } from '../shared'; import { getColorByIndex } from '../utils'; diff --git a/public/app/features/trails/AutomaticMetricQueries/AutoVizPanel.tsx b/public/app/features/trails/autoQuery/components/AutoVizPanel.tsx similarity index 87% rename from public/app/features/trails/AutomaticMetricQueries/AutoVizPanel.tsx rename to public/app/features/trails/autoQuery/components/AutoVizPanel.tsx index 4912922b8e4..16774e2573c 100644 --- a/public/app/features/trails/AutomaticMetricQueries/AutoVizPanel.tsx +++ b/public/app/features/trails/autoQuery/components/AutoVizPanel.tsx @@ -1,11 +1,11 @@ import { SceneObjectState, SceneObjectBase, SceneComponentProps, VizPanel, SceneQueryRunner } from '@grafana/scenes'; -import { AddToExplorationButton } from '../MetricSelect/AddToExplorationsButton'; -import { MDP_METRIC_OVERVIEW, trailDS } from '../shared'; -import { getMetricSceneFor } from '../utils'; +import { AddToExplorationButton } from '../../MetricSelect/AddToExplorationsButton'; +import { MDP_METRIC_OVERVIEW, trailDS } from '../../shared'; +import { getMetricSceneFor } from '../../utils'; +import { AutoQueryDef } from '../types'; import { AutoVizPanelQuerySelector } from './AutoVizPanelQuerySelector'; -import { AutoQueryDef } from './types'; export interface AutoVizPanelState extends SceneObjectState { panel?: VizPanel; diff --git a/public/app/features/trails/AutomaticMetricQueries/AutoVizPanelQuerySelector.tsx b/public/app/features/trails/autoQuery/components/AutoVizPanelQuerySelector.tsx similarity index 92% rename from public/app/features/trails/AutomaticMetricQueries/AutoVizPanelQuerySelector.tsx rename to public/app/features/trails/autoQuery/components/AutoVizPanelQuerySelector.tsx index 4b31d552681..4a30036c3fa 100644 --- a/public/app/features/trails/AutomaticMetricQueries/AutoVizPanelQuerySelector.tsx +++ b/public/app/features/trails/autoQuery/components/AutoVizPanelQuerySelector.tsx @@ -1,9 +1,8 @@ import { SceneObjectState, SceneObjectBase, SceneComponentProps } from '@grafana/scenes'; import { RadioButtonGroup } from '@grafana/ui'; -import { getMetricSceneFor } from '../utils'; - -import { AutoQueryDef } from './types'; +import { getMetricSceneFor } from '../../utils'; +import { AutoQueryDef } from '../types'; interface QuerySelectorState extends SceneObjectState { queryDef: AutoQueryDef; diff --git a/public/app/features/trails/AutomaticMetricQueries/AutoQueryEngine.test.ts b/public/app/features/trails/autoQuery/getAutoQueriesForMetric.test.ts similarity index 83% rename from public/app/features/trails/AutomaticMetricQueries/AutoQueryEngine.test.ts rename to public/app/features/trails/autoQuery/getAutoQueriesForMetric.test.ts index f9bda26a4aa..d110f539f54 100644 --- a/public/app/features/trails/AutomaticMetricQueries/AutoQueryEngine.test.ts +++ b/public/app/features/trails/autoQuery/getAutoQueriesForMetric.test.ts @@ -1,4 +1,7 @@ -import { getAutoQueriesForMetric } from './AutoQueryEngine'; +import { VAR_FILTERS_EXPR, VAR_METRIC_EXPR, VAR_OTEL_JOIN_QUERY_EXPR } from '../shared'; + +import { getAutoQueriesForMetric } from './getAutoQueriesForMetric'; +import { generateBaseQuery } from './queryGenerators/baseQuery'; function expandExpr(shortenedExpr: string) { return shortenedExpr.replace('...', '${metric}{${filters}}'); @@ -105,7 +108,7 @@ describe('getAutoQueriesForMetric', () => { test('preview panel has heatmap query', () => { const [{ expr }] = result.preview.queries; - const expected = 'sum by(le) (rate(${metric}{${filters}}[$__rate_interval])${otel_join_query})'; + const expected = 'sum by(le) (rate(${metric}{${filters}}[$__rate_interval]) ${otel_join_query})'; expect(expr).toBe(expected); }); @@ -161,9 +164,9 @@ describe('getAutoQueriesForMetric', () => { ], // ***WE DEFAULT TO HEATMAP HERE // Bucket - ['PREFIX_bucket', 'sum by(le) (rate(...[$__rate_interval])${otel_join_query})', 'short', 1], - ['PREFIX_seconds_bucket', 'sum by(le) (rate(...[$__rate_interval])${otel_join_query})', 's', 1], - ['PREFIX_bytes_bucket', 'sum by(le) (rate(...[$__rate_interval])${otel_join_query})', 'bytes', 1], + ['PREFIX_bucket', 'sum by(le) (rate(...[$__rate_interval]) ${otel_join_query})', 'short', 1], + ['PREFIX_seconds_bucket', 'sum by(le) (rate(...[$__rate_interval]) ${otel_join_query})', 's', 1], + ['PREFIX_bytes_bucket', 'sum by(le) (rate(...[$__rate_interval]) ${otel_join_query})', 'bytes', 1], ])('Given metric %p expect %p with unit %p', (metric, expr, unit, queryCount) => { const result = getAutoQueriesForMetric(metric); @@ -202,9 +205,9 @@ describe('getAutoQueriesForMetric', () => { 'bytes', ], // Bucket - ['PREFIX_bucket', 'sum by(le) (rate(...[$__rate_interval])${otel_join_query})', 'short'], - ['PREFIX_seconds_bucket', 'sum by(le) (rate(...[$__rate_interval])${otel_join_query})', 's'], - ['PREFIX_bytes_bucket', 'sum by(le) (rate(...[$__rate_interval])${otel_join_query})', 'bytes'], + ['PREFIX_bucket', 'sum by(le) (rate(...[$__rate_interval]) ${otel_join_query})', 'short'], + ['PREFIX_seconds_bucket', 'sum by(le) (rate(...[$__rate_interval]) ${otel_join_query})', 's'], + ['PREFIX_bytes_bucket', 'sum by(le) (rate(...[$__rate_interval]) ${otel_join_query})', 'bytes'], ])('Given metric %p expect %p with unit %p', (metric, expr, unit) => { const result = getAutoQueriesForMetric(metric); @@ -247,17 +250,17 @@ describe('getAutoQueriesForMetric', () => { // Bucket [ 'PREFIX_bucket', - 'histogram_quantile(0.5, sum by(le, ${groupby}) (rate(...[$__rate_interval])${otel_join_query}))', + 'histogram_quantile(0.5, sum by(le, ${groupby}) (rate(...[$__rate_interval]) ${otel_join_query}))', 'short', ], [ 'PREFIX_seconds_bucket', - 'histogram_quantile(0.5, sum by(le, ${groupby}) (rate(...[$__rate_interval])${otel_join_query}))', + 'histogram_quantile(0.5, sum by(le, ${groupby}) (rate(...[$__rate_interval]) ${otel_join_query}))', 's', ], [ 'PREFIX_bytes_bucket', - 'histogram_quantile(0.5, sum by(le, ${groupby}) (rate(...[$__rate_interval])${otel_join_query}))', + 'histogram_quantile(0.5, sum by(le, ${groupby}) (rate(...[$__rate_interval]) ${otel_join_query}))', 'bytes', ], ])('Given metric %p expect %p with unit %p', (metric, expr, unit) => { @@ -292,15 +295,15 @@ describe('getAutoQueriesForMetric', () => { variant: 'percentiles', unit: 'short', exprs: [ - 'histogram_quantile(0.99, sum by(le) (rate(${metric}{${filters}}[$__rate_interval])${otel_join_query}))', - 'histogram_quantile(0.9, sum by(le) (rate(${metric}{${filters}}[$__rate_interval])${otel_join_query}))', - 'histogram_quantile(0.5, sum by(le) (rate(${metric}{${filters}}[$__rate_interval])${otel_join_query}))', + 'histogram_quantile(0.99, sum by(le) (rate(${metric}{${filters}}[$__rate_interval]) ${otel_join_query}))', + 'histogram_quantile(0.9, sum by(le) (rate(${metric}{${filters}}[$__rate_interval]) ${otel_join_query}))', + 'histogram_quantile(0.5, sum by(le) (rate(${metric}{${filters}}[$__rate_interval]) ${otel_join_query}))', ], }, { variant: 'heatmap', unit: 'short', - exprs: ['sum by(le) (rate(${metric}{${filters}}[$__rate_interval])${otel_join_query})'], + exprs: ['sum by(le) (rate(${metric}{${filters}}[$__rate_interval]) ${otel_join_query})'], }, ], ], @@ -311,15 +314,15 @@ describe('getAutoQueriesForMetric', () => { variant: 'percentiles', unit: 's', exprs: [ - 'histogram_quantile(0.99, sum by(le) (rate(${metric}{${filters}}[$__rate_interval])${otel_join_query}))', - 'histogram_quantile(0.9, sum by(le) (rate(${metric}{${filters}}[$__rate_interval])${otel_join_query}))', - 'histogram_quantile(0.5, sum by(le) (rate(${metric}{${filters}}[$__rate_interval])${otel_join_query}))', + 'histogram_quantile(0.99, sum by(le) (rate(${metric}{${filters}}[$__rate_interval]) ${otel_join_query}))', + 'histogram_quantile(0.9, sum by(le) (rate(${metric}{${filters}}[$__rate_interval]) ${otel_join_query}))', + 'histogram_quantile(0.5, sum by(le) (rate(${metric}{${filters}}[$__rate_interval]) ${otel_join_query}))', ], }, { variant: 'heatmap', unit: 's', - exprs: ['sum by(le) (rate(${metric}{${filters}}[$__rate_interval])${otel_join_query})'], + exprs: ['sum by(le) (rate(${metric}{${filters}}[$__rate_interval]) ${otel_join_query})'], }, ], ], @@ -330,15 +333,15 @@ describe('getAutoQueriesForMetric', () => { variant: 'percentiles', unit: 'bytes', exprs: [ - 'histogram_quantile(0.99, sum by(le) (rate(${metric}{${filters}}[$__rate_interval])${otel_join_query}))', - 'histogram_quantile(0.9, sum by(le) (rate(${metric}{${filters}}[$__rate_interval])${otel_join_query}))', - 'histogram_quantile(0.5, sum by(le) (rate(${metric}{${filters}}[$__rate_interval])${otel_join_query}))', + 'histogram_quantile(0.99, sum by(le) (rate(${metric}{${filters}}[$__rate_interval]) ${otel_join_query}))', + 'histogram_quantile(0.9, sum by(le) (rate(${metric}{${filters}}[$__rate_interval]) ${otel_join_query}))', + 'histogram_quantile(0.5, sum by(le) (rate(${metric}{${filters}}[$__rate_interval]) ${otel_join_query}))', ], }, { variant: 'heatmap', unit: 'bytes', - exprs: ['sum by(le) (rate(${metric}{${filters}}[$__rate_interval])${otel_join_query})'], + exprs: ['sum by(le) (rate(${metric}{${filters}}[$__rate_interval]) ${otel_join_query})'], }, ], ], @@ -371,3 +374,53 @@ describe('getAutoQueriesForMetric', () => { ); }); }); + +describe('generateBaseQuery', () => { + it('should generate a non-rate non-UTF8 base query', () => { + expect(generateBaseQuery({ isRateQuery: false, isUtf8Metric: false })).toBe( + `${VAR_METRIC_EXPR}{${VAR_FILTERS_EXPR}} ${VAR_OTEL_JOIN_QUERY_EXPR}` + ); + }); + + it('should generate a rate non-UTF8 base query', () => { + expect(generateBaseQuery({ isRateQuery: true, isUtf8Metric: false })).toBe( + `rate(${VAR_METRIC_EXPR}{${VAR_FILTERS_EXPR}}[$__rate_interval]) ${VAR_OTEL_JOIN_QUERY_EXPR}` + ); + }); + + it('should generate a non-rate UTF8 base query', () => { + expect(generateBaseQuery({ isRateQuery: false, isUtf8Metric: true })).toBe( + `{"${VAR_METRIC_EXPR}", ${VAR_FILTERS_EXPR}} ${VAR_OTEL_JOIN_QUERY_EXPR}` + ); + }); + + it('should generate a rate UTF8 base query', () => { + expect(generateBaseQuery({ isRateQuery: true, isUtf8Metric: true })).toBe( + `rate({"${VAR_METRIC_EXPR}", ${VAR_FILTERS_EXPR}}[$__rate_interval]) ${VAR_OTEL_JOIN_QUERY_EXPR}` + ); + }); + + it('should generate a grouped non-UTF8 rate query', () => { + expect( + generateBaseQuery({ + isRateQuery: true, + isUtf8Metric: false, + groupings: ['le', 'job'], + }) + ).toBe( + `sum by(le, job) (rate(${VAR_METRIC_EXPR}{${VAR_FILTERS_EXPR}}[$__rate_interval]) ${VAR_OTEL_JOIN_QUERY_EXPR})` + ); + }); + + it('should generate a grouped UTF8 rate query', () => { + expect( + generateBaseQuery({ + isRateQuery: true, + isUtf8Metric: true, + groupings: ['le', 'instance'], + }) + ).toBe( + `sum by(le, instance) (rate({"${VAR_METRIC_EXPR}", ${VAR_FILTERS_EXPR}}[$__rate_interval]) ${VAR_OTEL_JOIN_QUERY_EXPR})` + ); + }); +}); diff --git a/public/app/features/trails/autoQuery/getAutoQueriesForMetric.ts b/public/app/features/trails/autoQuery/getAutoQueriesForMetric.ts new file mode 100644 index 00000000000..aa09aeb8486 --- /dev/null +++ b/public/app/features/trails/autoQuery/getAutoQueriesForMetric.ts @@ -0,0 +1,36 @@ +import { createDefaultMetricQueryDefs } from './queryGenerators/default'; +import { createHistogramMetricQueryDefs } from './queryGenerators/histogram'; +import { createSummaryMetricQueryDefs } from './queryGenerators/summary'; +import { AutoQueryContext, AutoQueryInfo } from './types'; +import { getUnit } from './units'; + +export function getAutoQueriesForMetric(metric: string): AutoQueryInfo { + const isUtf8Metric = false; + const metricParts = metric.split('_'); + const suffix = metricParts.at(-1); + + // If the suffix is null or is in the set of unsupported suffixes, throw an error because the metric should be delegated to a different generator (summary or histogram) + if (suffix == null) { + throw new Error(`This function does not support a metric suffix of "${suffix}"`); + } + + const unitSuffix = metricParts.at(-2); + const unit = getUnit(unitSuffix); + const ctx: AutoQueryContext = { + metricParts, + isUtf8Metric, + suffix, + unitSuffix, + unit, + }; + + if (suffix === 'sum') { + return createSummaryMetricQueryDefs(ctx); + } + + if (suffix === 'bucket') { + return createHistogramMetricQueryDefs(ctx); + } + + return createDefaultMetricQueryDefs(ctx); +} diff --git a/public/app/features/trails/autoQuery/graphBuilders.ts b/public/app/features/trails/autoQuery/graphBuilders.ts new file mode 100644 index 00000000000..0843499b245 --- /dev/null +++ b/public/app/features/trails/autoQuery/graphBuilders.ts @@ -0,0 +1,41 @@ +import { PanelBuilders } from '@grafana/scenes'; +import { SortOrder, TooltipDisplayMode } from '@grafana/schema/dist/esm/index'; + +import { HeatmapColorMode } from '../../../plugins/panel/heatmap/panelcfg.gen'; + +export type CommonVizParams = { + title: string; + unit: string; +}; + +export function simpleGraphBuilder({ title, unit }: CommonVizParams) { + return PanelBuilders.timeseries() // + .setTitle(title) + .setUnit(unit) + .setOption('legend', { showLegend: false }) + .setOption('tooltip', { mode: TooltipDisplayMode.Multi, sort: SortOrder.Descending }) + .setCustomFieldConfig('fillOpacity', 9); +} + +export function heatmapGraphBuilder({ title, unit }: CommonVizParams) { + return PanelBuilders.heatmap() // + .setTitle(title) + .setUnit(unit) + .setOption('calculate', false) + .setOption('color', { + mode: HeatmapColorMode.Scheme, + exponent: 0.5, + scheme: 'Spectral', + steps: 32, + reverse: false, + }); +} + +export function percentilesGraphBuilder({ title, unit }: CommonVizParams) { + return PanelBuilders.timeseries() + .setTitle(title) + .setUnit(unit) + .setCustomFieldConfig('fillOpacity', 9) + .setOption('tooltip', { mode: TooltipDisplayMode.Multi, sort: SortOrder.Descending }) + .setOption('legend', { showLegend: false }); +} diff --git a/public/app/features/trails/autoQuery/queryGenerators/baseQuery.test.ts b/public/app/features/trails/autoQuery/queryGenerators/baseQuery.test.ts new file mode 100644 index 00000000000..dd7ae9a2fe5 --- /dev/null +++ b/public/app/features/trails/autoQuery/queryGenerators/baseQuery.test.ts @@ -0,0 +1,43 @@ +import { generateBaseQuery } from './baseQuery'; + +describe('generateBaseQuery', () => { + it('should return base query without rate and groupings', () => { + const result = generateBaseQuery({}); + expect(result).toBe('${metric}{${filters}} ${otel_join_query}'); + }); + + it('should return rate base query without groupings', () => { + const result = generateBaseQuery({ isRateQuery: true }); + expect(result).toBe('rate(${metric}{${filters}}[$__rate_interval]) ${otel_join_query}'); + }); + + it('should return base query with groupings', () => { + const result = generateBaseQuery({ groupings: ['job', 'instance'] }); + expect(result).toBe('sum by(job, instance) (${metric}{${filters}} ${otel_join_query})'); + }); + + it('should return rate base query with groupings', () => { + const result = generateBaseQuery({ isRateQuery: true, groupings: ['job', 'instance'] }); + expect(result).toBe('sum by(job, instance) (rate(${metric}{${filters}}[$__rate_interval]) ${otel_join_query})'); + }); + + it('should return UTF-8 base query without rate and groupings', () => { + const result = generateBaseQuery({ isUtf8Metric: true }); + expect(result).toBe('{"${metric}", ${filters}} ${otel_join_query}'); + }); + + it('should return UTF-8 rate base query without groupings', () => { + const result = generateBaseQuery({ isRateQuery: true, isUtf8Metric: true }); + expect(result).toBe('rate({"${metric}", ${filters}}[$__rate_interval]) ${otel_join_query}'); + }); + + it('should return UTF-8 base query with groupings', () => { + const result = generateBaseQuery({ isUtf8Metric: true, groupings: ['job', 'instance'] }); + expect(result).toBe('sum by(job, instance) ({"${metric}", ${filters}} ${otel_join_query})'); + }); + + it('should return UTF-8 rate base query with groupings', () => { + const result = generateBaseQuery({ isRateQuery: true, isUtf8Metric: true, groupings: ['job', 'instance'] }); + expect(result).toBe('sum by(job, instance) (rate({"${metric}", ${filters}}[$__rate_interval]) ${otel_join_query})'); + }); +}); diff --git a/public/app/features/trails/autoQuery/queryGenerators/baseQuery.ts b/public/app/features/trails/autoQuery/queryGenerators/baseQuery.ts new file mode 100644 index 00000000000..c57d81b4908 --- /dev/null +++ b/public/app/features/trails/autoQuery/queryGenerators/baseQuery.ts @@ -0,0 +1,37 @@ +import { VAR_FILTERS_EXPR, VAR_METRIC_EXPR, VAR_OTEL_JOIN_QUERY_EXPR } from '../../shared'; + +// For usual non-utf8-metrics we use filters in the curly braces +// metric_name{filter_label="filter_value"} +const BASE_QUERY_TEMPLATE = `${VAR_METRIC_EXPR}{${VAR_FILTERS_EXPR}}`; +const RATE_BASE_QUERY_TEMPLATE = `rate(${BASE_QUERY_TEMPLATE}[$__rate_interval])`; + +// For utf8 metrics we need to put the metric name inside curly braces with filters +// {"utf8.metric", filter_label="filter_val"} +const BASE_QUERY_UTF8_METRIC_TEMPLATE = `{"${VAR_METRIC_EXPR}", ${VAR_FILTERS_EXPR}}`; +const RATE_BASE_QUERY_UTF8_METRIC_TEMPLATE = `rate(${BASE_QUERY_UTF8_METRIC_TEMPLATE}[$__rate_interval])`; + +export function generateBaseQuery({ + isRateQuery = false, + groupings = [], + isUtf8Metric = false, +}: { + isRateQuery?: boolean; + groupings?: string[]; + isUtf8Metric?: boolean; +}): string { + // Determine base query template + const baseQuery = isUtf8Metric + ? isRateQuery + ? RATE_BASE_QUERY_UTF8_METRIC_TEMPLATE + : BASE_QUERY_UTF8_METRIC_TEMPLATE + : isRateQuery + ? RATE_BASE_QUERY_TEMPLATE + : BASE_QUERY_TEMPLATE; + + // Apply groupings (e.g., `sum by(le, instance)`) + if (groupings.length > 0) { + return `sum by(${groupings.join(', ')}) (${baseQuery} ${VAR_OTEL_JOIN_QUERY_EXPR})`; + } + + return `${baseQuery} ${VAR_OTEL_JOIN_QUERY_EXPR}`; +} diff --git a/public/app/features/trails/autoQuery/queryGenerators/common.test.ts b/public/app/features/trails/autoQuery/queryGenerators/common.test.ts new file mode 100644 index 00000000000..3cf4d266df0 --- /dev/null +++ b/public/app/features/trails/autoQuery/queryGenerators/common.test.ts @@ -0,0 +1,80 @@ +import { VAR_GROUP_BY_EXP, VAR_METRIC_EXPR } from '../../shared'; + +import { CommonQueryInfoParams, generateCommonAutoQueryInfo } from './common'; + +describe('generateCommonAutoQueryInfo', () => { + const params: CommonQueryInfoParams = { + description: 'Test Description', + mainQueryExpr: 'rate(test_metric[5m])', + breakdownQueryExpr: 'sum by (label) (test_metric)', + unit: 'short', + }; + + it('should generate a valid AutoQueryInfo object with main, preview, and breakdown variants', () => { + const result = generateCommonAutoQueryInfo(params); + + expect(result).toHaveProperty('main'); + expect(result).toHaveProperty('preview'); + expect(result).toHaveProperty('breakdown'); + expect(result).toHaveProperty('variants'); + }); + + it('should configure the main variant correctly', () => { + const result = generateCommonAutoQueryInfo(params); + + const { main } = result; + expect(main).toMatchObject({ + title: params.description, + unit: params.unit, + queries: [ + { + refId: 'A', + expr: params.mainQueryExpr, + legendFormat: params.description, + }, + ], + variant: 'main', + }); + }); + + it('should configure the preview variant correctly', () => { + const result = generateCommonAutoQueryInfo(params); + + const { preview } = result; + expect(preview).toMatchObject({ + title: VAR_METRIC_EXPR, + unit: params.unit, + queries: [ + { + refId: 'A', + expr: params.mainQueryExpr, + legendFormat: params.description, + }, + ], + variant: 'preview', + }); + }); + + it('should configure the breakdown variant correctly', () => { + const result = generateCommonAutoQueryInfo(params); + + const { breakdown } = result; + expect(breakdown).toMatchObject({ + title: VAR_METRIC_EXPR, + unit: params.unit, + queries: [ + { + refId: 'A', + expr: params.breakdownQueryExpr, + legendFormat: `{{${VAR_GROUP_BY_EXP}}}`, + }, + ], + variant: 'breakdown', + }); + }); + + it('should return an empty variants array', () => { + const result = generateCommonAutoQueryInfo(params); + expect(result.variants).toEqual([]); + }); +}); diff --git a/public/app/features/trails/AutomaticMetricQueries/query-generators/common/generator.ts b/public/app/features/trails/autoQuery/queryGenerators/common.ts similarity index 80% rename from public/app/features/trails/AutomaticMetricQueries/query-generators/common/generator.ts rename to public/app/features/trails/autoQuery/queryGenerators/common.ts index fbc7ff5d3cb..b0e82b4baa3 100644 --- a/public/app/features/trails/AutomaticMetricQueries/query-generators/common/generator.ts +++ b/public/app/features/trails/autoQuery/queryGenerators/common.ts @@ -1,5 +1,6 @@ -import { VAR_GROUP_BY_EXP, VAR_METRIC_EXPR } from '../../../shared'; -import { simpleGraphBuilder } from '../../graph-builders/simple'; +import { VAR_GROUP_BY_EXP, VAR_METRIC_EXPR } from '../../shared'; +import { simpleGraphBuilder } from '../graphBuilders'; +import { AutoQueryInfo } from '../types'; export type CommonQueryInfoParams = { description: string; @@ -13,9 +14,9 @@ export function generateCommonAutoQueryInfo({ mainQueryExpr, breakdownQueryExpr, unit, -}: CommonQueryInfoParams) { +}: CommonQueryInfoParams): AutoQueryInfo { const common = { - title: `${VAR_METRIC_EXPR}`, + title: VAR_METRIC_EXPR, unit, }; @@ -34,8 +35,7 @@ export function generateCommonAutoQueryInfo({ }; const preview = { - ...main, - title: `${VAR_METRIC_EXPR}`, + ...common, queries: [{ ...mainQuery, legendFormat: description }], vizBuilder: () => simpleGraphBuilder(preview), variant: 'preview', diff --git a/public/app/features/trails/autoQuery/queryGenerators/default.test.ts b/public/app/features/trails/autoQuery/queryGenerators/default.test.ts new file mode 100644 index 00000000000..a5417eb1e4a --- /dev/null +++ b/public/app/features/trails/autoQuery/queryGenerators/default.test.ts @@ -0,0 +1,41 @@ +import { AutoQueryContext } from '../types'; + +import { createDefaultMetricQueryDefs } from './default'; + +describe('createDefaultMetricQueryDefs', () => { + it('should generate correct AutoQueryInfo for rate query with UTF-8 metric', () => { + const context: AutoQueryContext = { + metricParts: ['http.requests', 'total'], + suffix: 'total', + isUtf8Metric: true, + unit: 'cps', + }; + + const result = createDefaultMetricQueryDefs(context); + + expect(result.main.title).toBe('${metric} (overall per-second rate)'); + expect(result.main.queries[0].expr).toBe( + 'sum(rate({"${metric}", ${filters}}[$__rate_interval]) ${otel_join_query})' + ); + expect(result.breakdown.queries[0].expr).toBe( + 'sum(rate({"${metric}", ${filters}}[$__rate_interval]) ${otel_join_query})by(${groupby})' + ); + expect(result.preview.unit).toBe('cps'); + }); + + it('should generate correct AutoQueryInfo for non-rate query without UTF-8 metric', () => { + const context: AutoQueryContext = { + metricParts: ['cpu', 'usage', 'seconds'], + suffix: 'avg', + isUtf8Metric: false, + unit: 's', + }; + + const result = createDefaultMetricQueryDefs(context); + + expect(result.main.title).toBe('${metric} (average)'); + expect(result.main.queries[0].expr).toBe('avg(${metric}{${filters}} ${otel_join_query})'); + expect(result.breakdown.queries[0].expr).toBe('avg(${metric}{${filters}} ${otel_join_query})by(${groupby})'); + expect(result.preview.unit).toBe('short'); + }); +}); diff --git a/public/app/features/trails/autoQuery/queryGenerators/default.ts b/public/app/features/trails/autoQuery/queryGenerators/default.ts new file mode 100644 index 00000000000..bd8074429c0 --- /dev/null +++ b/public/app/features/trails/autoQuery/queryGenerators/default.ts @@ -0,0 +1,46 @@ +import { VAR_GROUP_BY_EXP, VAR_METRIC_EXPR } from '../../shared'; +import { AutoQueryContext, AutoQueryInfo } from '../types'; +import { getPerSecondRateUnit, getUnit } from '../units'; + +import { generateBaseQuery } from './baseQuery'; +import { generateCommonAutoQueryInfo } from './common'; + +const RATE_SUFFIXES = new Set(['count', 'total']); +const SPECIFIC_AGGREGATIONS_FOR_SUFFIX: Record = { + count: 'sum', + total: 'sum', +}; +const aggLabels: Record = { + avg: 'average', + sum: 'overall', +}; + +function getAggLabel(agg: string): string { + return aggLabels[agg] || agg; +} + +export function createDefaultMetricQueryDefs(context: AutoQueryContext): AutoQueryInfo { + const { metricParts, suffix, isUtf8Metric } = context; + const unitSuffix = suffix === 'total' ? metricParts.at(-2) : suffix; + + // Determine query type and unit + const isRateQuery = RATE_SUFFIXES.has(suffix); + const aggregation = SPECIFIC_AGGREGATIONS_FOR_SUFFIX[suffix] || 'avg'; + const unit = isRateQuery ? getPerSecondRateUnit(unitSuffix) : getUnit(unitSuffix); + + // Generate base query and descriptions + const baseQuery = generateBaseQuery({ isRateQuery, isUtf8Metric }); + const aggregationDescription = `${getAggLabel(aggregation)}${isRateQuery ? ' per-second rate' : ''}`; + const description = `${VAR_METRIC_EXPR} (${aggregationDescription})`; + + // Create query expressions + const mainQueryExpr = `${aggregation}(${baseQuery})`; + const breakdownQueryExpr = `${aggregation}(${baseQuery})by(${VAR_GROUP_BY_EXP})`; + + return generateCommonAutoQueryInfo({ + description, + mainQueryExpr, + breakdownQueryExpr, + unit, + }); +} diff --git a/public/app/features/trails/autoQuery/queryGenerators/histogram.test.ts b/public/app/features/trails/autoQuery/queryGenerators/histogram.test.ts new file mode 100644 index 00000000000..d02436389bf --- /dev/null +++ b/public/app/features/trails/autoQuery/queryGenerators/histogram.test.ts @@ -0,0 +1,101 @@ +import { AutoQueryContext } from '../types'; + +import { createHistogramMetricQueryDefs } from './histogram'; + +describe('createHistogramMetricQueryDefs utf8=false', () => { + const ctx: AutoQueryContext = { + metricParts: ['test', 'latency', 'seconds', 'bucket'], + isUtf8Metric: false, + suffix: 'bucket', + unitSuffix: 'seconds', + unit: 's', + }; + + it('should create the correct title and unit for metricParts', () => { + const result = createHistogramMetricQueryDefs(ctx); + expect(result.preview.title).toBe('${metric}'); + expect(result.preview.unit).toBe('s'); + }); + + it('should generate correct p50 AutoQueryDef', () => { + const result = createHistogramMetricQueryDefs(ctx); + const p50Query = result.breakdown.queries[0]; + + expect(p50Query.expr).toBe( + 'histogram_quantile(0.5, sum by(le, ${groupby}) (rate(${metric}{${filters}}[$__rate_interval]) ${otel_join_query}))' + ); + expect(p50Query.legendFormat).toBe('{{${groupby}}}'); + }); + + it('should generate correct percentiles AutoQueryDef', () => { + const result = createHistogramMetricQueryDefs(ctx); + const percentileQueries = result.variants[0].queries; + + expect(percentileQueries[0].expr).toBe( + 'histogram_quantile(0.99, sum by(le) (rate(${metric}{${filters}}[$__rate_interval]) ${otel_join_query}))' + ); + expect(percentileQueries[1].expr).toBe( + 'histogram_quantile(0.9, sum by(le) (rate(${metric}{${filters}}[$__rate_interval]) ${otel_join_query}))' + ); + expect(percentileQueries[2].expr).toBe( + 'histogram_quantile(0.5, sum by(le) (rate(${metric}{${filters}}[$__rate_interval]) ${otel_join_query}))' + ); + }); + + it('should generate correct heatmap AutoQueryDef', () => { + const result = createHistogramMetricQueryDefs(ctx); + const heatmapQuery = result.preview.queries[0]; + + expect(heatmapQuery.expr).toBe('sum by(le) (rate(${metric}{${filters}}[$__rate_interval]) ${otel_join_query})'); + expect(result.preview.variant).toBe('heatmap'); + }); +}); + +describe('createHistogramMetricQueryDefs utf8=true', () => { + const ctx: AutoQueryContext = { + metricParts: ['test', 'latency', 'seconds', 'bucket'], + isUtf8Metric: true, + suffix: 'bucket', + unitSuffix: 'seconds', + unit: 's', + }; + + it('should create the correct title and unit for metricParts', () => { + const result = createHistogramMetricQueryDefs(ctx); + expect(result.preview.title).toBe('${metric}'); + expect(result.preview.unit).toBe('s'); + }); + + it('should generate correct p50 AutoQueryDef', () => { + const result = createHistogramMetricQueryDefs(ctx); + const p50Query = result.breakdown.queries[0]; + + expect(p50Query.expr).toBe( + 'histogram_quantile(0.5, sum by(le, ${groupby}) (rate({"${metric}", ${filters}}[$__rate_interval]) ${otel_join_query}))' + ); + expect(p50Query.legendFormat).toBe('{{${groupby}}}'); + }); + + it('should generate correct percentiles AutoQueryDef', () => { + const result = createHistogramMetricQueryDefs(ctx); + const percentileQueries = result.variants[0].queries; + + expect(percentileQueries[0].expr).toBe( + 'histogram_quantile(0.99, sum by(le) (rate({"${metric}", ${filters}}[$__rate_interval]) ${otel_join_query}))' + ); + expect(percentileQueries[1].expr).toBe( + 'histogram_quantile(0.9, sum by(le) (rate({"${metric}", ${filters}}[$__rate_interval]) ${otel_join_query}))' + ); + expect(percentileQueries[2].expr).toBe( + 'histogram_quantile(0.5, sum by(le) (rate({"${metric}", ${filters}}[$__rate_interval]) ${otel_join_query}))' + ); + }); + + it('should generate correct heatmap AutoQueryDef', () => { + const result = createHistogramMetricQueryDefs(ctx); + const heatmapQuery = result.preview.queries[0]; + + expect(heatmapQuery.expr).toBe('sum by(le) (rate({"${metric}", ${filters}}[$__rate_interval]) ${otel_join_query})'); + expect(result.preview.variant).toBe('heatmap'); + }); +}); diff --git a/public/app/features/trails/autoQuery/queryGenerators/histogram.ts b/public/app/features/trails/autoQuery/queryGenerators/histogram.ts new file mode 100644 index 00000000000..c4fc17ad331 --- /dev/null +++ b/public/app/features/trails/autoQuery/queryGenerators/histogram.ts @@ -0,0 +1,77 @@ +import { VAR_GROUP_BY_EXP, VAR_METRIC_EXPR } from '../../shared'; +import { heatmapGraphBuilder, percentilesGraphBuilder, simpleGraphBuilder } from '../graphBuilders'; +import { AutoQueryContext, AutoQueryDef } from '../types'; + +import { generateBaseQuery } from './baseQuery'; + +export function createHistogramMetricQueryDefs(context: AutoQueryContext) { + const { unit } = context; + + const common = { + title: VAR_METRIC_EXPR, + unit, + }; + + const p50: AutoQueryDef = { + ...common, + variant: 'p50', + queries: [percentileQuery(context, 50)], + vizBuilder: () => simpleGraphBuilder(p50), + }; + + const breakdown: AutoQueryDef = { + ...common, + variant: 'p50', + queries: [percentileQuery(context, 50, [VAR_GROUP_BY_EXP])], + vizBuilder: () => simpleGraphBuilder(breakdown), + }; + + const percentiles: AutoQueryDef = { + ...common, + variant: 'percentiles', + queries: [99, 90, 50].map((p) => percentileQuery(context, p)), + vizBuilder: () => percentilesGraphBuilder(percentiles), + }; + + const heatmap: AutoQueryDef = { + ...common, + variant: 'heatmap', + queries: [ + { + refId: 'Heatmap', + expr: generateBaseQuery({ + isRateQuery: true, + isUtf8Metric: context.isUtf8Metric, + groupings: ['le'], + }), + format: 'heatmap', + }, + ], + vizBuilder: () => heatmapGraphBuilder(heatmap), + }; + + return { preview: heatmap, main: heatmap, variants: [percentiles, heatmap], breakdown: breakdown }; +} + +function percentileQuery(context: AutoQueryContext, percentile: number, groupings: string[] = []) { + const percent = percentile / 100; + + let legendFormat = `${percentile}th Percentile`; + + // For the breakdown view, show the label value variable we are grouping by + if (groupings[0]) { + legendFormat = `{{${groupings[0]}}}`; + } + + const query = generateBaseQuery({ + isRateQuery: true, + isUtf8Metric: context.isUtf8Metric, + groupings: ['le', ...groupings], + }); + + return { + refId: `Percentile${percentile}`, + expr: `histogram_quantile(${percent}, ${query})`, + legendFormat, + }; +} diff --git a/public/app/features/trails/autoQuery/queryGenerators/summary.test.ts b/public/app/features/trails/autoQuery/queryGenerators/summary.test.ts new file mode 100644 index 00000000000..48fcf4c55a1 --- /dev/null +++ b/public/app/features/trails/autoQuery/queryGenerators/summary.test.ts @@ -0,0 +1,49 @@ +import { AutoQueryContext } from '../types'; + +import { createSummaryMetricQueryDefs } from './summary'; + +describe('createSummaryMetricQueryDefs', () => { + it('should generate correct AutoQueryInfo with rate query and UTF-8 metric', () => { + const context: AutoQueryContext = { + metricParts: ['http.requests', 'sum'], + isUtf8Metric: true, + unit: 'ms', + suffix: 'sum', + }; + + const result = createSummaryMetricQueryDefs(context); + + expect(result.preview.title).toBe('${metric}'); + expect(result.main.title).toBe('http.requests (average)'); + expect(result.breakdown.title).toBe('${metric}'); + expect(result.preview.queries[0].expr).toBe( + 'sum(rate({"http.requests_sum", ${filters}}[$__rate_interval]) ${otel_join_query})/sum(rate({"http.requests_count", ${filters}}[$__rate_interval]) ${otel_join_query})' + ); + expect(result.breakdown.queries[0].expr).toBe( + 'sum(rate({"http.requests_sum", ${filters}}[$__rate_interval]) ${otel_join_query})by(${groupby})/sum(rate({"http.requests_count", ${filters}}[$__rate_interval]) ${otel_join_query})by(${groupby})' + ); + expect(result.preview.unit).toBe('ms'); + }); + + it('should generate correct AutoQueryInfo without UTF-8 metric', () => { + const context: AutoQueryContext = { + metricParts: ['cpu', 'usage', 'seconds', 'sum'], + isUtf8Metric: false, + unit: 's', + suffix: 'sum', + }; + + const result = createSummaryMetricQueryDefs(context); + + expect(result.preview.title).toBe('${metric}'); + expect(result.main.title).toBe('cpu_usage_seconds (average)'); + expect(result.breakdown.title).toBe('${metric}'); + expect(result.preview.queries[0].expr).toBe( + 'sum(rate(cpu_usage_seconds_sum{${filters}}[$__rate_interval]) ${otel_join_query})/sum(rate(cpu_usage_seconds_count{${filters}}[$__rate_interval]) ${otel_join_query})' + ); + expect(result.breakdown.queries[0].expr).toBe( + 'sum(rate(cpu_usage_seconds_sum{${filters}}[$__rate_interval]) ${otel_join_query})by(${groupby})/sum(rate(cpu_usage_seconds_count{${filters}}[$__rate_interval]) ${otel_join_query})by(${groupby})' + ); + expect(result.preview.unit).toBe('s'); + }); +}); diff --git a/public/app/features/trails/autoQuery/queryGenerators/summary.ts b/public/app/features/trails/autoQuery/queryGenerators/summary.ts new file mode 100644 index 00000000000..589c8228ebb --- /dev/null +++ b/public/app/features/trails/autoQuery/queryGenerators/summary.ts @@ -0,0 +1,27 @@ +import { VAR_GROUP_BY_EXP, VAR_METRIC_EXPR } from '../../shared'; +import { AutoQueryContext, AutoQueryInfo } from '../types'; + +import { generateBaseQuery } from './baseQuery'; +import { generateCommonAutoQueryInfo } from './common'; + +export function createSummaryMetricQueryDefs(context: AutoQueryContext): AutoQueryInfo { + const { metricParts, isUtf8Metric, unit } = context; + const subMetric = metricParts.slice(0, -1).join('_'); + const description = `${subMetric} (average)`; + const baseQuery = generateBaseQuery({ isRateQuery: true, isUtf8Metric }); + const mainQueryExpr = createMeanExpr(`sum(${baseQuery})`, subMetric); + const breakdownQueryExpr = createMeanExpr(`sum(${baseQuery})by(${VAR_GROUP_BY_EXP})`, subMetric); + + return generateCommonAutoQueryInfo({ + description, + mainQueryExpr, + breakdownQueryExpr, + unit, + }); +} + +function createMeanExpr(expr: string, subMetric: string): string { + const numerator = expr.replace(VAR_METRIC_EXPR, `${subMetric}_sum`); + const denominator = expr.replace(VAR_METRIC_EXPR, `${subMetric}_count`); + return `${numerator}/${denominator}`; +} diff --git a/public/app/features/trails/AutomaticMetricQueries/types.ts b/public/app/features/trails/autoQuery/types.ts similarity index 75% rename from public/app/features/trails/AutomaticMetricQueries/types.ts rename to public/app/features/trails/autoQuery/types.ts index e104dd6dab2..d6161524159 100644 --- a/public/app/features/trails/AutomaticMetricQueries/types.ts +++ b/public/app/features/trails/autoQuery/types.ts @@ -17,3 +17,11 @@ export interface AutoQueryInfo { } export type VizBuilder = () => VizPanelBuilder<{}, {}>; + +export type AutoQueryContext = { + metricParts: string[]; + isUtf8Metric: boolean; + unit: string; + suffix: string; + unitSuffix?: string; +}; diff --git a/public/app/features/trails/autoQuery/units.test.ts b/public/app/features/trails/autoQuery/units.test.ts new file mode 100644 index 00000000000..d039253b33c --- /dev/null +++ b/public/app/features/trails/autoQuery/units.test.ts @@ -0,0 +1,94 @@ +import { DEFAULT_RATE_UNIT, DEFAULT_UNIT, getPerSecondRateUnit, getUnit, getUnitFromMetric } from './units'; + +describe('getUnitFromMetric', () => { + it('should return null for an empty string input', () => { + expect(getUnitFromMetric('')).toBe(null); + }); + + it('should return the last part of the metric if it is a valid unit', () => { + expect(getUnitFromMetric('go_gc_gomemlimit_bytes')).toBe('bytes'); + expect(getUnitFromMetric('go_gc_duration_seconds')).toBe('seconds'); + }); + + it('should return the second to last part of the metric if it is a valid unit', () => { + expect(getUnitFromMetric('go_gc_heap_allocs_by_size_bytes_count')).toBe('bytes'); + expect(getUnitFromMetric('go_cpu_classes_gc_mark_assist_cpu_seconds_total')).toBe('seconds'); + }); + + it('should return null if no valid unit is found', () => { + expect(getUnitFromMetric('ALERTS')).toBe(null); + expect(getUnitFromMetric('utf8 metric with.dot')).toBe(null); + }); + + it('should handle metrics with extra underscores', () => { + expect(getUnitFromMetric('go_gc__duration__seconds')).toBe('seconds'); + }); + + it('should return null if the metric ends with an invalid unit', () => { + expect(getUnitFromMetric('go_gc_duration_invalidunit')).toBe(null); + }); + + it('should return the last unit if the metric contains only valid units', () => { + expect(getUnitFromMetric('bytes_seconds')).toBe('seconds'); + }); +}); + +describe('getUnit', () => { + it('should return the mapped unit for a valid metric part', () => { + expect(getUnit('bytes')).toBe('bytes'); + expect(getUnit('seconds')).toBe('s'); + }); + + it('should return the default unit if the metric part is undefined', () => { + expect(getUnit(undefined)).toBe(DEFAULT_UNIT); + }); + + it('should return the default unit if the metric part is an empty string', () => { + expect(getUnit('')).toBe(DEFAULT_UNIT); + }); + + it('should return the default unit if the metric part is not in UNIT_MAP', () => { + expect(getUnit('invalidPart')).toBe(DEFAULT_UNIT); + }); + + it('should handle case sensitivity correctly', () => { + expect(getUnit('BYTES')).toBe(DEFAULT_UNIT); + expect(getUnit('Seconds')).toBe(DEFAULT_UNIT); + }); + + it('should not throw errors for unusual input', () => { + expect(() => getUnit('123')).not.toThrow(); + expect(() => getUnit('some_random_string')).not.toThrow(); + expect(() => getUnit(undefined)).not.toThrow(); + }); +}); + +describe('getPerSecondRateUnit', () => { + it('should return the mapped rate unit for a valid metric part', () => { + expect(getPerSecondRateUnit('bytes')).toBe('Bps'); + expect(getPerSecondRateUnit('seconds')).toBe('short'); + }); + + it('should return the default rate unit if the metric part is undefined', () => { + expect(getPerSecondRateUnit(undefined)).toBe(DEFAULT_RATE_UNIT); + }); + + it('should return the default rate unit if the metric part is an empty string', () => { + expect(getPerSecondRateUnit('')).toBe(DEFAULT_RATE_UNIT); + }); + + it('should return the default rate unit if the metric part is not in RATE_UNIT_MAP', () => { + expect(getPerSecondRateUnit('invalidPart')).toBe(DEFAULT_RATE_UNIT); + }); + + it('should handle case sensitivity correctly', () => { + expect(getPerSecondRateUnit('BYTES')).toBe(DEFAULT_RATE_UNIT); + expect(getPerSecondRateUnit('Seconds')).toBe(DEFAULT_RATE_UNIT); + }); + + it('should not throw errors for unusual input', () => { + expect(() => getPerSecondRateUnit('123')).not.toThrow(); + expect(() => getPerSecondRateUnit('some_random_string')).not.toThrow(); + expect(() => getPerSecondRateUnit(undefined)).not.toThrow(); + }); +}); diff --git a/public/app/features/trails/autoQuery/units.ts b/public/app/features/trails/autoQuery/units.ts new file mode 100644 index 00000000000..ea9b819b4a7 --- /dev/null +++ b/public/app/features/trails/autoQuery/units.ts @@ -0,0 +1,35 @@ +export const DEFAULT_UNIT = 'short'; +export const DEFAULT_RATE_UNIT = 'cps'; // Count per second + +const UNIT_MAP: Record = { bytes: 'bytes', seconds: 's' }; +const UNIT_LIST = Object.keys(UNIT_MAP); +const RATE_UNIT_MAP: Record = { + bytes: 'Bps', // bytes per second + // seconds per second is unitless + // this may indicate a count of some resource that is active + seconds: 'short', +}; + +// Get unit from metric name (e.g. "go_gc_duration_seconds" -> "seconds") +export function getUnitFromMetric(metric: string) { + if (!metric) { + return null; + } + + const metricParts = metric.toLowerCase().split('_').slice(-2); // Get last two parts + for (let i = metricParts.length - 1; i >= 0; i--) { + if (UNIT_LIST.includes(metricParts[i])) { + return metricParts[i]; + } + } + return null; +} + +// Get Grafana unit for a panel (e.g. "go_gc_duration_seconds" -> "s") +export function getUnit(metricPart: string | undefined) { + return (metricPart && UNIT_MAP[metricPart]) || DEFAULT_UNIT; +} + +export function getPerSecondRateUnit(metricPart: string | undefined) { + return (metricPart && RATE_UNIT_MAP[metricPart]) || DEFAULT_RATE_UNIT; +} diff --git a/public/app/features/trails/shared.ts b/public/app/features/trails/shared.ts index 69af2ab8930..aab8bc4c736 100644 --- a/public/app/features/trails/shared.ts +++ b/public/app/features/trails/shared.ts @@ -15,7 +15,7 @@ export const TRAILS_ROUTE = '/explore/metrics/trail'; export const HOME_ROUTE = '/explore/metrics'; export const VAR_FILTERS = 'filters'; -export const VAR_FILTERS_EXPR = '{${filters}}'; +export const VAR_FILTERS_EXPR = '${filters}'; export const VAR_METRIC = 'metric'; export const VAR_METRIC_EXPR = '${metric}'; export const VAR_GROUP_BY = 'groupby'; From 0e8702369aaf5c5b3b1738a4d697439715685555 Mon Sep 17 00:00:00 2001 From: owensmallwood Date: Thu, 12 Dec 2024 14:08:46 -0600 Subject: [PATCH 13/28] Folders: Create default folder permissions within transaction (#97812) create default folder permissions within transaction --- pkg/services/folder/folderimpl/folder.go | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/pkg/services/folder/folderimpl/folder.go b/pkg/services/folder/folderimpl/folder.go index 586d0d3d3bc..b66f4e28d31 100644 --- a/pkg/services/folder/folderimpl/folder.go +++ b/pkg/services/folder/folderimpl/folder.go @@ -675,6 +675,7 @@ func (s *Service) Create(ctx context.Context, cmd *folder.CreateFolderCommand) ( var nestedFolder *folder.Folder var dash *dashboards.Dashboard + var f *folder.Folder err = s.db.InTransaction(ctx, func(ctx context.Context) error { if dash, err = s.dashboardStore.SaveDashboard(ctx, *saveDashboardCmd); err != nil { return toFolderError(err) @@ -696,17 +697,18 @@ func (s *Service) Create(ctx context.Context, cmd *folder.CreateFolderCommand) ( return err } + f = dashboards.FromDashboard(dash) + if nestedFolder != nil && nestedFolder.ParentUID != "" { + f.ParentUID = nestedFolder.ParentUID + } + if err = s.setDefaultFolderPermissions(ctx, cmd.OrgID, user, f); err != nil { + return err + } + return nil }) - if err != nil { - return nil, err - } - f := dashboards.FromDashboard(dash) - if nestedFolder != nil && nestedFolder.ParentUID != "" { - f.ParentUID = nestedFolder.ParentUID - } - if err = s.setDefaultFolderPermissions(ctx, cmd.OrgID, user, f); err != nil { + if err != nil { return nil, err } From 6dc41f0b18194d209f5838282b743980639a5eac Mon Sep 17 00:00:00 2001 From: Gilles De Mey Date: Thu, 12 Dec 2024 21:23:37 +0100 Subject: [PATCH 14/28] Alerting: Fix flakey FilterView test (#97882) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * increase timeout * update assertion * Add retry as well to prevent flakiness 🤞 --------- Co-authored-by: Tom Ratcliffe --- .../features/alerting/unified/rule-list/FilterView.test.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/public/app/features/alerting/unified/rule-list/FilterView.test.tsx b/public/app/features/alerting/unified/rule-list/FilterView.test.tsx index 01b8e64663c..b436e84941a 100644 --- a/public/app/features/alerting/unified/rule-list/FilterView.test.tsx +++ b/public/app/features/alerting/unified/rule-list/FilterView.test.tsx @@ -34,6 +34,7 @@ beforeEach(() => { const io = mockIntersectionObserver(); describe('RuleList - FilterView', () => { + jest.retryTimes(2); it('should render multiple pages of results', async () => { render(); @@ -53,12 +54,11 @@ describe('RuleList - FilterView', () => { await loadMoreResults(); - const matchingRule = await screen.findByRole('treeitem', { - name: /test-rule-8 test-mimir-namespace test-group-4501/, - }); + const matchingRule = (await screen.findAllByRole('treeitem')).at(0); expect(matchingRule).toBeInTheDocument(); expect(matchingRule).toHaveTextContent('test-rule-8'); + expect(matchingRule).toHaveTextContent('test-mimir-namespace'); expect(matchingRule).toHaveTextContent('test-group-4501'); expect(await screen.findByText(/No more results/)).toBeInTheDocument(); }); From 1509fadd5ba6d86fad0be0b867cdbb6d41a76843 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-Philippe=20Qu=C3=A9m=C3=A9ner?= Date: Thu, 12 Dec 2024 21:41:01 +0100 Subject: [PATCH 15/28] refactor(unified-storage): make datasyncer configurable (#97832) --- pkg/apiserver/rest/dualwriter.go | 72 +++++------ pkg/apiserver/rest/dualwriter_syncer.go | 122 +++++++++++++------ pkg/apiserver/rest/dualwriter_syncer_test.go | 26 +++- pkg/apiserver/rest/dualwriter_test.go | 13 +- pkg/services/apiserver/builder/helper.go | 31 ++++- pkg/services/apiserver/options/storage.go | 8 +- pkg/setting/setting.go | 4 + pkg/setting/setting_unified_storage.go | 9 ++ pkg/setting/setting_unified_storage_test.go | 9 ++ 9 files changed, 216 insertions(+), 78 deletions(-) diff --git a/pkg/apiserver/rest/dualwriter.go b/pkg/apiserver/rest/dualwriter.go index e803849ddbc..b0ee88ad854 100644 --- a/pkg/apiserver/rest/dualwriter.go +++ b/pkg/apiserver/rest/dualwriter.go @@ -12,7 +12,6 @@ import ( "k8s.io/apimachinery/pkg/api/meta" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" - "k8s.io/apiserver/pkg/endpoints/request" "k8s.io/apiserver/pkg/registry/rest" "k8s.io/klog/v2" ) @@ -149,16 +148,13 @@ type ServerLockService interface { func SetDualWritingMode( ctx context.Context, kvs NamespacedKVStore, - legacy LegacyStorage, - storage Storage, - entity string, - desiredMode DualWriterMode, - reg prometheus.Registerer, - serverLockService ServerLockService, - requestInfo *request.RequestInfo, + cfg *SyncerConfig, ) (DualWriterMode, error) { + if cfg == nil { + return Mode0, errors.New("syncer config is nil") + } // Mode0 means no DualWriter - if desiredMode == Mode0 { + if cfg.Mode == Mode0 { return Mode0, nil } @@ -174,58 +170,66 @@ func SetDualWritingMode( errDualWriterSetCurrentMode := errors.New("failed to set current dual writing mode") // Use entity name as key - m, ok, err := kvs.Get(ctx, entity) + m, ok, err := kvs.Get(ctx, cfg.Kind) if err != nil { return Mode0, errors.New("failed to fetch current dual writing mode") } - currentMode, valid := toMode[m] + currentMode, exists := toMode[m] - if !valid && ok { + // If the mode does not exist in our mapping, we log an error. + if !exists && ok { // Only log if "ok" because initially all instances will have mode unset for playlists. - klog.Infof("invalid dual writing mode for %s mode: %v", entity, m) + klog.Infof("invalid dual writing mode for %s mode: %v", cfg.Kind, m) } - if !valid || !ok { + // If the mode does not exist in our mapping, and we also didn't find an entry for this kind, fallback. + if !exists || !ok { // Default to mode 1 currentMode = Mode1 - - err := kvs.Set(ctx, entity, fmt.Sprint(currentMode)) - if err != nil { + if err := kvs.Set(ctx, cfg.Kind, fmt.Sprint(currentMode)); err != nil { return Mode0, errDualWriterSetCurrentMode } } + // Handle transitions to the desired mode. switch { - case desiredMode == Mode2 || desiredMode == Mode1: - currentMode = desiredMode - err := kvs.Set(ctx, entity, fmt.Sprint(currentMode)) - if err != nil { + case cfg.Mode == Mode2 || cfg.Mode == Mode1: + // Directly set the mode for Mode1 and Mode2. + currentMode = cfg.Mode + if err := kvs.Set(ctx, cfg.Kind, fmt.Sprint(currentMode)); err != nil { return Mode0, errDualWriterSetCurrentMode } - case desiredMode >= Mode3 && currentMode < Mode3: - syncOk, err := runDataSyncer(ctx, currentMode, legacy, storage, entity, reg, serverLockService, requestInfo) + case cfg.Mode >= Mode3 && currentMode < Mode3: + // Transitioning to Mode3 or higher requires data synchronization. + cfgModeTmp := cfg.Mode + // Before running the sync, set the syncer config to the current mode, as we have to run the syncer + // once in the current active mode before we can upgrade. + cfg.Mode = currentMode + syncOk, err := runDataSyncer(ctx, cfg) + // Once we are done with running the syncer, we can change the mode back on the config to the desired one. + cfg.Mode = cfgModeTmp if err != nil { klog.Info("data syncer failed for mode:", m) - return currentMode, err + return Mode0, err } if !syncOk { klog.Info("data syncer not ok for mode:", m) return currentMode, nil } - - err = kvs.Set(ctx, entity, fmt.Sprint(desiredMode)) - if err != nil { - return currentMode, errDualWriterSetCurrentMode + // If sync is successful, update the mode to the desired one. + if err := kvs.Set(ctx, cfg.Kind, fmt.Sprint(cfg.Mode)); err != nil { + return Mode0, errDualWriterSetCurrentMode } - return desiredMode, nil - case desiredMode >= Mode3 && currentMode >= Mode3: - currentMode = desiredMode - err := kvs.Set(ctx, entity, fmt.Sprint(currentMode)) - if err != nil { - return currentMode, errDualWriterSetCurrentMode + return cfg.Mode, nil + case cfg.Mode >= Mode3 && currentMode >= Mode3: + // If already in Mode3 or higher, simply update to the desired mode. + currentMode = cfg.Mode + if err := kvs.Set(ctx, cfg.Kind, fmt.Sprint(currentMode)); err != nil { + return Mode0, errDualWriterSetCurrentMode } default: + // Handle any unexpected cases (should not normally happen). return Mode0, errDualWriterSetCurrentMode } return currentMode, nil diff --git a/pkg/apiserver/rest/dualwriter_syncer.go b/pkg/apiserver/rest/dualwriter_syncer.go index 34bfef41a19..8f82e696a30 100644 --- a/pkg/apiserver/rest/dualwriter_syncer.go +++ b/pkg/apiserver/rest/dualwriter_syncer.go @@ -17,6 +17,7 @@ import ( "k8s.io/klog/v2" "github.com/grafana/authlib/claims" + "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/apimachinery/utils" ) @@ -29,12 +30,60 @@ type syncItem struct { accessorLegacy utils.GrafanaMetaAccessor } -const dataSyncerInterval = 60 * time.Minute +type SyncerConfig struct { + Kind string + RequestInfo *request.RequestInfo -// StartPeriodicDataSyncer starts a background job that will execute the DataSyncer every 60 minutes -func StartPeriodicDataSyncer(ctx context.Context, mode DualWriterMode, legacy LegacyStorage, storage Storage, - kind string, reg prometheus.Registerer, serverLockService ServerLockService, requestInfo *request.RequestInfo) { - log := klog.NewKlogr().WithName("legacyToUnifiedStorageDataSyncer").WithValues("mode", mode, "resource", kind) + Mode DualWriterMode + LegacyStorage LegacyStorage + Storage Storage + ServerLockService ServerLockService + + DataSyncerInterval time.Duration + DataSyncerRecordsLimit int + + Reg prometheus.Registerer +} + +func (s *SyncerConfig) Validate() error { + if s == nil { + return fmt.Errorf("syncer config is nil") + } + if s.Kind == "" { + return fmt.Errorf("kind must be specified") + } + if s.RequestInfo == nil { + return fmt.Errorf("requestInfo must be specified") + } + if s.ServerLockService == nil { + return fmt.Errorf("serverLockService must be specified") + } + if s.Storage == nil { + return fmt.Errorf("storage must be specified") + } + if s.LegacyStorage == nil { + return fmt.Errorf("legacy storage must be specified") + } + if s.DataSyncerInterval == 0 { + s.DataSyncerInterval = time.Hour + } + if s.DataSyncerRecordsLimit == 0 { + s.DataSyncerRecordsLimit = 1000 + } + if s.Reg == nil { + s.Reg = prometheus.DefaultRegisterer + } + return nil +} + +// StartPeriodicDataSyncer starts a background job that will execute the DataSyncer, syncing the data +// from the hosted grafana backend into the unified storage backend. This is run in the grafana instance. +func StartPeriodicDataSyncer(ctx context.Context, cfg *SyncerConfig) error { + if err := cfg.Validate(); err != nil { + return fmt.Errorf("invalid syncer config: %w", err) + } + + log := klog.NewKlogr().WithName("legacyToUnifiedStorageDataSyncer").WithValues("mode", cfg.Mode, "resource", cfg.Kind) log.Info("Starting periodic data syncer") @@ -47,62 +96,67 @@ func StartPeriodicDataSyncer(ctx context.Context, mode DualWriterMode, legacy Le time.Sleep(time.Second * time.Duration(jitterSeconds)) // run it immediately - syncOK, err := runDataSyncer(ctx, mode, legacy, storage, kind, reg, serverLockService, requestInfo) + syncOK, err := runDataSyncer(ctx, cfg) log.Info("data syncer finished", "syncOK", syncOK, "error", err) - ticker := time.NewTicker(dataSyncerInterval) + ticker := time.NewTicker(cfg.DataSyncerInterval) for { select { case <-ticker.C: - syncOK, err = runDataSyncer(ctx, mode, legacy, storage, kind, reg, serverLockService, requestInfo) + syncOK, err = runDataSyncer(ctx, cfg) log.Info("data syncer finished", "syncOK", syncOK, ", error", err) case <-ctx.Done(): return } } }() + return nil } // runDataSyncer will ensure that data between legacy storage and unified storage are in sync. // The sync implementation depends on the DualWriter mode -func runDataSyncer(ctx context.Context, mode DualWriterMode, legacy LegacyStorage, storage Storage, - kind string, reg prometheus.Registerer, serverLockService ServerLockService, requestInfo *request.RequestInfo) (bool, error) { +func runDataSyncer(ctx context.Context, cfg *SyncerConfig) (bool, error) { + if err := cfg.Validate(); err != nil { + return false, fmt.Errorf("invalid syncer config: %w", err) + } // ensure that execution takes no longer than necessary - const timeout = dataSyncerInterval - time.Minute + timeout := cfg.DataSyncerInterval - time.Minute ctx, cancelFn := context.WithTimeout(ctx, timeout) defer cancelFn() // implementation depends on the current DualWriter mode - switch mode { + switch cfg.Mode { case Mode1, Mode2: - return legacyToUnifiedStorageDataSyncer(ctx, mode, legacy, storage, kind, reg, serverLockService, requestInfo) + return legacyToUnifiedStorageDataSyncer(ctx, cfg) default: - klog.Info("data syncer not implemented for mode mode:", mode) + klog.Info("data syncer not implemented for mode:", cfg.Mode) return false, nil } } -func legacyToUnifiedStorageDataSyncer(ctx context.Context, mode DualWriterMode, legacy LegacyStorage, storage Storage, resource string, reg prometheus.Registerer, serverLockService ServerLockService, requestInfo *request.RequestInfo) (bool, error) { +func legacyToUnifiedStorageDataSyncer(ctx context.Context, cfg *SyncerConfig) (bool, error) { + if err := cfg.Validate(); err != nil { + return false, fmt.Errorf("invalid syncer config: %w", err) + } metrics := &dualWriterMetrics{} - metrics.init(reg) + metrics.init(cfg.Reg) - log := klog.NewKlogr().WithName("legacyToUnifiedStorageDataSyncer").WithValues("mode", mode, "resource", resource) + log := klog.NewKlogr().WithName("legacyToUnifiedStorageDataSyncer").WithValues("mode", cfg.Mode, "resource", cfg.Kind) everythingSynced := false outOfSync := 0 syncSuccess := 0 syncErr := 0 - maxInterval := dataSyncerInterval + 5*time.Minute + maxInterval := cfg.DataSyncerInterval + 5*time.Minute var errSync error - const maxRecordsSync = 1000 // LockExecuteAndRelease ensures that just a single Grafana server acquires a lock at a time // The parameter 'maxInterval' is a timeout safeguard, if the LastExecution in the // database is older than maxInterval, we will assume the lock as timeouted. The 'maxInterval' parameter should be so long // that is impossible for 2 processes to run at the same time. - err := serverLockService.LockExecuteAndRelease(ctx, fmt.Sprintf("legacyToUnifiedStorageDataSyncer-%d-%s", mode, resource), maxInterval, func(context.Context) { + err := cfg.ServerLockService.LockExecuteAndRelease(ctx, fmt.Sprintf("legacyToUnifiedStorageDataSyncer-%d-%s", cfg.Mode, cfg.Kind), maxInterval, func(context.Context) { log.Info("starting legacyToUnifiedStorageDataSyncer") startSync := time.Now() @@ -110,26 +164,26 @@ func legacyToUnifiedStorageDataSyncer(ctx context.Context, mode DualWriterMode, ctx = klog.NewContext(ctx, log) ctx = identity.WithRequester(ctx, getSyncRequester(orgId)) - ctx = request.WithNamespace(ctx, requestInfo.Namespace) - ctx = request.WithRequestInfo(ctx, requestInfo) + ctx = request.WithNamespace(ctx, cfg.RequestInfo.Namespace) + ctx = request.WithRequestInfo(ctx, cfg.RequestInfo) - storageList, err := getList(ctx, storage, &metainternalversion.ListOptions{ - Limit: maxRecordsSync, + storageList, err := getList(ctx, cfg.Storage, &metainternalversion.ListOptions{ + Limit: int64(cfg.DataSyncerRecordsLimit), }) if err != nil { log.Error(err, "unable to extract list from storage") return } - if len(storageList) >= maxRecordsSync { - errSync = fmt.Errorf("unified storage has more than %d records. Aborting sync", maxRecordsSync) + if len(storageList) >= cfg.DataSyncerRecordsLimit { + errSync = fmt.Errorf("unified storage has more than %d records. Aborting sync", cfg.DataSyncerRecordsLimit) log.Error(errSync, "Unified storage has more records to be synced than allowed") return } log.Info("got items from unified storage", "items", len(storageList)) - legacyList, err := getList(ctx, legacy, &metainternalversion.ListOptions{}) + legacyList, err := getList(ctx, cfg.LegacyStorage, &metainternalversion.ListOptions{}) if err != nil { log.Error(err, "unable to extract list from legacy storage") return @@ -189,7 +243,7 @@ func legacyToUnifiedStorageDataSyncer(ctx context.Context, mode DualWriterMode, } objInfo := rest.DefaultUpdatedObjectInfo(item.objLegacy, []rest.TransformFunc{}...) - res, _, err := storage.Update(ctx, + res, _, err := cfg.Storage.Update(ctx, name, objInfo, func(ctx context.Context, obj runtime.Object) error { return nil }, @@ -210,15 +264,15 @@ func legacyToUnifiedStorageDataSyncer(ctx context.Context, mode DualWriterMode, outOfSync++ ctx = request.WithRequestInfo(ctx, &request.RequestInfo{ - APIGroup: requestInfo.APIGroup, - Resource: requestInfo.Resource, + APIGroup: cfg.RequestInfo.APIGroup, + Resource: cfg.RequestInfo.Resource, Name: name, - Namespace: requestInfo.Namespace, + Namespace: cfg.RequestInfo.Namespace, }) log.Info("deleting item from unified storage", "name", name) - deletedS, _, err := storage.Delete(ctx, name, func(ctx context.Context, obj runtime.Object) error { return nil }, &metav1.DeleteOptions{}) + deletedS, _, err := cfg.Storage.Delete(ctx, name, func(ctx context.Context, obj runtime.Object) error { return nil }, &metav1.DeleteOptions{}) if err != nil && !apierrors.IsNotFound(err) { log.WithValues("objectList", deletedS).Error(err, "could not delete from storage") syncErr++ @@ -231,8 +285,8 @@ func legacyToUnifiedStorageDataSyncer(ctx context.Context, mode DualWriterMode, everythingSynced = outOfSync == syncSuccess - metrics.recordDataSyncerOutcome(mode, resource, everythingSynced) - metrics.recordDataSyncerDuration(err != nil, mode, resource, startSync) + metrics.recordDataSyncerOutcome(cfg.Mode, cfg.Kind, everythingSynced) + metrics.recordDataSyncerDuration(err != nil, cfg.Mode, cfg.Kind, startSync) log.Info("finished syncing items", "items", len(itemsByName), "updated", syncSuccess, "failed", syncErr, "outcome", everythingSynced) }) diff --git a/pkg/apiserver/rest/dualwriter_syncer_test.go b/pkg/apiserver/rest/dualwriter_syncer_test.go index ca30594c3a7..d9d94cea54c 100644 --- a/pkg/apiserver/rest/dualwriter_syncer_test.go +++ b/pkg/apiserver/rest/dualwriter_syncer_test.go @@ -196,7 +196,18 @@ func TestLegacyToUnifiedStorage_DataSyncer(t *testing.T) { tt.setupStorageFn(um) } - outcome, err := legacyToUnifiedStorageDataSyncer(context.Background(), Mode1, ls, us, "test.kind", p, &fakeServerLock{}, &request.RequestInfo{}) + outcome, err := legacyToUnifiedStorageDataSyncer(context.Background(), &SyncerConfig{ + Mode: Mode1, + LegacyStorage: ls, + Storage: us, + Kind: "test.kind", + Reg: p, + ServerLockService: &fakeServerLock{}, + RequestInfo: &request.RequestInfo{}, + + DataSyncerRecordsLimit: 1000, + DataSyncerInterval: time.Hour, + }) if tt.wantErr { assert.Error(t, err) return @@ -225,7 +236,18 @@ func TestLegacyToUnifiedStorage_DataSyncer(t *testing.T) { tt.setupStorageFn(um) } - outcome, err := legacyToUnifiedStorageDataSyncer(context.Background(), Mode2, ls, us, "test.kind", p, &fakeServerLock{}, &request.RequestInfo{}) + outcome, err := legacyToUnifiedStorageDataSyncer(context.Background(), &SyncerConfig{ + Mode: Mode2, + LegacyStorage: ls, + Storage: us, + Kind: "test.kind", + Reg: p, + ServerLockService: &fakeServerLock{}, + RequestInfo: &request.RequestInfo{}, + + DataSyncerRecordsLimit: 1000, + DataSyncerInterval: time.Hour, + }) if tt.wantErr { assert.Error(t, err) return diff --git a/pkg/apiserver/rest/dualwriter_test.go b/pkg/apiserver/rest/dualwriter_test.go index 583aa284832..e299ae91e3f 100644 --- a/pkg/apiserver/rest/dualwriter_test.go +++ b/pkg/apiserver/rest/dualwriter_test.go @@ -65,7 +65,18 @@ func TestSetDualWritingMode(t *testing.T) { ls := legacyStoreMock{m, l} us := storageMock{m, s} - dwMode, err := SetDualWritingMode(context.Background(), tt.kvStore, ls, us, "playlist.grafana.app/playlists", tt.desiredMode, p, &fakeServerLock{}, &request.RequestInfo{}) + dwMode, err := SetDualWritingMode(context.Background(), tt.kvStore, &SyncerConfig{ + LegacyStorage: ls, + Storage: us, + Kind: "playlist.grafana.app/playlists", + Mode: tt.desiredMode, + ServerLockService: &fakeServerLock{}, + RequestInfo: &request.RequestInfo{}, + Reg: p, + + DataSyncerRecordsLimit: 1000, + DataSyncerInterval: time.Hour, + }) assert.NoError(t, err) assert.Equal(t, tt.expectedMode, dwMode) } diff --git a/pkg/services/apiserver/builder/helper.go b/pkg/services/apiserver/builder/helper.go index 88c4b2eb8ef..902ad4f5646 100644 --- a/pkg/services/apiserver/builder/helper.go +++ b/pkg/services/apiserver/builder/helper.go @@ -186,12 +186,18 @@ func InstallAPIs( // when missing this will default to mode zero (legacy only) var mode = grafanarest.DualWriterMode(0) - var dualWriterPeriodicDataSyncJobEnabled bool + var ( + dualWriterPeriodicDataSyncJobEnabled bool + dataSyncerInterval = time.Hour + dataSyncerRecordsLimit = 1000 + ) resourceConfig, resourceExists := storageOpts.UnifiedStorageConfig[key] if resourceExists { mode = resourceConfig.DualWriterMode dualWriterPeriodicDataSyncJobEnabled = resourceConfig.DualWriterPeriodicDataSyncJobEnabled + dataSyncerInterval = resourceConfig.DataSyncerInterval + dataSyncerRecordsLimit = resourceConfig.DataSyncerRecordsLimit } // Force using storage only -- regardless of internal synchronization state @@ -205,7 +211,21 @@ func InstallAPIs( // Moving from one version to the next can only happen after the previous step has // successfully synchronized. requestInfo := getRequestInfo(gr, namespaceMapper) - currentMode, err := grafanarest.SetDualWritingMode(ctx, kvStore, legacy, storage, key, mode, reg, serverLock, requestInfo) + + syncerCfg := &grafanarest.SyncerConfig{ + Kind: key, + RequestInfo: requestInfo, + Mode: mode, + LegacyStorage: legacy, + Storage: storage, + ServerLockService: serverLock, + DataSyncerInterval: dataSyncerInterval, + DataSyncerRecordsLimit: dataSyncerRecordsLimit, + Reg: reg, + } + + // This also sets the currentMode on the syncer config. + currentMode, err := grafanarest.SetDualWritingMode(ctx, kvStore, syncerCfg) if err != nil { return nil, err } @@ -216,9 +236,12 @@ func InstallAPIs( return storage, nil default: } - if dualWriterPeriodicDataSyncJobEnabled { - grafanarest.StartPeriodicDataSyncer(ctx, currentMode, legacy, storage, key, reg, serverLock, requestInfo) + // The mode might have changed in SetDualWritingMode, so apply current mode first. + syncerCfg.Mode = currentMode + if err := grafanarest.StartPeriodicDataSyncer(ctx, syncerCfg); err != nil { + return nil, err + } } // when unable to use diff --git a/pkg/services/apiserver/options/storage.go b/pkg/services/apiserver/options/storage.go index 1cd9e19aabd..d8ee2674aa5 100644 --- a/pkg/services/apiserver/options/storage.go +++ b/pkg/services/apiserver/options/storage.go @@ -4,11 +4,12 @@ import ( "fmt" "net" - "github.com/grafana/grafana/pkg/services/featuremgmt" - "github.com/grafana/grafana/pkg/setting" "github.com/spf13/pflag" genericapiserver "k8s.io/apiserver/pkg/server" "k8s.io/apiserver/pkg/server/options" + + "github.com/grafana/grafana/pkg/services/featuremgmt" + "github.com/grafana/grafana/pkg/setting" ) type StorageType string @@ -21,7 +22,8 @@ const ( StorageTypeUnifiedGrpc StorageType = "unified-grpc" ) -type StorageOptions struct { // The desired storage type +type StorageOptions struct { + // The desired storage type StorageType StorageType // For unified-grpc, the address is required diff --git a/pkg/setting/setting.go b/pkg/setting/setting.go index 3679eabd92e..eb139492ba5 100644 --- a/pkg/setting/setting.go +++ b/pkg/setting/setting.go @@ -538,6 +538,10 @@ type Cfg struct { type UnifiedStorageConfig struct { DualWriterMode rest.DualWriterMode DualWriterPeriodicDataSyncJobEnabled bool + // DataSyncerInterval defines how often the data syncer should run for a resource on the grafana instance. + DataSyncerInterval time.Duration + // DataSyncerRecordsLimit defines how many records will be processed at max during a sync invocation. + DataSyncerRecordsLimit int } type InstallPlugin struct { diff --git a/pkg/setting/setting_unified_storage.go b/pkg/setting/setting_unified_storage.go index 51040160743..610bb843d2f 100644 --- a/pkg/setting/setting_unified_storage.go +++ b/pkg/setting/setting_unified_storage.go @@ -2,6 +2,7 @@ package setting import ( "strings" + "time" "github.com/grafana/grafana/pkg/apiserver/rest" ) @@ -29,9 +30,17 @@ func (cfg *Cfg) setUnifiedStorageConfig() { // parse dualWriter periodic data syncer config dualWriterPeriodicDataSyncJobEnabled := section.Key("dualWriterPeriodicDataSyncJobEnabled").MustBool(false) + // parse dataSyncerRecordsLimit from resource section + dataSyncerRecordsLimit := section.Key("dataSyncerRecordsLimit").MustInt(1000) + + // parse dataSyncerInterval from resource section + dataSyncerInterval := section.Key("dataSyncerInterval").MustDuration(time.Hour) + storageConfig[resourceName] = UnifiedStorageConfig{ DualWriterMode: rest.DualWriterMode(dualWriterMode), DualWriterPeriodicDataSyncJobEnabled: dualWriterPeriodicDataSyncJobEnabled, + DataSyncerRecordsLimit: dataSyncerRecordsLimit, + DataSyncerInterval: dataSyncerInterval, } } cfg.UnifiedStorage = storageConfig diff --git a/pkg/setting/setting_unified_storage_test.go b/pkg/setting/setting_unified_storage_test.go index b9227e3a3ee..50e6bab77e1 100644 --- a/pkg/setting/setting_unified_storage_test.go +++ b/pkg/setting/setting_unified_storage_test.go @@ -2,6 +2,7 @@ package setting import ( "testing" + "time" "github.com/stretchr/testify/assert" ) @@ -21,6 +22,12 @@ func TestCfg_setUnifiedStorageConfig(t *testing.T) { _, err = s.NewKey("dualWriterPeriodicDataSyncJobEnabled", "true") assert.NoError(t, err) + _, err = s.NewKey("dataSyncerRecordsLimit", "1001") + assert.NoError(t, err) + + _, err = s.NewKey("dataSyncerInterval", "10m") + assert.NoError(t, err) + cfg.setUnifiedStorageConfig() value, exists := cfg.UnifiedStorage["playlists.playlist.grafana.app"] @@ -29,6 +36,8 @@ func TestCfg_setUnifiedStorageConfig(t *testing.T) { assert.Equal(t, value, UnifiedStorageConfig{ DualWriterMode: 2, DualWriterPeriodicDataSyncJobEnabled: true, + DataSyncerRecordsLimit: 1001, + DataSyncerInterval: time.Minute * 10, }) }) } From a362769491cdaa2aa31b9a3e38a542884418b1b0 Mon Sep 17 00:00:00 2001 From: Isabella Siu Date: Thu, 12 Dec 2024 16:59:51 -0500 Subject: [PATCH 16/28] Cloudwatch: Update grafana-aws-sdk for AWS/AmplifyHosting metrics (#97799) Cloudwatch: update grafana-aws-sdk for AWS/AmplifyHosting metrics --- go.mod | 4 ++-- go.sum | 8 ++++---- go.work.sum | 9 +++++++++ 3 files changed, 15 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index e1a9bf66da7..412164566b5 100644 --- a/go.mod +++ b/go.mod @@ -84,7 +84,7 @@ require ( github.com/grafana/gofpdf v0.0.0-20231002120153-857cc45be447 // @grafana/sharing-squad github.com/grafana/gomemcache v0.0.0-20240805133030-fdaf6a95408e // @grafana/grafana-operator-experience-squad github.com/grafana/grafana-app-sdk v0.23.1 // @grafana/grafana-app-platform-squad - github.com/grafana/grafana-aws-sdk v0.31.3 // @grafana/aws-datasources + github.com/grafana/grafana-aws-sdk v0.31.5 // @grafana/aws-datasources github.com/grafana/grafana-azure-sdk-go/v2 v2.1.2 // @grafana/partner-datasources github.com/grafana/grafana-cloud-migration-snapshot v1.3.0 // @grafana/grafana-operator-experience-squad github.com/grafana/grafana-google-sdk-go v0.1.0 // @grafana/partner-datasources @@ -503,7 +503,7 @@ require ( github.com/golang/geo v0.0.0-20210211234256-740aa86cb551 // indirect github.com/grafana/jsonparser v0.0.0-20240425183733-ea80629e1a32 // indirect github.com/grafana/loki/pkg/push v0.0.0-20231124142027-e52380921608 // indirect - github.com/grafana/sqlds/v4 v4.1.0 // indirect + github.com/grafana/sqlds/v4 v4.1.3 // indirect github.com/hashicorp/consul/api v1.29.4 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect github.com/hashicorp/go-rootcerts v1.0.2 // indirect diff --git a/go.sum b/go.sum index c2e270f9043..ade05506fde 100644 --- a/go.sum +++ b/go.sum @@ -2314,8 +2314,8 @@ github.com/grafana/gomemcache v0.0.0-20240805133030-fdaf6a95408e h1:UlEET0InuoFa github.com/grafana/gomemcache v0.0.0-20240805133030-fdaf6a95408e/go.mod h1:IGRj8oOoxwJbHBYl1+OhS9UjQR0dv6SQOep7HqmtyFU= github.com/grafana/grafana-app-sdk v0.23.1 h1:BRpUG0bA0oVxjthkmO2thuJBo3nbjaRSSmZJHw+mA8I= github.com/grafana/grafana-app-sdk v0.23.1/go.mod h1:KzgPnTJfMeckGmMctv6CJb8Jr/o/5rwARDyjXoeR0Fc= -github.com/grafana/grafana-aws-sdk v0.31.3 h1:QlgIwyyozYYQf/dL279Baicyax+SuE99Set5chMnq1s= -github.com/grafana/grafana-aws-sdk v0.31.3/go.mod h1:5nt5Gmp6+GyM+Jr7xsXKJtbizxbYXXLmEac6kw5paQI= +github.com/grafana/grafana-aws-sdk v0.31.5 h1:4HpMQx7n4Qqoi7Bgu8KHQ2QKT9fYYdHilX/Gh3FZKBE= +github.com/grafana/grafana-aws-sdk v0.31.5/go.mod h1:5p4Cjyr5ZiR6/RT2nFWkJ8XpIKgX4lAUmUMu70m2yCM= github.com/grafana/grafana-azure-sdk-go/v2 v2.1.2 h1:fV6IgVtViXcYZ4VqTAMuVBTLuGAnI27HhQkaLttzbPE= github.com/grafana/grafana-azure-sdk-go/v2 v2.1.2/go.mod h1:Cbh94bfL5o6mUSaHFiOkx4r4CRKlo/DJLx4dPL8XrE0= github.com/grafana/grafana-cloud-migration-snapshot v1.3.0 h1:F0O9eTy4jHjEd1Z3/qIza2GdY7PYpTddUeaq9p3NKGU= @@ -2366,8 +2366,8 @@ github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc h1:GN2Lv3MGO7AS6PrR github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc/go.mod h1:+JKpmjMGhpgPL+rXZ5nsZieVzvarn86asRlBg4uNGnk= github.com/grafana/saml v0.4.15-0.20240917091248-ae3bbdad8a56 h1:SDGrP81Vcd102L3UJEryRd1eestRw73wt+b8vnVEFe0= github.com/grafana/saml v0.4.15-0.20240917091248-ae3bbdad8a56/go.mod h1:S4+611dxnKt8z/ulbvaJzcgSHsuhjVc1QHNTcr1R7Fw= -github.com/grafana/sqlds/v4 v4.1.0 h1:dPvqaxmLJYqj5/EgP08Xcyy6RVaDdO8hWHpP4c1FQX4= -github.com/grafana/sqlds/v4 v4.1.0/go.mod h1:3Z3r99mgA7+3mzQhABsNZnNtjLx7a0bGOgLVdAsFt2M= +github.com/grafana/sqlds/v4 v4.1.3 h1:+Hy5Yz+tSbD5N3yuLM0VKTsWlVaCzM1S1m1QEBZL7fE= +github.com/grafana/sqlds/v4 v4.1.3/go.mod h1:Lx8IR939lIrCBpCKthv7AXs7E7bmNWPgt0gene/idT8= github.com/grafana/tempo v1.5.1-0.20241001135150-ed943d7a56b2 h1:XMreZ1SPjLpd9zhql5FXKFYwAcgBzS2E2MOPx4n+FyY= github.com/grafana/tempo v1.5.1-0.20241001135150-ed943d7a56b2/go.mod h1:UKONJhBCxmL+0ri27VMledCVzZIJqnl6Ah24A5vCRzs= github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= diff --git a/go.work.sum b/go.work.sum index fe8c560aff3..ffd2d711651 100644 --- a/go.work.sum +++ b/go.work.sum @@ -1003,7 +1003,12 @@ github.com/grafana/go-json v0.0.0-20241115232854-f14426c40ff2 h1:8xGrYqQ1GM4aaMk github.com/grafana/go-json v0.0.0-20241115232854-f14426c40ff2/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/grafana/gomemcache v0.0.0-20240229205252-cd6a66d6fb56/go.mod h1:PGk3RjYHpxMM8HFPhKKo+vve3DdlPUELZLSDEFehPuU= github.com/grafana/grafana-app-sdk v0.19.0/go.mod h1:y0BgzYxc+a7CwOqkwUhN9zXd5cgZJjd2zAbgHEd/xzo= +github.com/grafana/grafana-aws-sdk v0.31.5 h1:4HpMQx7n4Qqoi7Bgu8KHQ2QKT9fYYdHilX/Gh3FZKBE= +github.com/grafana/grafana-aws-sdk v0.31.5/go.mod h1:5p4Cjyr5ZiR6/RT2nFWkJ8XpIKgX4lAUmUMu70m2yCM= +github.com/grafana/grafana-plugin-sdk-go v0.258.0/go.mod h1:jN19FbzhAcPTLPIy31X5nvx38rR5eoD/1rASiip0GBY= github.com/grafana/pyroscope-go/godeltaprof v0.1.6/go.mod h1:Tk376Nbldo4Cha9RgiU7ik8WKFkNpfds98aUzS8omLE= +github.com/grafana/sqlds/v4 v4.1.3 h1:+Hy5Yz+tSbD5N3yuLM0VKTsWlVaCzM1S1m1QEBZL7fE= +github.com/grafana/sqlds/v4 v4.1.3/go.mod h1:Lx8IR939lIrCBpCKthv7AXs7E7bmNWPgt0gene/idT8= github.com/grafana/tail v0.0.0-20230510142333-77b18831edf0 h1:bjh0PVYSVVFxzINqPFYJmAmJNrWPgnVjuSdYJGHmtFU= github.com/grafana/tail v0.0.0-20230510142333-77b18831edf0/go.mod h1:7t5XR+2IA8P2qggOAHTj/GCZfoLBle3OvNSYh1VkRBU= github.com/grafana/thema v0.0.0-20230511182720-3146087fcc26 h1:HX927q4X1n451pnGb8U0wq74i8PCzuxVjzv7TyD10kc= @@ -1026,6 +1031,7 @@ github.com/hashicorp/go-hclog v1.2.2/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVH github.com/hashicorp/go-immutable-radix v1.3.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= github.com/hashicorp/go-msgpack/v2 v2.1.1 h1:xQEY9yB2wnHitoSzk/B9UjXWRQ67QKu5AOm8aFp8N3I= github.com/hashicorp/go-msgpack/v2 v2.1.1/go.mod h1:upybraOAblm4S7rx0+jeNy+CWWhzywQsSRV5033mMu4= +github.com/hashicorp/go-plugin v1.6.1/go.mod h1:XPHFku2tFo3o3QKFgSYo+cghcUhw1NA1hZyMK0PWAw0= github.com/hashicorp/go-syslog v1.0.0 h1:KaodqZuhUoZereWVIYmpUgZysurB1kBLX2j0MwMrUAE= github.com/hashicorp/go.net v0.0.1 h1:sNCoNyDEvN1xa+X0baata4RdcpKwcMS6DH+xwfqPgjw= github.com/hashicorp/golang-lru/v2 v2.0.5/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= @@ -1695,11 +1701,14 @@ go.opentelemetry.io/contrib/exporters/autoexport v0.53.0 h1:13K+tY7E8GJInkrvRiPA go.opentelemetry.io/contrib/exporters/autoexport v0.53.0/go.mod h1:lyQF6xQ4iDnMg4sccNdFs1zf62xd79YI8vZqKjOTwMs= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.52.0/go.mod h1:BMsdeOxN04K0L5FNUBfjFdvwWGNe/rkmSwH4Aelu/X0= go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.53.0/go.mod h1:ImRBLMJv177/pwiLZ7tU7HDGNdBv7rS0HQ99eN/zBl8= +go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.56.0/go.mod h1:3qi2EEwMgB4xnKgPLqsDP3j9qxnHDZeHsnAxfjQqTko= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.56.0/go.mod h1:qxuZLtbq5QDtdeSHsS7bcf6EH6uO6jUAgk764zd3rhM= go.opentelemetry.io/contrib/propagators/b3 v1.23.0 h1:aaIGWc5JdfRGpCafLRxMJbD65MfTa206AwSKkvGS0Hg= go.opentelemetry.io/contrib/propagators/b3 v1.23.0/go.mod h1:Gyz7V7XghvwTq+mIhLFlTgcc03UDroOg8vezs4NLhwU= go.opentelemetry.io/contrib/propagators/b3 v1.27.0 h1:IjgxbomVrV9za6bRi8fWCNXENs0co37SZedQilP2hm0= go.opentelemetry.io/contrib/propagators/b3 v1.27.0/go.mod h1:Dv9obQz25lCisDvvs4dy28UPh974CxkahRDUPsY7y9E= +go.opentelemetry.io/contrib/propagators/jaeger v1.31.0/go.mod h1:OpgiBRssaVKOTM5lSKkOBIGQh/ixvfZRmxQXARK/kGQ= +go.opentelemetry.io/contrib/samplers/jaegerremote v0.25.0/go.mod h1:KjcLnhrgBMLiPPAqyoGhuNmF6jm38LCPV6hdbBeFOTU= go.opentelemetry.io/otel v1.28.0/go.mod h1:q68ijF8Fc8CnMHKyzqL6akLO46ePnjkgfIMIjUIX9z4= go.opentelemetry.io/otel v1.31.0/go.mod h1:O0C14Yl9FgkjqcCZAsE053C13OaddMYr/hz6clDkEJE= go.opentelemetry.io/otel/bridge/opencensus v1.26.0 h1:DZzxj9QjznMVoehskOJnFP2gsTCWtDTFBDvFhPAY7nc= From b3a12f486eba69e20dd7ff3a3d4dd065ede7a99f Mon Sep 17 00:00:00 2001 From: Isabella Siu Date: Thu, 12 Dec 2024 17:20:04 -0500 Subject: [PATCH 17/28] Elasticsearch: Use _field_caps instead of _mapping to get fields (#97607) --- .../feature-toggles/index.md | 1 + .../src/types/featureToggles.gen.ts | 1 + pkg/services/featuremgmt/registry.go | 6 + pkg/services/featuremgmt/toggles_gen.csv | 1 + pkg/services/featuremgmt/toggles_gen.go | 4 + pkg/services/featuremgmt/toggles_gen.json | 15 ++ pkg/tsdb/elasticsearch/elasticsearch.go | 12 +- .../elasticsearch/datasource.test.ts | 189 +++++++++++++++++- .../datasource/elasticsearch/datasource.ts | 70 ++++++- .../elasticsearch/hooks/useFields.ts | 2 +- 10 files changed, 296 insertions(+), 5 deletions(-) diff --git a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md index 8ec9eedebd7..1d10f7f7530 100644 --- a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md +++ b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md @@ -120,6 +120,7 @@ Most [generally available](https://grafana.com/docs/release-life-cycle/#general- | `ssoSettingsLDAP` | Use the new SSO Settings API to configure LDAP | | `useSessionStorageForRedirection` | Use session storage for handling the redirection after login | | `reportingUseRawTimeRange` | Uses the original report or dashboard time range instead of making an absolute transformation | +| `elasticsearchCrossClusterSearch` | Enables cross cluster search in the Elasticsearch datasource | ## Experimental feature toggles diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index 37c84438f1f..7241d05eaa7 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -242,4 +242,5 @@ export interface FeatureToggles { azureMonitorEnableUserAuth?: boolean; alertingNotificationsStepMode?: boolean; feedbackButton?: boolean; + elasticsearchCrossClusterSearch?: boolean; } diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index 7537e52b4d6..a59e0bdd43e 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -1676,6 +1676,12 @@ var ( Owner: grafanaOperatorExperienceSquad, HideFromDocs: true, }, + { + Name: "elasticsearchCrossClusterSearch", + Description: "Enables cross cluster search in the Elasticsearch datasource", + Stage: FeatureStagePublicPreview, + Owner: awsDatasourcesSquad, + }, } ) diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv index 1fde233fece..b7b6d1c3662 100644 --- a/pkg/services/featuremgmt/toggles_gen.csv +++ b/pkg/services/featuremgmt/toggles_gen.csv @@ -223,3 +223,4 @@ alertingUIOptimizeReducer,GA,@grafana/alerting-squad,false,false,true azureMonitorEnableUserAuth,GA,@grafana/partner-datasources,false,false,false alertingNotificationsStepMode,experimental,@grafana/alerting-squad,false,false,true feedbackButton,experimental,@grafana/grafana-operator-experience-squad,false,false,false +elasticsearchCrossClusterSearch,preview,@grafana/aws-datasources,false,false,false diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go index 4ba518fe8e9..2fe1fb9c22a 100644 --- a/pkg/services/featuremgmt/toggles_gen.go +++ b/pkg/services/featuremgmt/toggles_gen.go @@ -902,4 +902,8 @@ const ( // FlagFeedbackButton // Enables a button to send feedback from the Grafana UI FlagFeedbackButton = "feedbackButton" + + // FlagElasticsearchCrossClusterSearch + // Enables cross cluster search in the Elasticsearch datasource + FlagElasticsearchCrossClusterSearch = "elasticsearchCrossClusterSearch" ) diff --git a/pkg/services/featuremgmt/toggles_gen.json b/pkg/services/featuremgmt/toggles_gen.json index 8feb0ce7185..3a0d9740432 100644 --- a/pkg/services/featuremgmt/toggles_gen.json +++ b/pkg/services/featuremgmt/toggles_gen.json @@ -1269,6 +1269,21 @@ "frontend": true } }, + { + "metadata": { + "name": "elasticsearchCrossClusterSearch", + "resourceVersion": "1733848475752", + "creationTimestamp": "2024-12-09T13:53:38Z", + "annotations": { + "grafana.app/updatedTimestamp": "2024-12-10 16:34:35.752111 +0000 UTC" + } + }, + "spec": { + "description": "Enables cross cluster search in the Elasticsearch datasource", + "stage": "preview", + "codeowner": "@grafana/aws-datasources" + } + }, { "metadata": { "name": "enableDatagridEditing", diff --git a/pkg/tsdb/elasticsearch/elasticsearch.go b/pkg/tsdb/elasticsearch/elasticsearch.go index ddd8671a114..40073bf1740 100644 --- a/pkg/tsdb/elasticsearch/elasticsearch.go +++ b/pkg/tsdb/elasticsearch/elasticsearch.go @@ -182,14 +182,21 @@ func (s *Service) getDSInfo(ctx context.Context, pluginCtx backend.PluginContext return &instance, nil } +func isFieldCaps(url string) bool { + return strings.HasSuffix(url, "/_field_caps") || url == "_field_caps" +} + func (s *Service) CallResource(ctx context.Context, req *backend.CallResourceRequest, sender backend.CallResourceResponseSender) error { logger := s.logger.FromContext(ctx) // allowed paths for resource calls: // - empty string for fetching db version // - /_mapping for fetching index mapping, e.g. requests going to `index/_mapping` + // - /_field_caps for fetching field capabilities, e.g. requests going to `index/_field_caps` // - _msearch for executing getTerms queries // - _mapping for fetching "root" index mappings - if req.Path != "" && !strings.HasSuffix(req.Path, "/_mapping") && req.Path != "_msearch" && req.Path != "_mapping" { + // - _field_caps for fetching "root" field capabilities + if req.Path != "" && !isFieldCaps(req.Path) && req.Path != "_msearch" && + !strings.HasSuffix(req.Path, "/_mapping") && req.Path != "_mapping" { logger.Error("Invalid resource path", "path", req.Path) return fmt.Errorf("invalid resource URL: %s", req.Path) } @@ -266,6 +273,9 @@ func createElasticsearchURL(req *backend.CallResourceRequest, ds *es.DatasourceI } esUrl.Path = path.Join(esUrl.Path, req.Path) + if isFieldCaps(req.Path) { + esUrl.RawQuery = "fields=*" + } esUrlString := esUrl.String() // If the request path is empty and the URL does not end with a slash, add a slash to the URL. // This ensures that for version checks executed to the root URL, the URL ends with a slash. diff --git a/public/app/plugins/datasource/elasticsearch/datasource.test.ts b/public/app/plugins/datasource/elasticsearch/datasource.test.ts index beddbe42c05..c653ac8a132 100644 --- a/public/app/plugins/datasource/elasticsearch/datasource.test.ts +++ b/public/app/plugins/datasource/elasticsearch/datasource.test.ts @@ -12,7 +12,7 @@ import { TimeRange, toUtc, } from '@grafana/data'; -import { FetchResponse, reportInteraction, getBackendSrv, setBackendSrv, BackendSrv } from '@grafana/runtime'; +import { FetchResponse, reportInteraction, getBackendSrv, setBackendSrv, BackendSrv, config } from '@grafana/runtime'; import { ElasticDatasource } from './datasource'; import { createElasticDatasource, createElasticQuery, mockResponseFrames } from './mocks'; @@ -1253,4 +1253,191 @@ describe('ElasticDatasource', () => { }); }); }); + + describe('getFieldsFieldCap', () => { + const originalFeatureToggleValue = config.featureToggles.elasticsearchCrossClusterSearch; + + afterEach(() => { + config.featureToggles.elasticsearchCrossClusterSearch = originalFeatureToggleValue; + }); + const getFieldsMockData = { + fields: { + '@timestamp_millis': { + date: { + type: 'date', + metadata_field: false, + }, + }, + classification_terms: { + keyword: { + type: 'keyword', + metadata_field: false, + }, + }, + ip_address: { + ip: { + type: 'ip', + metadata_field: false, + }, + }, + 'justification_blob.criterion.keyword': { + keyword: { + type: 'keyword', + metadata_field: false, + }, + }, + 'justification_blob.criterion': { + text: { + type: 'text', + metadata_field: false, + }, + }, + justification_blob: { + object: { + type: 'object', + metadata_field: false, + }, + }, + 'justification_blob.shallow.jsi.sdb.dsel2.bootlegged-gille.botness': { + float: { + type: 'float', + metadata_field: false, + }, + }, + 'justification_blob.shallow.jsi.sdb.dsel2.bootlegged-gille.general_algorithm_score': { + float: { + type: 'float', + metadata_field: false, + }, + }, + 'justification_blob.shallow.jsi.sdb.dsel2.uncombed-boris.botness': { + float: { + type: 'float', + metadata_field: false, + }, + }, + 'justification_blob.shallow.jsi.sdb.dsel2.uncombed-boris.general_algorithm_score': { + float: { + type: 'float', + metadata_field: false, + }, + }, + overall_vote_score: { + float: { + type: 'float', + metadata_field: false, + }, + }, + _index: { + _index: { + type: '_index', + metadata_field: true, + }, + }, + }, + indices: ['[test-]YYYY.MM.DD'], + }; + + it('should not retry when ES is down', async () => { + config.featureToggles.elasticsearchCrossClusterSearch = true; + const twoDaysBefore = toUtc().subtract(2, 'day').format('YYYY.MM.DD'); + const ds = createElasticDatasource({ + jsonData: { interval: 'Daily' }, + }); + + ds.getResource = jest.fn().mockImplementation((options) => { + if (options.url === `test-${twoDaysBefore}/_field_caps`) { + return of({ + data: {}, + }); + } + return throwError({ status: 500 }); + }); + + const timeRange = { from: 1, to: 2 } as unknown as TimeRange; + await expect(ds.getFields(undefined, timeRange)).toEmitValuesWith((received) => { + expect(received.length).toBe(1); + expect(received[0]).toStrictEqual({ status: 500 }); + expect(ds.getResource).toBeCalledTimes(1); + }); + }); + + it('should not retry more than 7 indices', async () => { + config.featureToggles.elasticsearchCrossClusterSearch = true; + const ds = createElasticDatasource({ + jsonData: { interval: 'Daily' }, + }); + + ds.getResource = jest.fn().mockImplementation(() => { + return throwError({ status: 404 }); + }); + + const timeRange = createTimeRange(dateTime().subtract(2, 'week'), dateTime()); + + await expect(ds.getFields(undefined, timeRange)).toEmitValuesWith((received) => { + expect(received.length).toBe(1); + expect(received[0]).toStrictEqual('Could not find an available index for this time range.'); + expect(ds.getResource).toBeCalledTimes(7); + }); + }); + + it('should return nested fields', async () => { + config.featureToggles.elasticsearchCrossClusterSearch = true; + const ds = createElasticDatasource({ + jsonData: { interval: 'Daily' }, + }); + + ds.getResource = jest.fn().mockResolvedValue(getFieldsMockData); + + await expect(ds.getFields()).toEmitValuesWith((received) => { + expect(received.length).toBe(1); + + const fieldObjects = received[0]; + const fields = map(fieldObjects, 'text'); + expect(fields).toEqual([ + '@timestamp_millis', + 'classification_terms', + 'ip_address', + 'justification_blob.criterion.keyword', + 'justification_blob.criterion', + 'justification_blob.shallow.jsi.sdb.dsel2.bootlegged-gille.botness', + 'justification_blob.shallow.jsi.sdb.dsel2.bootlegged-gille.general_algorithm_score', + 'justification_blob.shallow.jsi.sdb.dsel2.uncombed-boris.botness', + 'justification_blob.shallow.jsi.sdb.dsel2.uncombed-boris.general_algorithm_score', + 'overall_vote_score', + ]); + }); + }); + it('should return number fields', async () => { + config.featureToggles.elasticsearchCrossClusterSearch = true; + ds.getResource = jest.fn().mockResolvedValue(getFieldsMockData); + + await expect(ds.getFields(['number'])).toEmitValuesWith((received) => { + expect(received.length).toBe(1); + + const fieldObjects = received[0]; + const fields = map(fieldObjects, 'text'); + expect(fields).toEqual([ + 'justification_blob.shallow.jsi.sdb.dsel2.bootlegged-gille.botness', + 'justification_blob.shallow.jsi.sdb.dsel2.bootlegged-gille.general_algorithm_score', + 'justification_blob.shallow.jsi.sdb.dsel2.uncombed-boris.botness', + 'justification_blob.shallow.jsi.sdb.dsel2.uncombed-boris.general_algorithm_score', + 'overall_vote_score', + ]); + }); + }); + + it('should return date fields', async () => { + config.featureToggles.elasticsearchCrossClusterSearch = true; + ds.getResource = jest.fn().mockResolvedValue(getFieldsMockData); + + await expect(ds.getFields(['date'])).toEmitValuesWith((received) => { + expect(received.length).toBe(1); + + const fieldObjects = received[0]; + const fields = map(fieldObjects, 'text'); + expect(fields).toEqual(['@timestamp_millis']); + }); + }); + }); }); diff --git a/public/app/plugins/datasource/elasticsearch/datasource.ts b/public/app/plugins/datasource/elasticsearch/datasource.ts index 584f970fef8..3cc574ab770 100644 --- a/public/app/plugins/datasource/elasticsearch/datasource.ts +++ b/public/app/plugins/datasource/elasticsearch/datasource.ts @@ -1,4 +1,4 @@ -import { cloneDeep, first as _first, isNumber, isObject, isString, map as _map, find } from 'lodash'; +import { cloneDeep, first as _first, isNumber, isString, map as _map, find, isObject } from 'lodash'; import { from, generate, lastValueFrom, Observable, of } from 'rxjs'; import { catchError, first, map, mergeMap, skipWhile, throwIfEmpty, tap } from 'rxjs/operators'; import { SemVer } from 'semver'; @@ -46,6 +46,7 @@ import { BackendSrvRequest, TemplateSrv, getTemplateSrv, + config, } from '@grafana/runtime'; import { IndexPattern, intervalMap } from './IndexPattern'; @@ -197,7 +198,7 @@ export class ElasticDatasource indexList = [this.indexPattern.getIndexForToday()]; } - const url = '_mapping'; + const url = config.featureToggles.elasticsearchCrossClusterSearch ? '_field_caps' : '_mapping'; const indexUrlList = indexList.map((index) => { // make sure `index` does not end with a slash @@ -743,6 +744,10 @@ export class ElasticDatasource * or fix the implementation. */ getFields(type?: string[], range?: TimeRange): Observable { + if (config.featureToggles.elasticsearchCrossClusterSearch) { + return this.getFieldsCrossCluster(type, range); + } + const typeMap: Record = { float: 'number', double: 'number', @@ -823,6 +828,67 @@ export class ElasticDatasource ); } + getFieldsCrossCluster(type?: string[], range?: TimeRange): Observable { + const typeMap: Record = { + float: 'number', + double: 'number', + integer: 'number', + long: 'number', + date: 'date', + date_nanos: 'date', + string: 'string', + text: 'string', + scaled_float: 'number', + nested: 'nested', + histogram: 'number', + }; + return this.requestAllIndices(range).pipe( + map((result) => { + interface FieldInfo { + metadata_field: string; + } + const shouldAddField = (obj: Record>) => { + // equal query type filter, or via type map translation + for (const objField in obj) { + if (objField === 'object') { + continue; + } + if (obj[objField].metadata_field) { + continue; + } + + if (!type || type.length === 0) { + return true; + } + + if (type.includes(objField) || type.includes(typeMap[objField])) { + return true; + } + } + return false; + }; + + const fields: Record = {}; + + const fieldsData = result['fields']; + for (const fieldName in fieldsData) { + const fieldInfo = fieldsData[fieldName]; + if (shouldAddField(fieldInfo)) { + fields[fieldName] = { + text: fieldName, + type: fieldInfo.type, + }; + } + } + + // transform to array + return _map(fields, (value) => { + return value; + }); + }) + ); + } + /** * Get values for a given field. * Used for example in getTagValues. diff --git a/public/app/plugins/datasource/elasticsearch/hooks/useFields.ts b/public/app/plugins/datasource/elasticsearch/hooks/useFields.ts index 8f36406a53d..df9d1271431 100644 --- a/public/app/plugins/datasource/elasticsearch/hooks/useFields.ts +++ b/public/app/plugins/datasource/elasticsearch/hooks/useFields.ts @@ -60,7 +60,7 @@ export const useFields = (type: AggregationType | string[]) => { let rawFields: MetricFindValue[]; return async (q?: string) => { - // _mapping doesn't support filtering, we avoid sending a request everytime q changes + // TODO: use _field_caps to support filtering if (!rawFields) { rawFields = await lastValueFrom(datasource.getFields(filter, range)); } From 886cbad27566fdb228b7556f8218286096ebfe1c Mon Sep 17 00:00:00 2001 From: Kat Yang <69819079+yangkb09@users.noreply.github.com> Date: Thu, 12 Dec 2024 17:55:16 -0500 Subject: [PATCH 18/28] ExploreMetrics: Improve semantic HTML and remove unused styles (#97367) Chore: Code cleanup --- .../features/trails/DataTrailBookmarks.tsx | 6 +--- public/app/features/trails/DataTrailCard.tsx | 29 ++----------------- public/app/features/trails/DataTrailsApp.tsx | 1 + public/app/features/trails/DataTrailsHome.tsx | 8 ++--- .../trails/DataTrailsRecentMetrics.tsx | 4 --- 5 files changed, 8 insertions(+), 40 deletions(-) diff --git a/public/app/features/trails/DataTrailBookmarks.tsx b/public/app/features/trails/DataTrailBookmarks.tsx index 4793110cf03..edf449c9b1b 100644 --- a/public/app/features/trails/DataTrailBookmarks.tsx +++ b/public/app/features/trails/DataTrailBookmarks.tsx @@ -84,12 +84,8 @@ function getStyles(theme: GrafanaTheme2) { header: css({ color: theme.colors.text.primary, textAlign: 'center', - /* H4 */ - fontFamily: 'Inter', fontSize: '18px', - fontStyle: 'normal', - fontWeight: '400', - lineHeight: '22px' /* 122.222% */, + lineHeight: '22px', letterSpacing: '0.045px', }), horizontalLine: css({ diff --git a/public/app/features/trails/DataTrailCard.tsx b/public/app/features/trails/DataTrailCard.tsx index b64afe1ed71..75967191964 100644 --- a/public/app/features/trails/DataTrailCard.tsx +++ b/public/app/features/trails/DataTrailCard.tsx @@ -59,7 +59,7 @@ export function DataTrailCard(props: Props) { const { filters, metric, createdAt } = values; return ( -
+
{truncateValue('', getMetricName(metric), 39)}
@@ -93,34 +93,18 @@ export function DataTrailCard(props: Props) {
{createdAt && dateTimeFormat(createdAt, { format: 'YYYY-MM-DD' })}
- + ); } export function getStyles(theme: GrafanaTheme2) { return { - metricLabel: css({ - display: 'inline', - color: theme.colors.text.primary, - fontFamily: 'Inter', - fontSize: '14px', - fontStyle: 'normal', - fontWeight: 400, - }), metricValue: css({ display: 'inline', color: theme.colors.text.primary, - fontFamily: 'Inter', - fontSize: '14px', - fontStyle: 'normal', fontWeight: 500, wordBreak: 'break-all', }), - tag: css({ - maxWidth: '260px', - overflow: 'hidden', - textOverflow: 'ellipsis', - }), card: css({ position: 'relative', width: '318px', @@ -159,19 +143,14 @@ export function getStyles(theme: GrafanaTheme2) { primaryFont: css({ display: 'inline', color: theme.colors.text.primary, - fontFamily: 'Inter', fontSize: '12px', - fontStyle: 'normal', fontWeight: '500', - lineHeight: '18px' /* 150% */, letterSpacing: '0.018px', }), secondaryFont: css({ display: 'inline', color: theme.colors.text.secondary, - fontFamily: 'Inter', fontSize: '12px', - fontStyle: 'normal', fontWeight: '400', lineHeight: '18px' /* 150% */, letterSpacing: '0.018px', @@ -181,9 +160,5 @@ export function getStyles(theme: GrafanaTheme2) { bottom: theme.spacing(1), right: theme.spacing(1), }), - wordwrap: css({ - overflow: 'hidden', - overflowWrap: 'anywhere', - }), }; } diff --git a/public/app/features/trails/DataTrailsApp.tsx b/public/app/features/trails/DataTrailsApp.tsx index f7f0112e456..c47be4d1b0b 100644 --- a/public/app/features/trails/DataTrailsApp.tsx +++ b/public/app/features/trails/DataTrailsApp.tsx @@ -155,6 +155,7 @@ export function getDataTrailsApp() { const getStyles = () => ({ topNavContainer: css({ width: '100%', + height: '100%', display: 'flex', flexDirection: 'row', justifyItems: 'flex-start', diff --git a/public/app/features/trails/DataTrailsHome.tsx b/public/app/features/trails/DataTrailsHome.tsx index 5c7ee62b185..c0c4d6d5995 100644 --- a/public/app/features/trails/DataTrailsHome.tsx +++ b/public/app/features/trails/DataTrailsHome.tsx @@ -57,8 +57,8 @@ export class DataTrailsHome extends SceneObjectBase { }; return ( -
-
+
+
{theme.isDark ? : }
@@ -87,10 +87,10 @@ export class DataTrailsHome extends SceneObjectBase {
-
+ - + ); }; } diff --git a/public/app/features/trails/DataTrailsRecentMetrics.tsx b/public/app/features/trails/DataTrailsRecentMetrics.tsx index cae68d8060a..921edcbd92e 100644 --- a/public/app/features/trails/DataTrailsRecentMetrics.tsx +++ b/public/app/features/trails/DataTrailsRecentMetrics.tsx @@ -64,12 +64,8 @@ function getStyles(theme: GrafanaTheme2) { header: css({ color: theme.colors.text.primary, textAlign: 'center', - /* H4 */ - fontFamily: 'Inter', fontSize: '18px', - fontStyle: 'normal', fontWeight: '400', - lineHeight: '22px' /* 122.222% */, letterSpacing: '0.045px', }), trailList: css({ From cc4afff8896823f0330bd4a7f842aa8ed83c844d Mon Sep 17 00:00:00 2001 From: Andreas Christou Date: Thu, 12 Dec 2024 23:26:44 +0000 Subject: [PATCH 19/28] Graphite: Set `maxDataPoints` based on user value in alerting (#97178) --- pkg/tsdb/graphite/graphite.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/tsdb/graphite/graphite.go b/pkg/tsdb/graphite/graphite.go index 0464a21121e..fa8b0166ded 100644 --- a/pkg/tsdb/graphite/graphite.go +++ b/pkg/tsdb/graphite/graphite.go @@ -109,7 +109,7 @@ func (s *Service) QueryData(ctx context.Context, req *backend.QueryDataRequest) "from": []string{from}, "until": []string{until}, "format": []string{"json"}, - "maxDataPoints": []string{"500"}, + "maxDataPoints": []string{fmt.Sprintf("%d", q.MaxDataPoints)}, "target": []string{}, } From c6a45061bec7a062a2d8ea39bb9d53900474d6bf Mon Sep 17 00:00:00 2001 From: Ariana <20644439+ariatron@users.noreply.github.com> Date: Thu, 12 Dec 2024 18:19:37 -0600 Subject: [PATCH 20/28] Docs: Update docs to make panel embedding more clear (#97587) Co-authored-by: Isabel Matwawana <76437239+imatwawana@users.noreply.github.com> --- docs/sources/dashboards/share-dashboards-panels/_index.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/sources/dashboards/share-dashboards-panels/_index.md b/docs/sources/dashboards/share-dashboards-panels/_index.md index 6df94211f9f..4bf68d24480 100644 --- a/docs/sources/dashboards/share-dashboards-panels/_index.md +++ b/docs/sources/dashboards/share-dashboards-panels/_index.md @@ -71,7 +71,7 @@ You must have an authorized viewer permission to see an image rendered by a dire The same permission is also required to view embedded links unless you have anonymous access permission enabled for your Grafana instance. {{< admonition type="note" >}} -Anonymous access permission is not available in Grafana Cloud. +Anonymous access permission is not available in Grafana Cloud. This feature is only supported for Grafana Enterprise and Grafana Open Source. {{< /admonition >}} ## Share dashboards {#share-a-dashboard} @@ -267,10 +267,10 @@ https://play.grafana.org/render/d-solo/ktMs4D6Mk?from=2024-09-03T11:55:44.442Z&t ### Share an embed -You can share a panel by embedding it on another website using an iframe. Users must be signed into Grafana to view the panel. +You can share a panel by embedding it on another website using an iframe. Users must be signed into Grafana to view the panel unless you have anonymous access permission enabled for your Grafana instance. {{< admonition type="note" >}} -Anonymous access permission is no longer available for Grafana Cloud. +Anonymous access permission is no longer available for Grafana Cloud even for panels in [externally shared dashboards](ref:shared-dashboards). This feature is only supported for Grafana Enterprise and Grafana Open Source. {{< /admonition >}} To create a panel that can be embedded, follow these steps: From 6011cffed78c839882077b2e7db0a883b02d66b4 Mon Sep 17 00:00:00 2001 From: Isabel Matwawana <76437239+imatwawana@users.noreply.github.com> Date: Fri, 13 Dec 2024 02:42:30 -0500 Subject: [PATCH 21/28] Docs: Fix broken link (#97891) --- .../sources/panels-visualizations/configure-thresholds/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/sources/panels-visualizations/configure-thresholds/index.md b/docs/sources/panels-visualizations/configure-thresholds/index.md index 220f8464450..a651064ac6f 100644 --- a/docs/sources/panels-visualizations/configure-thresholds/index.md +++ b/docs/sources/panels-visualizations/configure-thresholds/index.md @@ -135,7 +135,7 @@ On visualizations that support thresholds, Grafana has the following default thr - 80 = red - Base = green - Mode = Absolute -- Show thresholds = Off (for some visualizations); for more information, see the [Show thresholds](#show-threshold) option. +- Show thresholds = Off (for some visualizations); for more information, see the [Show thresholds](#show-thresholds) option. ## Thresholds options From 1c4d30f73c9ef9b90eaa4720d1c9b2142073a624 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Agn=C3=A8s=20Toulet?= <35176601+AgnesToulet@users.noreply.github.com> Date: Fri, 13 Dec 2024 09:42:35 +0100 Subject: [PATCH 22/28] Rendering: Add logs (#97885) * Rendering: add logs * fix tests and lint issues --- pkg/services/rendering/rendering.go | 35 ++++++++++++++---------- pkg/services/rendering/rendering_test.go | 7 +++-- 2 files changed, 26 insertions(+), 16 deletions(-) diff --git a/pkg/services/rendering/rendering.go b/pkg/services/rendering/rendering.go index 3f3598dab90..36799f69a7a 100644 --- a/pkg/services/rendering/rendering.go +++ b/pkg/services/rendering/rendering.go @@ -271,6 +271,16 @@ func (rs *RenderingService) Render(ctx context.Context, renderType RenderType, o } func (rs *RenderingService) render(ctx context.Context, renderType RenderType, opts Opts, renderKeyProvider renderKeyProvider) (*RenderResult, error) { + if !rs.IsAvailable(ctx) { + rs.log.Warn("Could not render image, no image renderer found/installed. " + + "For image rendering support please install the grafana-image-renderer plugin. " + + "Read more at https://grafana.com/docs/grafana/latest/administration/image_rendering/") + if opts.ErrorRenderUnavailable { + return nil, ErrRenderUnavailable + } + return rs.renderUnavailableImage(), nil + } + if int(atomic.LoadInt32(&rs.inProgressCount)) > opts.ConcurrentLimit { rs.log.Warn("Could not render image, hit the currency limit", "concurrencyLimit", opts.ConcurrentLimit, "path", opts.Path) if opts.ErrorConcurrentLimitReached { @@ -287,15 +297,10 @@ func (rs *RenderingService) render(ctx context.Context, renderType RenderType, o }, nil } - if !rs.IsAvailable(ctx) { - rs.log.Warn("Could not render image, no image renderer found/installed. " + - "For image rendering support please install the grafana-image-renderer plugin. " + - "Read more at https://grafana.com/docs/grafana/latest/administration/image_rendering/") - if opts.ErrorRenderUnavailable { - return nil, ErrRenderUnavailable - } - return rs.renderUnavailableImage(), nil - } + defer func() { + metrics.MRenderingQueue.Set(float64(atomic.AddInt32(&rs.inProgressCount, -1))) + }() + metrics.MRenderingQueue.Set(float64(atomic.AddInt32(&rs.inProgressCount, 1))) if renderType == RenderPDF { if !rs.features.IsEnabled(ctx, featuremgmt.FlagNewPDFRendering) { @@ -318,12 +323,14 @@ func (rs *RenderingService) render(ctx context.Context, renderType RenderType, o defer renderKeyProvider.afterRequest(ctx, opts.AuthOpts, renderKey) - defer func() { - metrics.MRenderingQueue.Set(float64(atomic.AddInt32(&rs.inProgressCount, -1))) - }() + res, err := rs.renderAction(ctx, renderType, renderKey, opts) + if err != nil { + rs.log.Error("Failed to render image", "path", opts.Path, "error", err) + return nil, err + } + rs.log.Debug("Successfully rendered image", "path", opts.Path) - metrics.MRenderingQueue.Set(float64(atomic.AddInt32(&rs.inProgressCount, 1))) - return rs.renderAction(ctx, renderType, renderKey, opts) + return res, nil } func (rs *RenderingService) RenderCSV(ctx context.Context, opts CSVOpts, session Session) (*RenderCSVResult, error) { diff --git a/pkg/services/rendering/rendering_test.go b/pkg/services/rendering/rendering_test.go index dc860d34600..cdc484baf31 100644 --- a/pkg/services/rendering/rendering_test.go +++ b/pkg/services/rendering/rendering_test.go @@ -121,7 +121,8 @@ func TestRenderLimitImage(t *testing.T) { rs := RenderingService{ Cfg: &setting.Cfg{ - HomePath: path, + HomePath: path, + RendererUrl: "http://localhost:8081/render", }, inProgressCount: 2, log: log.New("test"), @@ -161,7 +162,9 @@ func TestRenderLimitImage(t *testing.T) { func TestRenderLimitImageError(t *testing.T) { rs := RenderingService{ - Cfg: &setting.Cfg{}, + Cfg: &setting.Cfg{ + RendererUrl: "http://localhost:8081/render", + }, inProgressCount: 2, log: log.New("test"), } From 0f52065377e65945aaf9f5f45cb50e721a3a0e77 Mon Sep 17 00:00:00 2001 From: Tom Ratcliffe Date: Fri, 13 Dec 2024 09:19:11 +0000 Subject: [PATCH 23/28] Chore: Rename betterer stats file and attempt to fix (#97887) Rename betterer stats and attempt to fix --- package.json | 2 +- ...reportBettererStats.ts => reportBettererStats.mjs} | 11 ++++++----- 2 files changed, 7 insertions(+), 6 deletions(-) rename scripts/cli/{reportBettererStats.ts => reportBettererStats.mjs} (74%) diff --git a/package.json b/package.json index a94dcf98b91..e6ea47bfca8 100644 --- a/package.json +++ b/package.json @@ -57,7 +57,7 @@ "betterer": "betterer --tsconfig ./scripts/cli/tsconfig.json", "betterer:json": "ts-node --transpile-only --project ./scripts/cli/tsconfig.json ./scripts/cli/bettererResultsToJson.ts", "betterer:merge": "betterer merge --tsconfig ./scripts/cli/tsconfig.json", - "betterer:stats": "ts-node --transpile-only --project ./scripts/cli/tsconfig.json ./scripts/cli/reportBettererStats.ts", + "betterer:stats": "node ./scripts/cli/reportBettererStats.mjs", "betterer:issues": "ts-node --transpile-only --project ./scripts/cli/tsconfig.json ./scripts/cli/generateBettererIssues.ts", "plugin:build": "nx run-many -t build --projects='tag:scope:plugin'", "plugin:build:commit": "nx run-many -t build:commit --projects='tag:scope:plugin'", diff --git a/scripts/cli/reportBettererStats.ts b/scripts/cli/reportBettererStats.mjs similarity index 74% rename from scripts/cli/reportBettererStats.ts rename to scripts/cli/reportBettererStats.mjs index 135f98c7752..0f1eda1d646 100644 --- a/scripts/cli/reportBettererStats.ts +++ b/scripts/cli/reportBettererStats.mjs @@ -1,7 +1,8 @@ +// @ts-check import { betterer } from '@betterer/betterer'; -import { camelCase } from 'lodash'; +import _ from 'lodash'; -function logStat(name: string, value: number) { +function logStat(name, value) { // Note that this output format must match the parsing in ci-frontend-metrics.sh // which expects the two values to be separated by a space console.log(`${name} ${value}`); @@ -12,11 +13,11 @@ async function main() { for (const testResults of results.resultSummaries) { const countByMessage = {}; - const name = camelCase(testResults.name); + const name = _.camelCase(testResults.name); Object.values(testResults.details) .flatMap((v) => v) .forEach((detail) => { - const message = camelCase(detail.message); + const message = _.camelCase(detail.message); const metricName = `${name}_${message}`; if (metricName in countByMessage) { countByMessage[metricName]++; @@ -25,7 +26,7 @@ async function main() { } }); - for (const [metricName, count] of Object.entries(countByMessage)) { + for (const [metricName, count] of Object.entries(countByMessage)) { logStat(metricName, count); } } From f8686be56ab760d0953081d1caed7863e91f471a Mon Sep 17 00:00:00 2001 From: Dominik Prokop Date: Fri, 13 Dec 2024 10:20:22 +0100 Subject: [PATCH 24/28] Schema v2: Make DashboardScene accept v1 and v2 schema (#96931) * WIP * Refactor useSaveDashboard to make it dashboard type independent when used * DashboardPrompt: makeit Dashboard type independent * DashboardScene: accept both v1 and v2 schema * Update save dashboard command interface * Fix test * Lint * Refactor * Add onSaveComplete to DashboardSceneSerializer * Cleanup tests * Remove unused code * Refactor dashboard tracking information * added a todo * Fix betterer * Update betterer results --- .../saving/DashboardPrompt.test.tsx | 26 +- .../saving/DashboardPrompt.tsx | 19 +- .../saving/DetectChangesWorker.ts | 4 +- .../saving/SaveDashboardAsForm.tsx | 36 +- .../saving/SaveDashboardDrawer.tsx | 12 +- .../saving/SaveDashboardForm.tsx | 4 +- .../saving/getDashboardChanges.test.ts | 18 +- .../saving/getDashboardChanges.ts | 25 +- .../getDashboardChangesFromScene.test.ts | 287 ------------- .../saving/getDashboardChangesFromScene.ts | 35 -- .../dashboard-scene/saving/shared.tsx | 5 +- .../saving/useSaveDashboard.ts | 22 +- .../scene/DashboardScene.test.tsx | 14 - .../dashboard-scene/scene/DashboardScene.tsx | 46 ++- .../scene/NavToolbarActions.tsx | 19 +- .../DashboardSceneSerializer.test.ts | 384 ++++++++++++++++++ .../serialization/DashboardSceneSerializer.ts | 158 +++++++ .../transformSceneToSaveModelSchemaV2.ts | 2 +- .../settings/JsonModelEditView.tsx | 3 +- .../components/SaveDashboard/types.ts | 11 +- .../app/features/dashboard/utils/tracking.ts | 37 +- 21 files changed, 718 insertions(+), 449 deletions(-) delete mode 100644 public/app/features/dashboard-scene/saving/getDashboardChangesFromScene.test.ts delete mode 100644 public/app/features/dashboard-scene/saving/getDashboardChangesFromScene.ts create mode 100644 public/app/features/dashboard-scene/serialization/DashboardSceneSerializer.test.ts create mode 100644 public/app/features/dashboard-scene/serialization/DashboardSceneSerializer.ts diff --git a/public/app/features/dashboard-scene/saving/DashboardPrompt.test.tsx b/public/app/features/dashboard-scene/saving/DashboardPrompt.test.tsx index ac2014b6fd5..2b7624b779a 100644 --- a/public/app/features/dashboard-scene/saving/DashboardPrompt.test.tsx +++ b/public/app/features/dashboard-scene/saving/DashboardPrompt.test.tsx @@ -24,14 +24,14 @@ describe('DashboardPrompt', () => { describe('when called without original dashboard', () => { it('then it should return true', () => { const scene = buildTestScene(); - expect(ignoreChanges(scene, undefined)).toBe(true); + scene.setInitialSaveModel(undefined); + expect(ignoreChanges(scene)).toBe(true); }); }); describe('when called without current dashboard', () => { it('then it should return true', () => { - const scene = buildTestScene(); - expect(ignoreChanges(null, scene.getInitialSaveModel())).toBe(true); + expect(ignoreChanges(null)).toBe(true); }); }); @@ -45,7 +45,7 @@ describe('DashboardPrompt', () => { }); contextSrv.isEditor = false; - expect(ignoreChanges(scene, scene.getInitialSaveModel())).toBe(true); + expect(ignoreChanges(scene)).toBe(true); }); }); @@ -59,10 +59,11 @@ describe('DashboardPrompt', () => { }, }); const initialSaveModel = transformSceneToSaveModel(scene); + scene.setInitialSaveModel(initialSaveModel); contextSrv.isEditor = false; - expect(ignoreChanges(scene, initialSaveModel)).toBe(undefined); + expect(ignoreChanges(scene)).toBe(undefined); }); }); @@ -75,9 +76,10 @@ describe('DashboardPrompt', () => { }, }); const initialSaveModel = transformSceneToSaveModel(scene); + scene.setInitialSaveModel(initialSaveModel); contextSrv.isSignedIn = false; - expect(ignoreChanges(scene, initialSaveModel)).toBe(true); + expect(ignoreChanges(scene)).toBe(true); }); }); @@ -90,7 +92,9 @@ describe('DashboardPrompt', () => { }, }); const initialSaveModel = transformSceneToSaveModel(scene); - expect(ignoreChanges(scene, initialSaveModel)).toBe(true); + scene.setInitialSaveModel(initialSaveModel); + + expect(ignoreChanges(scene)).toBe(true); }); }); @@ -104,7 +108,9 @@ describe('DashboardPrompt', () => { }, }); const initialSaveModel = transformSceneToSaveModel(scene); - expect(ignoreChanges(scene, initialSaveModel)).toBe(true); + scene.setInitialSaveModel(initialSaveModel); + + expect(ignoreChanges(scene)).toBe(true); }); }); @@ -118,7 +124,9 @@ describe('DashboardPrompt', () => { }, }); const initialSaveModel = transformSceneToSaveModel(scene); - expect(ignoreChanges(scene, initialSaveModel)).toBe(undefined); + scene.setInitialSaveModel(initialSaveModel); + + expect(ignoreChanges(scene)).toBe(undefined); }); }); }); diff --git a/public/app/features/dashboard-scene/saving/DashboardPrompt.tsx b/public/app/features/dashboard-scene/saving/DashboardPrompt.tsx index 8c8fb1c4170..3b0760001c6 100644 --- a/public/app/features/dashboard-scene/saving/DashboardPrompt.tsx +++ b/public/app/features/dashboard-scene/saving/DashboardPrompt.tsx @@ -3,13 +3,12 @@ import * as H from 'history'; import { memo, useContext, useEffect, useMemo } from 'react'; import { locationService } from '@grafana/runtime'; -import { Dashboard } from '@grafana/schema/dist/esm/index.gen'; import { ModalsContext, Modal, Button, useStyles2 } from '@grafana/ui'; import { Prompt } from 'app/core/components/FormPrompt/Prompt'; import { contextSrv } from 'app/core/services/context_srv'; import { SaveLibraryVizPanelModal } from '../panel-edit/SaveLibraryVizPanelModal'; -import { DashboardScene } from '../scene/DashboardScene'; +import { DashboardScene, isV2Dashboard } from '../scene/DashboardScene'; import { getLibraryPanelBehavior, isLibraryPanel } from '../utils/utils'; interface DashboardPromptProps { @@ -23,7 +22,7 @@ export const DashboardPrompt = memo(({ dashboard }: DashboardPromptProps) => { useEffect(() => { const handleUnload = (event: BeforeUnloadEvent) => { - if (ignoreChanges(dashboard, dashboard.getInitialSaveModel())) { + if (ignoreChanges(dashboard)) { return; } @@ -72,7 +71,7 @@ export const DashboardPrompt = memo(({ dashboard }: DashboardPromptProps) => { return true; } - if (ignoreChanges(dashboard, dashboard.getInitialSaveModel())) { + if (ignoreChanges(dashboard)) { return true; } @@ -153,7 +152,13 @@ const getStyles = () => ({ /** * For some dashboards and users changes should be ignored * */ -export function ignoreChanges(current: DashboardScene | null, original?: Dashboard) { +export function ignoreChanges(scene: DashboardScene | null) { + const original = scene?.getInitialSaveModel(); + + if (original && isV2Dashboard(original)) { + throw new Error('isV2Dashboard is not implemented'); + } + if (!original) { return true; } @@ -168,11 +173,11 @@ export function ignoreChanges(current: DashboardScene | null, original?: Dashboa return true; } - if (!current) { + if (!scene) { return true; } - const { canSave, fromScript, fromFile } = current.state.meta; + const { canSave, fromScript, fromFile } = scene.state.meta; if (!contextSrv.isEditor && !canSave) { return true; } diff --git a/public/app/features/dashboard-scene/saving/DetectChangesWorker.ts b/public/app/features/dashboard-scene/saving/DetectChangesWorker.ts index 2d7cb6f8386..3da81d5ad5b 100644 --- a/public/app/features/dashboard-scene/saving/DetectChangesWorker.ts +++ b/public/app/features/dashboard-scene/saving/DetectChangesWorker.ts @@ -1,6 +1,6 @@ import { Dashboard } from '@grafana/schema'; -import { getDashboardChanges } from './getDashboardChanges'; +import { getRawDashboardChanges } from './getDashboardChanges'; function _debounce(f: (...args: T[]) => void, timeout: number) { let timeoutId: NodeJS.Timeout | undefined = undefined; @@ -13,6 +13,6 @@ function _debounce(f: (...args: T[]) => void, timeout: number) { } self.onmessage = _debounce((e: MessageEvent<{ initial: Dashboard; changed: Dashboard }>) => { - const result = getDashboardChanges(e.data.initial, e.data.changed, false, false, false); + const result = getRawDashboardChanges(e.data.initial, e.data.changed, false, false, false); self.postMessage(result); }, 500); diff --git a/public/app/features/dashboard-scene/saving/SaveDashboardAsForm.tsx b/public/app/features/dashboard-scene/saving/SaveDashboardAsForm.tsx index 84012e2f694..39dc0bca90a 100644 --- a/public/app/features/dashboard-scene/saving/SaveDashboardAsForm.tsx +++ b/public/app/features/dashboard-scene/saving/SaveDashboardAsForm.tsx @@ -3,7 +3,6 @@ import { ChangeEvent, useState } from 'react'; import { UseFormSetValue, useForm } from 'react-hook-form'; import { selectors } from '@grafana/e2e-selectors'; -import { Dashboard } from '@grafana/schema'; import { Button, Input, Switch, Field, Label, TextArea, Stack, Alert, Box } from '@grafana/ui'; import { FolderPicker } from 'app/core/components/Select/FolderPicker'; import { validationSrv } from 'app/features/manage-dashboards/services/ValidationSrv'; @@ -52,8 +51,17 @@ export function SaveDashboardAsForm({ dashboard, changeInfo }: Props) { const onSave = async (overwrite: boolean) => { const data = getValues(); - const dashboardToSave: Dashboard = getSaveAsDashboardSaveModel(changedSaveModel, data, changeInfo.isNew); - const result = await onSaveDashboard(dashboard, dashboardToSave, { overwrite, folderUid: data.folder.uid }); + const result = await onSaveDashboard(dashboard, { + overwrite, + folderUid: data.folder.uid, + + // save as config + saveAsCopy: true, + isNew: changeInfo.isNew, + copyTags: data.copyTags, + title: data.title, + description: data.description, + }); if (result.status === 'success') { dashboard.closeModal(); @@ -98,11 +106,7 @@ export function SaveDashboardAsForm({ dashboard, changeInfo }: Props) { return (
onSave(false))}> - } - invalid={!!errors.title} - error={errors.title?.message} - > + } invalid={!!errors.title} error={errors.title?.message}> } + label={} invalid={!!errors.description} error={errors.description?.message} > @@ -149,7 +153,6 @@ export function SaveDashboardAsForm({ dashboard, changeInfo }: Props) { } export interface TitleLabelProps { - dashboard: Dashboard; onChange: UseFormSetValue; } @@ -168,7 +171,6 @@ export function TitleFieldLabel(props: TitleLabelProps) { } export interface DescriptionLabelProps { - dashboard: Dashboard; onChange: UseFormSetValue; } @@ -198,15 +200,3 @@ async function validateDashboardName(title: string, formValues: SaveDashboardAsF return e instanceof Error ? e.message : 'Dashboard name is invalid'; } } - -function getSaveAsDashboardSaveModel(source: Dashboard, form: SaveDashboardAsFormDTO, isNew?: boolean): Dashboard { - // TODO remove old alerts and thresholds when copying (See getSaveAsDashboardClone) - return { - ...source, - id: null, - uid: '', - title: form.title, - description: form.description, - tags: isNew || form.copyTags ? source.tags : [], - }; -} diff --git a/public/app/features/dashboard-scene/saving/SaveDashboardDrawer.tsx b/public/app/features/dashboard-scene/saving/SaveDashboardDrawer.tsx index 3502d33c1e1..c54a5bbd45a 100644 --- a/public/app/features/dashboard-scene/saving/SaveDashboardDrawer.tsx +++ b/public/app/features/dashboard-scene/saving/SaveDashboardDrawer.tsx @@ -7,7 +7,6 @@ import { DashboardScene } from '../scene/DashboardScene'; import { SaveDashboardAsForm } from './SaveDashboardAsForm'; import { SaveDashboardForm } from './SaveDashboardForm'; import { SaveProvisionedDashboardForm } from './SaveProvisionedDashboardForm'; -import { getDashboardChangesFromScene } from './getDashboardChangesFromScene'; interface SaveDashboardDrawerState extends SceneObjectState { dashboardRef: SceneObjectRef; @@ -38,12 +37,11 @@ export class SaveDashboardDrawer extends SceneObjectBase) => { const { showDiff, saveAsCopy, saveTimeRange, saveVariables, saveRefresh } = model.useState(); - const changeInfo = getDashboardChangesFromScene( - model.state.dashboardRef.resolve(), - saveTimeRange, - saveVariables, - saveRefresh - ); + + const changeInfo = model.state.dashboardRef + .resolve() + .getDashboardChanges(saveTimeRange, saveVariables, saveRefresh); + const { changedSaveModel, initialSaveModel, diffs, diffCount, hasFolderChanges } = changeInfo; const changesCount = diffCount + (hasFolderChanges ? 1 : 0); const dashboard = model.state.dashboardRef.resolve(); diff --git a/public/app/features/dashboard-scene/saving/SaveDashboardForm.tsx b/public/app/features/dashboard-scene/saving/SaveDashboardForm.tsx index 871eb14ce92..39933216aa8 100644 --- a/public/app/features/dashboard-scene/saving/SaveDashboardForm.tsx +++ b/public/app/features/dashboard-scene/saving/SaveDashboardForm.tsx @@ -24,7 +24,7 @@ export interface Props { } export function SaveDashboardForm({ dashboard, drawer, changeInfo }: Props) { - const { changedSaveModel, hasChanges } = changeInfo; + const { hasChanges } = changeInfo; const { state, onSaveDashboard } = useSaveDashboard(false); const [options, setOptions] = useState({ @@ -32,7 +32,7 @@ export function SaveDashboardForm({ dashboard, drawer, changeInfo }: Props) { }); const onSave = async (overwrite: boolean) => { - const result = await onSaveDashboard(dashboard, changedSaveModel, { ...options, overwrite }); + const result = await onSaveDashboard(dashboard, { ...options, overwrite }); if (result.status === 'success') { dashboard.closeModal(); drawer.state.onSaveSuccess?.(); diff --git a/public/app/features/dashboard-scene/saving/getDashboardChanges.test.ts b/public/app/features/dashboard-scene/saving/getDashboardChanges.test.ts index e8635add8ed..0abdef8e56d 100644 --- a/public/app/features/dashboard-scene/saving/getDashboardChanges.test.ts +++ b/public/app/features/dashboard-scene/saving/getDashboardChanges.test.ts @@ -1,7 +1,7 @@ import { AdHocVariableModel } from '@grafana/data'; import { Dashboard, Panel } from '@grafana/schema'; -import { adHocVariableFiltersEqual, getDashboardChanges, getPanelChanges } from './getDashboardChanges'; +import { adHocVariableFiltersEqual, getRawDashboardChanges, getPanelChanges } from './getDashboardChanges'; describe('adHocVariableFiltersEqual', () => { it('should compare empty filters', () => { @@ -134,7 +134,7 @@ describe('getDashboardChanges', () => { hasRefreshChange: false, }; - const result = getDashboardChanges(initial, changed, false, false, false); + const result = getRawDashboardChanges(initial, changed, false, false, false); expect(result).toEqual(expectedChanges); }); @@ -165,7 +165,7 @@ describe('getDashboardChanges', () => { hasRefreshChange: false, }; - const result = getDashboardChanges(newDashInitial, changed, false, false, false); + const result = getRawDashboardChanges(newDashInitial, changed, false, false, false); expect(result).toEqual(expectedChanges); }); @@ -195,7 +195,7 @@ describe('getDashboardChanges', () => { hasRefreshChange: false, }; - const result = getDashboardChanges(initial, changed, false, false, false); + const result = getRawDashboardChanges(initial, changed, false, false, false); expect(result).toEqual(expectedChanges); }); @@ -236,7 +236,7 @@ describe('getDashboardChanges', () => { hasRefreshChange: false, }; - const result = getDashboardChanges(initial, changed, true, false, false); + const result = getRawDashboardChanges(initial, changed, true, false, false); expect(result).toEqual(expectedChanges); }); @@ -263,7 +263,7 @@ describe('getDashboardChanges', () => { hasRefreshChange: true, }; - const result = getDashboardChanges(initial, changed, false, false, false); + const result = getRawDashboardChanges(initial, changed, false, false, false); expect(result).toEqual(expectedChanges); }); @@ -301,7 +301,7 @@ describe('getDashboardChanges', () => { hasRefreshChange: true, }; - const result = getDashboardChanges(initial, changed, false, false, true); + const result = getRawDashboardChanges(initial, changed, false, false, true); expect(result).toEqual(expectedChanges); }); @@ -341,7 +341,7 @@ describe('getDashboardChanges', () => { hasRefreshChange: false, }; - const result = getDashboardChanges(initial, changed, false, false, false); + const result = getRawDashboardChanges(initial, changed, false, false, false); expect(result).toEqual(expectedChanges); }); @@ -392,7 +392,7 @@ describe('getDashboardChanges', () => { hasRefreshChange: false, }; - const result = getDashboardChanges(initial, changed, false, true, false); + const result = getRawDashboardChanges(initial, changed, false, true, false); expect(result).toEqual(expectedChanges); }); diff --git a/public/app/features/dashboard-scene/saving/getDashboardChanges.ts b/public/app/features/dashboard-scene/saving/getDashboardChanges.ts index 3a65b20e53c..79ef18ea047 100644 --- a/public/app/features/dashboard-scene/saving/getDashboardChanges.ts +++ b/public/app/features/dashboard-scene/saving/getDashboardChanges.ts @@ -1,8 +1,8 @@ // @ts-ignore -import jsonMap from 'json-source-map'; import type { AdHocVariableModel, TypedVariableModel } from '@grafana/data'; import { Dashboard, Panel, VariableOption } from '@grafana/schema'; +import { DashboardV2Spec } from '@grafana/schema/dist/esm/schema/dashboard/v2alpha0/dashboard.gen'; import { jsonDiff } from '../settings/version-history/utils'; @@ -30,7 +30,28 @@ export function isEqual(a: VariableOption | undefined, b: VariableOption | undef return a === b || (a && b && a.selected === b.selected && deepEqual(a.text, b.text) && deepEqual(a.value, b.value)); } -export function getDashboardChanges( +// TODO[schema v2] +export function getRawDashboardV2Changes( + initial: DashboardV2Spec, + changed: DashboardV2Spec, + saveTimeRange?: boolean, + saveVariables?: boolean, + saveRefresh?: boolean +) { + return { + changedSaveModel: changed, + initialSaveModel: initial, + diffs: jsonDiff(initial, changed), + diffCount: 0, + hasChanges: false, + hasTimeChanges: false, + isNew: false, + hasVariableValueChanges: false, + hasRefreshChange: false, + }; +} + +export function getRawDashboardChanges( initial: Dashboard, changed: Dashboard, saveTimeRange?: boolean, diff --git a/public/app/features/dashboard-scene/saving/getDashboardChangesFromScene.test.ts b/public/app/features/dashboard-scene/saving/getDashboardChangesFromScene.test.ts deleted file mode 100644 index 3a62712b70d..00000000000 --- a/public/app/features/dashboard-scene/saving/getDashboardChangesFromScene.test.ts +++ /dev/null @@ -1,287 +0,0 @@ -import { config } from '@grafana/runtime'; -import { - AdHocFiltersVariable, - GroupByVariable, - MultiValueVariable, - sceneGraph, - SceneRefreshPicker, -} from '@grafana/scenes'; -import { VariableModel } from '@grafana/schema'; - -import { buildPanelEditScene } from '../panel-edit/PanelEditor'; -import { transformSaveModelToScene } from '../serialization/transformSaveModelToScene'; -import { transformSceneToSaveModel } from '../serialization/transformSceneToSaveModel'; -import { findVizPanelByKey } from '../utils/utils'; - -import { getDashboardChangesFromScene } from './getDashboardChangesFromScene'; - -describe('getDashboardChangesFromScene', () => { - it('Can detect no changes', () => { - const dashboard = setup(); - const result = getDashboardChangesFromScene(dashboard, false); - expect(result.hasChanges).toBe(false); - expect(result.diffCount).toBe(0); - }); - - it('Can detect time changed', () => { - const dashboard = setup(); - - sceneGraph.getTimeRange(dashboard).setState({ from: 'now-1h', to: 'now' }); - - const result = getDashboardChangesFromScene(dashboard, false); - expect(result.hasChanges).toBe(false); - expect(result.diffCount).toBe(0); - expect(result.hasTimeChanges).toBe(true); - }); - - it('Can save time change', () => { - const dashboard = setup(); - - sceneGraph.getTimeRange(dashboard).setState({ from: 'now-1h', to: 'now' }); - - const result = getDashboardChangesFromScene(dashboard, true); - expect(result.hasChanges).toBe(true); - expect(result.diffCount).toBe(1); - }); - - it('Can detect folder change', () => { - const dashboard = setup(); - - dashboard.state.meta.folderUid = 'folder-2'; - - const result = getDashboardChangesFromScene(dashboard, false); - expect(result.hasChanges).toBe(true); - expect(result.diffCount).toBe(0); // Diff count is 0 because the diff contemplate only the model - expect(result.hasFolderChanges).toBe(true); - }); - - it('Can detect refresh changed', () => { - const dashboard = setup(); - - const refreshPicker = sceneGraph.findObject(dashboard, (obj) => obj instanceof SceneRefreshPicker); - if (refreshPicker instanceof SceneRefreshPicker) { - refreshPicker.setState({ refresh: '5s' }); - } - - const result = getDashboardChangesFromScene(dashboard, false, false, false); - expect(result.hasChanges).toBe(false); - expect(result.diffCount).toBe(0); - expect(result.hasRefreshChange).toBe(true); - }); - - it('Can save refresh change', () => { - const dashboard = setup(); - - const refreshPicker = sceneGraph.findObject(dashboard, (obj) => obj instanceof SceneRefreshPicker); - if (refreshPicker instanceof SceneRefreshPicker) { - refreshPicker.setState({ refresh: '5s' }); - } - - const result = getDashboardChangesFromScene(dashboard, false, false, true); - expect(result.hasChanges).toBe(true); - expect(result.diffCount).toBe(1); - }); - - describe('variable changes', () => { - it('Can detect variable change', () => { - const dashboard = setup(); - - const appVar = sceneGraph.lookupVariable('app', dashboard) as MultiValueVariable; - appVar.changeValueTo('app2'); - - const result = getDashboardChangesFromScene(dashboard, false, false); - - expect(result.hasVariableValueChanges).toBe(true); - expect(result.hasChanges).toBe(false); - expect(result.diffCount).toBe(0); - }); - - it('Can save variable value change', () => { - const dashboard = setup(); - - const appVar = sceneGraph.lookupVariable('app', dashboard) as MultiValueVariable; - appVar.changeValueTo('app2'); - - const result = getDashboardChangesFromScene(dashboard, false, true); - - expect(result.hasVariableValueChanges).toBe(true); - expect(result.hasChanges).toBe(true); - expect(result.diffCount).toBe(2); - }); - - describe('Experimental variables', () => { - beforeAll(() => { - config.featureToggles.groupByVariable = true; - }); - - afterAll(() => { - config.featureToggles.groupByVariable = false; - }); - - it('Can detect group by static options change', () => { - const dashboard = transformSaveModelToScene({ - dashboard: { - title: 'hello', - uid: 'my-uid', - schemaVersion: 30, - panels: [ - { - id: 1, - title: 'Panel 1', - type: 'text', - }, - ], - version: 10, - templating: { - list: [ - { - type: 'groupby', - datasource: { - type: 'ds', - uid: 'ds-uid', - }, - name: 'GroupBy', - options: [ - { - text: 'Host', - value: 'host', - }, - { - text: 'Region', - value: 'region', - }, - ], - }, - ], - }, - }, - meta: {}, - }); - const initialSaveModel = transformSceneToSaveModel(dashboard); - dashboard.setInitialSaveModel(initialSaveModel); - - const variable = sceneGraph.lookupVariable('GroupBy', dashboard) as GroupByVariable; - variable.setState({ defaultOptions: [{ text: 'Host', value: 'host' }] }); - const result = getDashboardChangesFromScene(dashboard, false, true); - - expect(result.hasVariableValueChanges).toBe(false); - expect(result.hasChanges).toBe(true); - expect(result.diffCount).toBe(1); - }); - - it('Can detect adhoc filter static options change', () => { - const adhocVar = { - id: 'adhoc', - name: 'adhoc', - label: 'Adhoc Label', - description: 'Adhoc Description', - type: 'adhoc', - datasource: { - uid: 'gdev-prometheus', - type: 'prometheus', - }, - filters: [], - baseFilters: [], - defaultKeys: [ - { - text: 'Host', - value: 'host', - }, - { - text: 'Region', - value: 'region', - }, - ], - } as VariableModel; - - const dashboard = transformSaveModelToScene({ - dashboard: { - title: 'hello', - uid: 'my-uid', - schemaVersion: 30, - panels: [ - { - id: 1, - title: 'Panel 1', - type: 'text', - }, - ], - version: 10, - templating: { - list: [adhocVar], - }, - }, - meta: {}, - }); - - const initialSaveModel = transformSceneToSaveModel(dashboard); - dashboard.setInitialSaveModel(initialSaveModel); - - const variable = sceneGraph.lookupVariable('adhoc', dashboard) as AdHocFiltersVariable; - variable.setState({ defaultKeys: [{ text: 'Host', value: 'host' }] }); - const result = getDashboardChangesFromScene(dashboard, false, false); - - expect(result.hasVariableValueChanges).toBe(false); - expect(result.hasChanges).toBe(true); - expect(result.diffCount).toBe(1); - }); - }); - }); - - describe('Saving from panel edit', () => { - it('Should commit panel edit changes', () => { - const dashboard = setup(); - const panel = findVizPanelByKey(dashboard, 'panel-1')!; - const editScene = buildPanelEditScene(panel); - - dashboard.onEnterEditMode(); - dashboard.setState({ editPanel: editScene }); - - editScene.state.panelRef.resolve().setState({ title: 'changed title' }); - - const result = getDashboardChangesFromScene(dashboard, false, true); - const panelSaveModel = result.changedSaveModel.panels![0]; - expect(panelSaveModel.title).toBe('changed title'); - }); - }); -}); - -interface ScenarioOptions { - fromPanelEdit?: boolean; -} - -function setup(options: ScenarioOptions = {}) { - const dashboard = transformSaveModelToScene({ - dashboard: { - title: 'hello', - uid: 'my-uid', - schemaVersion: 30, - panels: [ - { - id: 1, - title: 'Panel 1', - type: 'text', - }, - ], - version: 10, - templating: { - list: [ - { - name: 'app', - type: 'custom', - current: { - text: 'app1', - value: 'app1', - }, - }, - ], - }, - }, - meta: {}, - }); - - const initialSaveModel = transformSceneToSaveModel(dashboard); - dashboard.setInitialSaveModel(initialSaveModel); - - return dashboard; -} diff --git a/public/app/features/dashboard-scene/saving/getDashboardChangesFromScene.ts b/public/app/features/dashboard-scene/saving/getDashboardChangesFromScene.ts deleted file mode 100644 index 5b8a4a444b0..00000000000 --- a/public/app/features/dashboard-scene/saving/getDashboardChangesFromScene.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { DashboardScene } from '../scene/DashboardScene'; -import { transformSceneToSaveModel } from '../serialization/transformSceneToSaveModel'; - -import { getDashboardChanges as getDashboardSaveModelChanges } from './getDashboardChanges'; - -/** - * Get changes between the initial save model and the current scene. - * It also checks if the folder has changed. - * @param scene DashboardScene object - * @param saveTimeRange if true, compare the time range - * @param saveVariables if true, compare the variables - * @param saveRefresh if true, compare the refresh interval - * @returns - */ -export function getDashboardChangesFromScene( - scene: DashboardScene, - saveTimeRange?: boolean, - saveVariables?: boolean, - saveRefresh?: boolean -) { - const changeInfo = getDashboardSaveModelChanges( - scene.getInitialSaveModel()!, - transformSceneToSaveModel(scene), - saveTimeRange, - saveVariables, - saveRefresh - ); - const hasFolderChanges = scene.getInitialState()?.meta.folderUid !== scene.state.meta.folderUid; - - return { - ...changeInfo, - hasFolderChanges, - hasChanges: changeInfo.hasChanges || hasFolderChanges, - }; -} diff --git a/public/app/features/dashboard-scene/saving/shared.tsx b/public/app/features/dashboard-scene/saving/shared.tsx index 7fd584b666e..b2c3507c626 100644 --- a/public/app/features/dashboard-scene/saving/shared.tsx +++ b/public/app/features/dashboard-scene/saving/shared.tsx @@ -3,14 +3,15 @@ import * as React from 'react'; import { selectors } from '@grafana/e2e-selectors'; import { config, isFetchError } from '@grafana/runtime'; import { Dashboard } from '@grafana/schema'; +import { DashboardV2Spec } from '@grafana/schema/dist/esm/schema/dashboard/v2alpha0/dashboard.gen'; import { Alert, Box, Button, Stack } from '@grafana/ui'; import { t, Trans } from 'app/core/internationalization'; import { Diffs } from '../settings/version-history/utils'; export interface DashboardChangeInfo { - changedSaveModel: Dashboard; - initialSaveModel: Dashboard; + changedSaveModel: Dashboard | DashboardV2Spec; + initialSaveModel: Dashboard | DashboardV2Spec; diffs: Diffs; diffCount: number; hasChanges: boolean; diff --git a/public/app/features/dashboard-scene/saving/useSaveDashboard.ts b/public/app/features/dashboard-scene/saving/useSaveDashboard.ts index 04ab768aaad..7fa3694f584 100644 --- a/public/app/features/dashboard-scene/saving/useSaveDashboard.ts +++ b/public/app/features/dashboard-scene/saving/useSaveDashboard.ts @@ -7,7 +7,7 @@ import appEvents from 'app/core/app_events'; import { useAppNotification } from 'app/core/copy/appNotification'; import { updateDashboardName } from 'app/core/reducers/navBarTree'; import { useSaveDashboardMutation } from 'app/features/browse-dashboards/api/browseDashboardsAPI'; -import { SaveDashboardOptions } from 'app/features/dashboard/components/SaveDashboard/types'; +import { SaveDashboardAsOptions, SaveDashboardOptions } from 'app/features/dashboard/components/SaveDashboard/types'; import { useDispatch } from 'app/types'; import { DashboardSavedEvent } from 'app/types/events'; @@ -20,8 +20,26 @@ export function useSaveDashboard(isCopy = false) { const [saveDashboardRtkQuery] = useSaveDashboardMutation(); const [state, onSaveDashboard] = useAsyncFn( - async (scene: DashboardScene, saveModel: Dashboard, options: SaveDashboardOptions) => { + async ( + scene: DashboardScene, + options: SaveDashboardOptions & + SaveDashboardAsOptions & { + // When provided, will take precedence over the scene's save model + rawDashboardJSON?: Dashboard; + } + ) => { { + let saveModel = options.rawDashboardJSON ?? scene.getSaveModel(); + + if (options.saveAsCopy) { + saveModel = scene.getSaveAsModel({ + isNew: options.isNew, + title: options.title, + description: options.description, + copyTags: options.copyTags, + }); + } + const result = await saveDashboardRtkQuery({ dashboard: saveModel, folderUid: options.folderUid, diff --git a/public/app/features/dashboard-scene/scene/DashboardScene.test.tsx b/public/app/features/dashboard-scene/scene/DashboardScene.test.tsx index b50b2c7734f..19a1a023f3d 100644 --- a/public/app/features/dashboard-scene/scene/DashboardScene.test.tsx +++ b/public/app/features/dashboard-scene/scene/DashboardScene.test.tsx @@ -37,20 +37,6 @@ import { RowActions } from './row-actions/RowActions'; jest.mock('../settings/version-history/HistorySrv'); jest.mock('../serialization/transformSaveModelToScene'); -jest.mock('../saving/getDashboardChangesFromScene', () => ({ - // It compares the initial and changed save models and returns the differences - // By default we assume there are differences to have the dirty state test logic tested - getDashboardChangesFromScene: jest.fn(() => ({ - changedSaveModel: {}, - initialSaveModel: {}, - diffs: [], - diffCount: 0, - hasChanges: true, - hasTimeChanges: false, - isNew: false, - hasVariableValueChanges: false, - })), -})); jest.mock('../serialization/transformSceneToSaveModel'); jest.mock('@grafana/runtime', () => ({ ...jest.requireActual('@grafana/runtime'), diff --git a/public/app/features/dashboard-scene/scene/DashboardScene.tsx b/public/app/features/dashboard-scene/scene/DashboardScene.tsx index e839cea20ba..94ad83c9e3f 100644 --- a/public/app/features/dashboard-scene/scene/DashboardScene.tsx +++ b/public/app/features/dashboard-scene/scene/DashboardScene.tsx @@ -25,11 +25,13 @@ import { VizPanel, } from '@grafana/scenes'; import { Dashboard, DashboardLink, LibraryPanel } from '@grafana/schema'; +import { DashboardV2Spec } from '@grafana/schema/dist/esm/schema/dashboard/v2alpha0/dashboard.gen'; import appEvents from 'app/core/app_events'; import { ScrollRefElement } from 'app/core/components/NativeScrollbar'; import { LS_PANEL_COPY_KEY } from 'app/core/constants'; import { getNavModel } from 'app/core/selectors/navModel'; import store from 'app/core/store'; +import { SaveDashboardAsOptions } from 'app/features/dashboard/components/SaveDashboard/types'; import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv'; import { DashboardModel } from 'app/features/dashboard/state/DashboardModel'; import { PanelModel } from 'app/features/dashboard/state/PanelModel'; @@ -43,6 +45,8 @@ import { DashboardEditPane } from '../edit-pane/DashboardEditPane'; import { PanelEditor } from '../panel-edit/PanelEditor'; import { DashboardSceneChangeTracker } from '../saving/DashboardSceneChangeTracker'; import { SaveDashboardDrawer } from '../saving/SaveDashboardDrawer'; +import { DashboardChangeInfo } from '../saving/shared'; +import { DashboardSceneSerializerLike, getDashboardSceneSerializer } from '../serialization/DashboardSceneSerializer'; import { buildGridItemForPanel, transformSaveModelToScene } from '../serialization/transformSaveModelToScene'; import { gridItemToPanel } from '../serialization/transformSceneToSaveModel'; import { DecoratedRevisionModel } from '../settings/VersionsEditView'; @@ -149,10 +153,6 @@ export class DashboardScene extends SceneObjectBase { * State before editing started */ private _initialState?: DashboardSceneState; - /** - * The save model which the scene was originally created from - */ - private _initialSaveModel?: Dashboard; /** * Url state before editing started */ @@ -172,6 +172,9 @@ export class DashboardScene extends SceneObjectBase { private _scrollRef?: ScrollRefElement; private _prevScrollPos?: number; + // TODO: use feature toggle to allow v2 serializer + private _serializer: DashboardSceneSerializerLike = getDashboardSceneSerializer(true); + public constructor(state: Partial) { super({ title: 'Dashboard', @@ -261,13 +264,8 @@ export class DashboardScene extends SceneObjectBase { this._changeTracker.startTrackingChanges(); }; - public saveCompleted(saveModel: Dashboard, result: SaveDashboardResponseDTO, folderUid?: string) { - this._initialSaveModel = { - ...saveModel, - id: result.id, - uid: result.uid, - version: result.version, - }; + public saveCompleted(saveModel: Dashboard | DashboardV2Spec, result: SaveDashboardResponseDTO, folderUid?: string) { + this._serializer.onSaveComplete(saveModel, result); this._changeTracker.stopTrackingChanges(); @@ -640,12 +638,16 @@ export class DashboardScene extends SceneObjectBase { } public getInitialSaveModel() { - return this._initialSaveModel; + return this._serializer.initialSaveModel; } /** Hacky temp function until we refactor transformSaveModelToScene a bit */ - public setInitialSaveModel(saveModel: Dashboard) { - this._initialSaveModel = saveModel; + public setInitialSaveModel(saveModel?: Dashboard | DashboardV2Spec) { + this._serializer.initialSaveModel = saveModel; + } + + public getTrackingInformation() { + return this._serializer.getTrackingInformation(); } public async onDashboardDelete() { @@ -688,6 +690,18 @@ export class DashboardScene extends SceneObjectBase { this._scrollRef?.scrollTo(0, this._prevScrollPos!); } } + + getSaveModel(): Dashboard | DashboardV2Spec { + return this._serializer.getSaveModel(this); + } + + getSaveAsModel(options: SaveDashboardAsOptions): Dashboard | DashboardV2Spec { + return this._serializer.getSaveAsModel(this, options); + } + + getDashboardChanges(saveTimeRange?: boolean, saveVariables?: boolean, saveRefresh?: boolean): DashboardChangeInfo { + return this._serializer.getDashboardChangesFromScene(this, { saveTimeRange, saveVariables, saveRefresh }); + } } export class DashboardVariableDependency implements SceneVariableDependencyConfigLike { @@ -751,3 +765,7 @@ export class DashboardVariableDependency implements SceneVariableDependencyConfi } } } + +export function isV2Dashboard(model: Dashboard | DashboardV2Spec): model is DashboardV2Spec { + return 'elements' in model; +} diff --git a/public/app/features/dashboard-scene/scene/NavToolbarActions.tsx b/public/app/features/dashboard-scene/scene/NavToolbarActions.tsx index f72c9d30db7..680b613ad60 100644 --- a/public/app/features/dashboard-scene/scene/NavToolbarActions.tsx +++ b/public/app/features/dashboard-scene/scene/NavToolbarActions.tsx @@ -34,7 +34,7 @@ import { DashboardInteractions } from '../utils/interactions'; import { DynamicDashNavButtonModel, dynamicDashNavActions } from '../utils/registerDynamicDashNavAction'; import { isLibraryPanel } from '../utils/utils'; -import { DashboardScene } from './DashboardScene'; +import { DashboardScene, isV2Dashboard } from './DashboardScene'; import { GoToSnapshotOriginButton } from './GoToSnapshotOriginButton'; interface Props { @@ -140,12 +140,17 @@ export function ToolbarActions({ dashboard }: Props) { toolbarActions.push({ group: 'icon-actions', condition: meta.isSnapshot && !meta.dashboardNotFound && !isEditing, - render: () => ( - - ), + render: () => { + const saveModel = dashboard.getInitialSaveModel(); + + if (saveModel && isV2Dashboard(saveModel)) { + throw new Error('v2 schema not implemented'); + } + + return ( + + ); + }, }); if (!isEditingPanel && !isEditing) { diff --git a/public/app/features/dashboard-scene/serialization/DashboardSceneSerializer.test.ts b/public/app/features/dashboard-scene/serialization/DashboardSceneSerializer.test.ts new file mode 100644 index 00000000000..4d03005c5cb --- /dev/null +++ b/public/app/features/dashboard-scene/serialization/DashboardSceneSerializer.test.ts @@ -0,0 +1,384 @@ +import { config } from '@grafana/runtime'; +import { + AdHocFiltersVariable, + GroupByVariable, + MultiValueVariable, + sceneGraph, + SceneRefreshPicker, +} from '@grafana/scenes'; +import { Dashboard, VariableModel } from '@grafana/schema'; +import { DashboardV2Spec } from '@grafana/schema/dist/esm/schema/dashboard/v2alpha0/dashboard.gen'; + +import { buildPanelEditScene } from '../panel-edit/PanelEditor'; +import { transformSaveModelToScene } from '../serialization/transformSaveModelToScene'; +import { transformSceneToSaveModel } from '../serialization/transformSceneToSaveModel'; +import { findVizPanelByKey } from '../utils/utils'; + +import { V1DashboardSerializer, V2DashboardSerializer } from './DashboardSceneSerializer'; + +describe('DashboardSceneSerializer', () => { + describe('v1 schema', () => { + it('Can detect no changes', () => { + const dashboard = setup(); + const result = dashboard.getDashboardChanges(false); + expect(result.hasChanges).toBe(false); + expect(result.diffCount).toBe(0); + }); + + it('Can detect time changed', () => { + const dashboard = setup(); + + sceneGraph.getTimeRange(dashboard).setState({ from: 'now-1h', to: 'now' }); + + const result = dashboard.getDashboardChanges(false); + expect(result.hasChanges).toBe(false); + expect(result.diffCount).toBe(0); + expect(result.hasTimeChanges).toBe(true); + }); + + it('Can save time change', () => { + const dashboard = setup(); + + sceneGraph.getTimeRange(dashboard).setState({ from: 'now-1h', to: 'now' }); + + const result = dashboard.getDashboardChanges(true); + expect(result.hasChanges).toBe(true); + expect(result.diffCount).toBe(1); + }); + + it('Can detect folder change', () => { + const dashboard = setup(); + + dashboard.state.meta.folderUid = 'folder-2'; + + const result = dashboard.getDashboardChanges(false); + expect(result.hasChanges).toBe(true); + expect(result.diffCount).toBe(0); // Diff count is 0 because the diff contemplate only the model + expect(result.hasFolderChanges).toBe(true); + }); + + it('Can detect refresh changed', () => { + const dashboard = setup(); + + const refreshPicker = sceneGraph.findObject(dashboard, (obj) => obj instanceof SceneRefreshPicker); + if (refreshPicker instanceof SceneRefreshPicker) { + refreshPicker.setState({ refresh: '5s' }); + } + + const result = dashboard.getDashboardChanges(false, false, false); + expect(result.hasChanges).toBe(false); + expect(result.diffCount).toBe(0); + expect(result.hasRefreshChange).toBe(true); + }); + + it('Can save refresh change', () => { + const dashboard = setup(); + + const refreshPicker = sceneGraph.findObject(dashboard, (obj) => obj instanceof SceneRefreshPicker); + if (refreshPicker instanceof SceneRefreshPicker) { + refreshPicker.setState({ refresh: '5s' }); + } + + const result = dashboard.getDashboardChanges(false, false, true); + expect(result.hasChanges).toBe(true); + expect(result.diffCount).toBe(1); + }); + + describe('variable changes', () => { + it('Can detect variable change', () => { + const dashboard = setup(); + + const appVar = sceneGraph.lookupVariable('app', dashboard) as MultiValueVariable; + appVar.changeValueTo('app2'); + + const result = dashboard.getDashboardChanges(false, false); + + expect(result.hasVariableValueChanges).toBe(true); + expect(result.hasChanges).toBe(false); + expect(result.diffCount).toBe(0); + }); + + it('Can save variable value change', () => { + const dashboard = setup(); + + const appVar = sceneGraph.lookupVariable('app', dashboard) as MultiValueVariable; + appVar.changeValueTo('app2'); + + const result = dashboard.getDashboardChanges(false, true); + + expect(result.hasVariableValueChanges).toBe(true); + expect(result.hasChanges).toBe(true); + expect(result.diffCount).toBe(2); + }); + + describe('Experimental variables', () => { + beforeAll(() => { + config.featureToggles.groupByVariable = true; + }); + + afterAll(() => { + config.featureToggles.groupByVariable = false; + }); + + it('Can detect group by static options change', () => { + const dashboard = transformSaveModelToScene({ + dashboard: { + title: 'hello', + uid: 'my-uid', + schemaVersion: 30, + panels: [ + { + id: 1, + title: 'Panel 1', + type: 'text', + }, + ], + version: 10, + templating: { + list: [ + { + type: 'groupby', + datasource: { + type: 'ds', + uid: 'ds-uid', + }, + name: 'GroupBy', + options: [ + { + text: 'Host', + value: 'host', + }, + { + text: 'Region', + value: 'region', + }, + ], + }, + ], + }, + }, + meta: {}, + }); + const initialSaveModel = transformSceneToSaveModel(dashboard); + dashboard.setInitialSaveModel(initialSaveModel); + + const variable = sceneGraph.lookupVariable('GroupBy', dashboard) as GroupByVariable; + variable.setState({ defaultOptions: [{ text: 'Host', value: 'host' }] }); + const result = dashboard.getDashboardChanges(false, true); + + expect(result.hasVariableValueChanges).toBe(false); + expect(result.hasChanges).toBe(true); + expect(result.diffCount).toBe(1); + }); + + it('Can detect adhoc filter static options change', () => { + const adhocVar = { + id: 'adhoc', + name: 'adhoc', + label: 'Adhoc Label', + description: 'Adhoc Description', + type: 'adhoc', + datasource: { + uid: 'gdev-prometheus', + type: 'prometheus', + }, + filters: [], + baseFilters: [], + defaultKeys: [ + { + text: 'Host', + value: 'host', + }, + { + text: 'Region', + value: 'region', + }, + ], + } as VariableModel; + + const dashboard = transformSaveModelToScene({ + dashboard: { + title: 'hello', + uid: 'my-uid', + schemaVersion: 30, + panels: [ + { + id: 1, + title: 'Panel 1', + type: 'text', + }, + ], + version: 10, + templating: { + list: [adhocVar], + }, + }, + meta: {}, + }); + + const initialSaveModel = transformSceneToSaveModel(dashboard); + dashboard.setInitialSaveModel(initialSaveModel); + + const variable = sceneGraph.lookupVariable('adhoc', dashboard) as AdHocFiltersVariable; + variable.setState({ defaultKeys: [{ text: 'Host', value: 'host' }] }); + const result = dashboard.getDashboardChanges(false, false); + + expect(result.hasVariableValueChanges).toBe(false); + expect(result.hasChanges).toBe(true); + expect(result.diffCount).toBe(1); + }); + }); + }); + + describe('Saving from panel edit', () => { + it('Should commit panel edit changes', () => { + const dashboard = setup(); + const panel = findVizPanelByKey(dashboard, 'panel-1')!; + const editScene = buildPanelEditScene(panel); + + dashboard.onEnterEditMode(); + dashboard.setState({ editPanel: editScene }); + + editScene.state.panelRef.resolve().setState({ title: 'changed title' }); + + const result = dashboard.getDashboardChanges(false, true); + const panelSaveModel = (result.changedSaveModel as Dashboard).panels![0]; + expect(panelSaveModel.title).toBe('changed title'); + }); + }); + + describe('tracking information', () => { + it('provides dashboard tracking information with no initial save model', () => { + const serializer = new V1DashboardSerializer(); + expect(serializer.getTrackingInformation()).toBe(undefined); + }); + + it('provides dashboard tracking information with from initial save model', () => { + const serializer = new V1DashboardSerializer(); + serializer.initialSaveModel = { + schemaVersion: 30, + version: 10, + uid: 'my-uid', + title: 'hello', + liveNow: true, + panels: [ + { + type: 'text', + }, + { + type: 'text', + }, + { + type: 'timeseries', + }, + ], + + templating: { + list: [ + { + type: 'query', + name: 'server', + }, + { + type: 'query', + name: 'host', + }, + { + type: 'textbox', + name: 'search', + }, + ], + }, + }; + + expect(serializer.getTrackingInformation()).toEqual({ + uid: 'my-uid', + title: 'hello', + schemaVersion: 30, + panels_count: 3, + panel_type_text_count: 2, + panel_type_timeseries_count: 1, + variable_type_query_count: 2, + variable_type_textbox_count: 1, + settings_nowdelay: undefined, + settings_livenow: true, + version_before_migration: 10, + }); + }); + }); + }); + + describe('v2 schema', () => { + it('should throw on getSaveAsModel', () => { + const serializer = new V2DashboardSerializer(); + const dashboard = setup(); + expect(() => serializer.getSaveAsModel(dashboard, {})).toThrow('Method not implemented.'); + }); + + it('should throw on getDashboardChangesFromScene', () => { + const serializer = new V2DashboardSerializer(); + const dashboard = setup(); + expect(() => serializer.getDashboardChangesFromScene(dashboard)).toThrow('Method not implemented.'); + }); + + it('should throw on onSaveComplete', () => { + const serializer = new V2DashboardSerializer(); + + expect(() => + serializer.onSaveComplete({} as DashboardV2Spec, { + id: 1, + uid: 'aa', + slug: 'slug', + url: 'url', + version: 2, + status: 'status', + }) + ).toThrow('Method not implemented.'); + }); + + it('should throw on getDashboardChangesFromScene', () => { + const serializer = new V2DashboardSerializer(); + expect(() => serializer.getTrackingInformation()).toThrow('Method not implemented.'); + }); + }); +}); + +interface ScenarioOptions { + fromPanelEdit?: boolean; +} + +function setup(options: ScenarioOptions = {}) { + const dashboard = transformSaveModelToScene({ + dashboard: { + title: 'hello', + uid: 'my-uid', + schemaVersion: 30, + panels: [ + { + id: 1, + title: 'Panel 1', + type: 'text', + }, + ], + version: 10, + templating: { + list: [ + { + name: 'app', + type: 'custom', + current: { + text: 'app1', + value: 'app1', + }, + }, + ], + }, + }, + meta: {}, + }); + + const initialSaveModel = transformSceneToSaveModel(dashboard); + dashboard.setInitialSaveModel(initialSaveModel); + + return dashboard; +} diff --git a/public/app/features/dashboard-scene/serialization/DashboardSceneSerializer.ts b/public/app/features/dashboard-scene/serialization/DashboardSceneSerializer.ts new file mode 100644 index 00000000000..5edefb6d1c6 --- /dev/null +++ b/public/app/features/dashboard-scene/serialization/DashboardSceneSerializer.ts @@ -0,0 +1,158 @@ +import { config } from '@grafana/runtime'; +import { Dashboard } from '@grafana/schema'; +import { DashboardV2Spec } from '@grafana/schema/dist/esm/schema/dashboard/v2alpha0/dashboard.gen'; +import { SaveDashboardAsOptions } from 'app/features/dashboard/components/SaveDashboard/types'; +import { getV1SchemaPanelCounts, getV1SchemaVariables } from 'app/features/dashboard/utils/tracking'; +import { SaveDashboardResponseDTO } from 'app/types'; + +import { getRawDashboardChanges } from '../saving/getDashboardChanges'; +import { DashboardChangeInfo } from '../saving/shared'; +import { DashboardScene } from '../scene/DashboardScene'; + +import { transformSceneToSaveModel } from './transformSceneToSaveModel'; +import { transformSceneToSaveModelSchemaV2 } from './transformSceneToSaveModelSchemaV2'; + +export interface DashboardSceneSerializerLike { + /** + * The save model which the dashboard scene was originally created from + */ + initialSaveModel?: T; + getSaveModel: (s: DashboardScene) => T; + getSaveAsModel: (s: DashboardScene, options: SaveDashboardAsOptions) => T; + getDashboardChangesFromScene: ( + scene: DashboardScene, + options: { + saveTimeRange?: boolean; + saveVariables?: boolean; + saveRefresh?: boolean; + } + ) => DashboardChangeInfo; + onSaveComplete(saveModel: T, result: SaveDashboardResponseDTO): void; + getTrackingInformation: () => DashboardTrackingInfo | undefined; +} + +interface DashboardTrackingInfo { + uid?: string; + title?: string; + schemaVersion: number; + version_before_migration?: number; + panels_count: number; + settings_nowdelay?: number; + settings_livenow?: boolean; +} + +export class V1DashboardSerializer implements DashboardSceneSerializerLike { + initialSaveModel?: Dashboard; + + getSaveModel(s: DashboardScene) { + return transformSceneToSaveModel(s); + } + + getSaveAsModel(s: DashboardScene, options: SaveDashboardAsOptions) { + const saveModel = this.getSaveModel(s); + + return { + ...saveModel, + id: null, + uid: '', + title: options.title || '', + description: options.description || '', + tags: options.isNew || options.copyTags ? saveModel.tags : [], + }; + } + + getDashboardChangesFromScene( + scene: DashboardScene, + options: { saveTimeRange?: boolean; saveVariables?: boolean; saveRefresh?: boolean } + ) { + const changedSaveModel = this.getSaveModel(scene); + const changeInfo = getRawDashboardChanges( + this.initialSaveModel!, + changedSaveModel, + options.saveTimeRange, + options.saveVariables, + options.saveRefresh + ); + + const hasFolderChanges = scene.getInitialState()?.meta.folderUid !== scene.state.meta.folderUid; + + return { + ...changeInfo, + hasFolderChanges, + hasChanges: changeInfo.hasChanges || hasFolderChanges, + }; + } + + onSaveComplete(saveModel: Dashboard, result: SaveDashboardResponseDTO): void { + this.initialSaveModel = { + ...saveModel, + id: result.id, + uid: result.uid, + version: result.version, + }; + } + + getTrackingInformation(): DashboardTrackingInfo | undefined { + const panels = getV1SchemaPanelCounts(this.initialSaveModel?.panels || []); + const variables = getV1SchemaVariables(this.initialSaveModel?.templating?.list || []); + + if (this.initialSaveModel) { + return { + uid: this.initialSaveModel.uid, + title: this.initialSaveModel.title, + schemaVersion: this.initialSaveModel.schemaVersion, + version_before_migration: this.initialSaveModel.version, + panels_count: this.initialSaveModel.panels?.length || 0, + settings_nowdelay: undefined, + settings_livenow: !!this.initialSaveModel.liveNow, + ...panels, + ...variables, + }; + } + return undefined; + } +} + +export class V2DashboardSerializer implements DashboardSceneSerializerLike { + initialSaveModel?: DashboardV2Spec; + + getSaveModel(s: DashboardScene) { + return transformSceneToSaveModelSchemaV2(s); + } + + getSaveAsModel(s: DashboardScene, options: SaveDashboardAsOptions) { + throw new Error('Method not implemented.'); + // eslint-disable-next-line + return {} as DashboardV2Spec; + } + + getDashboardChangesFromScene(scene: DashboardScene) { + throw new Error('v2 schema: Method not implemented.'); + // eslint-disable-next-line + return {} as DashboardChangeInfo; + } + + onSaveComplete(saveModel: DashboardV2Spec, result: SaveDashboardResponseDTO): void { + throw new Error('v2 schema: Method not implemented.'); + } + + getTrackingInformation() { + throw new Error('v2 schema: Method not implemented.'); + return undefined; + } +} + +export function getDashboardSceneSerializer( + forceLegacy?: boolean +): DashboardSceneSerializerLike { + // When we have end-to-end v2 API integration, this will be controlled by a feature toggle, no need for forceLegacy + if (forceLegacy) { + return new V1DashboardSerializer(); + } + + if (config.featureToggles.dashboardSchemaV2) { + return new V2DashboardSerializer(); + } + + return new V1DashboardSerializer(); +} diff --git a/public/app/features/dashboard-scene/serialization/transformSceneToSaveModelSchemaV2.ts b/public/app/features/dashboard-scene/serialization/transformSceneToSaveModelSchemaV2.ts index bb0059effd2..92780e9fcde 100644 --- a/public/app/features/dashboard-scene/serialization/transformSceneToSaveModelSchemaV2.ts +++ b/public/app/features/dashboard-scene/serialization/transformSceneToSaveModelSchemaV2.ts @@ -57,7 +57,7 @@ type DeepPartial = T extends object } : T; -export function transformSceneToSaveModelSchemaV2(scene: DashboardScene, isSnapshot = false): Partial { +export function transformSceneToSaveModelSchemaV2(scene: DashboardScene, isSnapshot = false): DashboardV2Spec { const oldDash = scene.state; const timeRange = oldDash.$timeRange!.state; diff --git a/public/app/features/dashboard-scene/settings/JsonModelEditView.tsx b/public/app/features/dashboard-scene/settings/JsonModelEditView.tsx index 30db2382027..c1a4b3edeea 100644 --- a/public/app/features/dashboard-scene/settings/JsonModelEditView.tsx +++ b/public/app/features/dashboard-scene/settings/JsonModelEditView.tsx @@ -90,9 +90,10 @@ export class JsonModelEditView extends SceneObjectBase i const { jsonText } = model.useState(); const onSave = async (overwrite: boolean) => { - const result = await onSaveDashboard(dashboard, JSON.parse(model.state.jsonText), { + const result = await onSaveDashboard(dashboard, { folderUid: dashboard.state.meta.folderUid, overwrite, + rawDashboardJSON: JSON.parse(model.state.jsonText), }); setIsSaving(true); diff --git a/public/app/features/dashboard/components/SaveDashboard/types.ts b/public/app/features/dashboard/components/SaveDashboard/types.ts index 2c2482dd683..204e9702852 100644 --- a/public/app/features/dashboard/components/SaveDashboard/types.ts +++ b/public/app/features/dashboard/components/SaveDashboard/types.ts @@ -1,4 +1,5 @@ import { Dashboard } from '@grafana/schema'; +import { DashboardV2Spec } from '@grafana/schema/dist/esm/schema/dashboard/v2alpha0/dashboard.gen'; import { ObjectMeta } from 'app/features/apiserver/types'; import { CloneOptions, DashboardModel } from 'app/features/dashboard/state/DashboardModel'; import { Diffs } from 'app/features/dashboard-scene/settings/version-history/utils'; @@ -18,8 +19,16 @@ export interface SaveDashboardOptions extends CloneOptions { makeEditable?: boolean; } +export interface SaveDashboardAsOptions { + saveAsCopy?: boolean; + isNew?: boolean; + copyTags?: boolean; + title?: string; + description?: string; +} + export interface SaveDashboardCommand { - dashboard: Dashboard; + dashboard: Dashboard | DashboardV2Spec; message?: string; folderUid?: string; overwrite?: boolean; diff --git a/public/app/features/dashboard/utils/tracking.ts b/public/app/features/dashboard/utils/tracking.ts index a9cca265776..847d0600c03 100644 --- a/public/app/features/dashboard/utils/tracking.ts +++ b/public/app/features/dashboard/utils/tracking.ts @@ -7,9 +7,9 @@ import { PanelModel } from '../state/PanelModel'; export function trackDashboardLoaded(dashboard: DashboardModel, duration?: number, versionBeforeMigration?: number) { // Count the different types of variables - const variables = getVariables(dashboard.templating.list); + const variables = getV1SchemaVariables(dashboard.templating.list); // Count the different types of panels - const panels = getPanelCounts(dashboard.panels); + const panels = getV1SchemaPanelCounts(dashboard.panels); DashboardInteractions.dashboardInitialized({ uid: dashboard.uid, @@ -28,28 +28,17 @@ export function trackDashboardLoaded(dashboard: DashboardModel, duration?: numbe } export function trackDashboardSceneLoaded(dashboard: DashboardScene, duration?: number) { - const initialSaveModel = dashboard.getInitialSaveModel(); - if (initialSaveModel) { - const panels = getPanelCounts(initialSaveModel.panels || []); - const variables = getVariables(initialSaveModel.templating?.list || []); - DashboardInteractions.dashboardInitialized({ - uid: initialSaveModel.uid, - title: initialSaveModel.title, - theme: undefined, - schemaVersion: initialSaveModel.schemaVersion, - version_before_migration: initialSaveModel.version, - panels_count: initialSaveModel.panels?.length || 0, - ...panels, - ...variables, - settings_nowdelay: undefined, - settings_livenow: !!initialSaveModel.liveNow, - duration, - isScene: true, - }); - } + const trackingInformation = dashboard.getTrackingInformation(); + + DashboardInteractions.dashboardInitialized({ + theme: undefined, + duration, + isScene: true, + ...trackingInformation, + }); } -function getPanelCounts(panels: Panel[] | PanelModel[]) { +export function getV1SchemaPanelCounts(panels: Panel[] | PanelModel[]) { return panels .map((p) => p.type) .reduce((r: Record, p) => { @@ -58,7 +47,7 @@ function getPanelCounts(panels: Panel[] | PanelModel[]) { }, {}); } -function getVariables(variableList: VariableModel[]) { +export function getV1SchemaVariables(variableList: VariableModel[]) { return variableList .map((v) => v.type) .reduce((r: Record, k) => { @@ -67,5 +56,5 @@ function getVariables(variableList: VariableModel[]) { }, {}); } -const variableName = (type: string) => `variable_type_${type}_count`; +export const variableName = (type: string) => `variable_type_${type}_count`; const panelName = (type: string) => `panel_type_${type}_count`; From 1e0d807572a4f080041d0ce434c4885cd61e4059 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 13 Dec 2024 09:21:06 +0000 Subject: [PATCH 25/28] Update dependency @grafana/plugin-e2e to v1.13.1 (#97851) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- yarn.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/yarn.lock b/yarn.lock index f5dc7e17c99..359a12c4739 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3589,16 +3589,16 @@ __metadata: linkType: soft "@grafana/plugin-e2e@npm:^1.11.0": - version: 1.13.0 - resolution: "@grafana/plugin-e2e@npm:1.13.0" + version: 1.13.1 + resolution: "@grafana/plugin-e2e@npm:1.13.1" dependencies: - "@grafana/e2e-selectors": "npm:^11.5.0-211617" + "@grafana/e2e-selectors": "npm:^11.5.0-212629" semver: "npm:^7.5.4" uuid: "npm:^11.0.2" yaml: "npm:^2.3.4" peerDependencies: "@playwright/test": ^1.41.2 - checksum: 10/d6493935b6364eac389d5db69f245ca8f28656a0b77339d8d2ba89c0a8f7dc4c6a4c7fff62f2de8e9789e1319fe5991f131e0b864cfcf1385ddb04956efb5855 + checksum: 10/22916f41828c1fd446073f7c9fa8b6c4517fc7dd4bfa8428cde3d8a5b99aac9ef08e73815b5c885001114a398d1be43c41948875430626e37c2be471d9999034 languageName: node linkType: hard From 78af0fb53318db96eb0fcfcbdceb741080e0f0dd Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 13 Dec 2024 09:22:27 +0000 Subject: [PATCH 26/28] Update dependency @types/react-color to v3.0.13 (#97878) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- packages/grafana-ui/package.json | 2 +- yarn.lock | 40 +++++++------------------------- 2 files changed, 9 insertions(+), 33 deletions(-) diff --git a/packages/grafana-ui/package.json b/packages/grafana-ui/package.json index bbf9e7513d2..7c0ad7b5d72 100644 --- a/packages/grafana-ui/package.json +++ b/packages/grafana-ui/package.json @@ -147,7 +147,7 @@ "@types/node": "22.10.2", "@types/prismjs": "1.26.5", "@types/react": "18.3.3", - "@types/react-color": "3.0.12", + "@types/react-color": "3.0.13", "@types/react-dom": "18.2.25", "@types/react-highlight-words": "0.20.0", "@types/react-test-renderer": "18.3.0", diff --git a/yarn.lock b/yarn.lock index 359a12c4739..fa50eb71ad0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3959,7 +3959,7 @@ __metadata: "@types/node": "npm:22.10.2" "@types/prismjs": "npm:1.26.5" "@types/react": "npm:18.3.3" - "@types/react-color": "npm:3.0.12" + "@types/react-color": "npm:3.0.13" "@types/react-dom": "npm:18.2.25" "@types/react-highlight-words": "npm:0.20.0" "@types/react-table": "npm:7.7.20" @@ -9511,16 +9511,7 @@ __metadata: languageName: node linkType: hard -"@types/node@npm:*, @types/node@npm:>=13.7.0, @types/node@npm:^22.0.0": - version: 22.9.0 - resolution: "@types/node@npm:22.9.0" - dependencies: - undici-types: "npm:~6.19.8" - checksum: 10/a7df3426891868b0f5fb03e46aeddd8446178233521c624a44531c92a040cf08a82d8235f7e1e02af731fd16984665d4d71f3418caf9c2788313b10f040d615d - languageName: node - linkType: hard - -"@types/node@npm:22.10.2": +"@types/node@npm:*, @types/node@npm:22.10.2, @types/node@npm:>=10.0.0, @types/node@npm:>=13.7.0, @types/node@npm:>=13.7.4, @types/node@npm:^22.0.0": version: 22.10.2 resolution: "@types/node@npm:22.10.2" dependencies: @@ -9529,15 +9520,6 @@ __metadata: languageName: node linkType: hard -"@types/node@npm:>=10.0.0, @types/node@npm:>=13.7.4": - version: 22.10.1 - resolution: "@types/node@npm:22.10.1" - dependencies: - undici-types: "npm:~6.20.0" - checksum: 10/c802a526da2f3fa3ccefd00a71244e7cb825329951719e79e8fec62b1dbc2855388c830489770611584665ce10be23c05ed585982038b24924e1ba2c2cce03fd - languageName: node - linkType: hard - "@types/nodemailer@npm:*": version: 6.4.15 resolution: "@types/nodemailer@npm:6.4.15" @@ -9644,13 +9626,14 @@ __metadata: languageName: node linkType: hard -"@types/react-color@npm:3.0.12": - version: 3.0.12 - resolution: "@types/react-color@npm:3.0.12" +"@types/react-color@npm:3.0.13": + version: 3.0.13 + resolution: "@types/react-color@npm:3.0.13" dependencies: - "@types/react": "npm:*" "@types/reactcss": "npm:*" - checksum: 10/d8ed71d297d026faded787588720ee5e158edfa45ee23c5e517d8bb1441ccaa25f8a0c88069fd63829f7263bbbdc78a7ae2df5957061847a2d2c6351c46c13dd + peerDependencies: + "@types/react": "*" + checksum: 10/2e1500c915d4ae3916935993320e77c13d687bca8833525d011024e355c1ccfc6c4091fb4563ae1f92c591494c3a9fa3acb1e58a60a5d967ded87600f94e5d8b languageName: node linkType: hard @@ -29808,13 +29791,6 @@ __metadata: languageName: node linkType: hard -"undici-types@npm:~6.19.8": - version: 6.19.8 - resolution: "undici-types@npm:6.19.8" - checksum: 10/cf0b48ed4fc99baf56584afa91aaffa5010c268b8842f62e02f752df209e3dea138b372a60a963b3b2576ed932f32329ce7ddb9cb5f27a6c83040d8cd74b7a70 - languageName: node - linkType: hard - "undici-types@npm:~6.20.0": version: 6.20.0 resolution: "undici-types@npm:6.20.0" From 69ca52fcb3ad0c51342466d778df89e65ae4181c Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 13 Dec 2024 09:24:29 +0000 Subject: [PATCH 27/28] Update dependency @types/react-transition-group to v4.4.12 (#97879) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package.json | 2 +- packages/grafana-ui/package.json | 2 +- yarn.lock | 16 ++++++++-------- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/package.json b/package.json index e6ea47bfca8..a8e4fb1313b 100644 --- a/package.json +++ b/package.json @@ -139,7 +139,7 @@ "@types/react-router-dom": "5.3.3", "@types/react-table": "7.7.20", "@types/react-test-renderer": "18.3.0", - "@types/react-transition-group": "4.4.11", + "@types/react-transition-group": "4.4.12", "@types/react-virtualized-auto-sizer": "1.0.4", "@types/react-window": "1.8.8", "@types/react-window-infinite-loader": "^1", diff --git a/packages/grafana-ui/package.json b/packages/grafana-ui/package.json index 7c0ad7b5d72..6011195fcb2 100644 --- a/packages/grafana-ui/package.json +++ b/packages/grafana-ui/package.json @@ -151,7 +151,7 @@ "@types/react-dom": "18.2.25", "@types/react-highlight-words": "0.20.0", "@types/react-test-renderer": "18.3.0", - "@types/react-transition-group": "4.4.11", + "@types/react-transition-group": "4.4.12", "@types/react-window": "1.8.8", "@types/slate": "0.47.11", "@types/slate-plain-serializer": "0.7.5", diff --git a/yarn.lock b/yarn.lock index fa50eb71ad0..0ac671144ba 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3964,7 +3964,7 @@ __metadata: "@types/react-highlight-words": "npm:0.20.0" "@types/react-table": "npm:7.7.20" "@types/react-test-renderer": "npm:18.3.0" - "@types/react-transition-group": "npm:4.4.11" + "@types/react-transition-group": "npm:4.4.12" "@types/react-window": "npm:1.8.8" "@types/slate": "npm:0.47.11" "@types/slate-plain-serializer": "npm:0.7.5" @@ -9712,12 +9712,12 @@ __metadata: languageName: node linkType: hard -"@types/react-transition-group@npm:4.4.11, @types/react-transition-group@npm:^4.4.0": - version: 4.4.11 - resolution: "@types/react-transition-group@npm:4.4.11" - dependencies: - "@types/react": "npm:*" - checksum: 10/a7f4de6e5f57d9fcdea027e22873c633f96a803c96d422db8b99a45c36a9cceb7882d152136bbc31c7158fc1827e37aea5070d369724bb71dd11b5687332bc4d +"@types/react-transition-group@npm:4.4.12, @types/react-transition-group@npm:^4.4.0": + version: 4.4.12 + resolution: "@types/react-transition-group@npm:4.4.12" + peerDependencies: + "@types/react": "*" + checksum: 10/ea14bc84f529a3887f9954b753843820ac8a3c49fcdfec7840657ecc6a8800aad98afdbe4b973eb96c7252286bde38476fcf64b1c09527354a9a9366e516d9a2 languageName: node linkType: hard @@ -17643,7 +17643,7 @@ __metadata: "@types/react-router-dom": "npm:5.3.3" "@types/react-table": "npm:7.7.20" "@types/react-test-renderer": "npm:18.3.0" - "@types/react-transition-group": "npm:4.4.11" + "@types/react-transition-group": "npm:4.4.12" "@types/react-virtualized-auto-sizer": "npm:1.0.4" "@types/react-window": "npm:1.8.8" "@types/react-window-infinite-loader": "npm:^1" From fc7805957ef3556c0cb7254f72bdc72b67f2b9cd Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 13 Dec 2024 09:25:55 +0000 Subject: [PATCH 28/28] Update dependency @faker-js/faker to v9.3.0 (#97883) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 0ac671144ba..a3045d4ab9f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2601,9 +2601,9 @@ __metadata: linkType: hard "@faker-js/faker@npm:^9.0.0": - version: 9.2.0 - resolution: "@faker-js/faker@npm:9.2.0" - checksum: 10/c233aaa41ebc4f4bed36c1304c399c424b9e5abca9cb8fe54ce73d4162ec7acd66f294b90c85ed7a5df774400e44f0a10364bdd41f49e72cb519a48b5948939f + version: 9.3.0 + resolution: "@faker-js/faker@npm:9.3.0" + checksum: 10/fa0718766dc46dea06602b1acdccdf3714e5a076f02eab6dbe26d7dc421ccc43a2e7e08767f7832de963fa2a273c2d9e5d4a2a504247ea5fba119a713efc145c languageName: node linkType: hard