diff --git a/.betterer.results b/.betterer.results index 81d2c506962..96b2c33454a 100644 --- a/.betterer.results +++ b/.betterer.results @@ -44,9 +44,6 @@ exports[`better eslint`] = { [0, 0, 0, "Do not use any type assertions.", "3"], [0, 0, 0, "Do not use any type assertions.", "4"] ], - "packages/grafana-data/src/dataframe/dimensions.ts:5381": [ - [0, 0, 0, "Unexpected any. Specify a different type.", "0"] - ], "packages/grafana-data/src/dataframe/processDataFrame.test.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"] ], @@ -73,9 +70,7 @@ exports[`better eslint`] = { [0, 0, 0, "Do not use any type assertions.", "19"] ], "packages/grafana-data/src/datetime/datemath.ts:5381": [ - [0, 0, 0, "Unexpected any. Specify a different type.", "0"], - [0, 0, 0, "Do not use any type assertions.", "1"], - [0, 0, 0, "Unexpected any. Specify a different type.", "2"] + [0, 0, 0, "Do not use any type assertions.", "0"] ], "packages/grafana-data/src/datetime/durationutil.ts:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"] @@ -91,32 +86,22 @@ exports[`better eslint`] = { [0, 0, 0, "Do not use any type assertions.", "7"], [0, 0, 0, "Do not use any type assertions.", "8"] ], - "packages/grafana-data/src/events/EventBus.ts:5381": [ - [0, 0, 0, "Unexpected any. Specify a different type.", "0"], - [0, 0, 0, "Unexpected any. Specify a different type.", "1"] - ], "packages/grafana-data/src/events/types.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"], [0, 0, 0, "Unexpected any. Specify a different type.", "1"], - [0, 0, 0, "Unexpected any. Specify a different type.", "2"], - [0, 0, 0, "Unexpected any. Specify a different type.", "3"], - [0, 0, 0, "Unexpected any. Specify a different type.", "4"] + [0, 0, 0, "Unexpected any. Specify a different type.", "2"] ], "packages/grafana-data/src/field/displayProcessor.ts:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"], [0, 0, 0, "Do not use any type assertions.", "1"], [0, 0, 0, "Do not use any type assertions.", "2"] ], - "packages/grafana-data/src/field/fieldState.ts:5381": [ - [0, 0, 0, "Unexpected any. Specify a different type.", "0"] - ], "packages/grafana-data/src/field/overrides/processors.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"], [0, 0, 0, "Unexpected any. Specify a different type.", "1"], [0, 0, 0, "Unexpected any. Specify a different type.", "2"], [0, 0, 0, "Unexpected any. Specify a different type.", "3"], - [0, 0, 0, "Unexpected any. Specify a different type.", "4"], - [0, 0, 0, "Unexpected any. Specify a different type.", "5"] + [0, 0, 0, "Unexpected any. Specify a different type.", "4"] ], "packages/grafana-data/src/field/standardFieldConfigEditorRegistry.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"], @@ -128,8 +113,7 @@ exports[`better eslint`] = { [0, 0, 0, "Unexpected any. Specify a different type.", "6"] ], "packages/grafana-data/src/geo/layer.ts:5381": [ - [0, 0, 0, "Unexpected any. Specify a different type.", "0"], - [0, 0, 0, "Unexpected any. Specify a different type.", "1"] + [0, 0, 0, "Unexpected any. Specify a different type.", "0"] ], "packages/grafana-data/src/panel/PanelPlugin.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"], @@ -159,9 +143,6 @@ exports[`better eslint`] = { "packages/grafana-data/src/themes/createColors.ts:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"] ], - "packages/grafana-data/src/transformations/fieldReducer.ts:5381": [ - [0, 0, 0, "Do not use any type assertions.", "0"] - ], "packages/grafana-data/src/transformations/matchers/valueMatchers/types.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"], [0, 0, 0, "Unexpected any. Specify a different type.", "1"] @@ -172,9 +153,6 @@ exports[`better eslint`] = { "packages/grafana-data/src/transformations/transformDataFrame.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"] ], - "packages/grafana-data/src/transformations/transformers/joinDataFrames.ts:5381": [ - [0, 0, 0, "Unexpected any. Specify a different type.", "0"] - ], "packages/grafana-data/src/transformations/transformers/nulls/nullInsertThreshold.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"], [0, 0, 0, "Unexpected any. Specify a different type.", "1"] @@ -196,6 +174,11 @@ exports[`better eslint`] = { [0, 0, 0, "Unexpected any. Specify a different type.", "0"], [0, 0, 0, "Unexpected any. Specify a different type.", "1"] ], + "packages/grafana-data/src/types/action.ts:5381": [ + [0, 0, 0, "Unexpected any. Specify a different type.", "0"], + [0, 0, 0, "Unexpected any. Specify a different type.", "1"], + [0, 0, 0, "Unexpected any. Specify a different type.", "2"] + ], "packages/grafana-data/src/types/annotations.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"], [0, 0, 0, "Unexpected any. Specify a different type.", "1"], @@ -590,6 +573,10 @@ exports[`better eslint`] = { [0, 0, 0, "No untranslated strings. Wrap text with ", "0"], [0, 0, 0, "No untranslated strings. Wrap text with ", "1"] ], + "packages/grafana-ui/src/components/Combobox/Combobox.tsx:5381": [ + [0, 0, 0, "Do not use any type assertions.", "0"], + [0, 0, 0, "Do not use any type assertions.", "1"] + ], "packages/grafana-ui/src/components/ConfirmButton/ConfirmButton.tsx:5381": [ [0, 0, 0, "No untranslated strings. Wrap text with ", "0"] ], @@ -950,48 +937,6 @@ exports[`better eslint`] = { [0, 0, 0, "Do not use any type assertions.", "0"], [0, 0, 0, "Unexpected any. Specify a different type.", "1"] ], - "packages/grafana-ui/src/graveyard/Graph/Graph.tsx:5381": [ - [0, 0, 0, "No untranslated strings. Wrap text with ", "0"] - ], - "packages/grafana-ui/src/graveyard/Graph/GraphContextMenu.tsx:5381": [ - [0, 0, 0, "Unexpected any. Specify a different type.", "0"] - ], - "packages/grafana-ui/src/graveyard/Graph/utils.ts:5381": [ - [0, 0, 0, "Unexpected any. Specify a different type.", "0"] - ], - "packages/grafana-ui/src/graveyard/GraphNG/GraphNG.tsx:5381": [ - [0, 0, 0, "Unexpected any. Specify a different type.", "0"], - [0, 0, 0, "Unexpected any. Specify a different type.", "1"], - [0, 0, 0, "Unexpected any. Specify a different type.", "2"], - [0, 0, 0, "Unexpected any. Specify a different type.", "3"], - [0, 0, 0, "Unexpected any. Specify a different type.", "4"], - [0, 0, 0, "Do not use any type assertions.", "5"], - [0, 0, 0, "Do not use any type assertions.", "6"], - [0, 0, 0, "Do not use any type assertions.", "7"], - [0, 0, 0, "Unexpected any. Specify a different type.", "8"], - [0, 0, 0, "Do not use any type assertions.", "9"], - [0, 0, 0, "Do not use any type assertions.", "10"], - [0, 0, 0, "Do not use any type assertions.", "11"] - ], - "packages/grafana-ui/src/graveyard/GraphNG/hooks.ts:5381": [ - [0, 0, 0, "Do not use any type assertions.", "0"] - ], - "packages/grafana-ui/src/graveyard/GraphNG/nullInsertThreshold.ts:5381": [ - [0, 0, 0, "Do not use any type assertions.", "0"], - [0, 0, 0, "Unexpected any. Specify a different type.", "1"], - [0, 0, 0, "Unexpected any. Specify a different type.", "2"], - [0, 0, 0, "Unexpected any. Specify a different type.", "3"] - ], - "packages/grafana-ui/src/graveyard/GraphNG/nullToUndefThreshold.ts:5381": [ - [0, 0, 0, "Unexpected any. Specify a different type.", "0"], - [0, 0, 0, "Unexpected any. Specify a different type.", "1"], - [0, 0, 0, "Do not use any type assertions.", "2"] - ], - "packages/grafana-ui/src/graveyard/TimeSeries/utils.ts:5381": [ - [0, 0, 0, "Unexpected any. Specify a different type.", "0"], - [0, 0, 0, "Do not use any type assertions.", "1"], - [0, 0, 0, "Unexpected any. Specify a different type.", "2"] - ], "packages/grafana-ui/src/options/builder/hideSeries.tsx:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"] ], @@ -1216,10 +1161,7 @@ exports[`better eslint`] = { ], "public/app/core/components/TagFilter/TagFilter.tsx:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"], - [0, 0, 0, "Unexpected any. Specify a different type.", "1"], - [0, 0, 0, "Unexpected any. Specify a different type.", "2"], - [0, 0, 0, "Unexpected any. Specify a different type.", "3"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "4"] + [0, 0, 0, "No untranslated strings. Wrap text with ", "1"] ], "public/app/core/components/TimeSeries/utils.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"], @@ -1372,6 +1314,17 @@ exports[`better eslint`] = { [0, 0, 0, "Do not use any type assertions.", "0"], [0, 0, 0, "Do not use any type assertions.", "1"] ], + "public/app/features/actions/ActionEditorModalContent.tsx:5381": [ + [0, 0, 0, "No untranslated strings. Wrap text with ", "0"], + [0, 0, 0, "No untranslated strings. Wrap text with ", "1"] + ], + "public/app/features/actions/ActionsInlineEditor.tsx:5381": [ + [0, 0, 0, "No untranslated strings. Wrap text with ", "0"], + [0, 0, 0, "No untranslated strings. Wrap text with ", "1"] + ], + "public/app/features/actions/ParamsEditor.tsx:5381": [ + [0, 0, 0, "Do not use any type assertions.", "0"] + ], "public/app/features/admin/AdminEditOrgPage.tsx:5381": [ [0, 0, 0, "No untranslated strings. Wrap text with ", "0"], [0, 0, 0, "No untranslated strings. Wrap text with ", "1"], @@ -1863,10 +1816,9 @@ exports[`better eslint`] = { [0, 0, 0, "No untranslated strings. Wrap text with ", "2"] ], "public/app/features/alerting/unified/components/receivers/form/ChannelOptions.tsx:5381": [ - [0, 0, 0, "No untranslated strings. Wrap text with ", "0"], - [0, 0, 0, "Unexpected any. Specify a different type.", "1"], - [0, 0, 0, "Do not use any type assertions.", "2"], - [0, 0, 0, "Unexpected any. Specify a different type.", "3"] + [0, 0, 0, "Unexpected any. Specify a different type.", "0"], + [0, 0, 0, "Do not use any type assertions.", "1"], + [0, 0, 0, "Unexpected any. Specify a different type.", "2"] ], "public/app/features/alerting/unified/components/receivers/form/ChannelSubForm.tsx:5381": [ [0, 0, 0, "No untranslated strings. Wrap text with ", "0"], @@ -2232,8 +2184,7 @@ exports[`better eslint`] = { "public/app/features/alerting/unified/components/rules/CloudRules.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"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "3"] + [0, 0, 0, "No untranslated strings. Wrap text with ", "2"] ], "public/app/features/alerting/unified/components/rules/EditRuleGroupModal.tsx:5381": [ [0, 0, 0, "No untranslated strings. Wrap text with ", "0"], @@ -2243,6 +2194,22 @@ exports[`better eslint`] = { [0, 0, 0, "No untranslated strings. Wrap text with ", "4"], [0, 0, 0, "No untranslated strings. Wrap text with ", "5"] ], + "public/app/features/alerting/unified/components/rules/Filter/RulesFilter.v1.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"], + [0, 0, 0, "No untranslated strings. Wrap text with ", "3"], + [0, 0, 0, "No untranslated strings. Wrap text with ", "4"], + [0, 0, 0, "No untranslated strings. Wrap text with ", "5"], + [0, 0, 0, "No untranslated strings. Wrap text with ", "6"], + [0, 0, 0, "No untranslated strings. Wrap text with ", "7"], + [0, 0, 0, "No untranslated strings. Wrap text with ", "8"], + [0, 0, 0, "No untranslated strings. Wrap text with ", "9"], + [0, 0, 0, "No untranslated strings. Wrap text with ", "10"], + [0, 0, 0, "No untranslated strings. Wrap text with ", "11"], + [0, 0, 0, "No untranslated strings. Wrap text with ", "12"], + [0, 0, 0, "No untranslated strings. Wrap text with ", "13"] + ], "public/app/features/alerting/unified/components/rules/GrafanaRules.tsx:5381": [ [0, 0, 0, "No untranslated strings. Wrap text with ", "0"] ], @@ -2295,22 +2262,6 @@ exports[`better eslint`] = { "public/app/features/alerting/unified/components/rules/RuleStats.tsx:5381": [ [0, 0, 0, "No untranslated strings. Wrap text with ", "0"] ], - "public/app/features/alerting/unified/components/rules/RulesFilter.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"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "3"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "4"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "5"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "6"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "7"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "8"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "9"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "10"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "11"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "12"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "13"] - ], "public/app/features/alerting/unified/components/rules/RulesGroup.tsx:5381": [ [0, 0, 0, "No untranslated strings. Wrap text with ", "0"], [0, 0, 0, "No untranslated strings. Wrap text with ", "1"], @@ -2766,10 +2717,6 @@ exports[`better eslint`] = { [0, 0, 0, "No untranslated strings. Wrap text with ", "2"], [0, 0, 0, "No untranslated strings. Wrap text with ", "3"] ], - "public/app/features/dashboard-scene/saving/DashboardSceneChangeTracker.test.ts:5381": [ - [0, 0, 0, "Unexpected any. Specify a different type.", "0"], - [0, 0, 0, "Unexpected any. Specify a different type.", "1"] - ], "public/app/features/dashboard-scene/saving/SaveDashboardAsForm.tsx:5381": [ [0, 0, 0, "No untranslated strings. Wrap text with ", "0"], [0, 0, 0, "No untranslated strings. Wrap text with ", "1"], @@ -2806,9 +2753,6 @@ exports[`better eslint`] = { "public/app/features/dashboard-scene/scene/DashboardControls.tsx:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"] ], - "public/app/features/dashboard-scene/scene/NavToolbarActions.test.tsx:5381": [ - [0, 0, 0, "Unexpected any. Specify a different type.", "0"] - ], "public/app/features/dashboard-scene/scene/NavToolbarActions.tsx:5381": [ [0, 0, 0, "No untranslated strings. Wrap text with ", "0"], [0, 0, 0, "No untranslated strings. Wrap text with ", "1"], @@ -2827,9 +2771,6 @@ exports[`better eslint`] = { "public/app/features/dashboard-scene/scene/PanelMenuBehavior.tsx:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"] ], - "public/app/features/dashboard-scene/scene/RowRepeaterBehavior.ts:5381": [ - [0, 0, 0, "Do not use any type assertions.", "0"] - ], "public/app/features/dashboard-scene/scene/row-actions/RowActions.tsx:5381": [ [0, 0, 0, "No untranslated strings. Wrap text with ", "0"], [0, 0, 0, "No untranslated strings. Wrap text with ", "1"], @@ -2845,28 +2786,10 @@ exports[`better eslint`] = { "public/app/features/dashboard-scene/serialization/buildNewDashboardSaveModel.ts:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"] ], - "public/app/features/dashboard-scene/serialization/transformSaveModelToScene.test.ts:5381": [ - [0, 0, 0, "Unexpected any. Specify a different type.", "0"], - [0, 0, 0, "Unexpected any. Specify a different type.", "1"], - [0, 0, 0, "Unexpected any. Specify a different type.", "2"], - [0, 0, 0, "Unexpected any. Specify a different type.", "3"] - ], "public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.test.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"], [0, 0, 0, "Unexpected any. Specify a different type.", "1"], - [0, 0, 0, "Unexpected any. Specify a different type.", "2"], - [0, 0, 0, "Unexpected any. Specify a different type.", "3"], - [0, 0, 0, "Unexpected any. Specify a different type.", "4"], - [0, 0, 0, "Unexpected any. Specify a different type.", "5"], - [0, 0, 0, "Unexpected any. Specify a different type.", "6"], - [0, 0, 0, "Unexpected any. Specify a different type.", "7"], - [0, 0, 0, "Unexpected any. Specify a different type.", "8"], - [0, 0, 0, "Unexpected any. Specify a different type.", "9"], - [0, 0, 0, "Unexpected any. Specify a different type.", "10"], - [0, 0, 0, "Unexpected any. Specify a different type.", "11"], - [0, 0, 0, "Unexpected any. Specify a different type.", "12"], - [0, 0, 0, "Unexpected any. Specify a different type.", "13"], - [0, 0, 0, "Unexpected any. Specify a different type.", "14"] + [0, 0, 0, "Unexpected any. Specify a different type.", "2"] ], "public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.ts:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"], @@ -3178,16 +3101,14 @@ exports[`better eslint`] = { ], "public/app/features/dashboard/components/PanelEditor/getVisualizationOptions.tsx:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"], - [0, 0, 0, "Unexpected any. Specify a different type.", "1"], - [0, 0, 0, "Unexpected any. Specify a different type.", "2"] + [0, 0, 0, "Unexpected any. Specify a different type.", "1"] ], "public/app/features/dashboard/components/PanelEditor/utils.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"], - [0, 0, 0, "Unexpected any. Specify a different type.", "1"], - [0, 0, 0, "Do not use any type assertions.", "2"], - [0, 0, 0, "Unexpected any. Specify a different type.", "3"], - [0, 0, 0, "Do not use any type assertions.", "4"], - [0, 0, 0, "Unexpected any. Specify a different type.", "5"] + [0, 0, 0, "Do not use any type assertions.", "1"], + [0, 0, 0, "Unexpected any. Specify a different type.", "2"], + [0, 0, 0, "Do not use any type assertions.", "3"], + [0, 0, 0, "Unexpected any. Specify a different type.", "4"] ], "public/app/features/dashboard/components/PublicDashboardNotAvailable/PublicDashboardNotAvailable.tsx:5381": [ [0, 0, 0, "No untranslated strings. Wrap text with ", "0"] @@ -3226,21 +3147,17 @@ exports[`better eslint`] = { [0, 0, 0, "No untranslated strings. Wrap text with ", "1"], [0, 0, 0, "No untranslated strings. Wrap text with ", "2"] ], - "public/app/features/dashboard/components/SaveDashboard/forms/SaveDashboardAsForm.test.tsx:5381": [ - [0, 0, 0, "Unexpected any. Specify a different type.", "0"] - ], "public/app/features/dashboard/components/SaveDashboard/forms/SaveDashboardAsForm.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/dashboard/components/SaveDashboard/forms/SaveDashboardForm.tsx:5381": [ - [0, 0, 0, "Unexpected any. Specify a different type.", "0"], + [0, 0, 0, "Use data-testid for E2E selectors instead of aria-label", "0"], [0, 0, 0, "Use data-testid for E2E selectors instead of aria-label", "1"], - [0, 0, 0, "Use data-testid for E2E selectors instead of aria-label", "2"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "3"], - [0, 0, 0, "Use data-testid for E2E selectors instead of aria-label", "4"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "5"] + [0, 0, 0, "No untranslated strings. Wrap text with ", "2"], + [0, 0, 0, "Use data-testid for E2E selectors instead of aria-label", "3"], + [0, 0, 0, "No untranslated strings. Wrap text with ", "4"] ], "public/app/features/dashboard/components/SaveDashboard/forms/SaveProvisionedDashboardForm.tsx:5381": [ [0, 0, 0, "\'HorizontalGroup\' import from \'@grafana/ui\' is restricted from being used by a pattern. Use Stack component instead.", "0"], @@ -3253,9 +3170,6 @@ exports[`better eslint`] = { [0, 0, 0, "No untranslated strings. Wrap text with ", "7"], [0, 0, 0, "No untranslated strings. Wrap text with ", "8"] ], - "public/app/features/dashboard/components/SaveDashboard/types.ts:5381": [ - [0, 0, 0, "Unexpected any. Specify a different type.", "0"] - ], "public/app/features/dashboard/components/SaveDashboard/useDashboardSave.tsx:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"] ], @@ -3347,9 +3261,6 @@ exports[`better eslint`] = { "public/app/features/dashboard/containers/SoloPanelPage.tsx:5381": [ [0, 0, 0, "No untranslated strings. Wrap text with ", "0"] ], - "public/app/features/dashboard/dashgrid/PanelStateWrapper.tsx:5381": [ - [0, 0, 0, "Unexpected any. Specify a different type.", "0"] - ], "public/app/features/dashboard/dashgrid/SeriesVisibilityConfigFactory.ts:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"] ], @@ -3736,8 +3647,7 @@ exports[`better eslint`] = { [0, 0, 0, "Do not use export all (\`export * from ...\`)", "7"] ], "public/app/features/dimensions/scale.ts:5381": [ - [0, 0, 0, "Do not use any type assertions.", "0"], - [0, 0, 0, "Unexpected any. Specify a different type.", "1"] + [0, 0, 0, "Do not use any type assertions.", "0"] ], "public/app/features/dimensions/types.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"] @@ -4282,9 +4192,6 @@ exports[`better eslint`] = { [0, 0, 0, "Do not re-export imported variable (\`./TNil\`)", "3"], [0, 0, 0, "Do not re-export imported variable (\`./links\`)", "4"] ], - "public/app/features/explore/TraceView/components/types/trace.ts:5381": [ - [0, 0, 0, "Unexpected any. Specify a different type.", "0"] - ], "public/app/features/explore/TraceView/components/utils/DraggableManager/demo/DraggableManagerDemo.tsx:5381": [ [0, 0, 0, "No untranslated strings. Wrap text with ", "0"], [0, 0, 0, "No untranslated strings. Wrap text with ", "1"], @@ -4760,10 +4667,7 @@ exports[`better eslint`] = { [0, 0, 0, "Unexpected any. Specify a different type.", "0"] ], "public/app/features/panel/panellinks/link_srv.ts:5381": [ - [0, 0, 0, "Unexpected any. Specify a different type.", "0"], - [0, 0, 0, "Unexpected any. Specify a different type.", "1"], - [0, 0, 0, "Unexpected any. Specify a different type.", "2"], - [0, 0, 0, "Unexpected any. Specify a different type.", "3"] + [0, 0, 0, "Unexpected any. Specify a different type.", "0"] ], "public/app/features/playlist/PlaylistForm.tsx:5381": [ [0, 0, 0, "Use data-testid for E2E selectors instead of aria-label", "0"], @@ -4956,15 +4860,9 @@ exports[`better eslint`] = { [0, 0, 0, "Unexpected any. Specify a different type.", "1"], [0, 0, 0, "Do not use any type assertions.", "2"], [0, 0, 0, "Unexpected any. Specify a different type.", "3"], - [0, 0, 0, "Unexpected any. Specify a different type.", "4"], + [0, 0, 0, "Do not use any type assertions.", "4"], [0, 0, 0, "Unexpected any. Specify a different type.", "5"], - [0, 0, 0, "Unexpected any. Specify a different type.", "6"], - [0, 0, 0, "Unexpected any. Specify a different type.", "7"], - [0, 0, 0, "Do not use any type assertions.", "8"], - [0, 0, 0, "Unexpected any. Specify a different type.", "9"], - [0, 0, 0, "Do not use any type assertions.", "10"], - [0, 0, 0, "Unexpected any. Specify a different type.", "11"], - [0, 0, 0, "Do not use any type assertions.", "12"] + [0, 0, 0, "Do not use any type assertions.", "6"] ], "public/app/features/plugins/extensions/usePluginComponents.tsx:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"] @@ -5367,13 +5265,13 @@ exports[`better eslint`] = { [0, 0, 0, "Do not use any type assertions.", "10"], [0, 0, 0, "Do not use any type assertions.", "11"] ], - "public/app/features/trails/ActionTabs/AddToFiltersGraphAction.tsx:5381": [ + "public/app/features/trails/Breakdown/AddToFiltersGraphAction.tsx:5381": [ [0, 0, 0, "No untranslated strings. Wrap text with ", "0"] ], - "public/app/features/trails/ActionTabs/BreakdownScene.tsx:5381": [ - [0, 0, 0, "No untranslated strings. Wrap text with ", "0"] + "public/app/features/trails/Breakdown/types.ts:5381": [ + [0, 0, 0, "Do not use any type assertions.", "0"] ], - "public/app/features/trails/ActionTabs/utils.ts:5381": [ + "public/app/features/trails/Breakdown/utils.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"] ], "public/app/features/trails/DataTrailCard.tsx:5381": [ @@ -5381,8 +5279,7 @@ exports[`better eslint`] = { [0, 0, 0, "No untranslated strings. Wrap text with ", "1"] ], "public/app/features/trails/DataTrailSettings.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 ", "0"] ], "public/app/features/trails/DataTrailsHistory.tsx:5381": [ [0, 0, 0, "No untranslated strings. Wrap text with ", "0"] @@ -5598,6 +5495,12 @@ exports[`better eslint`] = { "public/app/features/transformers/standardTransformers.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"] ], + "public/app/features/transformers/suggestionsInput/SuggestionsInput.tsx:5381": [ + [0, 0, 0, "Do not use any type assertions.", "0"], + [0, 0, 0, "Do not use any type assertions.", "1"], + [0, 0, 0, "Do not use any type assertions.", "2"], + [0, 0, 0, "Do not use any type assertions.", "3"] + ], "public/app/features/users/TokenRevokedModal.tsx:5381": [ [0, 0, 0, "No untranslated strings. Wrap text with ", "0"], [0, 0, 0, "No untranslated strings. Wrap text with ", "1"], @@ -6235,8 +6138,7 @@ exports[`better eslint`] = { ], "public/app/plugins/datasource/grafana-testdata-datasource/components/SimulationSchemaForm.tsx:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"], - [0, 0, 0, "Unexpected any. Specify a different type.", "1"], - [0, 0, 0, "Styles should be written using objects.", "2"] + [0, 0, 0, "Styles should be written using objects.", "1"] ], "public/app/plugins/datasource/grafana-testdata-datasource/components/index.ts:5381": [ [0, 0, 0, "Do not re-export imported variable (\`./StreamingClientEditor\`)", "0"], @@ -6246,9 +6148,6 @@ exports[`better eslint`] = { [0, 0, 0, "Do not use any type assertions.", "0"], [0, 0, 0, "Unexpected any. Specify a different type.", "1"] ], - "public/app/plugins/datasource/grafana-testdata-datasource/runStreams.ts:5381": [ - [0, 0, 0, "Unexpected any. Specify a different type.", "0"] - ], "public/app/plugins/datasource/grafana-testdata-datasource/webpack.config.ts:5381": [ [0, 0, 0, "Do not re-export imported variable (\`config\`)", "0"] ], @@ -6297,7 +6196,7 @@ exports[`better eslint`] = { [0, 0, 0, "Styles should be written using objects.", "2"] ], "public/app/plugins/datasource/graphite/components/MetricTankMetaInspector.tsx:5381": [ - [0, 0, 0, "Do not use any type assertions.", "0"], + [0, 0, 0, "Styles should be written using objects.", "0"], [0, 0, 0, "Styles should be written using objects.", "1"], [0, 0, 0, "Styles should be written using objects.", "2"], [0, 0, 0, "Styles should be written using objects.", "3"], @@ -6306,8 +6205,7 @@ exports[`better eslint`] = { [0, 0, 0, "Styles should be written using objects.", "6"], [0, 0, 0, "Styles should be written using objects.", "7"], [0, 0, 0, "Styles should be written using objects.", "8"], - [0, 0, 0, "Styles should be written using objects.", "9"], - [0, 0, 0, "Styles should be written using objects.", "10"] + [0, 0, 0, "Styles should be written using objects.", "9"] ], "public/app/plugins/datasource/graphite/components/TagsSection.tsx:5381": [ [0, 0, 0, "Styles should be written using objects.", "0"] @@ -6350,8 +6248,7 @@ exports[`better eslint`] = { [0, 0, 0, "Unexpected any. Specify a different type.", "26"], [0, 0, 0, "Unexpected any. Specify a different type.", "27"], [0, 0, 0, "Unexpected any. Specify a different type.", "28"], - [0, 0, 0, "Unexpected any. Specify a different type.", "29"], - [0, 0, 0, "Unexpected any. Specify a different type.", "30"] + [0, 0, 0, "Unexpected any. Specify a different type.", "29"] ], "public/app/plugins/datasource/graphite/gfunc.ts:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"], @@ -6374,14 +6271,11 @@ exports[`better eslint`] = { [0, 0, 0, "Unexpected any. Specify a different type.", "9"], [0, 0, 0, "Unexpected any. Specify a different type.", "10"], [0, 0, 0, "Unexpected any. Specify a different type.", "11"], - [0, 0, 0, "Unexpected any. Specify a different type.", "12"], + [0, 0, 0, "Do not use any type assertions.", "12"], [0, 0, 0, "Unexpected any. Specify a different type.", "13"], [0, 0, 0, "Unexpected any. Specify a different type.", "14"], - [0, 0, 0, "Do not use any type assertions.", "15"], - [0, 0, 0, "Unexpected any. Specify a different type.", "16"], - [0, 0, 0, "Unexpected any. Specify a different type.", "17"], - [0, 0, 0, "Unexpected any. Specify a different type.", "18"], - [0, 0, 0, "Unexpected any. Specify a different type.", "19"] + [0, 0, 0, "Unexpected any. Specify a different type.", "15"], + [0, 0, 0, "Unexpected any. Specify a different type.", "16"] ], "public/app/plugins/datasource/graphite/lexer.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"], @@ -6450,10 +6344,7 @@ exports[`better eslint`] = { ], "public/app/plugins/datasource/influxdb/influx_query_model.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"], - [0, 0, 0, "Unexpected any. Specify a different type.", "1"], - [0, 0, 0, "Unexpected any. Specify a different type.", "2"], - [0, 0, 0, "Unexpected any. Specify a different type.", "3"], - [0, 0, 0, "Unexpected any. Specify a different type.", "4"] + [0, 0, 0, "Unexpected any. Specify a different type.", "1"] ], "public/app/plugins/datasource/influxdb/influx_series.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"], @@ -6467,17 +6358,7 @@ exports[`better eslint`] = { [0, 0, 0, "Unexpected any. Specify a different type.", "8"], [0, 0, 0, "Unexpected any. Specify a different type.", "9"], [0, 0, 0, "Unexpected any. Specify a different type.", "10"], - [0, 0, 0, "Unexpected any. Specify a different type.", "11"], - [0, 0, 0, "Unexpected any. Specify a different type.", "12"], - [0, 0, 0, "Unexpected any. Specify a different type.", "13"], - [0, 0, 0, "Unexpected any. Specify a different type.", "14"], - [0, 0, 0, "Unexpected any. Specify a different type.", "15"], - [0, 0, 0, "Unexpected any. Specify a different type.", "16"], - [0, 0, 0, "Unexpected any. Specify a different type.", "17"], - [0, 0, 0, "Unexpected any. Specify a different type.", "18"] - ], - "public/app/plugins/datasource/influxdb/migrations.ts:5381": [ - [0, 0, 0, "Unexpected any. Specify a different type.", "0"] + [0, 0, 0, "Unexpected any. Specify a different type.", "11"] ], "public/app/plugins/datasource/influxdb/query_part.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"], @@ -6493,8 +6374,7 @@ exports[`better eslint`] = { [0, 0, 0, "Unexpected any. Specify a different type.", "10"], [0, 0, 0, "Unexpected any. Specify a different type.", "11"], [0, 0, 0, "Unexpected any. Specify a different type.", "12"], - [0, 0, 0, "Unexpected any. Specify a different type.", "13"], - [0, 0, 0, "Unexpected any. Specify a different type.", "14"] + [0, 0, 0, "Unexpected any. Specify a different type.", "13"] ], "public/app/plugins/datasource/influxdb/response_parser.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"] @@ -6509,9 +6389,6 @@ exports[`better eslint`] = { "public/app/plugins/datasource/jaeger/_importedDependencies/types/index.tsx:5381": [ [0, 0, 0, "Do not re-export imported variable (\`./trace\`)", "0"] ], - "public/app/plugins/datasource/jaeger/_importedDependencies/types/trace.ts:5381": [ - [0, 0, 0, "Unexpected any. Specify a different type.", "0"] - ], "public/app/plugins/datasource/jaeger/components/QueryEditor.tsx:5381": [ [0, 0, 0, "\'HorizontalGroup\' import from \'@grafana/ui\' is restricted from being used by a pattern. Use Stack component instead.", "0"] ], @@ -6523,9 +6400,6 @@ exports[`better eslint`] = { [0, 0, 0, "Do not use any type assertions.", "0"], [0, 0, 0, "Unexpected any. Specify a different type.", "1"] ], - "public/app/plugins/datasource/jaeger/types.ts:5381": [ - [0, 0, 0, "Unexpected any. Specify a different type.", "0"] - ], "public/app/plugins/datasource/loki/LanguageProvider.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"] ], @@ -6607,9 +6481,6 @@ exports[`better eslint`] = { [0, 0, 0, "Styles should be written using objects.", "1"], [0, 0, 0, "Styles should be written using objects.", "2"] ], - "public/app/plugins/datasource/loki/streaming.ts:5381": [ - [0, 0, 0, "Unexpected any. Specify a different type.", "0"] - ], "public/app/plugins/datasource/loki/types.ts:5381": [ [0, 0, 0, "Do not re-export imported variable (\`LokiQueryDirection\`)", "0"], [0, 0, 0, "Do not re-export imported variable (\`LokiQueryType\`)", "1"], @@ -6646,9 +6517,6 @@ exports[`better eslint`] = { [0, 0, 0, "Unexpected any. Specify a different type.", "16"], [0, 0, 0, "Unexpected any. Specify a different type.", "17"] ], - "public/app/plugins/datasource/opentsdb/migrations.ts:5381": [ - [0, 0, 0, "Unexpected any. Specify a different type.", "0"] - ], "public/app/plugins/datasource/parca/QueryEditor/LabelsEditor.tsx:5381": [ [0, 0, 0, "Styles should be written using objects.", "0"], [0, 0, 0, "Styles should be written using objects.", "1"] @@ -6720,12 +6588,8 @@ exports[`better eslint`] = { "public/app/plugins/datasource/tempo/resultTransformer.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"], [0, 0, 0, "Do not use any type assertions.", "1"], - [0, 0, 0, "Unexpected any. Specify a different type.", "2"], - [0, 0, 0, "Do not use any type assertions.", "3"], - [0, 0, 0, "Unexpected any. Specify a different type.", "4"], - [0, 0, 0, "Do not use any type assertions.", "5"], - [0, 0, 0, "Do not use any type assertions.", "6"], - [0, 0, 0, "Unexpected any. Specify a different type.", "7"] + [0, 0, 0, "Do not use any type assertions.", "2"], + [0, 0, 0, "Unexpected any. Specify a different type.", "3"] ], "public/app/plugins/datasource/tempo/webpack.config.ts:5381": [ [0, 0, 0, "Do not re-export imported variable (\`config\`)", "0"] @@ -6789,9 +6653,6 @@ exports[`better eslint`] = { "public/app/plugins/panel/barchart/quadtree.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"] ], - "public/app/plugins/panel/barchart/utils.ts:5381": [ - [0, 0, 0, "Unexpected any. Specify a different type.", "0"] - ], "public/app/plugins/panel/candlestick/CandlestickPanel.tsx:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"], [0, 0, 0, "Unexpected any. Specify a different type.", "1"] @@ -6806,15 +6667,6 @@ exports[`better eslint`] = { [0, 0, 0, "Do not re-export imported variable (\`CandlestickFieldMap\`)", "6"], [0, 0, 0, "Do not re-export imported variable (\`FieldConfig\`)", "7"] ], - "public/app/plugins/panel/dashlist/styles.ts:5381": [ - [0, 0, 0, "Styles should be written using objects.", "0"], - [0, 0, 0, "Styles should be written using objects.", "1"], - [0, 0, 0, "Styles should be written using objects.", "2"], - [0, 0, 0, "Styles should be written using objects.", "3"], - [0, 0, 0, "Styles should be written using objects.", "4"], - [0, 0, 0, "Styles should be written using objects.", "5"], - [0, 0, 0, "Styles should be written using objects.", "6"] - ], "public/app/plugins/panel/datagrid/utils.ts:5381": [ [0, 0, 0, "Styles should be written using objects.", "0"], [0, 0, 0, "Styles should be written using objects.", "1"], @@ -6938,12 +6790,8 @@ exports[`better eslint`] = { [0, 0, 0, "Do not use any type assertions.", "15"], [0, 0, 0, "Do not use any type assertions.", "16"] ], - "public/app/plugins/panel/histogram/migrations.ts:5381": [ - [0, 0, 0, "Unexpected any. Specify a different type.", "0"] - ], "public/app/plugins/panel/live/LiveChannelEditor.tsx:5381": [ - [0, 0, 0, "Unexpected any. Specify a different type.", "0"], - [0, 0, 0, "Styles should be written using objects.", "1"] + [0, 0, 0, "Styles should be written using objects.", "0"] ], "public/app/plugins/panel/live/LivePanel.tsx:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"], @@ -7057,17 +6905,14 @@ exports[`better eslint`] = { [0, 0, 0, "Unexpected any. Specify a different type.", "0"], [0, 0, 0, "Unexpected any. Specify a different type.", "1"], [0, 0, 0, "Unexpected any. Specify a different type.", "2"], - [0, 0, 0, "Unexpected any. Specify a different type.", "3"], - [0, 0, 0, "Unexpected any. Specify a different type.", "4"], - [0, 0, 0, "Unexpected any. Specify a different type.", "5"] + [0, 0, 0, "Unexpected any. Specify a different type.", "3"] ], "public/app/plugins/panel/text/TextPanel.tsx:5381": [ [0, 0, 0, "Styles should be written using objects.", "0"], [0, 0, 0, "Styles should be written using objects.", "1"] ], "public/app/plugins/panel/text/TextPanelEditor.tsx:5381": [ - [0, 0, 0, "Unexpected any. Specify a different type.", "0"], - [0, 0, 0, "Styles should be written using objects.", "1"] + [0, 0, 0, "Styles should be written using objects.", "0"] ], "public/app/plugins/panel/text/textPanelMigrationHandler.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"] @@ -7131,9 +6976,8 @@ exports[`better eslint`] = { [0, 0, 0, "Styles should be written using objects.", "5"] ], "public/app/plugins/panel/xychart/AutoEditor.tsx:5381": [ - [0, 0, 0, "Unexpected any. Specify a different type.", "0"], - [0, 0, 0, "Styles should be written using objects.", "1"], - [0, 0, 0, "Styles should be written using objects.", "2"] + [0, 0, 0, "Styles should be written using objects.", "0"], + [0, 0, 0, "Styles should be written using objects.", "1"] ], "public/app/plugins/panel/xychart/ManualEditor.tsx:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"] @@ -7158,12 +7002,10 @@ exports[`better eslint`] = { [0, 0, 0, "Do not use any type assertions.", "0"], [0, 0, 0, "Do not use any type assertions.", "1"], [0, 0, 0, "Do not use any type assertions.", "2"], - [0, 0, 0, "Do not use any type assertions.", "3"], - [0, 0, 0, "Do not use any type assertions.", "4"] + [0, 0, 0, "Do not use any type assertions.", "3"] ], "public/app/plugins/panel/xychart/v2/migrations.ts:5381": [ - [0, 0, 0, "Do not use any type assertions.", "0"], - [0, 0, 0, "Do not use any type assertions.", "1"] + [0, 0, 0, "Do not use any type assertions.", "0"] ], "public/app/plugins/panel/xychart/v2/scatter.ts:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"], @@ -7185,9 +7027,6 @@ exports[`better eslint`] = { [0, 0, 0, "Do not use any type assertions.", "16"], [0, 0, 0, "Do not use any type assertions.", "17"] ], - "public/app/plugins/panel/xychart/v2/utils.ts:5381": [ - [0, 0, 0, "Unexpected any. Specify a different type.", "0"] - ], "public/app/plugins/sdk.ts:5381": [ [0, 0, 0, "Do not re-export imported variable (\`loadPluginCss\`)", "0"] ], @@ -7213,9 +7052,7 @@ exports[`better eslint`] = { [0, 0, 0, "Unexpected any. Specify a different type.", "1"], [0, 0, 0, "Unexpected any. Specify a different type.", "2"], [0, 0, 0, "Unexpected any. Specify a different type.", "3"], - [0, 0, 0, "Unexpected any. Specify a different type.", "4"], - [0, 0, 0, "Unexpected any. Specify a different type.", "5"], - [0, 0, 0, "Unexpected any. Specify a different type.", "6"] + [0, 0, 0, "Unexpected any. Specify a different type.", "4"] ], "public/app/types/dashboard.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"] @@ -7229,11 +7066,7 @@ exports[`better eslint`] = { [0, 0, 0, "Unexpected any. Specify a different type.", "5"], [0, 0, 0, "Unexpected any. Specify a different type.", "6"], [0, 0, 0, "Unexpected any. Specify a different type.", "7"], - [0, 0, 0, "Unexpected any. Specify a different type.", "8"], - [0, 0, 0, "Unexpected any. Specify a different type.", "9"], - [0, 0, 0, "Unexpected any. Specify a different type.", "10"], - [0, 0, 0, "Unexpected any. Specify a different type.", "11"], - [0, 0, 0, "Unexpected any. Specify a different type.", "12"] + [0, 0, 0, "Unexpected any. Specify a different type.", "8"] ], "public/app/types/index.ts:5381": [ [0, 0, 0, "Do not use export all (\`export * from ...\`)", "0"], @@ -7277,18 +7110,13 @@ exports[`better eslint`] = { "public/app/types/unified-alerting-dto.ts:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"] ], - "public/swagger/K8sNameLookup.tsx:5381": [ - [0, 0, 0, "Unexpected any. Specify a different type.", "0"] - ], "public/swagger/SwaggerPage.tsx:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"], [0, 0, 0, "No untranslated strings. Wrap text with ", "1"] ], "public/swagger/index.tsx:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"], - [0, 0, 0, "Do not use any type assertions.", "1"], - [0, 0, 0, "Do not use any type assertions.", "2"], - [0, 0, 0, "Do not use any type assertions.", "3"] + [0, 0, 0, "Do not use any type assertions.", "1"] ], "public/swagger/plugins.tsx:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"], @@ -7298,8 +7126,7 @@ exports[`better eslint`] = { [0, 0, 0, "Unexpected any. Specify a different type.", "0"], [0, 0, 0, "Unexpected any. Specify a different type.", "1"], [0, 0, 0, "Unexpected any. Specify a different type.", "2"], - [0, 0, 0, "Unexpected any. Specify a different type.", "3"], - [0, 0, 0, "Unexpected any. Specify a different type.", "4"] + [0, 0, 0, "Unexpected any. Specify a different type.", "3"] ], "public/test/core/thunk/thunkTester.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"], @@ -7307,9 +7134,7 @@ exports[`better eslint`] = { [0, 0, 0, "Unexpected any. Specify a different type.", "2"], [0, 0, 0, "Unexpected any. Specify a different type.", "3"], [0, 0, 0, "Unexpected any. Specify a different type.", "4"], - [0, 0, 0, "Unexpected any. Specify a different type.", "5"], - [0, 0, 0, "Unexpected any. Specify a different type.", "6"], - [0, 0, 0, "Unexpected any. Specify a different type.", "7"] + [0, 0, 0, "Unexpected any. Specify a different type.", "5"] ], "public/test/global-jquery-shim.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"] @@ -7323,21 +7148,12 @@ exports[`better eslint`] = { "public/test/jest-setup.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"] ], - "public/test/lib/common.ts:5381": [ - [0, 0, 0, "Unexpected any. Specify a different type.", "0"] - ], "public/test/specs/helpers.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"], [0, 0, 0, "Unexpected any. Specify a different type.", "1"], [0, 0, 0, "Unexpected any. Specify a different type.", "2"], [0, 0, 0, "Unexpected any. Specify a different type.", "3"], - [0, 0, 0, "Unexpected any. Specify a different type.", "4"], - [0, 0, 0, "Unexpected any. Specify a different type.", "5"], - [0, 0, 0, "Unexpected any. Specify a different type.", "6"], - [0, 0, 0, "Unexpected any. Specify a different type.", "7"], - [0, 0, 0, "Unexpected any. Specify a different type.", "8"], - [0, 0, 0, "Unexpected any. Specify a different type.", "9"], - [0, 0, 0, "Unexpected any. Specify a different type.", "10"] + [0, 0, 0, "Unexpected any. Specify a different type.", "4"] ] }` }; @@ -7563,65 +7379,6 @@ exports[`no gf-form usage`] = { [0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"], [0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"] ], - "packages/grafana-ui/src/themes/GlobalStyles/forms.ts:5381": [ - [0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"], - [0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"], - [0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"], - [0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"], - [0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"], - [0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"], - [0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"], - [0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"], - [0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"], - [0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"], - [0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"], - [0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"], - [0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"], - [0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"], - [0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"], - [0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"], - [0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"], - [0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"], - [0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"], - [0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"], - [0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"], - [0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"], - [0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"], - [0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"], - [0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"], - [0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"] - ], - "packages/grafana-ui/src/themes/GlobalStyles/legacySelect.ts:5381": [ - [0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"], - [0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"], - [0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"], - [0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"], - [0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"], - [0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"], - [0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"], - [0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"], - [0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"], - [0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"], - [0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"], - [0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"], - [0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"], - [0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"], - [0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"], - [0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"], - [0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"], - [0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"], - [0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"], - [0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"], - [0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"], - [0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"], - [0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"], - [0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"], - [0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"], - [0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"], - [0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"], - [0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"], - [0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"] - ], "public/app/angular/components/PageHeader/PageHeader.tsx:5381": [ [0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"], [0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"] diff --git a/.betterer.ts b/.betterer.ts index 6b92c439de6..305b6c35961 100644 --- a/.betterer.ts +++ b/.betterer.ts @@ -7,9 +7,10 @@ import { glob } from 'glob'; // Why are we ignoring these? // They're all deprecated/being removed so doesn't make sense to fix types const eslintPathsToIgnore = [ - 'public/app/angular', // will be removed in Grafana 11 - 'public/app/plugins/panel/graph', // will be removed alongside angular - 'public/app/plugins/panel/table-old', // will be removed alongside angular + 'packages/grafana-ui/src/graveyard', // will be removed alongside angular in Grafana 12 + 'public/app/angular', // will be removed in Grafana 12 + 'public/app/plugins/panel/graph', // will be removed alongside angular in Grafana 12 + 'public/app/plugins/panel/table-old', // will be removed alongside angular in Grafana 12 'e2e/test-plugins', ]; @@ -21,10 +22,9 @@ export default { .exclude(new RegExp(eslintPathsToIgnore.join('|'))), 'no undocumented stories': () => countUndocumentedStories().include('**/!(*.internal).story.tsx'), 'no gf-form usage': () => - regexp( - /gf-form/gm, - 'gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.' - ).include('**/*.{ts,tsx,html}'), + regexp(/gf-form/gm, 'gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.') + .include('**/*.{ts,tsx,html}') + .exclude(new RegExp('packages/grafana-ui/src/themes/GlobalStyles')), }; function countUndocumentedStories() { diff --git a/.drone.yml b/.drone.yml index 9438829a087..09e7ec86cc7 100644 --- a/.drone.yml +++ b/.drone.yml @@ -5941,7 +5941,7 @@ name: gar --- get: name: pat - path: infra/data/ci/github/grafanabot + path: ci/data/repo/grafana/grafana/grafanabot kind: secret name: github_token --- @@ -6108,6 +6108,6 @@ kind: secret name: gcr_credentials --- kind: signature -hmac: 64d991988575d9d3a608d0be4fae0d5e6b903b015d9b060e3254b5107ccc4ad7 +hmac: 7335b2e56769f72716f5dac524741e423abb99eacf775fa635e59c2d658c8aee ... diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index c3acb119e5d..9858f0a295b 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -73,6 +73,7 @@ /hack/ @grafana/grafana-app-platform-squad /apps/alerting/ @grafana/alerting-backend +/apps/playlist/ @grafana/grafana-app-platform-squad /pkg/api/ @grafana/grafana-backend-group /pkg/apis/ @grafana/grafana-app-platform-squad /pkg/apis/alerting_notifications @grafana/grafana-app-platform-squad @grafana/alerting-backend @grafana/alerting-frontend @@ -133,6 +134,7 @@ /pkg/services/playlist/ @grafana/grafana-app-platform-squad /pkg/services/preference/ @grafana/grafana-backend-group /pkg/services/provisioning/ @grafana/grafana-search-and-storage +/pkg/services/provisioning/alerting/ @grafana/alerting-backend /pkg/services/query/ @grafana/grafana-app-platform-squad /pkg/services/queryhistory/ @grafana/explore-squad /pkg/services/quota/ @grafana/grafana-search-and-storage @@ -197,6 +199,7 @@ /devenv/docker/blocks/mariadb/ @grafana/oss-big-tent /devenv/docker/blocks/memcached/ @grafana/grafana-backend-group /devenv/docker/blocks/mimir_backend/ @grafana/alerting-backend +/devenv/docker/blocks/mqtt/ @grafana/alerting-backend /devenv/docker/blocks/mssql/ @grafana/partner-datasources /devenv/docker/blocks/mssql_arm64/ @grafana/partner-datasources /devenv/docker/blocks/mssql_tests/ @grafana/partner-datasources @@ -413,6 +416,7 @@ playwright.config.ts @grafana/plugins-platform-frontend # Temp owners until Enterprise team takes over /public/app/features/migrate-to-cloud @grafana/grafana-frontend-platform +/public/app/features/actions/ @grafana/dataviz-squad /public/app/features/auth-config/ @grafana/identity-squad /public/app/features/annotations/ @grafana/dashboards-squad /public/app/features/api-keys/ @grafana/identity-squad @@ -506,7 +510,6 @@ playwright.config.ts @grafana/plugins-platform-frontend /public/lib/ @grafana/grafana-frontend-platform /public/lib/monaco-languages/kusto.ts @grafana/partner-datasources /public/maps/ @ryantxu -/public/mockServiceWorker.js @grafana/frontend-ops /public/robots.txt @grafana/frontend-ops /public/fonts/ @grafana/grafana-frontend-platform /public/sass/ @grafana/grafana-frontend-platform @@ -699,7 +702,6 @@ embed.go @grafana/grafana-as-code /.github/workflows/commands.yml @torkelo /.github/workflows/community-release.yml @grafana/grafana-release-guild /.github/workflows/detect-breaking-changes-* @grafana/plugins-platform-frontend -/.github/workflows/auto-triager.yml @grafana/plugins-platform-frontend /.github/workflows/doc-validator.yml @grafana/docs-tooling /.github/workflows/epic-add-to-platform-ux-parent-project.yml @meanmina /.github/workflows/github-release.yml @grafana/grafana-release-guild diff --git a/.github/commands.json b/.github/commands.json index 568ea8d6316..fdf90eb4722 100644 --- a/.github/commands.json +++ b/.github/commands.json @@ -555,14 +555,6 @@ "url": "https://github.com/orgs/grafana/projects/599" } }, - { - "type": "label", - "name": "area/provisioning", - "action": "addToProject", - "addToProject": { - "url": "https://github.com/orgs/grafana/projects/52" - } - }, { "type": "label", "name": "area/scenes", @@ -1098,5 +1090,13 @@ "addToProject": { "url": "https://github.com/orgs/grafana/projects/457" } + }, + { + "type": "label", + "name": "area/query-library", + "action": "addToProject", + "addToProject": { + "url": "https://github.com/orgs/grafana/projects/736" + } } ] diff --git a/.github/workflows/auto-triager.yml b/.github/workflows/auto-triager.yml deleted file mode 100644 index 80eeba554cb..00000000000 --- a/.github/workflows/auto-triager.yml +++ /dev/null @@ -1,75 +0,0 @@ -# This workflow is triggered when a new issue is opened -# It will run an internal github action to try to automate the triage process -name: Auto Triage Issues -on: - issues: - types: [opened] - -jobs: - config: - runs-on: "ubuntu-latest" - outputs: - has-secrets: ${{ steps.check.outputs.has-secrets }} - steps: - - name: "Check for secrets" - id: check - shell: bash - run: | - if [ -n "${{ (secrets.GRAFANA_DELIVERY_BOT_APP_ID != '' && - secrets.GRAFANA_DELIVERY_BOT_APP_PEM != '' && - secrets.OPENAI_API_KEY != '' && - secrets.SLACK_WEBHOOK_URL != '' - ) || '' }}" ]; then - echo "has-secrets=1" >> "$GITHUB_OUTPUT" - fi - auto-triage: - needs: config - if: needs.config.outputs.has-secrets - runs-on: ubuntu-latest - steps: - - name: "Generate token" - id: generate_token - uses: tibdex/github-app-token@b62528385c34dbc9f38e5f4225ac829252d1ea92 - with: - app_id: ${{ secrets.GRAFANA_DELIVERY_BOT_APP_ID }} - private_key: ${{ secrets.GRAFANA_DELIVERY_BOT_APP_PEM }} - - - name: Checkout auto-triager repository - uses: actions/checkout@v4 - with: - repository: grafana/auto-triager - path: auto-triager - token: ${{ steps.generate_token.outputs.token }} - - - name: Send issue to the auto triager action - id: auto_triage - # https://github.com/grafana/auto-triager/blob/main/action.yml - #uses: grafana/auto-triager@main - uses: ./auto-triager - with: - token: ${{ secrets.GITHUB_TOKEN }} - issue_number: ${{ github.event.issue.number }} - openai_api_key: ${{ secrets.OPENAI_API_KEY }} - # Leaving the actionin monitoring mode for now - # should be set to true when ready to use - # add_labels: true - add_labels: false - - - name: Labels from auto triage - run: | - echo ${{ steps.auto_triage.outputs.triage_labels }} - - - name: "Send Slack notification" - if : ${{ steps.auto_triage.outputs.triage_labels != '' }} - uses: slackapi/slack-github-action@v1.27.0 - with: - payload: > - { - "icon_emoji": ":robocto:", - "username": "Auto Triager", - "type": "mrkdwn", - "text": "Auto triager found the following labels: ${{ steps.auto_triage.outputs.triage_labels }} for [issue #${{ github.event.issue.number }}](${{ github.event.issue.html_url }})", - "channel": "#triage-automation-ci" - } - env: - SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} diff --git a/.github/workflows/commands.yml b/.github/workflows/commands.yml index c0244a0274a..f733b2f244d 100644 --- a/.github/workflows/commands.yml +++ b/.github/workflows/commands.yml @@ -1,11 +1,21 @@ name: Run commands when issues are labeled or comments added + +# important: this workflow uses a github app that is strictly limited +# to issues. If you want to change the triggers for this workflow, +# please review if the permissions are still sufficient. on: issues: types: [labeled, unlabeled] issue_comment: types: [created] + concurrency: group: issue-commands-${{ github.event.issue.number }} + +permissions: + contents: read + id-token: write + jobs: config: runs-on: "ubuntu-latest" @@ -16,7 +26,7 @@ jobs: id: check shell: bash run: | - if [ -n "${{ (secrets.GRAFANA_MISC_STATS_API_KEY != '' && secrets.ISSUE_COMMANDS_TOKEN != '') || '' }}" ]; then + if [ "${{ github.repository }}" == "grafana/grafana" ] && [ -n "${{ secrets.GRAFANA_MISC_STATS_API_KEY }}" ]; then echo "has-secrets=1" >> "$GITHUB_OUTPUT" fi @@ -25,17 +35,34 @@ jobs: if: needs.config.outputs.has-secrets runs-on: ubuntu-latest steps: + - name: "Get vault secrets" + id: vault-secrets + uses: grafana/shared-workflows/actions/get-vault-secrets@main + with: + # Secrets placed in the ci/repo/grafana/grafana/plugins_platform_issue_commands_github_bot path in Vault + repo_secrets: | + GH_APP_ID=plugins_platform_issue_commands_github_bot:app_id + GH_APP_PEM=plugins_platform_issue_commands_github_bot:app_pem + + - name: "Generate token" + id: generate_token + uses: tibdex/github-app-token@b62528385c34dbc9f38e5f4225ac829252d1ea92 + with: + app_id: ${{ env.GH_APP_ID }} + private_key: ${{ env.GH_APP_PEM }} + - name: Checkout Actions uses: actions/checkout@v4 with: repository: "grafana/grafana-github-actions" path: ./actions ref: main + - name: Install Actions run: npm install --production --prefix ./actions - name: Run Commands uses: ./actions/commands with: metricsWriteAPIKey: ${{secrets.GRAFANA_MISC_STATS_API_KEY}} - token: ${{secrets.ISSUE_COMMANDS_TOKEN}} + token: ${{ steps.generate_token.outputs.token }} configPath: commands diff --git a/.github/workflows/issue-labeled.yml b/.github/workflows/issue-labeled.yml index 8cc4f303c8b..09513a7d5cc 100644 --- a/.github/workflows/issue-labeled.yml +++ b/.github/workflows/issue-labeled.yml @@ -49,8 +49,8 @@ jobs: fi # set environment for next steps - echo "CHANNEL=${CHANNEL}" >> $GITHUB_ENV - echo "TEAM=${TEAM}" >> $GITHUB_ENV + echo "CHANNEL=${CHANNEL}" >> "$GITHUB_ENV" + echo "TEAM=${TEAM}" >> "$GITHUB_ENV" - name: "Prepare payload" uses: frabert/replace-string-action@v2.5 @@ -64,22 +64,22 @@ jobs: - name: Get Token id: get_workflow_token - uses: peter-murray/workflow-application-token-action@v3 + uses: tibdex/github-app-token@3beb63f4bd073e61482598c45c71c1019b59b73a with: - application_id: ${{ secrets.APP_GRAFANA_TEAM_CHECKER_ID }} - application_private_key: ${{ secrets.APP_GRAFANA_TEAM_CHECKER_KEY }} + app_id: ${{ secrets.APP_GRAFANA_TEAM_CHECKER_ID }} + private_key: ${{ secrets.APP_GRAFANA_TEAM_CHECKER_KEY }} - name: "Check that issue author is not part of the team" if: ${{ env.TEAM != 'null' }} run: | response=$(gh api /orgs/grafana/teams/${{ env.TEAM }}/memberships/${{ github.event.issue.user.login }} -i -H "Accept: application/vnd.github.v3+json") STATUS_CODE=$(echo "$response" | head -n 1 | cut -d' ' -f2) - if [ "$status_code" -eq 404 ]; then + if [ "$STATUS_CODE" -eq "404" ]; then echo "The user was not found in the team." - echo "USER_FOUND=false" >> $GITHUB_ENV + echo "USER_FOUND=false" >> "$GITHUB_ENV" else echo "The user was potentially found in the team" - echo "USER_FOUND=maybe" >> $GITHUB_ENV + echo "USER_FOUND=maybe" >> "$GITHUB_ENV" fi env: GITHUB_TOKEN: ${{ steps.get_workflow_token.outputs.token }} diff --git a/.github/workflows/issue-opened.yml b/.github/workflows/issue-opened.yml index 5df1de30a2c..15b9f593a98 100644 --- a/.github/workflows/issue-opened.yml +++ b/.github/workflows/issue-opened.yml @@ -1,28 +1,120 @@ name: Run commands when issues are opened + +# important: this workflow uses a github app that is strictly limited +# to issues. If you want to change the triggers for this workflow, +# please review if the permissions are still sufficient. on: issues: types: [opened] + concurrency: group: issue-opened-${{ github.event.issue.number }} + +permissions: + contents: read + id-token: write + jobs: main: runs-on: ubuntu-latest + if: github.repository == 'grafana/grafana' steps: + - name: Checkout Actions uses: actions/checkout@v4 with: repository: "grafana/grafana-github-actions" path: ./actions ref: main + - name: Install Actions run: npm install --production --prefix ./actions + # give issue-openers a chance to add labels after submit - name: Sleep for 2 minutes run: sleep 2m shell: bash + + - name: "Get vault secrets" + id: vault-secrets + uses: grafana/shared-workflows/actions/get-vault-secrets@main + with: + # Secrets placed in the ci/repo/grafana/grafana/plugins_platform_issue_commands_github_bot path in Vault + repo_secrets: | + GH_APP_ID=plugins_platform_issue_commands_github_bot:app_id + GH_APP_PEM=plugins_platform_issue_commands_github_bot:app_pem + + - name: "Generate token" + id: generate_token + uses: tibdex/github-app-token@b62528385c34dbc9f38e5f4225ac829252d1ea92 + with: + app_id: ${{ env.GH_APP_ID }} + private_key: ${{ env.GH_APP_PEM }} + - name: Run Commands uses: ./actions/commands with: metricsWriteAPIKey: ${{secrets.GRAFANA_MISC_STATS_API_KEY}} - token: ${{secrets.ISSUE_COMMANDS_TOKEN}} + token: ${{ steps.generate_token.outputs.token }} configPath: "issue-opened" + + auto-triage: + needs: [main] + if: github.repository == 'grafana/grafana' && github.event.issue.author_association != 'MEMBER' && github.event.issue.author_association != 'OWNER' + runs-on: ubuntu-latest + steps: + + - name: "Get vault secrets" + id: vault-secrets + uses: grafana/shared-workflows/actions/get-vault-secrets@main + with: + # Secrets placed in the ci/repo/grafana/grafana/plugins_platform_issue_triager path in Vault + repo_secrets: | + AUTOTRIAGER_OPENAI_API_KEY=plugins_platform_issue_triager:AUTOTRIAGER_OPENAI_API_KEY + AUTOTRIAGER_SLACK_WEBHOOK_URL=plugins_platform_issue_triager:AUTOTRIAGER_SLACK_WEBHOOK_URL + GH_APP_ID=plugins_platform_issue_commands_github_bot:app_id + GH_APP_PEM=plugins_platform_issue_commands_github_bot:app_pem + + - name: "Generate token" + id: generate_token + uses: tibdex/github-app-token@b62528385c34dbc9f38e5f4225ac829252d1ea92 + with: + app_id: ${{ env.GH_APP_ID }} + private_key: ${{ env.GH_APP_PEM }} + + - name: Checkout auto-triager repository + uses: actions/checkout@v4 + with: + repository: grafana/auto-triager + path: auto-triager + token: ${{ steps.generate_token.outputs.token }} + + - name: Send issue to the auto triager action + id: auto_triage + # https://github.com/grafana/auto-triager/blob/main/action.yml + #uses: grafana/auto-triager@main + uses: ./auto-triager + with: + token: ${{ steps.generate_token.outputs.token }} + issue_number: ${{ github.event.issue.number }} + openai_api_key: ${{ env.AUTOTRIAGER_OPENAI_API_KEY }} + add_labels: true + + - name: Labels from auto triage + run: | + echo ${{ steps.auto_triage.outputs.triage_labels }} + + - name: "Send Slack notification" + if: ${{ steps.auto_triage.outputs.triage_labels != '' }} + uses: slackapi/slack-github-action@v1.27.0 + with: + payload: > + { + "icon_emoji": ":robocto:", + "username": "Auto Triager", + "type": "mrkdwn", + "text": "Auto triager found the following labels: ${{ steps.auto_triage.outputs.triage_labels }} for issue ${{ github.event.issue.html_url }}", + "channel": "#triage-automation-ci" + } + env: + SLACK_WEBHOOK_URL: ${{ env.AUTOTRIAGER_SLACK_WEBHOOK_URL }} diff --git a/.github/workflows/publish-technical-documentation-next.yml b/.github/workflows/publish-technical-documentation-next.yml index 56fc984700e..6b2cd7489b3 100644 --- a/.github/workflows/publish-technical-documentation-next.yml +++ b/.github/workflows/publish-technical-documentation-next.yml @@ -1,38 +1,21 @@ -name: "publish-technical-documentation-next" +name: publish-technical-documentation-next on: push: branches: - - "main" + - main paths: - "docs/sources/**" workflow_dispatch: jobs: sync: if: github.repository == 'grafana/grafana' - runs-on: "ubuntu-latest" + permissions: + contents: read + id-token: write + runs-on: ubuntu-latest steps: - - name: "Checkout Grafana repo" - uses: "actions/checkout@v4" - - - name: "Clone website-sync Action" - # WEBSITE_SYNC_TOKEN is a fine-grained GitHub Personal Access Token that expires. - # It must be regenerated in the grafanabot GitHub account and requires a Grafana organization - # GitHub administrator to update the organization secret. - # The IT helpdesk can update the organization secret. - run: "git clone --single-branch --no-tags --depth 1 -b master https://grafanabot:${{ secrets.WEBSITE_SYNC_TOKEN }}@github.com/grafana/website-sync ./.github/actions/website-sync" - - - name: "Publish to website repository (next)" - uses: "./.github/actions/website-sync" - id: "publish-next" + - uses: actions/checkout@v4 + - uses: grafana/writers-toolkit/publish-technical-documentation@publish-technical-documentation/v1 with: - repository: "grafana/website" - branch: "master" - host: "github.com" - # PUBLISH_TO_WEBSITE_TOKEN is a fine-grained GitHub Personal Access Token that expires. - # It must be regenerated in the grafanabot GitHub account and requires a Grafana organization - # GitHub administrator to update the organization secret. - # The IT helpdesk can update the organization secret. - github_pat: "grafanabot:${{ secrets.PUBLISH_TO_WEBSITE_TOKEN }}" - source_folder: "docs/sources" - target_folder: "content/docs/grafana/next" + website_directory: content/docs/grafana/next diff --git a/.github/workflows/publish-technical-documentation-release.yml b/.github/workflows/publish-technical-documentation-release.yml index c22bb41cb48..13f7c93df59 100644 --- a/.github/workflows/publish-technical-documentation-release.yml +++ b/.github/workflows/publish-technical-documentation-release.yml @@ -1,4 +1,4 @@ -name: "publish-technical-documentation-release" +name: publish-technical-documentation-release on: push: @@ -12,63 +12,18 @@ on: jobs: sync: if: github.repository == 'grafana/grafana' - runs-on: "ubuntu-latest" + permissions: + contents: read + id-token: write + runs-on: ubuntu-latest steps: - - name: "Checkout Grafana repo" - uses: "actions/checkout@v4" + - uses: actions/checkout@v4 with: fetch-depth: 0 - - - name: "Checkout Actions library" - uses: "actions/checkout@v4" + - uses: grafana/writers-toolkit/publish-technical-documentation-release@publish-technical-documentation-release/v1 with: - repository: "grafana/grafana-github-actions" - path: "./actions" - - - name: "Install Actions from library" - run: "npm install --production --prefix ./actions" - - - name: "Determine if there is a matching release tag" - id: "has-matching-release-tag" - uses: "./actions/has-matching-release-tag" - with: - ref_name: "${{ github.ref_name }}" release_tag_regexp: "^v(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)$" release_branch_regexp: "^v(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.x$" - - - name: "Determine technical documentation version" - if: "steps.has-matching-release-tag.outputs.bool == 'true'" - uses: "./actions/docs-target" - id: "target" - with: - ref_name: "${{ github.ref_name }}" - - - name: "Clone website-sync Action" - if: "steps.has-matching-release-tag.outputs.bool == 'true'" - # WEBSITE_SYNC_TOKEN is a fine-grained GitHub Personal Access Token that expires. - # It must be regenerated in the grafanabot GitHub account and requires a Grafana organization - # GitHub administrator to update the organization secret. - # The IT helpdesk can update the organization secret. - run: "git clone --single-branch --no-tags --depth 1 -b master https://grafanabot:${{ secrets.WEBSITE_SYNC_TOKEN }}@github.com/grafana/website-sync ./.github/actions/website-sync" - - - name: "Switch to HEAD of version branch for tags" - # Tags aren't necessarily made to the HEAD of the version branch. - # The documentation to be published is always on the HEAD of the version branch. - if: "steps.has-matching-release-tag.outputs.bool == 'true' && github.ref_type == 'tag'" - run: "git switch --detach origin/${{ steps.target.outputs.target }}.x" - - - name: "Publish to website repository (release)" - if: "steps.has-matching-release-tag.outputs.bool == 'true'" - uses: "./.github/actions/website-sync" - id: "publish-release" - with: - repository: "grafana/website" - branch: "master" - host: "github.com" - # PUBLISH_TO_WEBSITE_TOKEN is a fine-grained GitHub Personal Access Token that expires. - # It must be regenerated in the grafanabot GitHub account and requires a Grafana organization - # GitHub administrator to update the organization secret. - # The IT helpdesk can update the organization secret. - github_pat: "grafanabot:${{ secrets.PUBLISH_TO_WEBSITE_TOKEN }}" - source_folder: "docs/sources" - target_folder: "content/docs/grafana/${{ steps.target.outputs.target }}" + release_branch_with_patch_regexp: "^v(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)$" + website_directory: content/docs/grafana + version_suffix: "" diff --git a/.gitignore b/.gitignore index f289c47dced..576e0c5258b 100644 --- a/.gitignore +++ b/.gitignore @@ -220,3 +220,6 @@ public/app/plugins/**/dist/ # Locally enabling the Go race detector until we can globally do so .go-race-enabled-locally + +# Mock service worker used for fake API responses in frontend development +public/mockServiceWorker.js diff --git a/.golangci.toml b/.golangci.toml index e021433a9a6..dc3ebd63afc 100644 --- a/.golangci.toml +++ b/.golangci.toml @@ -153,6 +153,17 @@ files = [ "./pkg/storage/unified/apistore/**/*" ] +[linters-settings.depguard.rules.apps-playlist] +list-mode = "lax" +allow = [] +deny = [ + { pkg = "github.com/grafana/grafana/pkg", desc = "apps/playlist is not allowed to import grafana core" } +] +files = [ + "./apps/playlist/*", + "./apps/playlist/**/*" +] + [linters-settings.gocritic] enabled-checks = ["ruleguard"] [linters-settings.gocritic.settings.ruleguard] diff --git a/Dockerfile b/Dockerfile index 098a40a938b..f66b04bd747 100644 --- a/Dockerfile +++ b/Dockerfile @@ -66,6 +66,7 @@ COPY pkg/storage/unified/resource/go.* pkg/storage/unified/resource/ COPY pkg/storage/unified/apistore/go.* pkg/storage/unified/apistore/ COPY pkg/semconv/go.* pkg/semconv/ COPY pkg/aggregator/go.* pkg/aggregator/ +COPY apps/playlist/go.* apps/playlist/ RUN go mod download RUN if [[ "$BINGO" = "true" ]]; then \ diff --git a/Makefile b/Makefile index b171aacd2d6..6f61e20a974 100644 --- a/Makefile +++ b/Makefile @@ -346,27 +346,32 @@ build-docker-full-ubuntu: ## Build Docker image based on Ubuntu for development. ##@ Services -# create docker-compose file with provided sources and start them -# example: make devenv sources=postgres,auth/openldap +COMPOSE := $(shell if docker compose --help >/dev/null 2>&1; then echo docker compose; else echo docker-compose; fi) +ifeq ($(COMPOSE),docker-compose) +$(warning From July 2023 Compose V1 (docker-compose) stopped receiving updates. Migrate to Compose V2 (docker compose). https://docs.docker.com/compose/migrate/) +endif + +# Create a Docker Compose file with provided sources and start them. +# For example, `make devenv sources=postgres,auth/openldap` .PHONY: devenv ifeq ($(sources),) devenv: - @printf 'You have to define sources for this command \nexample: make devenv sources=postgres,openldap\n' + @printf 'You have to define sources for this command \nexample: make devenv sources=postgres,auth/openldap\n' else -devenv: devenv-down ## Start optional services, e.g. postgres, prometheus, and elasticsearch. +devenv: devenv-down ## Start optional services like Postgresql, Prometheus, or Elasticsearch. @cd devenv; \ ./create_docker_compose.sh $(targets) || \ (rm -rf {docker-compose.yaml,conf.tmp,.env}; exit 1) @cd devenv; \ - docker-compose up -d --build + $(COMPOSE) up -d --build endif .PHONY: devenv-down devenv-down: ## Stop optional services. @cd devenv; \ test -f docker-compose.yaml && \ - docker-compose down || exit 0; + $(COMPOSE) down || exit 0; .PHONY: devenv-postgres devenv-postgres: diff --git a/apps/playlist/Makefile b/apps/playlist/Makefile new file mode 100644 index 00000000000..c0b234151e2 --- /dev/null +++ b/apps/playlist/Makefile @@ -0,0 +1,3 @@ +.PHONY: generate +generate: + @grafana-app-sdk generate -g ./apis --kindgrouping=group --postprocess \ No newline at end of file diff --git a/apps/playlist/apis/playlist/v0alpha1/playlist_codec_gen.go b/apps/playlist/apis/playlist/v0alpha1/playlist_codec_gen.go new file mode 100644 index 00000000000..219b0802d35 --- /dev/null +++ b/apps/playlist/apis/playlist/v0alpha1/playlist_codec_gen.go @@ -0,0 +1,28 @@ +// +// Code generated by grafana-app-sdk. DO NOT EDIT. +// + +package v0alpha1 + +import ( + "encoding/json" + "io" + + "github.com/grafana/grafana-app-sdk/resource" +) + +// PlaylistJSONCodec is an implementation of resource.Codec for kubernetes JSON encoding +type PlaylistJSONCodec struct{} + +// Read reads JSON-encoded bytes from `reader` and unmarshals them into `into` +func (*PlaylistJSONCodec) Read(reader io.Reader, into resource.Object) error { + return json.NewDecoder(reader).Decode(into) +} + +// Write writes JSON-encoded bytes into `writer` marshaled from `from` +func (*PlaylistJSONCodec) Write(writer io.Writer, from resource.Object) error { + return json.NewEncoder(writer).Encode(from) +} + +// Interface compliance checks +var _ resource.Codec = &PlaylistJSONCodec{} diff --git a/apps/playlist/apis/playlist/v0alpha1/playlist_metadata_gen.go b/apps/playlist/apis/playlist/v0alpha1/playlist_metadata_gen.go new file mode 100644 index 00000000000..bfcd806c494 --- /dev/null +++ b/apps/playlist/apis/playlist/v0alpha1/playlist_metadata_gen.go @@ -0,0 +1,32 @@ +package v0alpha1 + +import ( + "time" +) + +// PlaylistMetadata defines model for PlaylistMetadata. +type PlaylistMetadata struct { + CreatedBy string `json:"createdBy"` + CreationTimestamp time.Time `json:"creationTimestamp"` + DeletionTimestamp *time.Time `json:"deletionTimestamp,omitempty"` + Finalizers []string `json:"finalizers"` + Generation int64 `json:"generation"` + Labels map[string]string `json:"labels"` + ResourceVersion string `json:"resourceVersion"` + Uid string `json:"uid"` + UpdateTimestamp time.Time `json:"updateTimestamp"` + UpdatedBy string `json:"updatedBy"` +} + +// _kubeObjectMetadata is metadata found in a kubernetes object's metadata field. +// It is not exhaustive and only includes fields which may be relevant to a kind's implementation, +// As it is also intended to be generic enough to function with any API Server. +type PlaylistKubeObjectMetadata struct { + CreationTimestamp time.Time `json:"creationTimestamp"` + DeletionTimestamp *time.Time `json:"deletionTimestamp,omitempty"` + Finalizers []string `json:"finalizers"` + Generation int64 `json:"generation"` + Labels map[string]string `json:"labels"` + ResourceVersion string `json:"resourceVersion"` + Uid string `json:"uid"` +} diff --git a/apps/playlist/apis/playlist/v0alpha1/playlist_object_gen.go b/apps/playlist/apis/playlist/v0alpha1/playlist_object_gen.go new file mode 100644 index 00000000000..3d501ef0ca6 --- /dev/null +++ b/apps/playlist/apis/playlist/v0alpha1/playlist_object_gen.go @@ -0,0 +1,265 @@ +// +// Code generated by grafana-app-sdk. DO NOT EDIT. +// + +package v0alpha1 + +import ( + "fmt" + "github.com/grafana/grafana-app-sdk/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/types" + "time" +) + +// +k8s:openapi-gen=true +type Playlist struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata"` + Spec PlaylistSpec `json:"spec"` + PlaylistStatus PlaylistStatus `json:"status"` +} + +func (o *Playlist) GetSpec() any { + return o.Spec +} + +func (o *Playlist) SetSpec(spec any) error { + cast, ok := spec.(PlaylistSpec) + if !ok { + return fmt.Errorf("cannot set spec type %#v, not of type Spec", spec) + } + o.Spec = cast + return nil +} + +func (o *Playlist) GetSubresources() map[string]any { + return map[string]any{ + "status": o.PlaylistStatus, + } +} + +func (o *Playlist) GetSubresource(name string) (any, bool) { + switch name { + case "status": + return o.PlaylistStatus, true + default: + return nil, false + } +} + +func (o *Playlist) SetSubresource(name string, value any) error { + switch name { + case "status": + cast, ok := value.(PlaylistStatus) + if !ok { + return fmt.Errorf("cannot set status type %#v, not of type PlaylistStatus", value) + } + o.PlaylistStatus = cast + return nil + default: + return fmt.Errorf("subresource '%s' does not exist", name) + } +} + +func (o *Playlist) GetStaticMetadata() resource.StaticMetadata { + return resource.StaticMetadata{ + Name: o.ObjectMeta.Name, + Namespace: o.ObjectMeta.Namespace, + Group: o.GroupVersionKind().Group, + Version: o.GroupVersionKind().Version, + Kind: o.GroupVersionKind().Kind, + } +} + +func (o *Playlist) SetStaticMetadata(metadata resource.StaticMetadata) { + o.Name = metadata.Name + o.Namespace = metadata.Namespace + o.SetGroupVersionKind(schema.GroupVersionKind{ + Group: metadata.Group, + Version: metadata.Version, + Kind: metadata.Kind, + }) +} + +func (o *Playlist) GetCommonMetadata() resource.CommonMetadata { + dt := o.DeletionTimestamp + var deletionTimestamp *time.Time + if dt != nil { + deletionTimestamp = &dt.Time + } + // Legacy ExtraFields support + extraFields := make(map[string]any) + if o.Annotations != nil { + extraFields["annotations"] = o.Annotations + } + if o.ManagedFields != nil { + extraFields["managedFields"] = o.ManagedFields + } + if o.OwnerReferences != nil { + extraFields["ownerReferences"] = o.OwnerReferences + } + return resource.CommonMetadata{ + UID: string(o.UID), + ResourceVersion: o.ResourceVersion, + Generation: o.Generation, + Labels: o.Labels, + CreationTimestamp: o.CreationTimestamp.Time, + DeletionTimestamp: deletionTimestamp, + Finalizers: o.Finalizers, + UpdateTimestamp: o.GetUpdateTimestamp(), + CreatedBy: o.GetCreatedBy(), + UpdatedBy: o.GetUpdatedBy(), + ExtraFields: extraFields, + } +} + +func (o *Playlist) SetCommonMetadata(metadata resource.CommonMetadata) { + o.UID = types.UID(metadata.UID) + o.ResourceVersion = metadata.ResourceVersion + o.Generation = metadata.Generation + o.Labels = metadata.Labels + o.CreationTimestamp = metav1.NewTime(metadata.CreationTimestamp) + if metadata.DeletionTimestamp != nil { + dt := metav1.NewTime(*metadata.DeletionTimestamp) + o.DeletionTimestamp = &dt + } else { + o.DeletionTimestamp = nil + } + o.Finalizers = metadata.Finalizers + if o.Annotations == nil { + o.Annotations = make(map[string]string) + } + if !metadata.UpdateTimestamp.IsZero() { + o.SetUpdateTimestamp(metadata.UpdateTimestamp) + } + if metadata.CreatedBy != "" { + o.SetCreatedBy(metadata.CreatedBy) + } + if metadata.UpdatedBy != "" { + o.SetUpdatedBy(metadata.UpdatedBy) + } + // Legacy support for setting Annotations, ManagedFields, and OwnerReferences via ExtraFields + if metadata.ExtraFields != nil { + if annotations, ok := metadata.ExtraFields["annotations"]; ok { + if cast, ok := annotations.(map[string]string); ok { + o.Annotations = cast + } + } + if managedFields, ok := metadata.ExtraFields["managedFields"]; ok { + if cast, ok := managedFields.([]metav1.ManagedFieldsEntry); ok { + o.ManagedFields = cast + } + } + if ownerReferences, ok := metadata.ExtraFields["ownerReferences"]; ok { + if cast, ok := ownerReferences.([]metav1.OwnerReference); ok { + o.OwnerReferences = cast + } + } + } +} + +func (o *Playlist) GetCreatedBy() string { + if o.ObjectMeta.Annotations == nil { + o.ObjectMeta.Annotations = make(map[string]string) + } + + return o.ObjectMeta.Annotations["grafana.com/createdBy"] +} + +func (o *Playlist) SetCreatedBy(createdBy string) { + if o.ObjectMeta.Annotations == nil { + o.ObjectMeta.Annotations = make(map[string]string) + } + + o.ObjectMeta.Annotations["grafana.com/createdBy"] = createdBy +} + +func (o *Playlist) GetUpdateTimestamp() time.Time { + if o.ObjectMeta.Annotations == nil { + o.ObjectMeta.Annotations = make(map[string]string) + } + + parsed, _ := time.Parse(time.RFC3339, o.ObjectMeta.Annotations["grafana.com/updateTimestamp"]) + return parsed +} + +func (o *Playlist) SetUpdateTimestamp(updateTimestamp time.Time) { + if o.ObjectMeta.Annotations == nil { + o.ObjectMeta.Annotations = make(map[string]string) + } + + o.ObjectMeta.Annotations["grafana.com/updateTimestamp"] = updateTimestamp.Format(time.RFC3339) +} + +func (o *Playlist) GetUpdatedBy() string { + if o.ObjectMeta.Annotations == nil { + o.ObjectMeta.Annotations = make(map[string]string) + } + + return o.ObjectMeta.Annotations["grafana.com/updatedBy"] +} + +func (o *Playlist) SetUpdatedBy(updatedBy string) { + if o.ObjectMeta.Annotations == nil { + o.ObjectMeta.Annotations = make(map[string]string) + } + + o.ObjectMeta.Annotations["grafana.com/updatedBy"] = updatedBy +} + +func (o *Playlist) Copy() resource.Object { + return resource.CopyObject(o) +} + +func (o *Playlist) DeepCopyObject() runtime.Object { + return o.Copy() +} + +// Interface compliance compile-time check +var _ resource.Object = &Playlist{} + +// +k8s:openapi-gen=true +type PlaylistList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata"` + Items []Playlist `json:"items"` +} + +func (o *PlaylistList) DeepCopyObject() runtime.Object { + return o.Copy() +} + +func (o *PlaylistList) Copy() resource.ListObject { + cpy := &PlaylistList{ + TypeMeta: o.TypeMeta, + Items: make([]Playlist, len(o.Items)), + } + o.ListMeta.DeepCopyInto(&cpy.ListMeta) + for i := 0; i < len(o.Items); i++ { + if item, ok := o.Items[i].Copy().(*Playlist); ok { + cpy.Items[i] = *item + } + } + return cpy +} + +func (o *PlaylistList) GetItems() []resource.Object { + items := make([]resource.Object, len(o.Items)) + for i := 0; i < len(o.Items); i++ { + items[i] = &o.Items[i] + } + return items +} + +func (o *PlaylistList) SetItems(items []resource.Object) { + o.Items = make([]Playlist, len(items)) + for i := 0; i < len(items); i++ { + o.Items[i] = *items[i].(*Playlist) + } +} + +// Interface compliance compile-time check +var _ resource.ListObject = &PlaylistList{} diff --git a/apps/playlist/apis/playlist/v0alpha1/playlist_schema_gen.go b/apps/playlist/apis/playlist/v0alpha1/playlist_schema_gen.go new file mode 100644 index 00000000000..2dda8d4b1f3 --- /dev/null +++ b/apps/playlist/apis/playlist/v0alpha1/playlist_schema_gen.go @@ -0,0 +1,34 @@ +// +// Code generated by grafana-app-sdk. DO NOT EDIT. +// + +package v0alpha1 + +import ( + "github.com/grafana/grafana-app-sdk/resource" +) + +// schema is unexported to prevent accidental overwrites +var ( + schemaPlaylist = resource.NewSimpleSchema("playlist.grafana.app", "v0alpha1", &Playlist{}, &PlaylistList{}, resource.WithKind("Playlist"), + resource.WithPlural("playlists"), resource.WithScope(resource.NamespacedScope)) + kindPlaylist = resource.Kind{ + Schema: schemaPlaylist, + Codecs: map[resource.KindEncoding]resource.Codec{ + resource.KindEncodingJSON: &PlaylistJSONCodec{}, + }, + } +) + +// Kind returns a resource.Kind for this Schema with a JSON codec +func PlaylistKind() resource.Kind { + return kindPlaylist +} + +// Schema returns a resource.SimpleSchema representation of Playlist +func PlaylistSchema() *resource.SimpleSchema { + return schemaPlaylist +} + +// Interface compliance checks +var _ resource.Schema = kindPlaylist diff --git a/apps/playlist/apis/playlist/v0alpha1/playlist_spec_gen.go b/apps/playlist/apis/playlist/v0alpha1/playlist_spec_gen.go new file mode 100644 index 00000000000..367d75365ba --- /dev/null +++ b/apps/playlist/apis/playlist/v0alpha1/playlist_spec_gen.go @@ -0,0 +1,36 @@ +package v0alpha1 + +// Defines values for PlaylistItemType. +const ( + PlaylistItemTypeDashboardById PlaylistItemType = "dashboard_by_id" + PlaylistItemTypeDashboardByTag PlaylistItemType = "dashboard_by_tag" + PlaylistItemTypeDashboardByUid PlaylistItemType = "dashboard_by_uid" +) + +// PlaylistItem defines model for PlaylistItem. +// +k8s:openapi-gen=true +type PlaylistItem struct { + // type of the item. + Type PlaylistItemType `json:"type"` + + // Value depends on type and describes the playlist item. + // - dashboard_by_id: The value is an internal numerical identifier set by Grafana. This + // is not portable as the numerical identifier is non-deterministic between different instances. + // Will be replaced by dashboard_by_uid in the future. (deprecated) + // - dashboard_by_tag: The value is a tag which is set on any number of dashboards. All + // dashboards behind the tag will be added to the playlist. + // - dashboard_by_uid: The value is the dashboard UID + Value string `json:"value"` +} + +// PlaylistItemType type of the item. +// +k8s:openapi-gen=true +type PlaylistItemType string + +// PlaylistSpec defines model for PlaylistSpec. +// +k8s:openapi-gen=true +type PlaylistSpec struct { + Interval string `json:"interval"` + Items []PlaylistItem `json:"items"` + Title string `json:"title"` +} diff --git a/apps/playlist/apis/playlist/v0alpha1/playlist_status_gen.go b/apps/playlist/apis/playlist/v0alpha1/playlist_status_gen.go new file mode 100644 index 00000000000..b1484546051 --- /dev/null +++ b/apps/playlist/apis/playlist/v0alpha1/playlist_status_gen.go @@ -0,0 +1,70 @@ +package v0alpha1 + +// Defines values for PlaylistOperatorStateState. +const ( + PlaylistOperatorStateStateFailed PlaylistOperatorStateState = "failed" + PlaylistOperatorStateStateInProgress PlaylistOperatorStateState = "in_progress" + PlaylistOperatorStateStateSuccess PlaylistOperatorStateState = "success" +) + +// Defines values for PlayliststatusOperatorStateState. +const ( + PlayliststatusOperatorStateStateFailed PlayliststatusOperatorStateState = "failed" + PlayliststatusOperatorStateStateInProgress PlayliststatusOperatorStateState = "in_progress" + PlayliststatusOperatorStateStateSuccess PlayliststatusOperatorStateState = "success" +) + +// PlaylistOperatorState defines model for PlaylistOperatorState. +// +k8s:openapi-gen=true +type PlaylistOperatorState struct { + // descriptiveState is an optional more descriptive state field which has no requirements on format + DescriptiveState *string `json:"descriptiveState,omitempty"` + + // details contains any extra information that is operator-specific + Details map[string]interface{} `json:"details,omitempty"` + + // lastEvaluation is the ResourceVersion last evaluated + LastEvaluation string `json:"lastEvaluation"` + + // state describes the state of the lastEvaluation. + // It is limited to three possible states for machine evaluation. + State PlaylistOperatorStateState `json:"state"` +} + +// PlaylistOperatorStateState state describes the state of the lastEvaluation. +// It is limited to three possible states for machine evaluation. +// +k8s:openapi-gen=true +type PlaylistOperatorStateState string + +// PlaylistStatus defines model for PlaylistStatus. +// +k8s:openapi-gen=true +type PlaylistStatus struct { + // additionalFields is reserved for future use + AdditionalFields map[string]interface{} `json:"additionalFields,omitempty"` + + // operatorStates is a map of operator ID to operator state evaluations. + // Any operator which consumes this kind SHOULD add its state evaluation information to this field. + OperatorStates map[string]PlayliststatusOperatorState `json:"operatorStates,omitempty"` +} + +// PlayliststatusOperatorState defines model for Playliststatus.#OperatorState. +// +k8s:openapi-gen=true +type PlayliststatusOperatorState struct { + // descriptiveState is an optional more descriptive state field which has no requirements on format + DescriptiveState *string `json:"descriptiveState,omitempty"` + + // details contains any extra information that is operator-specific + Details map[string]interface{} `json:"details,omitempty"` + + // lastEvaluation is the ResourceVersion last evaluated + LastEvaluation string `json:"lastEvaluation"` + + // state describes the state of the lastEvaluation. + // It is limited to three possible states for machine evaluation. + State PlayliststatusOperatorStateState `json:"state"` +} + +// PlayliststatusOperatorStateState state describes the state of the lastEvaluation. +// It is limited to three possible states for machine evaluation. +// +k8s:openapi-gen=true +type PlayliststatusOperatorStateState string diff --git a/apps/playlist/apis/playlist/v0alpha1/zz_openapi_gen.go b/apps/playlist/apis/playlist/v0alpha1/zz_openapi_gen.go new file mode 100644 index 00000000000..7aa4de07f4b --- /dev/null +++ b/apps/playlist/apis/playlist/v0alpha1/zz_openapi_gen.go @@ -0,0 +1,340 @@ +//go:build !ignore_autogenerated +// +build !ignore_autogenerated + +// Code generated by grafana-app-sdk. DO NOT EDIT. + +package v0alpha1 + +import ( + common "k8s.io/kube-openapi/pkg/common" + spec "k8s.io/kube-openapi/pkg/validation/spec" +) + +func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenAPIDefinition { + return map[string]common.OpenAPIDefinition{ + "github.com/grafana/grafana/apps/playlist/apis/playlist/v0alpha1.Playlist": schema_playlist_apis_playlist_v0alpha1_Playlist(ref), + "github.com/grafana/grafana/apps/playlist/apis/playlist/v0alpha1.PlaylistItem": schema_playlist_apis_playlist_v0alpha1_PlaylistItem(ref), + "github.com/grafana/grafana/apps/playlist/apis/playlist/v0alpha1.PlaylistList": schema_playlist_apis_playlist_v0alpha1_PlaylistList(ref), + "github.com/grafana/grafana/apps/playlist/apis/playlist/v0alpha1.PlaylistOperatorState": schema_playlist_apis_playlist_v0alpha1_PlaylistOperatorState(ref), + "github.com/grafana/grafana/apps/playlist/apis/playlist/v0alpha1.PlaylistSpec": schema_playlist_apis_playlist_v0alpha1_PlaylistSpec(ref), + "github.com/grafana/grafana/apps/playlist/apis/playlist/v0alpha1.PlaylistStatus": schema_playlist_apis_playlist_v0alpha1_PlaylistStatus(ref), + "github.com/grafana/grafana/apps/playlist/apis/playlist/v0alpha1.PlayliststatusOperatorState": schema_playlist_apis_playlist_v0alpha1_PlayliststatusOperatorState(ref), + } +} + +func schema_playlist_apis_playlist_v0alpha1_Playlist(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "kind": { + SchemaProps: spec.SchemaProps{ + Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + Type: []string{"string"}, + Format: "", + }, + }, + "apiVersion": { + SchemaProps: spec.SchemaProps{ + Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + Type: []string{"string"}, + Format: "", + }, + }, + "metadata": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"), + }, + }, + "spec": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/apps/playlist/apis/playlist/v0alpha1.PlaylistSpec"), + }, + }, + "status": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/apps/playlist/apis/playlist/v0alpha1.PlaylistStatus"), + }, + }, + }, + Required: []string{"metadata", "spec", "status"}, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/apps/playlist/apis/playlist/v0alpha1.PlaylistSpec", "github.com/grafana/grafana/apps/playlist/apis/playlist/v0alpha1.PlaylistStatus", "k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"}, + } +} + +func schema_playlist_apis_playlist_v0alpha1_PlaylistItem(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "PlaylistItem defines model for PlaylistItem.", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "type": { + SchemaProps: spec.SchemaProps{ + Description: "type of the item.", + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "value": { + SchemaProps: spec.SchemaProps{ + Description: "Value depends on type and describes the playlist item.\n - dashboard_by_id: The value is an internal numerical identifier set by Grafana. This\n is not portable as the numerical identifier is non-deterministic between different instances.\n Will be replaced by dashboard_by_uid in the future. (deprecated)\n - dashboard_by_tag: The value is a tag which is set on any number of dashboards. All\n dashboards behind the tag will be added to the playlist.\n - dashboard_by_uid: The value is the dashboard UID", + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + }, + Required: []string{"type", "value"}, + }, + }, + } +} + +func schema_playlist_apis_playlist_v0alpha1_PlaylistList(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "kind": { + SchemaProps: spec.SchemaProps{ + Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + Type: []string{"string"}, + Format: "", + }, + }, + "apiVersion": { + SchemaProps: spec.SchemaProps{ + Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + Type: []string{"string"}, + Format: "", + }, + }, + "metadata": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"), + }, + }, + "items": { + SchemaProps: spec.SchemaProps{ + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/apps/playlist/apis/playlist/v0alpha1.Playlist"), + }, + }, + }, + }, + }, + }, + Required: []string{"metadata", "items"}, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/apps/playlist/apis/playlist/v0alpha1.Playlist", "k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"}, + } +} + +func schema_playlist_apis_playlist_v0alpha1_PlaylistOperatorState(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "PlaylistOperatorState defines model for PlaylistOperatorState.", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "descriptiveState": { + SchemaProps: spec.SchemaProps{ + Description: "descriptiveState is an optional more descriptive state field which has no requirements on format", + Type: []string{"string"}, + Format: "", + }, + }, + "details": { + SchemaProps: spec.SchemaProps{ + Description: "details contains any extra information that is operator-specific", + Type: []string{"object"}, + AdditionalProperties: &spec.SchemaOrBool{ + Allows: true, + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Format: "", + }, + }, + }, + }, + }, + "lastEvaluation": { + SchemaProps: spec.SchemaProps{ + Description: "lastEvaluation is the ResourceVersion last evaluated", + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "state": { + SchemaProps: spec.SchemaProps{ + Description: "state describes the state of the lastEvaluation. It is limited to three possible states for machine evaluation.", + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + }, + Required: []string{"lastEvaluation", "state"}, + }, + }, + } +} + +func schema_playlist_apis_playlist_v0alpha1_PlaylistSpec(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "PlaylistSpec defines model for PlaylistSpec.", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "interval": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "items": { + SchemaProps: spec.SchemaProps{ + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/apps/playlist/apis/playlist/v0alpha1.PlaylistItem"), + }, + }, + }, + }, + }, + "title": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + }, + Required: []string{"interval", "items", "title"}, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/apps/playlist/apis/playlist/v0alpha1.PlaylistItem"}, + } +} + +func schema_playlist_apis_playlist_v0alpha1_PlaylistStatus(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "PlaylistStatus defines model for PlaylistStatus.", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "additionalFields": { + SchemaProps: spec.SchemaProps{ + Description: "additionalFields is reserved for future use", + Type: []string{"object"}, + AdditionalProperties: &spec.SchemaOrBool{ + Allows: true, + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Format: "", + }, + }, + }, + }, + }, + "operatorStates": { + SchemaProps: spec.SchemaProps{ + Description: "operatorStates is a map of operator ID to operator state evaluations. Any operator which consumes this kind SHOULD add its state evaluation information to this field.", + Type: []string{"object"}, + AdditionalProperties: &spec.SchemaOrBool{ + Allows: true, + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/apps/playlist/apis/playlist/v0alpha1.PlayliststatusOperatorState"), + }, + }, + }, + }, + }, + }, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/apps/playlist/apis/playlist/v0alpha1.PlayliststatusOperatorState"}, + } +} + +func schema_playlist_apis_playlist_v0alpha1_PlayliststatusOperatorState(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "PlayliststatusOperatorState defines model for Playliststatus.#OperatorState.", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "descriptiveState": { + SchemaProps: spec.SchemaProps{ + Description: "descriptiveState is an optional more descriptive state field which has no requirements on format", + Type: []string{"string"}, + Format: "", + }, + }, + "details": { + SchemaProps: spec.SchemaProps{ + Description: "details contains any extra information that is operator-specific", + Type: []string{"object"}, + AdditionalProperties: &spec.SchemaOrBool{ + Allows: true, + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Format: "", + }, + }, + }, + }, + }, + "lastEvaluation": { + SchemaProps: spec.SchemaProps{ + Description: "lastEvaluation is the ResourceVersion last evaluated", + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "state": { + SchemaProps: spec.SchemaProps{ + Description: "state describes the state of the lastEvaluation. It is limited to three possible states for machine evaluation.", + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + }, + Required: []string{"lastEvaluation", "state"}, + }, + }, + } +} diff --git a/apps/playlist/go.mod b/apps/playlist/go.mod new file mode 100644 index 00000000000..b6dd9d23beb --- /dev/null +++ b/apps/playlist/go.mod @@ -0,0 +1,38 @@ +module github.com/grafana/grafana/apps/playlist + +go 1.23.1 + +require ( + github.com/grafana/grafana-app-sdk v0.19.0 + k8s.io/apimachinery v0.31.0 + k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 +) + +require ( + github.com/emicklei/go-restful/v3 v3.11.0 // indirect + github.com/fxamacker/cbor/v2 v2.7.0 // indirect + github.com/go-logr/logr v1.4.2 // indirect + github.com/go-openapi/jsonpointer v0.21.0 // indirect + github.com/go-openapi/jsonreference v0.21.0 // indirect + github.com/go-openapi/swag v0.23.0 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang/protobuf v1.5.4 // indirect + github.com/google/gnostic-models v0.6.8 // indirect + github.com/google/gofuzz v1.2.0 // indirect + github.com/josharian/intern v1.0.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/mailru/easyjson v0.7.7 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/x448/float16 v0.8.4 // indirect + golang.org/x/net v0.29.0 // indirect + golang.org/x/text v0.18.0 // indirect + google.golang.org/protobuf v1.34.2 // indirect + gopkg.in/inf.v0 v0.9.1 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + k8s.io/klog/v2 v2.130.1 // indirect + k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 // indirect + sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect + sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect +) diff --git a/apps/playlist/go.sum b/apps/playlist/go.sum new file mode 100644 index 00000000000..ca1c932647b --- /dev/null +++ b/apps/playlist/go.sum @@ -0,0 +1,117 @@ +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= +github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= +github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= +github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= +github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ= +github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4= +github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= +github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= +github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= +github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/grafana/grafana-app-sdk v0.19.0 h1:RY9HvCFR+4WUy81n53wejZnof9qE6++pd6r24d6+JYs= +github.com/grafana/grafana-app-sdk v0.19.0/go.mod h1:y0BgzYxc+a7CwOqkwUhN9zXd5cgZJjd2zAbgHEd/xzo= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= +github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= +github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo= +golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224= +golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= +google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +k8s.io/apimachinery v0.31.0 h1:m9jOiSr3FoSSL5WO9bjm1n6B9KROYYgNZOb4tyZ1lBc= +k8s.io/apimachinery v0.31.0/go.mod h1:rsPdaZJfTfLsNJSQzNHQvYoTmxhoOEofxtOsF3rtsMo= +k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= +k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= +k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 h1:BZqlfIlq5YbRMFko6/PM7FjZpUb45WallggurYhKGag= +k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340/go.mod h1:yD4MZYeKMBwQKVht279WycxKyM84kkAx2DPrTXaeb98= +k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 h1:pUdcCO1Lk/tbT5ztQWOBi5HBgbBP1J8+AsQnQCKsi8A= +k8s.io/utils v0.0.0-20240711033017-18e509b52bc8/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo= +sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= +sigs.k8s.io/structured-merge-diff/v4 v4.4.1 h1:150L+0vs/8DA78h1u02ooW1/fFq/Lwr+sGiqlzvrtq4= +sigs.k8s.io/structured-merge-diff/v4 v4.4.1/go.mod h1:N8hJocpFajUSSeSJ9bOZ77VzejKZaXsTtZo4/u7Io08= +sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= +sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= diff --git a/apps/playlist/kinds/cue.mod/module.cue b/apps/playlist/kinds/cue.mod/module.cue new file mode 100644 index 00000000000..8867221c7b7 --- /dev/null +++ b/apps/playlist/kinds/cue.mod/module.cue @@ -0,0 +1 @@ +module: "github.com/grafana/grafana/apps/playlist/kinds" diff --git a/apps/playlist/kinds/playlist.cue b/apps/playlist/kinds/playlist.cue new file mode 100644 index 00000000000..72a0813b987 --- /dev/null +++ b/apps/playlist/kinds/playlist.cue @@ -0,0 +1,39 @@ +package core + +externalName: { + kind: "Playlist" + group: "playlist" + apiResource: { + groupOverride: "playlist.grafana.app" + } + codegen: { + frontend: false + backend: true + } + pluralName: "Playlists" + current: "v0alpha1" + versions: { + "v0alpha1": { + schema: { + #Item: { + // type of the item. + type: "dashboard_by_tag" | "dashboard_by_uid" | "dashboard_by_id" @cuetsy(kind="enum") + // Value depends on type and describes the playlist item. + // - dashboard_by_id: The value is an internal numerical identifier set by Grafana. This + // is not portable as the numerical identifier is non-deterministic between different instances. + // Will be replaced by dashboard_by_uid in the future. (deprecated) + // - dashboard_by_tag: The value is a tag which is set on any number of dashboards. All + // dashboards behind the tag will be added to the playlist. + // - dashboard_by_uid: The value is the dashboard UID + value: string + } + + spec: { + title: string + interval: string + items: [...#Item] + } + } + } + } +} diff --git a/conf/defaults.ini b/conf/defaults.ini index 49209a9ea69..79ecacdf39b 100644 --- a/conf/defaults.ini +++ b/conf/defaults.ini @@ -406,6 +406,9 @@ csrf_always_check = false # Comma-separated list of plugins ids that won't be loaded inside the frontend sandbox disable_frontend_sandbox_for_plugins = grafana-incident-app +# Comma-separated list of paths for POST/PUT URL in actions. Empty will allow anything that is not on the same origin +actions_allow_post_url = + [security.encryption] # Defines the time-to-live (TTL) for decrypted data encryption keys stored in memory (cache). # Please note that small values may cause performance issues due to a high frequency decryption operations. @@ -1729,7 +1732,7 @@ install_token = hide_angular_deprecation = # Comma separated list of plugin ids for which environment variables should be forwarded. Used only when feature flag pluginsSkipHostEnvVars is enabled. forward_host_env_vars = -# Comma separated list of plugin ids to install as part of the startup process. Used only when feature flag backgroundPluginInstaller is enabled. +# Comma separated list of plugin ids to install as part of the startup process. preinstall = # Controls whether preinstall plugins asynchronously (in the background) or synchronously (blocking). Useful when preinstalled plugins are used with provisioning. preinstall_async = true diff --git a/conf/sample.ini b/conf/sample.ini index f94888ae679..3368d7e288e 100644 --- a/conf/sample.ini +++ b/conf/sample.ini @@ -411,6 +411,9 @@ # Comma-separated list of plugins ids that won't be loaded inside the frontend sandbox ;disable_frontend_sandbox_for_plugins = +# Comma-separated list of paths for POST/PUT URL in actions. Empty will allow anything that is not on the same origin +;actions_allow_post_url = + [security.encryption] # Defines the time-to-live (TTL) for decrypted data encryption keys stored in memory (cache). # Please note that small values may cause performance issues due to a high frequency decryption operations. diff --git a/contribute/ISSUE_TRIAGE.md b/contribute/ISSUE_TRIAGE.md index 71e4bd7770d..ed9d6c9418e 100644 --- a/contribute/ISSUE_TRIAGE.md +++ b/contribute/ISSUE_TRIAGE.md @@ -270,17 +270,17 @@ In case there is an uncertainty around the prioritization of an issue, please as 1. If applicable, label the issue `priority/support-subscription`. 1. Add the issue to the next upcoming patch or major/minor stable release milestone. Ask maintainers for help if unsure if it's a patch or not. Create a new milestone if there are none. 1. Make sure to add the issue to a suitable backlog of a GitHub project and prioritize it or assign someone to work on it now or very soon. -1. Consider requesting [help from the community](#5-requesting-help-from-the-community), even though it may be problematic given a short amount of time until it should be released. +1. Consider requesting [help from the community](#5-request-help-from-the-community), even though it may be problematic given a short amount of time until it should be released. **Important long-term** 1. Label the issue `priority/important-longterm`. -1. Consider requesting [help from the community](#5-requesting-help-from-the-community). +1. Consider requesting [help from the community](#5-request-help-from-the-community). **Nice to have** 1. Label the issue `priority/nice-to-have`. -1. Consider requesting [help from the community](#5-requesting-help-from-the-community). +1. Consider requesting [help from the community](#5-request-help-from-the-community). **Not critical, but unsure?** diff --git a/contribute/backend/README.md b/contribute/backend/README.md index 9f9d53695e5..a86f504da50 100644 --- a/contribute/backend/README.md +++ b/contribute/backend/README.md @@ -25,4 +25,4 @@ If you want to make or review large changes to the backend, be sure to habituall ## Guidelines for dependency management -If you work with a dependency that requires an upgrade, refer to [Upgrading dependencies](/contribute/backend/upgrading-dependencies.md). +If you work with a dependency that requires an upgrade, refer to [Upgrade dependencies](/contribute/backend/upgrade-dependencies.md). diff --git a/contribute/backend/database.md b/contribute/backend/database.md index 16965d0ba98..c40caa3dee9 100644 --- a/contribute/backend/database.md +++ b/contribute/backend/database.md @@ -18,7 +18,7 @@ Grafana uses the [XORM](https://xorm.io) framework for persisting objects to the > **Deprecated:** We are deprecating `sqlstore` handlers in favor of using the `SQLStore` object directly in each service. Since most services still use the `sqlstore` handlers, we still want to explain how they work. -The `sqlstore` package allows you to register [command handlers](communication.md#handle-commands) that either store or retrieve objects from the database. The `sqlstore` handlers are similar to services: +The `sqlstore` package allows you to register [command handlers](communication.md#commands-and-queries) that either store or retrieve objects from the database. The `sqlstore` handlers are similar to services: - [Services](services.md) are command handlers that _contain business logic_. - `sqlstore` handlers are command handlers that _access the database_. @@ -30,7 +30,7 @@ The `sqlstore` package allows you to register [command handlers](communication.m To register a handler: - Create a new file, `myrepo.go`, in the `sqlstore` package. -- Create a [command handler](communication.md#handle-commands). +- Create a [command handler](communication.md#commands-and-queries). - Register the handler in the `init` function: ```go @@ -60,7 +60,7 @@ type MyService struct { } ``` -You can now make SQL queries in any of your [command handlers](communication.md#handle-commands) or [event listeners](communication.md#subscribe-to-an-event): +You can now make SQL queries in any of your [command handlers](communication.md#commands-and-queries) or [event listeners](communication.md#subscribe-to-an-event): ```go func (s *MyService) DeleteDashboard(ctx context.Context, cmd *models.DeleteDashboardCommand) error { @@ -107,7 +107,7 @@ To add a migration: ### Implement `DatabaseMigrator` -During initialization, SQL store queries the service registry, and runs migrations for every service that implements the [DatabaseMigrator](https://github.com/grafana/grafana/blob/44c2007498c76c2dbb48e8366b4af410f1ee1b98/pkg/registry/registry.go#L101-L106) interface. +During initialization, SQL store queries the service registry, and runs migrations for every service that implements the [DatabaseMigrator](https://github.com/grafana/grafana/blob/d27c3822f28e5f26199b4817892d6d24a7a26567/pkg/registry/registry.go#L46-L50) interface. To add a migration: diff --git a/contribute/backend/instrumentation.md b/contribute/backend/instrumentation.md index 5960137c372..b2cd4d64869 100644 --- a/contribute/backend/instrumentation.md +++ b/contribute/backend/instrumentation.md @@ -55,7 +55,7 @@ When should you use each log level? Use a contextual logger to include additional key/value pairs attached to `context.Context`. For example, a `traceID`, used to allow correlating logs with traces, correlate logs with a common identifier, either or both. -You must [Enable tracing in Grafana](#2-enable-tracing-in-grafana) to get a `traceID`. +You must [Enable tracing in Grafana](#enable-tracing-in-grafana) to get a `traceID`. For example: @@ -156,7 +156,7 @@ A distributed trace is data that tracks an application request as it flows throu ### Usage -Grafana uses [OpenTelemetry](https://opentelemetry.io/) for distributed tracing. There's an interface `Tracer` in the `pkg/infra/tracing` package that implements the [OpenTelemetry Tracer interface](go.opentelemetry.io/otel/trace), which you can use to create traces and spans. To access `Tracer` you need to get it injected as a dependency of your service. Refer to [Services](services.md) for more details. For more information, you may also refer to [The OpenTelemetry documentation](https://opentelemetry.io/docs/instrumentation/go/manual/). +Grafana uses [OpenTelemetry](https://opentelemetry.io/) for distributed tracing. There's an interface `Tracer` in the `pkg/infra/tracing` package that implements the [OpenTelemetry Tracer interface](https://pkg.go.dev/go.opentelemetry.io/otel/trace), which you can use to create traces and spans. To access `Tracer` you need to get it injected as a dependency of your service. Refer to [Services](services.md) for more details. For more information, you may also refer to [The OpenTelemetry documentation](https://opentelemetry.io/docs/instrumentation/go/manual/). For example: @@ -269,7 +269,7 @@ attribute.Key("org_id").Int64(proxy.ctx.SignedInUser.OrgID) make devenv sources=jaeger ``` -1. Enable tracing in Grafana +1. Enable tracing in Grafana To enable tracing in Grafana, you must set the address in your `config.ini` file: diff --git a/contribute/backend/services.md b/contribute/backend/services.md index 29668768a72..0bf6aa00cfe 100644 --- a/contribute/backend/services.md +++ b/contribute/backend/services.md @@ -121,7 +121,7 @@ For an example of the `IsDisabled` method and custom initialization code when th ## Run Wire (generate code) -Running `make run` calls `make gen-go` on the first run. The `gen-go` in turn calls the Wire binary and generates the code in [`wire_gen.go`](/pkg/server/wire_gen.go) and [`wire_gen.go`](/pkg/cmd/grafana-cli/runner/wire_gen.go). The Wire binary is installed using [`bingo`](https://github.com/bwplotka/bingo) which downloads and installs all the tools needed, including the Wire binary at the specified version. +Running `make run` calls `make gen-go` on the first run. The `gen-go` in turn calls the Wire binary and generates the code in [`wire_gen.go`](/pkg/server/wire_gen.go). The Wire binary is installed using [`bingo`](https://github.com/bwplotka/bingo) which downloads and installs all the tools needed, including the Wire binary at the specified version. ## OSS vs. Enterprise diff --git a/contribute/merge-pull-request.md b/contribute/merge-pull-request.md index fb8131c9a67..0e60768892f 100644 --- a/contribute/merge-pull-request.md +++ b/contribute/merge-pull-request.md @@ -30,7 +30,7 @@ The pull request title should be formatted according to `: ` (Bot Keep the summary short and understandable for the community as a whole. -All commits in a pull request are squashed when merged and the pull request title will be the default subject line of the squashed commit message. It's also used for the [changelog](#include-in-changelog-and-release-notes). +All commits in a pull request are squashed when merged and the pull request title will be the default subject line of the squashed commit message. It's also used for the [changelog](#what-to-include-in-changelog-and-release-notes). **Example:** @@ -40,7 +40,7 @@ See [formatting guidelines](create-pull-request.md#formatting-guidelines) for mo ### Assign a milestone (automated) -The Grafana release process uses a bot to automatically assign pull requests to a milestone to make it easier for release managers to track changes. For example, [generating changelog (release note)](#include-in-changelog-and-release-notes) must be in a milestone. +The Grafana release process uses a bot to automatically assign pull requests to a milestone to make it easier for release managers to track changes. For example, [generating changelog (release note)](#what-to-include-in-changelog-and-release-notes) must be in a milestone. That being said, _you don't have to assign a milestone manually_ to a pull request. Instead, when it is merged and closed, a bot will then look for the most appropriate milestone and assign it to the pull request. @@ -104,7 +104,7 @@ In case the pull request introduces a deprecation you should document this. Labe ``` -**Breaking changes:** +**Breaking changes:** In case the pull request introduces a breaking change you should document this. Label the pull request with `add to changelog` and `breaking change` and use the following template at the end of the pull request description describing the breaking change: diff --git a/contribute/style-guides/styling.md b/contribute/style-guides/styling.md index 4393814d376..791cdff37aa 100644 --- a/contribute/style-guides/styling.md +++ b/contribute/style-guides/styling.md @@ -4,7 +4,7 @@ ## Usage -For styling components, use [Emotion's `css` function](https://emotion.sh/docs/emotion#css). +For styling components, use [Emotion's `css` function](https://emotion.sh/docs/@emotion/css#css). ### Basic styling diff --git a/contribute/triage-issues.md b/contribute/triage-issues.md index e4832681f79..57ab47c7913 100644 --- a/contribute/triage-issues.md +++ b/contribute/triage-issues.md @@ -7,7 +7,7 @@ Triage helps ensure that GitHub issues resolve quickly by: - Lowering the issue count by preventing duplicate issues. - Streamlining the development process by preventing duplicate discussions. -This document gives you some ideas on what you can do to help. For more information, read more about [how the core Grafana team triage issues](/ISSUE_TRIAGE.md). +This document gives you some ideas on what you can do to help. For more information, read more about [how the core Grafana team triage issues](/contribute/ISSUE_TRIAGE.md). ## Improve issues @@ -23,9 +23,9 @@ Investigate issues that we haven't been able to reproduce yet. In some cases, th ## Vote on issues -Use [GitHub reactions](https://help.github.com/en/articles/about-conversations-on-github#reacting-to-ideas-in-comments) to let us know what's important to you. Vote on bugs if you've experienced the same problem. **Don't vote, or react, by commenting on the issue.** +Use [GitHub reactions](https://github.blog/news-insights/product-news/add-reactions-to-pull-requests-issues-and-comments/) to let us know what's important to you. Vote on bugs if you've experienced the same problem. **Don't vote, or react, by commenting on the issue.** -Read more about [how we prioritize issues](/ISSUE_TRIAGE.md#4-prioritization-of-issues). +Read more about [how we prioritize issues](/contribute/ISSUE_TRIAGE.md#4-prioritization-of-issues). ## Report duplicates diff --git a/devenv/docker/blocks/auth/jwt_proxy/docker-compose.yaml b/devenv/docker/blocks/auth/jwt_proxy/docker-compose.yaml index 618a096cd1c..552664f1c97 100644 --- a/devenv/docker/blocks/auth/jwt_proxy/docker-compose.yaml +++ b/devenv/docker/blocks/auth/jwt_proxy/docker-compose.yaml @@ -51,7 +51,7 @@ depends_on: - oauthkeycloak extra_hosts: - - "env.grafana.local:host.containers.internal" + - "env.grafana.local:host-gateway" ports: - 8088:8088 restart: unless-stopped \ No newline at end of file diff --git a/devenv/docker/blocks/collectd/README.md b/devenv/docker/blocks/collectd/README.md index c972d5074e2..ebd744284d1 100644 --- a/devenv/docker/blocks/collectd/README.md +++ b/devenv/docker/blocks/collectd/README.md @@ -30,7 +30,7 @@ Environment variables - Graphite prefix - Optional, defaults to collectd. - `REPORT_BY_CPU` - - Report per-CPU metrics if true, global sum of CPU metrics if false (details: [collectd.conf man page](https://collectd.org/documentation/manpages/collectd.conf.5.shtml#plugin_cpu)) + - Report per-CPU metrics if true, global sum of CPU metrics if false (details: [collectd.conf man page](https://www.collectd.org/documentation/manpages/collectd.conf.html)) - Optional, defaults to false. - `COLLECT_INTERVAL` - Collection interval and thus resolution of metrics diff --git a/devenv/docker/blocks/mqtt/README.md b/devenv/docker/blocks/mqtt/README.md new file mode 100644 index 00000000000..6724fa17666 --- /dev/null +++ b/devenv/docker/blocks/mqtt/README.md @@ -0,0 +1,31 @@ +# NanoMQ MQTT broker + +Starts a [NanoMQ MQTT broker](https://nanomq.io/docs/en/latest/). + +## Authentication + +The broker is configured to use a simple username/password authentication. +See [./nanomq_pwd.conf](./nanomq_pwd.conf) for the default credentials. + +## TLS Certificates + +If you want to configure an MQTT contact point in Grafana Alerting with TLS, you need to provide a certificate and key. + +You can find them in `/etc/certs` directory in the container: + +``` shell +docker exec devenv-mqtt-1 ls /etc/certs/ +``` + +### CA Certificate + +``` shell +docker exec devenv-mqtt-1 cat /etc/certs/ca.pem +``` + +### Client certificates + +``` shell +docker exec devenv-mqtt-1 cat /etc/certs/client.pem +docker exec devenv-mqtt-1 cat /etc/certs/client.key +``` diff --git a/devenv/docker/blocks/mqtt/build/Dockerfile b/devenv/docker/blocks/mqtt/build/Dockerfile new file mode 100644 index 00000000000..e3f97cdba74 --- /dev/null +++ b/devenv/docker/blocks/mqtt/build/Dockerfile @@ -0,0 +1,9 @@ +FROM emqx/nanomq:0.21.11-full + +RUN apt-get update && apt-get install -y \ + openssl \ + && rm -rf /var/lib/apt/lists/* + +COPY ./san.cnf /etc/certs/san.cnf +COPY ./gen_certs.sh /etc/certs/gen_certs.sh +RUN /etc/certs/gen_certs.sh diff --git a/devenv/docker/blocks/mqtt/build/gen_certs.sh b/devenv/docker/blocks/mqtt/build/gen_certs.sh new file mode 100755 index 00000000000..c1ea129d60c --- /dev/null +++ b/devenv/docker/blocks/mqtt/build/gen_certs.sh @@ -0,0 +1,18 @@ +#!/bin/bash + +DAYS_VALID=3650 +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +# Create CA certificate +openssl genpkey -algorithm RSA -out "$SCRIPT_DIR/ca.key" +openssl req -new -x509 -days $DAYS_VALID -key "$SCRIPT_DIR/ca.key" -out "$SCRIPT_DIR/ca.pem" -subj "/CN=My CA" + +# Create server certificate +openssl genpkey -algorithm RSA -out "$SCRIPT_DIR/server.key" +openssl req -new -key "$SCRIPT_DIR/server.key" -out "$SCRIPT_DIR/server.csr" -subj "/CN=localhost" +openssl x509 -req -days $DAYS_VALID -in "$SCRIPT_DIR/server.csr" -CA "$SCRIPT_DIR/ca.pem" -CAkey "$SCRIPT_DIR/ca.key" -CAcreateserial -out "$SCRIPT_DIR/server.pem" -extfile "$SCRIPT_DIR/san.cnf" -extensions v3_req + +# Create client key and certificate +openssl genpkey -algorithm RSA -out "$SCRIPT_DIR/client.key" +openssl req -new -key "$SCRIPT_DIR/client.key" -out "$SCRIPT_DIR/client.csr" -subj "/CN=Client" +openssl x509 -req -days $DAYS_VALID -in "$SCRIPT_DIR/client.csr" -CA "$SCRIPT_DIR/ca.pem" -CAkey "$SCRIPT_DIR/ca.key" -CAcreateserial -out "$SCRIPT_DIR/client.pem" -extfile "$SCRIPT_DIR/san.cnf" -extensions v3_req diff --git a/devenv/docker/blocks/mqtt/build/san.cnf b/devenv/docker/blocks/mqtt/build/san.cnf new file mode 100644 index 00000000000..d7148ac8d4f --- /dev/null +++ b/devenv/docker/blocks/mqtt/build/san.cnf @@ -0,0 +1,7 @@ +[ v3_req ] +subjectAltName = @alt_names + +[ alt_names ] +DNS.1 = localhost +IP.1 = 127.0.0.1 +IP.2 = ::1 diff --git a/devenv/docker/blocks/mqtt/docker-compose.yaml b/devenv/docker/blocks/mqtt/docker-compose.yaml new file mode 100644 index 00000000000..bdd5437f500 --- /dev/null +++ b/devenv/docker/blocks/mqtt/docker-compose.yaml @@ -0,0 +1,12 @@ + mqtt: + build: + context: docker/blocks/mqtt/build + ports: + - "127.0.0.1:1883:1883" # MQTT + - "127.0.0.1:8883:8883" # MQTT over TLS + - "127.0.0.1:8083:8083" # MQTT over WS + - "127.0.0.1:8443:8443" # MQTT over WSS + volumes: + - ${PWD}/docker/blocks/mqtt/nanomq.conf:/etc/nanomq.conf + - ${PWD}/docker/blocks/mqtt/nanomq_pwd.conf:/etc/nanomq_pwd.conf + - ${PWD}/docker/blocks/mqtt/nanomq_acl.conf:/etc/nanomq_acl.conf diff --git a/devenv/docker/blocks/mqtt/nanomq.conf b/devenv/docker/blocks/mqtt/nanomq.conf new file mode 100644 index 00000000000..90a27f22404 --- /dev/null +++ b/devenv/docker/blocks/mqtt/nanomq.conf @@ -0,0 +1,40 @@ +log { + to=console + level=info +} + +listeners.tcp { + bind = "0.0.0.0:1883" +} + + +listeners.ssl { + bind = "0.0.0.0:8883" + + keyfile = "/etc/certs/server.key" + certfile = "/etc/certs/server.pem" + cacertfile = "/etc/certs/ca.pem" + + # Change these settings to true if you want to deny + # access for clients that don't have a certificate. + verify_peer = false + fail_if_no_peer_cert = false +} + +listeners.ws { + bind = "0.0.0.0:8083" +} + +listeners.wss { + bind = "0.0.0.0:8443" +} + +auth { + allow_anonymous = false + no_match = deny + deny_action = disconnect + password = {include "/etc/nanomq_pwd.conf"} + acl = { + include "/etc/nanomq_acl.conf" + } +} diff --git a/devenv/docker/blocks/mqtt/nanomq_acl.conf b/devenv/docker/blocks/mqtt/nanomq_acl.conf new file mode 100644 index 00000000000..cc12fe9090b --- /dev/null +++ b/devenv/docker/blocks/mqtt/nanomq_acl.conf @@ -0,0 +1,7 @@ +rules = [ + {"permit": "allow", "username": "grafana", "action": "subscribe", "topics": ["#"]} + {"permit": "allow", "username": "grafana", "action": "publish", "topics": ["#"]} + {"permit": "allow", "username": "admin", "action": "subscribe", "topics": ["#"]} + {"permit": "allow", "username": "admin", "action": "publish", "topics": ["#"]} + {"permit": "deny"} +] diff --git a/devenv/docker/blocks/mqtt/nanomq_pwd.conf b/devenv/docker/blocks/mqtt/nanomq_pwd.conf new file mode 100644 index 00000000000..7fd24d15e41 --- /dev/null +++ b/devenv/docker/blocks/mqtt/nanomq_pwd.conf @@ -0,0 +1,2 @@ +admin:admin +grafana:grafana diff --git a/devenv/docker/blocks/webdav/README.md b/devenv/docker/blocks/webdav/README.md index a687d4acb75..dfddb0d4d4f 100644 --- a/devenv/docker/blocks/webdav/README.md +++ b/devenv/docker/blocks/webdav/README.md @@ -1,6 +1,6 @@ # README for the image storage WebDAV docker block -This block is used for testing the [WebDAV](https://grafana.com/docs/grafana/latest/setup-grafana/configure-grafana/#external_image_storagewebdav) option for external image storage which is used in [alert notifications](https://grafana.com/docs/grafana/latest/alerting/manage-notifications/images-in-notifications/). This uses the simplest WebDav server that is still being maintained, a project called [Dufs](https://github.com/sigoden/dufs). +This block is used for testing the [WebDAV](https://grafana.com/docs/grafana/latest/setup-grafana/configure-grafana/#external_image_storagewebdav) option for external image storage which is used in [alert notifications](https://grafana.com/docs/grafana/latest/alerting/configure-notifications/template-notifications/images-in-notifications/). This uses the simplest WebDav server that is still being maintained, a project called [Dufs](https://github.com/sigoden/dufs). ## Using Dufs diff --git a/docs/sources/administration/data-source-management/teamlbac/_index.md b/docs/sources/administration/data-source-management/teamlbac/_index.md index cda80b0cec0..a9bc856fd81 100644 --- a/docs/sources/administration/data-source-management/teamlbac/_index.md +++ b/docs/sources/administration/data-source-management/teamlbac/_index.md @@ -8,67 +8,63 @@ labels: products: - enterprise - cloud -title: Team LBAC +title: Label Based Access Control (LBAC) for data sources weight: 100 --- -# Team LBAC +# Label Based Access Control (LBAC) for data sources -Team Label Based Access Control (LBAC) simplifies and streamlines data source access management based on team memberships. +Label Based Access Control (LBAC) simplifies and streamlines data source access management based on team memberships. {{< admonition type="note" >}} -Creating Team LBAC rules is available for preview for logs with Loki in Grafana Cloud. +LBAC rules is available for preview for logs with Loki in Grafana Cloud. Report any unexpected behavior to the Grafana Support team. -To use Team LBAC rules you must enable the `teamHttpHeaders` feature toggle because the feature uses HTTP headers for the LBAC rules requests. - -- Be sure that you are running Grafana Enterprise. - {{< /admonition >}} +To use LBAC rules you must enable the `teamHttpHeaders` feature toggle because the feature uses HTTP headers for the LBAC rules requests. +{{< /admonition >}} You can configure user access based upon team memberships using LogQL. -Team LBAC controls access to logs depending on the rules set for each team. +LBAC for data sources controls access to logs depending on the rules set for each team. This feature addresses two common challenges faced by Grafana users: 1. Having a high number of Grafana Cloud data sources. - Team LBAC lets Grafana administrators reduce the total number of data sources per instance from hundreds, to one. + LBAC for data sources lets Grafana administrators reduce the total number of data sources per instance from hundreds, to one. 1. Using the same dashboard across multiple teams. - Team LBAC lets Grafana Teams use the same dashboard with different access control rules. + LBAC for data sources lets Grafana Teams use the same dashboard with different access control rules. -To set up Team LBAC for a Loki data source, refer to [Configure Team LBAC](https://grafana.com/docs/grafana//administration/data-source-management/teamlbac/configure-teamlbac-for-loki/). +To set up LBAC for data sources for a Loki data source, refer to [Configure LBAC for data sources](https://grafana.com/docs/grafana//administration/data-source-management/teamlbac/configure-teamlbac-for-loki/). ## Limitations -- There is a set number of rules to be configured within a datasource, depending on the size of the rules. +- There is a set number of rules to be configured within a data source, depending on the size of the rules. - Around ~500-600 rules is the upper limit. -- If there are no Team LBAC rules for a user's team, that user can query all logs. -- If an administrator is part of a team with Team LBAC rules, those rules are applied to the administrator requests. -- Cloud Access Policies (CAP) LBAC rules override Team LBAC rules. - Cloud Access Policies are the access controls from Grafana Cloud. - If there are any CAP LBAC rules configured for the same data source, then only the CAP LBAC rules are applied. +- If there are no LBAC for data sources rules for a user's team, that user can query all logs. +- If an administrator is part of a team with LBAC for data sources rules, those rules are applied to the administrator requests. +- Cloud Access Policy (CAP) LBAC rules override LBAC for data sources rules. + CAP are the access controls from Grafana Cloud. - You must remove any label selectors from your Cloud Access Policies to use Team LBAC. - For more information about CAP label selectors, refer to [Use label-based access control (LBAC) with access policies](https://grafana.com/docs/grafana-cloud/account-management/authentication-and-permissions/access-policies/label-access-policies/). +You must remove any label selectors from your Cloud Access Policy that is configured for the Loki data source, otherwise the CAP label selectors override the LBAC for data sources rules. For more information about CAP label selectors, refer to [Use label-based access control (LBAC) with access policies](https://grafana.com/docs/grafana-cloud/account-management/authentication-and-permissions/access-policies/label-access-policies/). ## Data source permissions -Data source permissions allow the users access to query the data source. -Administrators set the permissions at the data source level. -All the teams and users that are part of the data source inherit those permissions. +- Data source permissions allow the users access to query the data source. +- Administrators set the permissions at the data source level. +- All the teams and users that are part of the data source inherit those permissions. ## Recommended setup -It's recommended that you create a single Loki data source for using Team LBAC rules so you have a clear separation of data sources using Team LBAC and those that aren't. +It's recommended that you create a single Loki data source for using LBAC for data sources rules so you have a clear separation of data sources using LBAC for data sources and those that aren't. All teams should have with only teams having `query` permission. -You should create another Loki data source configured without Team LBAC for full access to the logs. +You should create another Loki data source configured without LBAC for data sources for full access to the logs. -## Team LBAC rules +## LBAC rules -Grafana adds Team LBAC rules to the HTTP request via the Loki data source. +Grafana adds LBAC for data sources rules to the HTTP request via the Loki data source. If you configure multiple rules for a team, each rule is evaluated separately. Query results include lines that match any of the rules. -Only users with data source `Admin` permissions can edit Team LBAC rules in the **Data source permissions** tab because changing LBAC rules requires the same access level as editing data source permissions. +Only users with data source `Admin` permissions can edit LBAC for data sources rules in the **Data source permissions** tab because changing LBAC rules requires the same access level as editing data source permissions. -To set up Team LBAC for a Loki data source, refer to [Configure Team LBAC](https://grafana.com/docs/grafana//administration/data-source-management/teamlbac/configure-teamlbac-for-loki/). +To set up LBAC for data sources for a Loki data source, refer to [Configure LBAC for data sources](https://grafana.com/docs/grafana//administration/data-source-management/teamlbac/configure-teamlbac-for-loki/). diff --git a/docs/sources/administration/data-source-management/teamlbac/configure-teamlbac-for-loki/index.md b/docs/sources/administration/data-source-management/teamlbac/configure-teamlbac-for-loki/index.md index ff8e543c7fa..f6381fb8d43 100644 --- a/docs/sources/administration/data-source-management/teamlbac/configure-teamlbac-for-loki/index.md +++ b/docs/sources/administration/data-source-management/teamlbac/configure-teamlbac-for-loki/index.md @@ -1,5 +1,5 @@ --- -description: Configure Team LBAC for Loki data source on Grafana Cloud +description: Configure LBAC for data sources for Loki data source on Grafana Cloud keywords: - loki - datasource @@ -7,40 +7,39 @@ keywords: labels: products: - cloud -title: Configure Team LBAC for Loki +title: Configure LBAC for data sources for Loki weight: 250 --- -# Configure Team LBAC for Loki data source on Grafana Cloud +# Configure LBAC for data sources for Loki data source on Grafana Cloud -Team LBAC is available in private preview on Grafana Cloud for Loki created with basic authentication. Loki datasources for Team LBAC can only be created, provisioning is currently not available. +LBAC for data sources is available in private preview on Grafana Cloud for Loki created with basic authentication. Loki data sources for LBAC for data sources can only be created, provisioning is currently not available. ## Before you begin -To be able to use Team LBAC rules, you need to enable the feature toggle `teamHttpHeaders` on your Grafana instance. Contact support to enable the feature toggle for you. +To be able to use LBAC for data sources rules, you need to enable the feature toggle `teamHttpHeaders` on your Grafana instance. Contact support to enable the feature toggle for you. -- Be sure that you are running Grafana Enterprise. -- Be sure that you have the permission setup to create a loki tenant in Grafana Cloud +- Be sure that you have the permission setup to create a Loki tenant in Grafana Cloud - Be sure that you have admin data source permissions for Grafana. ### Permissions -We recommend that you remove all permissions for roles and teams that are not required to access the data source. This will help to ensure that only the required teams have access to the data source. The recommended permissions are `Admin` permission and only add the teams `Query` permissions that you want to add Team LBAC rules for. +We recommend that you remove all permissions for roles and teams that are not required to access the data source. This will help to ensure that only the required teams have access to the data source. The recommended permissions are `Admin` permission and only add the teams `Query` permissions that you want to add LBAC for data sources rules for. -## Task 1: Configure Team LBAC for a new Loki data source +## Task 1: LBAC Configuration for New Loki Data Source 1. Access Loki data sources details for your stack through grafana.com -1. Copy Loki Details and Create a CAP +1. Copy Loki details and create a CAP - Copy the details of your Loki setup. - Create a Cloud Access Policy (CAP) for the Loki data source in grafana.com. - Ensure the CAP includes `logs:read` permissions. - Ensure the CAP does not include `labels` rules. -1. Create a New Loki Data Source +1. Create a new Loki data source - In Grafana, proceed to add a new data source and select Loki as the type. 1. Navigate back to the Loki data source - Set up the Loki data source using basic authentication. Use the userID as the username. Use the generated CAP token as the password. - Save and connect. -1. Navigate to Data Source Permissions - - Go to the permissions tab of the newly created Loki data source. Here, you'll find the Team LBAC rules section. +1. Navigate to data source permissions + - Go to the permissions tab of the newly created Loki data source. Here, you'll find the LBAC for data sources rules section. -For more information on how to setup Team LBAC rules for a Loki data source, refer to [Create Team LBAC rules for the Loki data source](https://grafana.com/docs/grafana//administration/data-source-management/teamlbac/create-teamlbac-rules/). +For more information on how to setup LBAC for data sources rules for a Loki data source, refer to [Create LBAC for data sources rules for the Loki data source](https://grafana.com/docs/grafana//administration/data-source-management/teamlbac/create-teamlbac-rules/). diff --git a/docs/sources/administration/data-source-management/teamlbac/create-teamlbac-rules/index.md b/docs/sources/administration/data-source-management/teamlbac/create-teamlbac-rules/index.md index 3cb872a8ed4..cba9d8b3e30 100644 --- a/docs/sources/administration/data-source-management/teamlbac/create-teamlbac-rules/index.md +++ b/docs/sources/administration/data-source-management/teamlbac/create-teamlbac-rules/index.md @@ -1,42 +1,41 @@ --- -description: Learn how to create Team LBAC rules for the Loki data source. +description: Learn how to create LBAC for data sources rules for the Loki data source. keywords: - loki - lbac - team labels: products: - - enterprise - cloud -title: Create Team LBAC rules for the Loki data source +title: Create LBAC for data sources rules for the Loki data source weight: 250 --- -# Create Team LBAC rules for the Loki data source +# Create LBAC for data sources rules for the Loki data source -Team LBAC is available on Cloud for data sources created with basic authentication. Any managed Loki data source can **NOT** be configured with Team LBAC rules. +LBAC for data sources is available on Cloud for Loki data sources created with basic authentication. Managed/Provisioned Loki data source can **NOT** be configured with LBAC for data sources as of now. ## Before you begin -To be able to use Team LBAC rules, you need to enable the feature toggle `teamHttpHeaders` on your Grafana instance. Contact support to enable the feature toggle for you. +To be able to use LBAC for data sources rules, you need to enable the feature toggle `teamHttpHeaders` on your Grafana instance. Contact support to enable the feature toggle for you. -- Be sure that you are running Grafana Enterprise. +- Be sure that you have the permission setup to create a Loki tenant in Grafana Cloud. - Be sure that you have admin data source permissions for Grafana. - Be sure that you have a team setup in Grafana. -### Create a Team LBAC Rule for a team +### Create a LBAC for data sources Rule for a team -1. Navigate to your Loki datasource +1. Navigate to your Loki data source 1. Navigate to the permissions tab - - Here, you'll find the Team LBAC rules section. -1. Add a Team LBAC Rule - - Add a new rule for the team in the Team LBAC rules section. -1. Define Label Selector for the Rule + - Here, you'll find the LBAC for data sources rules section. +1. Add a LBAC for data sources Rule + - Add a new rule for the team in the LBAC for data sources rules section. +1. Define a label selector for the rule - Add a label selector to the rule. Refer to Loki query documentation for guidance on the types of log selections you can specify. ### LBAC rule -A LBAC rule is a `logql` query that runs as a query to the loki instance for your logs. Each rule is it's own filtering operating independently from the other rules within a team. For example, you can create a label policy that includes all log lines with the label. +A LBAC rule is a `logql` query that runs as a query to the Loki instance for your logs. Each rule operates independently as its own filter, separate from other rules within a team. For example, you can create a label policy that includes all log lines with a specific label. One rule `{namespace="dev", cluster="us-west-0"}` created with multiple namespaces will be seen as `namespace="dev"` **AND** `cluster="us-west-0"`. Two rules `{namespace="dev"}`, `{cluster="us-west-0"}` created for a team will be seen as `namespace="dev"` **OR** `cluster="us-west-0"`. @@ -47,11 +46,11 @@ We recommend you only add `query` permissions for teams that should use the data We recommend for a first setup, setting up as few rules as possible for each team and make them additive for simplicity. -For validating the rules, we recommend testing the rules in the Loki Explore view. This will allow you to see the logs that would be returned for the rule. +To validate the rules, we recommend testing the rules in the Loki Explore view. This will allow you to see the logs that would be returned for the rule. #### Tasks -### Task 1: One rule setup for each team +### Task 1: One rule set up for each team One common use case for creating an LBAC policy is to have specific access to logs that have a specific label. For example, you can create a label policy that includes all log lines with the label. @@ -67,9 +66,9 @@ A user that is part of Team B will have access to logs that match `namespace="pr A user that is part of Team A and Team B will have access to logs that match `namespace="dev"` OR `namespace="prod"`. -### Task 2: One rule setup for a team Exclude a label +### Task 2: Set up a rule to exclude a label for a team -One common use case for creating an LBAC policy is to exclude logs that have a specific label. For example, you can create a label policy that excludes all log lines with the label secret=true by adding a selector with `secret!="true"` when you create an access policy: +One common use case for creating an LBAC policy is to exclude logs that have a specific label. For example, you can create a label policy that excludes all log lines with the label `secret=true` by adding a selector with `secret!="true"` when you create an access policy: We have one team, Team A `Query` permissions. Loki access is setup with `Admin` roles to have `Admin` permission only. @@ -77,7 +76,7 @@ We have one team, Team A `Query` permissions. Loki access is setup with `Admin` A user that is part of Team A will **NOT** have access to logs that match `secret!="true"`. -### Task 3: Multiple rules setup for one team +### Task 3: Set up multiple rules for a team We have two teams, Team A and Team B with `Query` permissions. Loki access is setup with `Admin` roles having `Admin` permission. @@ -113,7 +112,7 @@ A user in Team B will have access to logs that match `namespace!="dev"`. > _NOTE:_ A user that is part of Team A and Team B will have access to all logs that match `namespace="dev"` `OR` `namespace!="dev"`. -### Task 5: One rule setup for a Team +### Task 5: Single rule setup for a team We have two teams, Team A and Team B. Loki access is setup with `Editor`, `Viewer` roles to have `Query` permission. diff --git a/docs/sources/administration/plugin-management/index.md b/docs/sources/administration/plugin-management/index.md index 38d155c65c1..510c488b9ff 100644 --- a/docs/sources/administration/plugin-management/index.md +++ b/docs/sources/administration/plugin-management/index.md @@ -56,7 +56,7 @@ Use app plugins when you want an out-of-the-box monitoring experience. ### Managing access for app plugins -Customize access to app plugins with [RBAC]({{< relref "../roles-and-permissions/access-control/#about-rbac" >}}). +Customize access to app plugins with [RBAC]({{< relref "../roles-and-permissions/access-control/rbac-for-app-plugins" >}}). By default, the Viewer, Editor and Admin roles have access to all app plugins that their Organization role allows them to access. Access is granted by the `fixed:plugins.app:reader` role. diff --git a/docs/sources/administration/provisioning/index.md b/docs/sources/administration/provisioning/index.md index 22526d4a05c..12e3abce9d9 100644 --- a/docs/sources/administration/provisioning/index.md +++ b/docs/sources/administration/provisioning/index.md @@ -506,15 +506,26 @@ The following sections detail the supported settings and secure settings for eac #### Alert notification `MQTT` +| Name | Secure setting | +| ------------- | -------------- | +| brokerUrl | | +| clientId | | +| topic | | +| messageFormat | | +| username | | +| password | yes | +| retain | | +| qos | | +| tlsConfig | | + +##### TLS config + | Name | Secure setting | | ------------------ | -------------- | -| brokerUrl | | -| clientId | | -| topic | | -| messageFormat | -| username | | -| password | yes | | insecureSkipVerify | | +| clientCertificate | yes | +| clientKey | yes | +| caCertificate | yes | #### Alert notification `pagerduty` diff --git a/docs/sources/administration/roles-and-permissions/access-control/_index.md b/docs/sources/administration/roles-and-permissions/access-control/_index.md index 3460f0e60f7..888006f4ac7 100644 --- a/docs/sources/administration/roles-and-permissions/access-control/_index.md +++ b/docs/sources/administration/roles-and-permissions/access-control/_index.md @@ -93,6 +93,11 @@ refs: destination: /docs/grafana//dashboards/manage-dashboards/#folder-permissions - pattern: /docs/grafana-cloud/ destination: /docs/grafana-cloud/visualizations/dashboards/manage-dashboards/#folder-permissions + migrate-api-keys: + - pattern: /docs/grafana/ + destination: /docs/grafana//administration/service-accounts/migrate-api-keys/ + - pattern: /docs/grafana-cloud/ + destination: /docs/grafana-cloud/account-management/authentication-and-permissions/service-accounts/migrate-api-keys/ --- # Role-based access control (RBAC) @@ -172,7 +177,7 @@ Assign fixed roles when the basic roles do not meet your permission requirements - [Alerting](ref:alerting) - [Annotations](ref:dashboards-annotate-visualizations) -- [API keys](/docs/grafana//administration/service-accounts/migrate-api-keys/) +- [API keys](ref:migrate-api-keys) - [Dashboards and folders](ref:dashboards) - [Data sources](ref:data-sources) - [Explore](/docs/grafana//explore/) @@ -185,8 +190,8 @@ Assign fixed roles when the basic roles do not meet your permission requirements - [Provisioning](/docs/grafana//administration/provisioning/) - [Reports](ref:dashboards-create-reports) - [Roles](ref:roles-and-permissions) -- [Settings](/docs/grafana//setup-grafana/configure-grafana/settings-updates-at-runtime/) - [Service accounts](ref:service-accounts) +- [Settings](/docs/grafana//setup-grafana/configure-grafana/settings-updates-at-runtime/) - [Teams](/docs/grafana//administration/team-management/) - [Users](/docs/grafana//administration/user-management/) diff --git a/docs/sources/administration/roles-and-permissions/access-control/custom-role-actions-scopes/index.md b/docs/sources/administration/roles-and-permissions/access-control/custom-role-actions-scopes/index.md index 82b5269f979..396ad7dea81 100644 --- a/docs/sources/administration/roles-and-permissions/access-control/custom-role-actions-scopes/index.md +++ b/docs/sources/administration/roles-and-permissions/access-control/custom-role-actions-scopes/index.md @@ -65,13 +65,13 @@ The following list contains role-based access control actions. | `alert.provisioning.secrets:read` | None | Same as `alert.provisioning:read` plus ability to export resources with decrypted secrets. | | `alert.provisioning:write` | None | Update all Grafana alert rules, notification policies, etc via provisioning API. Permissions to folders and datasource are not required. | | `alert.provisioning.provenance:write` | None | Set provisioning status for alerting resources. Cannot be used alone. Requires user to have permissions to access resources | -| `annotations:create` |
  • `annotations:*`
  • `annotations:type:*`
| Create annotations. | -| `annotations:delete` |
  • `annotations:*`
  • `annotations:type:*`
| Delete annotations. | -| `annotations:read` |
  • `annotations:*`
  • `annotations:type:*`
| Read annotations and annotation tags. | -| `annotations:write` |
  • `annotations:*`
  • `annotations:type:*`
| Update annotations. | -| `apikeys:create` | None | Create API keys. | +| `annotations:create` |
  • `annotations:*`
  • `annotations:type:*`
  • `dashboards:*`
  • `dashboards:uid:*`
  • `folders:*`
  • `folders:uid:*`
| Create annotations. | +| `annotations:delete` |
  • `annotations:*`
  • `annotations:type:*`
  • `dashboards:*`
  • `dashboards:uid:*`
  • `folders:*`
  • `folders:uid:*`
| Delete annotations. | +| `annotations:read` |
  • `annotations:*`
  • `annotations:type:*`
  • `dashboards:*`
  • `dashboards:uid:*`
  • `folders:*`
  • `folders:uid:*`
| Read annotations and annotation tags. | +| `annotations:write` |
  • `annotations:*`
  • `annotations:type:*`
  • `dashboards:*`
  • `dashboards:uid:*`
  • `folders:*`
  • `folders:uid:*`
| Update annotations. | | `apikeys:read` |
  • `apikeys:*`
  • `apikeys:id:*`
| Read API keys. | | `apikeys:delete` |
  • `apikeys:*`
  • `apikeys:id:*`
| Delete API keys. | +| `banners:write` | None | Create [announcement banners](/docs/grafana-cloud/whats-new/2024-09-10-announcement-banner/). | | `dashboards:create` |
  • `folders:*`
  • `folders:uid:*`
| Create dashboards in one or more folders and their subfolders. | | `dashboards:delete` |
  • `dashboards:*`
  • `dashboards:uid:*`
  • `folders:*`
  • `folders:uid:*`
| Delete one or more dashboards. | | `dashboards.insights:read` | None | Read dashboard insights data and see presence indicators. | @@ -182,38 +182,6 @@ The following list contains role-based access control actions. { .no-spacing-list } -### Grafana OnCall action definitions (beta) - -The following list contains role-based access control actions used by Grafana OnCall application plugin. - -| Action | Applicable scopes | Description | -| ------------------------------------------------ | ----------------- | ------------------------------------------------- | -| `grafana-oncall-app.alert-groups:read` | None | Read OnCall alert groups. | -| `grafana-oncall-app.alert-groups:write` | None | Create, edit and delete OnCall alert groups. | -| `grafana-oncall-app.integrations:read` | None | Read OnCall integrations. | -| `grafana-oncall-app.integrations:write` | None | Create, edit and delete OnCall integrations. | -| `grafana-oncall-app.integrations:test` | None | Test OnCall integrations. | -| `grafana-oncall-app.escalation-chains:read` | None | Read OnCall escalation chains. | -| `grafana-oncall-app.escalation-chains:write` | None | Create, edit and delete OnCall escalation chains. | -| `grafana-oncall-app.schedules:read` | None | Read OnCall schedules. | -| `grafana-oncall-app.schedules:write` | None | Create, edit and delete OnCall schedules. | -| `grafana-oncall-app.schedules:export` | None | Export OnCall schedules. | -| `grafana-oncall-app.chatops:read` | None | Read OnCall ChatOps. | -| `grafana-oncall-app.chatops:write` | None | Edit OnCall ChatOps. | -| `grafana-oncall-app.chatops:update-settings` | None | Edit OnCall ChatOps settings. | -| `grafana-oncall-app.maintenance:read` | None | Read OnCall maintenance. | -| `grafana-oncall-app.maintenance:write` | None | Edit OnCall maintenance. | -| `grafana-oncall-app.api-keys:read` | None | Read OnCall API keys. | -| `grafana-oncall-app.api-keys:write` | None | Create, edit and delete OnCall API keys. | -| `grafana-oncall-app.notifications:read` | None | Receive OnCall notifications. | -| `grafana-oncall-app.notification-settings:read` | None | Read OnCall notification settings. | -| `grafana-oncall-app.notification-settings:write` | None | Edit OnCall notification settings. | -| `grafana-oncall-app.user-settings:read` | None | Read user's own OnCall user settings. | -| `grafana-oncall-app.user-settings:write` | None | Edit user's own OnCall user settings. | -| `grafana-oncall-app.user-settings:admin` | None | Read and edit all users' OnCall user settings. | -| `grafana-oncall-app.other-settings:read` | None | Read OnCall settings. | -| `grafana-oncall-app.other-settings:write` | None | Edit OnCall settings. | - ### Grafana Adaptive Metrics action definitions The following list contains role-based access control actions used by Grafana Adaptive Metrics. diff --git a/docs/sources/administration/roles-and-permissions/access-control/manage-rbac-roles/index.md b/docs/sources/administration/roles-and-permissions/access-control/manage-rbac-roles/index.md index ab631eb44c2..5594ec08ef7 100644 --- a/docs/sources/administration/roles-and-permissions/access-control/manage-rbac-roles/index.md +++ b/docs/sources/administration/roles-and-permissions/access-control/manage-rbac-roles/index.md @@ -74,8 +74,6 @@ refs: Available in [Grafana Enterprise](/docs/grafana//introduction/grafana-enterprise/) and [Grafana Cloud](/docs/grafana-cloud). {{% /admonition %}} -{{< table-of-contents >}} - This section includes instructions for how to view permissions associated with roles, create custom roles, and update and delete roles. The following example includes the base64 username:password Basic Authorization. You cannot use authorization tokens in the request. diff --git a/docs/sources/administration/roles-and-permissions/access-control/rbac-for-app-plugins/index.md b/docs/sources/administration/roles-and-permissions/access-control/rbac-for-app-plugins/index.md new file mode 100644 index 00000000000..020bf9c982c --- /dev/null +++ b/docs/sources/administration/roles-and-permissions/access-control/rbac-for-app-plugins/index.md @@ -0,0 +1,82 @@ +--- +aliases: + - ../../../enterprise/access-control/rbac-for-app-plugins/ +description: Learn about how to configure access to app plugins using RBAC +labels: + products: + - cloud +menuTitle: RBAC for app plugins +title: RBAC for app plugins +weight: 90 +refs: + manage-rbac-roles-update-basic-role-permissions: + - pattern: /docs/grafana/ + destination: /docs/grafana//administration/roles-and-permissions/access-control/manage-rbac-roles/#update-basic-role-permissions + - pattern: /docs/grafana-cloud/ + destination: /docs/grafana-cloud/account-management/authentication-and-permissions/access-control/manage-rbac-roles/#update-basic-role-permissions + restrict-access-to-app-plugin-example: + - pattern: /docs/grafana/ + destination: /docs/grafana//administration/roles-and-permissions/access-control/plan-rbac-rollout-strategy/#prevent-viewers-from-accessing-an-app-plugin + - pattern: /docs/grafana-cloud/ + destination: /docs/grafana-cloud/account-management/authentication-and-permissions/access-control/plan-rbac-rollout-strategy/#prevent-viewers-from-accessing-an-app-plugin + adaptive-metrics-permissions: + - pattern: /docs/grafana/ + destination: /docs/grafana//administration/roles-and-permissions/access-control/custom-role-actions-scopes/#grafana-adaptive-metrics-action-definitions + - pattern: /docs/grafana-cloud/ + destination: /docs/grafana-cloud/account-management/authentication-and-permissions/access-control/custom-role-actions-scopes/#grafana-adaptive-metrics-action-definitions + rbac-role-definitions: + - pattern: /docs/grafana/ + destination: /docs/grafana//administration/roles-and-permissions/access-control/rbac-fixed-basic-role-definitions/ + - pattern: /docs/grafana-cloud/ + destination: /docs/grafana-cloud/account-management/authentication-and-permissions/access-control/rbac-fixed-basic-role-definitions/ +--- + +# RBAC for app plugins + +{{% admonition type="note" %}} +Available in [Grafana Cloud](/docs/grafana-cloud). +{{% /admonition %}} + +RBAC can be used to manage access to [app plugins](https://grafana.com/docs/grafana/latest/administration/plugin-management/#app-plugins). +Each app plugin grants the basic Viewer, Editor and Admin organization roles a default set of plugin permissions. +You can use RBAC to restrict which app plugins a basic organization role has access to. +Some app plugins have fine-grained RBAC support, which allows you to grant additional access to these app plugins to teams and users regardless of their basic organization roles. + +## Restricting access to app plugins + +By default, Viewers, Editors and Admins have access to all App Plugins that their organization role allows them to access. +To change this default behavior and prevent a basic organization role from accessing an App plugin, you must [update the basic role's permissions](ref:manage-rbac-roles-update-basic-role-permissions). +See an example of [preventing Viewers from accessing an app plugin](ref:restrict-access-to-app-plugin-example) to learn more. +To grant access to a limited set of app plugins, you will need plugin IDs. You can find them in `plugin.json` files or in the URL when you open the app plugin in the Grafana Cloud UI. + +Note that unless an app plugin has fine-grained RBAC support, it is not possible to grant access to this app plugin for a user whose organization role does not have access to that app plugin. + +## Fine-grained access to app plugins + +Plugins with fine-grained RBAC support allow you to manage access to plugin features at a more granular level. +For instance, you can grant admin access to an app plugin to a user with Viewer organization role. Or restrict the Editor organization role from being able to edit plugin resources. + +Please refer to plugin documentation to see what RBAC permissions the plugin has and what default access the plugin grants to Viewer, Editor and Admin organization roles. + +The following list contains app plugins that have fine-grained RBAC support. + +| App plugin | App plugin ID | App plugin permission documentation | +| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| [Access policies](https://grafana.com/docs/grafana-cloud/account-management/authentication-and-permissions/access-policies/) | `grafana-auth-app` | n/a | +| [Adaptive metrics](https://grafana.com/docs/grafana-cloud/cost-management-and-billing/reduce-costs/metrics-costs/control-metrics-usage-via-adaptive-metrics/adaptive-metrics-plugin/) | `grafana-adaptive-metrics-app` | [RBAC actions for Adaptive Metrics](ref:adaptive-metrics-permissions) | +| [Incident](https://grafana.com/docs/grafana-cloud/alerting-and-irm/irm/incident/) | `grafana-incident-app` | n/a | +| [OnCall](https://grafana.com/docs/grafana-cloud/alerting-and-irm/irm/oncall/) | `grafana-oncall-app` | [Configure RBAC for OnCall](https://grafana.com/docs/grafana-cloud/alerting-and-irm/irm/oncall/manage/user-and-team-management/#manage-users-and-teams-for-grafana-oncall) | +| [Performance Testing (K6)](https://grafana.com/docs/grafana-cloud/testing/k6/) | `k6-app` | [Configure RBAC for K6](https://grafana.com/docs/grafana-cloud/testing/k6/projects-and-users/configure-rbac/) | +| [Private data source connect (PDC)](https://grafana.com/docs/grafana-cloud/connect-externally-hosted/private-data-source-connect/) | `grafana-pdc-app` | n/a | +| [Service Level Objective (SLO)](https://grafana.com/docs/grafana-cloud/alerting-and-irm/slo/) | `grafana-slo-app` | [Configure RBAC for SLO](https://grafana.com/docs/grafana-cloud/alerting-and-irm/slo/set-up/rbac/) | + +### Revoke fine-grained access from app plugins + +To list all the permissions granted to a basic role, use the [HTTP API endpoint to query for the role](https://grafana.com/docs/grafana/latest/developers/http_api/access_control/#get-a-role). +Basic role UIDs are listed in [RBAC role definitions list](ref:rbac-role-definitions). +To remove the undesired plugin permissions from a basic role, you must [update the basic role's permissions](ref:manage-rbac-roles-update-basic-role-permissions). + +### Grant additional access to app plugins + +To grant access to app plugins, you can use the predefined [fixed plugin roles](https://grafana.com/docs/grafana/latest/administration/roles-and-permissions/access-control/#fixed-roles) or create [custom roles](https://grafana.com/docs/grafana/latest/administration/roles-and-permissions/access-control/#custom-roles) with specific plugin permissions. +To learn about how to assign an RBAC role, refer to [the documentation on assigning RBAC roles](https://grafana.com/docs/grafana/latest/administration/roles-and-permissions/access-control/assign-rbac-roles/#assign-rbac-roles). diff --git a/docs/sources/administration/roles-and-permissions/access-control/troubleshooting/index.md b/docs/sources/administration/roles-and-permissions/access-control/troubleshooting/index.md index 207c47ac66b..81120f03b63 100644 --- a/docs/sources/administration/roles-and-permissions/access-control/troubleshooting/index.md +++ b/docs/sources/administration/roles-and-permissions/access-control/troubleshooting/index.md @@ -8,7 +8,7 @@ labels: - enterprise menuTitle: Troubleshooting RBAC title: Troubleshooting RBAC -weight: 80 +weight: 100 --- # Troubleshooting RBAC diff --git a/docs/sources/alerting/alerting-rules/create-mimir-loki-managed-recording-rule.md b/docs/sources/alerting/alerting-rules/create-mimir-loki-managed-recording-rule.md index fdaffcd2e39..6208d35c2cd 100644 --- a/docs/sources/alerting/alerting-rules/create-mimir-loki-managed-recording-rule.md +++ b/docs/sources/alerting/alerting-rules/create-mimir-loki-managed-recording-rule.md @@ -16,7 +16,7 @@ labels: - cloud - enterprise - oss -title: Create recording rules +title: Configure recording rules weight: 300 refs: configure-grafana: @@ -29,22 +29,31 @@ refs: destination: /docs/grafana-cloud/alerting-and-irm/alerting/fundamentals/alert-rules/annotation-label/ --- -# Create recording rules +# Configure recording rules + +{{< admonition type="note" >}} +In Grafana Cloud, you can only create data source-managed recording rules. + +In Grafana OSS and Enterprise, you can create both Grafana-managed and data source-managed recording rules if you enable the `grafanaManagedRecordingRules` feature flag. + +For more information on enabling feature toggles, refer to [Configure feature toggles](https://grafana.com/docs/grafana//setup-grafana/configure-grafana/feature-toggles). +{{< /admonition >}} -You can create and manage recording rules for an external Grafana Mimir or Loki instance. Recording rules calculate frequently needed expressions or computationally expensive expressions in advance and save the result as a new set of time series. Querying this new time series is faster, especially for dashboards since they query the same expression every time the dashboards refresh. For more information on recording rules in Prometheus, refer to [Defining recording rules in Prometheus](https://prometheus.io/docs/prometheus/latest/configuration/recording_rules/). -**Note:** - Recording rules are run as instant rules, which means that they run every 10s. To overwrite this configuration, update the min_interval in your custom configuration file. [min_interval](ref:configure-grafana) sets the minimum interval to enforce between rule evaluations. The default value is 10s which equals the scheduler interval. Rules will be adjusted if they are less than this value or if they are not multiple of the scheduler interval (10s). Higher values can help with resource management as fewer evaluations are scheduled over time. This setting has precedence over each individual rule frequency. If a rule frequency is lower than this value, then this value is enforced. -## Before you begin +## Configure data source-managed recording rules + +Configure data source-managed recording rules. + +### Before you begin - Verify that you have write permission to the Prometheus or Loki data source. Otherwise, you will not be able to create or update Grafana Mimir managed alerting rules. @@ -54,25 +63,122 @@ This setting has precedence over each individual rule frequency. If a rule frequ - **Grafana Mimir** - use the `/prometheus` prefix. The Prometheus data source supports both Grafana Mimir and Prometheus, and Grafana expects that both the [Query API](/docs/mimir/latest/operators-guide/reference-http-api/#querier--query-frontend) and [Ruler API](/docs/mimir/latest/operators-guide/reference-http-api/#ruler) are under the same URL. You cannot provide a separate URL for the Ruler API. -## Create recording rules - -To create recording rules, follow these steps. +To configure data-source managed recording rules, complete the following steps. 1. Click **Alerts & IRM** -> **Alerting** -> **Alert rules**. -1. Select **Rule type** -> **Recording**. -1. Click **+New recording rule**. +1. Scroll to the **Data source-managed section** and click **+New recording rule**. -1. Enter recording rule name. +#### Enter recording rule name - The recording rule name must be a Prometheus metric name and contain no whitespace. +The recording rule name must be a Prometheus metric name and contain no whitespace. + +#### Define recording rule + +Select your data source and enter a query. + +#### Add namespace and group + +1. From the **Namespace** dropdown, select an existing rule namespace or add a new one. + + Namespaces can contain one or more rule groups and only have an organizational purpose. + +1. From the **Group** dropdown, select an existing group within the selected namespace or add a new one. + + Newly created rules are appended to the end of the group. Rules within a group are run sequentially at a regular interval, with the same evaluation time. + +#### Add labels + +1. Add custom labels selecting existing key-value pairs from the drop down, or add new labels by entering the new key or value. -1. Define recording rule. - - Select your Loki or Prometheus data source. - - Enter a query. -1. Add namespace and group. - - From the **Namespace** dropdown, select an existing rule namespace or add a new one. Namespaces can contain one or more rule groups and only have an organizational purpose. - - From the **Group** dropdown, select an existing group within the selected namespace or add a new one. Newly created rules are appended to the end of the group. Rules within a group are run sequentially at a regular interval, with the same evaluation time. -1. Add labels. - - Add custom labels selecting existing key-value pairs from the drop down, or add new labels by entering the new key or value . 1. Click **Save rule** to save the rule or **Save rule and exit** to save the rule and go back to the Alerting page. + +## Configure Grafana-managed recording rules + +Configure Grafana-managed recording rules. + +{{< admonition type="note" >}} +This feature is only available for Grafana OSS and Enterprise users. It is not available in Grafana Cloud. +{{< /admonition >}} + +### Before you begin + +- Enable the `grafanaManagedRecordingRules` [feature flag](https://grafana.com/docs/grafana//setup-grafana/configure-grafana/feature-toggles/). + +To configure Grafana-managed recording rules, complete the following steps. + +1. Click **Alerts & IRM** -> **Alerting** -> + **Alert rules**. +1. Scroll to the **Grafana-managed section** and click **+New recording rule**. + +#### Enter a recording rule and metric name + +Enter a names to identify your recording rule and metric. The metric name must be a Prometheus metric name and contain no whitespace. + +For more information, refer to [Metrics and labels](https://prometheus.io/docs/concepts/data_model/#metric-names-and-labels). + +#### Define recording rule + +Define a query to get the data you want to measure and a condition that needs to be met before an alert rule fires. + +1. Select a data source. +1. From the **Options** dropdown, specify a time range. + +{{< admonition type="note" >}} +Grafana Alerting only supports fixed relative time ranges, for example, `now-24hr: now`. + +It does not support absolute time ranges: `2021-12-02 00:00:00 to 2021-12-05 23:59:592` or semi-relative time ranges: `now/d to: now`. +{{< /admonition >}} + +1. Add a query. + + To add multiple queries, click **Add query**. + + All alert rules are managed by Grafana by default. If you want to switch to a data source-managed alert rule, click **Switch to data source-managed alert rule**. + +2. Add one or more [expressions]. + + a. For each expression, select either **Classic condition** to create a single alert rule, or choose from the **Math**, **Reduce**, and **Resample** options to generate separate alert for each series. + + {{% admonition type="note" %}} + When using Prometheus, you can use an instant vector and built-in functions, so you don't need to add additional expressions. + {{% /admonition %}} + + b. Click **Preview** to verify that the expression is successful. + +3. To add a recovery threshold, turn the **Custom recovery threshold** toggle on and fill in a value for when your alert rule should stop firing. + + You can only add one recovery threshold in a query and it must be the alert condition. + +4. Click **Set as alert condition** on the query or expression you want to set as your alert condition. + +#### Set evaluation behavior + +Use alert rule evaluation to determine how frequently an alert rule should be evaluated and how quickly it should change its state. + +To do this, you need to make sure that your alert rule is in the right evaluation group and set a pending period time that works best for your use case. + +1. Select a folder or click **+ New folder**. +1. Select an evaluation group or click **+ New evaluation group**. + + If you are creating a new evaluation group, specify the interval for the group. + + All rules within the same group are evaluated concurrently over the same time interval. + +1. Enter a pending period. + + The pending period is the period in which an alert rule can be in breach of the condition until it fires. + + Once a condition is met, the alert goes into the **Pending** state. If the condition remains active for the duration specified, the alert transitions to the **Firing** state, else it reverts to the **Normal** state. + +1. Turn on pause alert notifications, if required. + + {{< admonition type="note" >}} + You can pause alert rule evaluation to prevent noisy alerting while tuning your alerts. + Pausing stops alert rule evaluation and doesn't create any alert instances. + This is different to mute timings, which stop notifications from being delivered, but still allows for alert rule evaluation and the creation of alert instances. + {{< /admonition >}} + +#### Add labels + +Add labels to your rule for searching, silencing, or routing to a notification policy. diff --git a/docs/sources/alerting/alerting-rules/create-mimir-loki-managed-rule.md b/docs/sources/alerting/alerting-rules/create-mimir-loki-managed-rule.md index a213449754f..4e691b8a960 100644 --- a/docs/sources/alerting/alerting-rules/create-mimir-loki-managed-rule.md +++ b/docs/sources/alerting/alerting-rules/create-mimir-loki-managed-rule.md @@ -43,33 +43,35 @@ refs: # Configure data source-managed alert rules -Create alert rules for an external Grafana Mimir or Loki instance that has ruler API enabled; these are called data source-managed alert rules. +Create data source-managed alert rules for Grafana Mimir or Grafana Loki data sources, which have been configured to support rule creation. + +To configure your Grafana Mimir or Loki data source for alert rule creation, enable either the Loki Ruler API or the Mimir Ruler API. + +For more information, refer to [Loki Ruler API](/docs/loki//api/#ruler) or [Mimir Ruler API](/docs/mimir//references/http-api/#ruler). **Note**: -Alert rules for an external Grafana Mimir or Loki instance can be edited or deleted by users with Editor or Admin roles. +Alert rules for a Grafana Mimir or Loki instance can be edited or deleted by users with Editor or Admin roles. If you delete an alerting resource created in the UI, you can no longer retrieve it. To make a backup of your configuration and to be able to restore deleted alerting resources, create your alerting resources using file provisioning, Terraform, or the Alerting API. ## Before you begin -- Verify that you have write permission to the Prometheus or Loki data source. Otherwise, you will not be able to create or update Grafana Mimir managed alert rules. +- Verify that you have write permission to the Mimir or Loki data source. Otherwise, you cannot create or update Grafana Mimir or Loki-managed alert rules. -- For Grafana Mimir and Loki data sources, enable the Ruler API by configuring their respective services. +- Enable the Mimir or Loki Ruler API. - **Loki** - The `local` rule storage type, default for the Loki data source, supports only viewing of rules. To edit rules, configure one of the other rule storage types. - **Grafana Mimir** - use the `/prometheus` prefix. The Prometheus data source supports both Grafana Mimir and Prometheus, and Grafana expects that both the [Query API](/docs/mimir/latest/operators-guide/reference-http-api/#querier--query-frontend) and [Ruler API](/docs/mimir/latest/operators-guide/reference-http-api/#ruler) are under the same URL. You cannot provide a separate URL for the Ruler API. -Watch this video to learn more about how to create a Mimir managed alert rule: {{< vimeo 720001865 >}} +Watch this video to learn more about how to create a Mimir-managed alert rule: {{< vimeo 720001865 >}} {{% admonition type="note" %}} -If you do not want to manage alert rules for a particular Loki or Prometheus data source, go to its settings and clear the **Manage alerts via Alerting UI** checkbox. +If you do not want to manage alert rules for a particular Loki or Mimir data source, go to its settings and clear the **Manage alerts via Alerting UI** checkbox. {{% /admonition %}} -In the following sections, we’ll guide you through the process of creating your data source-managed alert rules. - To create a data source-managed alert rule, use the in-product alert creation flow and follow these steps to help you. ## Set alert rule name diff --git a/docs/sources/alerting/configure-notifications/template-notifications/create-notification-templates.md b/docs/sources/alerting/configure-notifications/template-notifications/create-notification-templates.md index 42c39fcda40..0e6d8a78eb3 100644 --- a/docs/sources/alerting/configure-notifications/template-notifications/create-notification-templates.md +++ b/docs/sources/alerting/configure-notifications/template-notifications/create-notification-templates.md @@ -53,7 +53,7 @@ To create a notification template that contains more than one template: ## Preview notification templates -Preview how your notification templates will look before using them in your contact points, helping you understand the result of the template you are creating as well as enabling you to fix any errors before saving it. +Preview how your notification templates should look before using them in your contact points, helping you understand the result of the template you are creating as well as enabling you to fix any errors before saving it. **Note:** This feature is only for Grafana Alertmanager. @@ -81,7 +81,7 @@ To preview your notification templates: c. Click **Add alert data**. - d. Click **Refresh preview** to see what your template content will look like and the corresponding payload data. + d. Click **Refresh preview** to see what your template content should look like and the corresponding payload data. If there are any errors in your template, they are displayed in the Preview and you can correct them before saving. @@ -162,6 +162,86 @@ Resolved alerts: {{ template "email.message" . }} ``` +## Group multiple alert instances into one email notification + +To make alerts more concise, you can group multiple instances of a firing alert into a single email notification in a table format. This way, you avoid long, repetitive emails and make alerts easier to digest. + +Follow these steps to create a custom notification template that consolidates alert instances into a table. + +1. Modify the alert rule to include an annotation that is referenced in the notification template later on. +1. Enter a name for the **custom annotation**: In this example, _ServerInfo_. +1. Enter the following code as the value for the annotation. It retrieves the server's instance name and a corresponding metric value, formatted as a table row: + + ``` + {{ index $labels "instance" }}{{- "\t" -}}{{ index $values "A"}}{{- "\n" -}} + ``` + + This line of code returns the labels and their values in the form of a table. Assuming $labels has `{"instance": "node1"}` and $values has `{"A": "123"}`, the output would be: + + ``` + node1 123 + ``` + +1. Create a notification template that references the _ServerInfo_ annotation. + + ```go + {{ define "Table" }} + {{- "\nHost\t\tValue\n" -}} + {{ range .Alerts -}} + {{ range .Annotations.SortedPairs -}} + {{ if (eq .Name "ServerInfo") -}} + {{ .Value -}} + {{- end }} + {{- end }} + {{- end }} + {{ end }} + ``` + + The notification template outputs a list of server information from the "ServerInfo" annotation for each alert instance. + +1. Navigate to your contact point in Grafana +1. In the **Message** field, reference the template by name (see **Optional Email settings** section): + + ``` + {{ template "Table" . }} + ``` + + This generates a neatly formatted table in the email, grouping information for all affected servers into a single notification. + +## Conditional notification template + +Template alert notifications based on a label. In this example the label represents a namespace. + +1. Use the following code in your notification template to display different messages based on the namespace: + + ```go + {{ define "my_conditional_notification" }} + {{ if eq .CommonLabels.namespace "namespace-a" }} + Alert: CPU limits have reached 80% in namespace-a. + {{ else if eq .CommonLabels.namespace "namespace-b" }} + Alert: CPU limits have reached 80% in namespace-b. + {{ else if eq .CommonLabels.namespace "namespace-c" }} + Alert: CPU limits have reached 80% in namespace-c. + {{ else }} + Alert: CPU limits have reached 80% for {{ .CommonLabels.namespace }} namespace. + {{ end }} + {{ end }} + ``` + + `.CommonLabels` is a map containing the labels that are common to all the alerts firing. + + Make sure to replace the `.namespace` label with a label that exists in your alert rule. + +1. Replace `namespace-a`, `namespace-b`, and `namespace-c` with your specific namespace values. +1. Navigate to your contact point in Grafana +1. In the **Message** field, reference the template by name (see **Optional settings** section): + + ``` + {{ template "my_conditional_notification" . }} + ``` + + This template alters the content of alert notifications depending on the namespace value. + ## Template the title of a Slack message Template the title of a Slack message to contain the number of firing and resolved alerts: diff --git a/docs/sources/alerting/fundamentals/alert-rules/_index.md b/docs/sources/alerting/fundamentals/alert-rules/_index.md index 5cdc3b5442a..b948591475f 100644 --- a/docs/sources/alerting/fundamentals/alert-rules/_index.md +++ b/docs/sources/alerting/fundamentals/alert-rules/_index.md @@ -69,7 +69,7 @@ Grafana supports two different alert rule types: Grafana-managed alert rules and ## Grafana-managed alert rules -Grafana-managed alert rules are the most flexible alert rule type. They allow you to create alerts that can act on data from any of the [supported data sources](#supported-data-sources), and use multiple data sources in a single alert rule. +Grafana-managed alert rules are the most flexible alert rule type. They allow you to create alert rules that can act on data from any of the [supported data sources](#supported-data-sources), and use multiple data sources in a single alert rule. You can also add expressions to transform your data and set alert conditions. Using images in alert notifications is also supported. Additionally, you can also add [expressions to transform your data](ref:expression-queries), set custom alert conditions, and include [images in alert notifications](ref:notification-images). @@ -87,9 +87,11 @@ Find the public data sources supporting Alerting in the [Grafana Plugins directo ## Data source-managed alert rules -Data source-managed alert rules can improve query performance via [recording rules](#recording-rules) and ensure high-availability and fault tolerance when implementing a distributed architecture. +Data source-managed alert rules can be used for Grafana Mimir or Grafana Loki data sources which have been configured to support rule creation. -They are only supported for Prometheus-based or Loki data sources with the Ruler API enabled. For more information, refer to the [Loki Ruler API](/docs/loki//api/#ruler) or [Mimir Ruler API](/docs/mimir//references/http-api/#ruler). +They can improve query performance via [recording rules](#recording-rules) and ensure high-availability and fault tolerance when implementing a distributed architecture. + +They are only supported for Grafana Mimir or Grafana Loki data sources with the Ruler API enabled. For more information, refer to the [Loki Ruler API](/docs/loki//api/#ruler) or [Mimir Ruler API](/docs/mimir//references/http-api/#ruler). {{< figure src="/media/docs/alerting/mimir-managed-alerting-architecture-v2.png" max-width="750px" caption="Mimir-managed alerting architecture" >}} @@ -98,7 +100,7 @@ They are only supported for Prometheus-based or Loki data sources with the Ruler 1. Alert rules are evaluated by the Alert Rule Evaluation Engine. 1. Firing and resolved alert instances are forwarded to [handle their notifications](ref:notifications). -### Recording rules +## Recording rules A recording rule allows you to pre-compute frequently needed or computationally expensive expressions and save their result as a new set of time series. This is useful if you want to run alerts on aggregated data or if you have dashboards that query computationally expensive expressions repeatedly. @@ -114,7 +116,7 @@ When choosing which alert rule type to use, consider the following comparison be | ------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | | Create alert rules based on data from any of the supported data sources | Yes | No. You can only create alert rules that are based on Prometheus-based data. | | Mix and match data sources | Yes | No | -| Includes support for recording rules | No | Yes | +| Includes support for recording rules | Yes. Only for Grafana OSS users with the `grafanaManagedRecordingRules` feature flag enabled. | Yes | | Add expressions to transform your data and set alert conditions | Yes | No | | Use images in alert notifications | Yes | No | | Organization | Organize and manage access with folders | Use namespaces | diff --git a/docs/sources/alerting/set-up/configure-alertmanager/index.md b/docs/sources/alerting/set-up/configure-alertmanager/index.md index 7b20693e689..86956ad6837 100644 --- a/docs/sources/alerting/set-up/configure-alertmanager/index.md +++ b/docs/sources/alerting/set-up/configure-alertmanager/index.md @@ -34,23 +34,27 @@ Grafana Alerting is based on the architecture of the Prometheus alerting system. {{< figure src="/media/docs/alerting/alerting-alertmanager-architecture.png" max-width="750px" alt="A diagram with the alert generator and alert manager architecture" >}} -Grafana has its own pre-configured Alertmanager, referred to as "Grafana" in the user interface: +**Grafana Alertmanager** -- **Grafana Alertmanager** is the default internal Alertmanager if you run Grafana on-premises or as open source. It can receive alerts from Grafana but cannot receive alerts from external alert generators such as Mimir or Loki. +Grafana has its own built-in Alertmanager, referred to as "Grafana" in the user interface. It is the default Alertmanager and can only handle Grafana-managed alerts. -- **Cloud Alertmanager** runs in Grafana Cloud and can receive Grafana-managed alerts and Data sources-managed alerts like Mimir, Loki, and Prometheus. +**Cloud Alertmanager** -Grafana Alerting also supports sending alerts to **External Alertmanagers**, such as the [Prometheus Alertmanager](https://prometheus.io/docs/alerting/latest/alertmanager/), which can receive alerts from Grafana, Loki, Mimir, and Prometheus. +Each Grafana Cloud instance comes preconfigured with an additional Alertmanager (`grafanacloud-STACK_NAME-ngalertmanager`) from the Mimir (Prometheus) instance running in the Grafana Cloud Stack. The Cloud Alertmanager can handle both Grafana-managed and data source-managed alerts. -You can use both internal and external Alertmanagers. The decision often depends on your alerting setup and where your alerts are being generated. Here are two examples of when you may want to [add an external Alertmanager](#add-an-external-alertmanager) and send your alerts there instead of the default Grafana Alertmanager: +**Other Alertmanagers** + +Grafana Alerting also supports sending alerts to other alertmanagers, such as the [Prometheus Alertmanager](https://prometheus.io/docs/alerting/latest/alertmanager/), which can handle Grafana-managed alerts and data sources-managed alerts such as alerts from Loki, Mimir, and Prometheus. + +You can use a combination of Alertmanagers. The decision often depends on your alerting setup and where your alerts are being generated. Here are two examples of when you may want to add an Alertmanager and send your alerts there instead of using the built-in Grafana Alertmanager. 1. You may already have Alertmanagers on-premises in your own Cloud infrastructure that you still want to use because you have other alert generators, such as Prometheus. 2. You want to use both Prometheus on-premises and hosted Grafana to send alerts to the same Alertmanager that runs in your Cloud infrastructure. -## Add an external Alertmanager +## Add an Alertmanager -From Grafana, you can configure and administer your own external Alertmanager to receive Grafana alerts. +From Grafana, you can configure and administer your own Alertmanager to receive Grafana alerts. {{% admonition type="note" %}} Grafana Alerting does not support sending alerts to the AWS Managed Service for Prometheus due to the lack of sigv4 support in Prometheus. @@ -60,23 +64,23 @@ After you have added the Alertmanager, you can use the Grafana Alerting UI to ma {{< figure src="/media/docs/alerting/alerting-choose-alertmanager.png" max-width="750px" alt="A screenshot choosing an Alertmanager in the notification policies UI" >}} -External alertmanagers should now be configured as data sources using Grafana Configuration from the main Grafana navigation menu. This enables you to manage the contact points and notification policies of external alertmanagers from within Grafana and also encrypts HTTP basic authentication credentials. +Alertmanagers should now be configured as data sources using Grafana Configuration from the main Grafana navigation menu. This enables you to manage the contact points and notification policies of external alertmanagers from within Grafana and also encrypts HTTP basic authentication credentials. -To add an external Alertmanager, complete the following steps. +To add an Alertmanager, complete the following steps. 1. Click **Connections** in the left-side menu. -1. On the Connections page, search for `Alertmanager`. -1. Click the **Create a new data source** button. +2. On the Connections page, search for `Alertmanager`. +3. Click the **Create a new data source** button. If you don't see this button, you may need to install the plugin, relaunch your Cloud instance, and then repeat steps 1 and 2. -1. Fill out the fields on the page, as required. +4. Fill out the fields on the page, as required. If you are provisioning your data source, set the flag `handleGrafanaManagedAlerts` in the `jsonData` field to `true` to send Grafana-managed alerts to this Alertmanager. **Note:** Prometheus, Grafana Mimir, and Cortex implementations of Alertmanager are supported. For Prometheus, contact points and notification policies are read-only in the Grafana Alerting UI. -1. Click **Save & test**. +5. Click **Save & test**. {{< admonition type="note" >}} On the Settings page, you can manage your Alertmanager configurations and configure where Grafana-managed alert instances are forwarded. diff --git a/docs/sources/alerting/set-up/configure-high-availability/_index.md b/docs/sources/alerting/set-up/configure-high-availability/_index.md index 6df014f9808..c8af083643c 100644 --- a/docs/sources/alerting/set-up/configure-high-availability/_index.md +++ b/docs/sources/alerting/set-up/configure-high-availability/_index.md @@ -60,6 +60,7 @@ Since gossiping of notifications and silences uses both TCP and UDP port `9094`, You must have at least one (1) Grafana instance added to the `ha_peers` section. 1. Set `[ha_listen_address]` to the instance IP address using a format of `host:port` (or the [Pod's](https://kubernetes.io/docs/concepts/workloads/pods/) IP in the case of using Kubernetes). By default, it is set to listen to all interfaces (`0.0.0.0`). +1. Set `[ha_advertise_address]` to the instance's hostname or IP address in the format "host:port". Use this setting when the instance is behind NAT (Network Address Translation), such as in Docker Swarm or Kubernetes service, where external and internal addresses differ. This address helps other cluster instances communicate with it. The setting is optional. 1. Set `[ha_peer_timeout]` in the `[unified_alerting]` section of the custom.ini to specify the time to wait for an instance to send a notification via the Alertmanager. The default value is 15s, but it may increase if Grafana servers are located in different geographic regions or if the network latency between them is high. For a demo, see this [example using Docker Compose](https://github.com/grafana/alerting-ha-docker-examples/tree/main/memberlist). @@ -75,6 +76,7 @@ database for HA and cannot support the meshing of all Grafana servers. 1. Optional: Set the username and password if authentication is enabled on the Redis server using `ha_redis_username` and `ha_redis_password`. 1. Optional: Set `ha_redis_prefix` to something unique if you plan to share the Redis server with multiple Grafana instances. 1. Optional: Set `ha_redis_tls_enabled` to `true` and configure the corresponding `ha_redis_tls_*` fields to secure communications between Grafana and Redis with Transport Layer Security (TLS). +1. Set `[ha_advertise_address]` to `ha_advertise_address = "${POD_IP}:9094"` This is required if the instance doesn't have an IP address that is part of RFC 6890 with a default route. For a demo, see this [example using Docker Compose](https://github.com/grafana/alerting-ha-docker-examples/tree/main/redis). diff --git a/docs/sources/alerting/set-up/provision-alerting-resources/file-provisioning/index.md b/docs/sources/alerting/set-up/provision-alerting-resources/file-provisioning/index.md index f90c5bfa9db..11d92dc2243 100644 --- a/docs/sources/alerting/set-up/provision-alerting-resources/file-provisioning/index.md +++ b/docs/sources/alerting/set-up/provision-alerting-resources/file-provisioning/index.md @@ -362,8 +362,20 @@ settings: username: grafana # password: password1 + # + qos: 0 # - insecureSkipVerify: false + retain: false + # + tlsConfig: + # + insecureSkipVerify: false + # + clientCertificate: certificate in PEM format + # + clientKey: key in PEM format + # + caCertificate: CA certificate in PEM format ``` {{< /collapse >}} diff --git a/docs/sources/alerting/set-up/provision-alerting-resources/terraform-provisioning/index.md b/docs/sources/alerting/set-up/provision-alerting-resources/terraform-provisioning/index.md index c0dc488f100..724e4bdf3da 100644 --- a/docs/sources/alerting/set-up/provision-alerting-resources/terraform-provisioning/index.md +++ b/docs/sources/alerting/set-up/provision-alerting-resources/terraform-provisioning/index.md @@ -60,6 +60,8 @@ refs: service-accounts: - pattern: /docs/ destination: /docs/grafana//administration/service-accounts/ + - pattern: /docs/grafana-cloud/ + destination: /docs/grafana-cloud/account-management/authentication-and-permissions/service-accounts/ rbac-role-definitions: - pattern: /docs/ destination: /docs/grafana//administration/roles-and-permissions/access-control/rbac-fixed-basic-role-definitions/ diff --git a/docs/sources/developers/http_api/annotations.md b/docs/sources/developers/http_api/annotations.md index 4d58b32a829..365c717062e 100644 --- a/docs/sources/developers/http_api/annotations.md +++ b/docs/sources/developers/http_api/annotations.md @@ -32,9 +32,12 @@ Annotations are saved in the Grafana database (sqlite, mysql or postgres). Annot See note in the [introduction]({{< ref "#annotations-api" >}}) for an explanation. -| Action | Scope | -| ---------------- | ----------------------- | -| annotations:read | annotations:type: | + +| Action | Scope | +| ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `annotations:read` |
  • `annotations:*`
  • `annotations:type:*`
  • `dashboards:*`
  • `dashboards:uid:*`
  • `folders:*`
  • `folders:uid:*`
| +{ .no-spacing-list } + **Example Request**: @@ -122,9 +125,12 @@ The format for `time` and `timeEnd` should be epoch numbers in millisecond resol See note in the [introduction]({{< ref "#annotations-api" >}}) for an explanation. -| Action | Scope | -| ------------------ | ----------------------- | -| annotations:create | annotations:type: | + +| Action | Scope | +| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `annotations:create` |
  • `annotations:*`
  • `annotations:type:*`
  • `dashboards:*`
  • `dashboards:uid:*`
  • `folders:*`
  • `folders:uid:*`
| +{ .no-spacing-list } + **Required JSON Body Fields** @@ -174,9 +180,9 @@ format (string with multiple tags being separated by a space). See note in the [introduction]({{< ref "#annotations-api" >}}) for an explanation. -| Action | Scope | -| ------------------ | ----------------------------- | -| annotations:create | annotations:type:organization | +| Action | Scope | +| -------------------- | ------------------------------- | +| `annotations:create` | `annotations:type:organization` | **Example Request**: @@ -215,9 +221,12 @@ Updates all properties of an annotation that matches the specified id. To only u See note in the [introduction]({{< ref "#annotations-api" >}}) for an explanation. -| Action | Scope | -| ----------------- | ----------------------- | -| annotations:write | annotations:type: | + +| Action | Scope | +| ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `annotations:write` |
  • `annotations:*`
  • `annotations:type:*`
  • `dashboards:*`
  • `dashboards:uid:*`
  • `folders:*`
  • `folders:uid:*`
| +{ .no-spacing-list } + **Example Request**: @@ -260,9 +269,12 @@ This operation currently supports updating of the `text`, `tags`, `time` and `ti See note in the [introduction]({{< ref "#annotations-api" >}}) for an explanation. -| Action | Scope | -| ----------------- | ----------------------- | -| annotations:write | annotations:type: | + +| Action | Scope | +| ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `annotations:write` |
  • `annotations:*`
  • `annotations:type:*`
  • `dashboards:*`
  • `dashboards:uid:*`
  • `folders:*`
  • `folders:uid:*`
| +{ .no-spacing-list } + **Example Request**: @@ -299,9 +311,12 @@ Deletes the annotation that matches the specified id. See note in the [introduction]({{< ref "#annotations-api" >}}) for an explanation. -| Action | Scope | -| ------------------ | ----------------------- | -| annotations:delete | annotations:type: | + +| Action | Scope | +| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `annotations:delete` |
  • `annotations:*`
  • `annotations:type:*`
  • `dashboards:*`
  • `dashboards:uid:*`
  • `folders:*`
  • `folders:uid:*`
| +{ .no-spacing-list } + **Example Request**: @@ -333,9 +348,9 @@ Find all the event tags created in the annotations. See note in the [introduction]({{< ref "#annotations-api" >}}) for an explanation. -| Action | Scope | -| ---------------- | ----- | -| annotations:read | N/A | +| Action | Scope | +| ------------------ | ----- | +| `annotations:read` | N/A | **Example Request**: diff --git a/docs/sources/developers/http_api/dashboard.md b/docs/sources/developers/http_api/dashboard.md index e51af8a6575..27d33d661e8 100644 --- a/docs/sources/developers/http_api/dashboard.md +++ b/docs/sources/developers/http_api/dashboard.md @@ -43,9 +43,13 @@ Creates a new dashboard or updates an existing dashboard. When updating existing See note in the [introduction]({{< ref "#dashboard-api" >}}) for an explanation. -| Action | Scope | -| ------------------- | ----------- | -| `dashboards:create` | `folders:*` | + +| Action | Scope | +| ------------------- | ------------------------------------------------------------------------------------------------------- | +| `dashboards:create` |
  • `folders:*`
  • `folders:uid:*`
| +| `dashboards:write` |
  • `dashboards:*`
  • `dashboards:uid:*`
  • `folders:*`
  • `folders:uid:*`
| +{ .no-spacing-list } + **Example Request for new dashboard**: @@ -164,9 +168,12 @@ Will return the dashboard given the dashboard unique identifier (uid). Informati See note in the [introduction]({{< ref "#dashboard-api" >}}) for an explanation. -| Action | Scope | -| ----------------- | -------------- | -| `dashboards:read` | `dashboards:*` | + +| Action | Scope | +| ----------------- | ------------------------------------------------------------------------------------------------------- | +| `dashboards:read` |
  • `dashboards:*`
  • `dashboards:uid:*`
  • `folders:*`
  • `folders:uid:*`
| +{ .no-spacing-list } + **Example Request**: @@ -220,9 +227,12 @@ Will delete the dashboard given the specified unique identifier (uid). See note in the [introduction]({{< ref "#dashboard-api" >}}) for an explanation. -| Action | Scope | -| ------------------- | ----------------------------- | -| `dashboards:delete` | `dashboards:*`
`folders:*` | + +| Action | Scope | +| ------------------- | ------------------------------------------------------------------------------------------------------- | +| `dashboards:delete` |
  • `dashboards:*`
  • `dashboards:uid:*`
  • `folders:*`
  • `folders:uid:*`
| +{ .no-spacing-list } + **Example Request**: @@ -267,9 +277,12 @@ Will delete permanently the dashboard given the specified unique identifier (uid See note in the [introduction]({{< ref "#dashboard-api" >}}) for an explanation. -| Action | Scope | -| ------------------- | ----------------------------- | -| `dashboards:delete` | `dashboards:*`
`folders:*` | + +| Action | Scope | +| ------------------- | ------------------------------------------------------------------------------------------------------- | +| `dashboards:delete` |
  • `dashboards:*`
  • `dashboards:uid:*`
  • `folders:*`
  • `folders:uid:*`
| +{ .no-spacing-list } + **Example Request**: @@ -314,9 +327,12 @@ Will restore a deleted dashboard given the specified unique identifier (uid). See note in the [introduction]({{< ref "#dashboard-api" >}}) for an explanation. -| Action | Scope | -| ------------------- | ----------------------------- | -| `dashboards:create` | `dashboards:*`
`folders:*` | + +| Action | Scope | +| ------------------- | ----------------------------------------------------- | +| `dashboards:create` |
  • `folders:*`
  • `folders:uid:*`
| +{ .no-spacing-list } + **Example Request**: diff --git a/docs/sources/developers/http_api/dashboard_permissions.md b/docs/sources/developers/http_api/dashboard_permissions.md index d9544943e1c..5f2844be2cb 100644 --- a/docs/sources/developers/http_api/dashboard_permissions.md +++ b/docs/sources/developers/http_api/dashboard_permissions.md @@ -44,9 +44,12 @@ Gets all existing permissions for the dashboard with the given `uid`. See note in the [introduction]({{< ref "#dashboard-permission-api" >}}) for an explanation. -| Action | Scope | -| ----------------------------- | ------------------------------------- | -| `dashboards.permissions:read` | `dashboards:uid:*`
`folders:uid:*` | + +| Action | Scope | +| ----------------------------- | ------------------------------------------------------------------------------------------------------- | +| `dashboards.permissions:read` |
  • `dashboards:*`
  • `dashboards:uid:*`
  • `folders:*`
  • `folders:uid:*`
| +{ .no-spacing-list } + **Example request**: @@ -123,9 +126,12 @@ Updates permissions for a dashboard. This operation will remove existing permiss See note in the [introduction]({{< ref "#dashboard-permission-api" >}}) for an explanation. -| Action | Scope | -| ------------------------------ | ------------------------------------- | -| `dashboards.permissions:write` | `dashboards:uid:*`
`folders:uid:*` | + +| Action | Scope | +| ------------------------------ | ------------------------------------------------------------------------------------------------------- | +| `dashboards.permissions:write` |
  • `dashboards:*`
  • `dashboards:uid:*`
  • `folders:*`
  • `folders:uid:*`
| +{ .no-spacing-list } + **Example request**: @@ -192,9 +198,12 @@ Gets all existing permissions for the dashboard with the given `dashboardId`. See note in the [introduction]({{< ref "#dashboard-permission-api" >}}) for an explanation. -| Action | Scope | -| ----------------------------- | ----------------------------- | -| `dashboards.permissions:read` | `dashboards:*`
`folders:*` | + +| Action | Scope | +| ----------------------------- | ------------------------------------------------------------------------------------------------------- | +| `dashboards.permissions:read` |
  • `dashboards:*`
  • `dashboards:uid:*`
  • `folders:*`
  • `folders:uid:*`
| +{ .no-spacing-list } + **Example request**: @@ -275,9 +284,12 @@ Updates permissions for a dashboard. This operation will remove existing permiss See note in the [introduction]({{< ref "#dashboard-permission-api" >}}) for an explanation. -| Action | Scope | -| ------------------------------ | ----------------------------- | -| `dashboards.permissions:write` | `dashboards:*`
`folders:*` | + +| Action | Scope | +| ------------------------------ | ------------------------------------------------------------------------------------------------------- | +| `dashboards.permissions:write` |
  • `dashboards:*`
  • `dashboards:uid:*`
  • `folders:*`
  • `folders:uid:*`
| +{ .no-spacing-list } + **Example request**: diff --git a/docs/sources/developers/http_api/data_source.md b/docs/sources/developers/http_api/data_source.md index ba58c35b578..252bb7c4187 100644 --- a/docs/sources/developers/http_api/data_source.md +++ b/docs/sources/developers/http_api/data_source.md @@ -604,7 +604,7 @@ Content-Type: application/json ``` {{% admonition type="note" %}} -Similar to [creating a data source](#create-a-data-source), `password` and `basicAuthPassword` should be defined under `secureJsonData` in order to be stored securely as an encrypted blob in the database. Then, the encrypted fields are listed under `secureJsonFields` section in the response.## Update an existing data source by id +Similar to [creating a data source](#create-a-data-source), `password` and `basicAuthPassword` should be defined under `secureJsonData` in order to be stored securely as an encrypted blob in the database. Then, the encrypted fields are listed under `secureJsonFields` section in the response. {{% /admonition %}} ## Delete an existing data source by id diff --git a/docs/sources/developers/http_api/datasource_lbac_rules.md b/docs/sources/developers/http_api/datasource_lbac_rules.md new file mode 100644 index 00000000000..002d43fe4f2 --- /dev/null +++ b/docs/sources/developers/http_api/datasource_lbac_rules.md @@ -0,0 +1,109 @@ +--- +aliases: + - ../../http_api/datasource_lbac_rules/ +canonical: /docs/grafana/latest/developers/http_api/datasource_lbac_rules/ +description: Data Source LBAC rules API +keywords: + - grafana + - http + - documentation + - api + - datasource + - lbac + - acl + - enterprise +labels: + products: + - cloud +title: Datasource LBAC rules HTTP API +--- + +# Data Source LBAC rules API + +> The Data Source LBAC rules are only available in Grafana Cloud. Only cloud loki data sources are supported. + +LBAC (Label-Based Access Control) rules can be set for teams. + +## Get LBAC rules for a data source + +`GET /api/datasources/:uid/lbac/teams` + +Gets all existing LBAC rules for the data source with the given `uid`. + +**Required permissions** + +| Action | Scope | +| ---------------- | ---------------------------------------------------------------------------------------- | +| datasources:read | datasources:_
datasources:uid:_
datasources:uid:my_datasource (single data source) | + +### Examples + +**Example request:** + +``` +GET /api/datasources/:uid/lbac/teams HTTP/1.1 +Accept: application/json +Content-Type: application/json +Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk +``` + +## Update LBAC rules for a data source + +`PUT /api/datasources/:uid/lbac/teams` + +Updates LBAC rules for teams associated with the data source with the given `uid`. Here you submit a list of teams and the rules for each team. +Deleting a team from the list will remove the team's LBAC rules. You have to submit all teams and their rules to be updated, to remove a team's rules, you have to submit the current list of rules without the team. + +**Required permissions** + +| Action | Scope | +| ----------------------------- | ---------------------------------------------------------------------------------------- | +| datasources:write | datasources:_
datasources:uid:_
datasources:uid:my_datasource (single data source) | +| datasources.permissions:write | datasources:_
datasources:uid:_
datasources:uid:my_datasource (single data source) | + +### Examples + +**Example request:** + +```http +PUT /api/datasources/my_datasource/lbac/teams +Accept: application/json +Content-Type: application/json +Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk + +{ + "teamId": 1, + "rules": [ + { + "header": "X-Prom-Label-Policy", + "value": "18042:{ foo=\"bar\" }" + } + ] +} +``` + +**Example response:** + +```http +HTTP/1.1 200 OK +Content-Type: application/json; charset=UTF-8 +Content-Length: 35 + +{ + "message": "Data source LBAC rules updated", + "id": 1, + "uid": "my_datasource", + "name": "My Data Source", + "lbacRules": [ + { + "teamId": 1, + "rules": [ + { + "header": "X-Prom-Label-Policy", + "value": "18042:{ foo=\"bar\" }" + } + ] + } + ] +} +``` diff --git a/docs/sources/developers/http_api/playlist.md b/docs/sources/developers/http_api/playlist.md index d1e3a4e7c90..96a7c2f014a 100644 --- a/docs/sources/developers/http_api/playlist.md +++ b/docs/sources/developers/http_api/playlist.md @@ -78,7 +78,7 @@ Content-Type: application/json { "id": 1, "playlistUid": "1", - "type": "dashboard_by_id", + "type": "dashboard_by_uid", "value": "3", "order": 1, "title":"my third dashboard" @@ -116,7 +116,7 @@ Content-Type: application/json { "id": 1, "playlistUid": "1", - "type": "dashboard_by_id", + "type": "dashboard_by_uid", "value": "3", "order": 1, "title":"my third dashboard" @@ -148,7 +148,7 @@ Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk "interval": "5m", "items": [ { - "type": "dashboard_by_id", + "type": "dashboard_by_uid", "value": "3", "order": 1, "title":"my third dashboard" @@ -192,7 +192,7 @@ Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk "items": [ { "playlistUid": "1", - "type": "dashboard_by_id", + "type": "dashboard_by_uid", "value": "3", "order": 1, "title":"my third dashboard" @@ -221,7 +221,7 @@ Content-Type: application/json { "id": 1, "playlistUid": "1", - "type": "dashboard_by_id", + "type": "dashboard_by_uid", "value": "3", "order": 1, "title":"my third dashboard" diff --git a/docs/sources/developers/plugins/plugin.schema.json b/docs/sources/developers/plugins/plugin.schema.json index 3442ae7d2a4..4a98dc1ea47 100644 --- a/docs/sources/developers/plugins/plugin.schema.json +++ b/docs/sources/developers/plugins/plugin.schema.json @@ -174,7 +174,7 @@ }, "plugins": { "type": "array", - "description": "An array of required plugins on which this plugin depends.", + "description": "An array of required plugins on which this plugin depends. Only non-core (that is, external plugins) have to be specified in this list.", "additionalItems": false, "items": { "type": "object", @@ -198,6 +198,19 @@ } } } + }, + "extensions": { + "type": "object", + "description": "Plugin extensions that this plugin depends on.", + "properties": { + "exposedComponents": { + "type": "array", + "description": "An array of exposed component ids that this plugin depends on.", + "items": { + "type": "string" + } + } + } } } }, @@ -566,26 +579,107 @@ } }, "extensions": { - "type": "array", - "description": "List of link and component extensions which the plugin registers to given extension points.", - "items": { - "type": "object", - "properties": { - "extensionPointId": { - "type": "string" - }, - "title": { - "type": "string" - }, - "description": { - "type": "string" - }, - "type": { - "type": "string", - "enum": ["link", "component"] + "type": "object", + "description": "Plugin extensions are a way to extend either the UI of core Grafana or other plugins.", + "properties": { + "addedComponents": { + "type": "array", + "description": "Any component extensions that your plugin registers to extension points.", + "items": { + "type": "object", + "properties": { + "targets": { + "type": "array", + "description": "The list of the targeted extension point ids that the component is added to.", + "items": { + "type": "string" + } + }, + "title": { + "type": "string", + "description": "An informative title for your component extension.", + "minLength": 10 + }, + "description": { + "type": "string", + "description": "Additional information about your component extension." + } + }, + "required": ["targets", "title"] } }, - "required": ["extensionPointId", "title", "type"] + "addedLinks": { + "type": "array", + "description": "Any link extensions that your plugin registers to extension points.", + "items": { + "type": "object", + "properties": { + "targets": { + "type": "array", + "description": "The list of the targeted extension point ids that the link is added to.", + "items": { + "type": "string" + } + }, + "title": { + "type": "string", + "description": "An informative title for your link extension.", + "minLength": 10 + }, + "description": { + "type": "string", + "description": "Additional information about your link extension." + } + }, + "required": ["targets", "title"] + } + }, + "exposedComponents": { + "type": "array", + "description": "Any React component that your plugin exposes so it can be reused by other app plugins.", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "A unique identifier for your exposed component. This is used to reference the component in other plugins. It must be in the following format: '{PLUGIN_ID}/name-of-component/v1'.", + "pattern": "^[0-9a-z]+-([0-9a-z]+-)?(app|panel|datasource|secretsmanager)\\/[a-zA-Z0-9_-]+\\/v[0-9_.-]+$" + }, + "title": { + "type": "string", + "description": "An informative title for your exposed component." + }, + "description": { + "type": "string", + "description": "Additional information about your exposed component." + } + }, + "required": ["id"] + } + }, + "extensionPoints": { + "type": "array", + "description": "Any extension points that your plugin provides.", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "A unique identifier for your extension point. This is used to reference the extension point in other plugins. It must be in the following format: '{PLUGIN_ID}/name-of-my-extension-point/v1'.", + "pattern": "^[0-9a-z]+-([0-9a-z]+-)?(app|panel|datasource|secretsmanager)\\/[a-zA-Z0-9_-]+\\/v[0-9_.-]+$" + }, + "title": { + "type": "string", + "description": "An informative title for your extension point." + }, + "description": { + "type": "string", + "description": "Additional information about your extension point." + } + }, + "required": ["id"] + } + } } } } diff --git a/docs/sources/explore/simplified-exploration/_index.md b/docs/sources/explore/simplified-exploration/_index.md index b778b3b3463..b3b454a2c5c 100644 --- a/docs/sources/explore/simplified-exploration/_index.md +++ b/docs/sources/explore/simplified-exploration/_index.md @@ -7,12 +7,37 @@ keywords: title: Simplified exploration menuTitle: Simplified exploration weight: 100 +hero: + title: Simplified exploration with the Explore apps + level: 1 + width: 100 + height: 100 + description: Use Explore Profiles to investigate and identify issues using profiling data. +cards: + title_class: pt-0 lh-1 + items: + - title: Explore Metrics + href: ./metrics/ + description: Quickly find related metrics with a few clicks, without needing to write PromQL queries to retrieve metrics. + height: 24 + - title: Explore Logs + href: ./logs/ + description: Visualize log volumes to easily detect anomalies or significant changes over time, without needing to compose LogQL queries. + height: 24 + - title: Explore Traces + href: ./traces/ + description: Use Rate, Errors, and Duration (RED) metrics derived from traces to investigate and understand errors and latency issues within complex distributed systems. + height: 24 + - title: Explore Profiles + href: ./profiles/ + description: View and analyze high-level service performance, identify problem processes for optimization, and diagnose issues to determine root causes. + height: 24 --- # Simplified exploration -Introducing the Grafana Explore apps, designed for effortless data exploration through intuitive, queryless interactions. +The Grafana Explore apps are designed for effortless data exploration through intuitive, queryless interactions. Easily explore telemetry signals with these specialized tools, tailored specifically for the Grafana databases to provide quick and accurate insights. -{{< section >}} +{{< card-grid key="cards" type="simple" >}} diff --git a/docs/sources/explore/simplified-exploration/metrics/index.md b/docs/sources/explore/simplified-exploration/metrics/index.md index e03eb7949a5..127dd6cb265 100644 --- a/docs/sources/explore/simplified-exploration/metrics/index.md +++ b/docs/sources/explore/simplified-exploration/metrics/index.md @@ -16,10 +16,6 @@ weight: 200 Grafana Explore Metrics is a query-less experience for browsing **Prometheus-compatible** metrics. Quickly find related metrics with just a few simple clicks, without needing to write PromQL queries to retrieve metrics. -{{% admonition type="caution" %}} -Explore Metrics is currently in [public preview](/docs/release-life-cycle/). Grafana Labs offers limited support, and breaking changes might occur prior to the feature being made generally available. -{{% /admonition %}} - With Explore Metrics, you can: - Easily segment metrics based on their labels, so you can immediately spot anomalies and identify issues. diff --git a/docs/sources/panels-visualizations/query-transform-data/transform-data/index.md b/docs/sources/panels-visualizations/query-transform-data/transform-data/index.md index 437e2f1ad2a..e6a01262253 100644 --- a/docs/sources/panels-visualizations/query-transform-data/transform-data/index.md +++ b/docs/sources/panels-visualizations/query-transform-data/transform-data/index.md @@ -188,6 +188,7 @@ Use this transformation to add a new field calculated from two other fields. Eac - **Field name** - Select the names of fields you want to use in the calculation for the new field. - **Calculation** - If you select **Reduce row** mode, then the **Calculation** field appears. Click in the field to see a list of calculation choices you can use to create the new field. For information about available calculations, refer to [Calculation types][]. - **Operation** - If you select **Binary operation** or **Unary operation** mode, then the **Operation** fields appear. These fields allow you to apply basic math operations on values in a single row from selected fields. You can also use numerical values for binary operations. + - **All number fields** - Set the left side of a **Binary operation** to apply the calculation to all number fields. - **As percentile** - If you select **Row index** mode, then the **As percentile** switch appears. This switch allows you to transform the row index as a percentage of the total number of rows. - **Alias** - (Optional) Enter the name of your new field. If you leave this blank, then the field will be named to match the calculation. - **Replace all fields** - (Optional) Select this option if you want to hide all other fields and display only your calculated field in the visualization. diff --git a/docs/sources/panels-visualizations/visualizations/gauge/index.md b/docs/sources/panels-visualizations/visualizations/gauge/index.md index d9528a6b427..0c8553138d9 100644 --- a/docs/sources/panels-visualizations/visualizations/gauge/index.md +++ b/docs/sources/panels-visualizations/visualizations/gauge/index.md @@ -25,16 +25,94 @@ refs: # Gauge -Gauges are single-value visualizations that can repeat a gauge for every series, column or row. +Gauges are single-value visualizations that allow you to quickly visualize where a value falls within a defined or calculated min and max range. With repeat options, you can display multiple gauges, each corresponding to a different series, column, or row. {{< figure src="/static/img/docs/v66/gauge_panel_cover.png" max-width="1025px" alt="A gauge visualization">}} -{{< docs/play title="Grafana Gauge Visualization" url="https://play.grafana.org/d/KIhkVD6Gk/" >}} +You can use gauges if you need to track: + +- Service level objectives (SLOs) +- How full a piece of equipment is +- How fast a vehicle is moving within a set of limits +- Network latency +- Equipment state with setpoint and alarm thresholds +- CPU consumption (0-100%) +- RAM availability + +## Configure a time series visualization The following video provides beginner steps for creating gauge panels. You'll learn the data requirements and caveats, special customizations, and much more: {{< youtube id="QwXj3y_YpnE" >}} +{{< docs/play title="Grafana Gauge Visualization" url="https://play.grafana.org/d/KIhkVD6Gk/" >}} + +## Supported data formats + +To create a gauge visualization you need a dataset containing at least one numeric field. These values are identified by the field name. Additional text fields aren’t required but can be used for identification and labeling. + +### Example - One value + +| GaugeName | GaugeValue | +| --------- | ---------- | +| MyGauge | 5 | + +![Gauge with single numeric value](/media/docs/grafana/panels-visualizations/screenshot-grafana-12.2-gauge-example1.png 'Gauge with single numeric value') + +This dataset generates a visualization with one empty gauge showing the numeric value. This is because the gauge visualization automatically defines the upper and lower range from the minimum and maximum values in the dataset. This dataset has only one value, so it’s set as both minimum and maximum. + +If you only have one value, but you want to define a different minimum and maximum, you can set them manually in the [Standard options](#standard-options) settings to generate a more typical looking gauge. + +![Gauge with single numeric value and hardcoded max and min](/media/docs/grafana/panels-visualizations/screenshot-grafana-12.2-gauge-example2.png 'Gauge with single numeric value and hardcoded max-min') + +### Example - One row, multiple values + +The gauge visualization can support multiple fields in a dataset. + +| Identifier | value1 | value2 | value3 | +| ---------- | ------ | ------ | ------ | +| Gauges | 5 | 3 | 10 | + +![Gauge visualization with multiple numeric values in a single row](/media/docs/grafana/panels-visualizations/screenshot-grafana-12.2-gauge-example3.png 'Gauge with multiple numeric values in a single row') + +When there are multiple values in the dataset, the visualization displays multiple gauges and automatically defines the minimum and maximum. In this case, those are 3 and 10. Because the minimum and maximum values are defined, each gauge is shaded in to show that value in relation to the minimum and maximum. + +### Example - Multiple rows and values + +The gauge visualization can display datasets with multiple rows of data or even multiple datasets. + +| Identifier | value1 | value2 | value3 | +| ---------- | ------ | ------ | ------ | +| Gauges | 5 | 3 | 10 | +| Indicators | 6 | 9 | 15 | +| Defaults | 1 | 4 | 8 | + +![Gauge visualization with multiple rows and columns of numeric values showing the last row](/media/docs/grafana/panels-visualizations/screenshot-grafana-12.2-gauge-example6.png 'Gauge viz with multiple rows and columns of numeric values showing the last row') + +By default, the visualization is configured to [calculate](#value-options) a single value per column or series and to display only the last row of data. However, it derives the minimum and maximum from the full dataset, even if those values aren’t visible. + +In this example, that means only the last row of data is displayed in the gauges and the minimum and maximum values are 1 and 10. The value 1 is displayed because it’s in the last row, while 10 is not. + +If you want to show one gauge per table cell, you can change the **Show** setting from **Calculate** to **All values**, and each gauge is labeled by concatenating the text column with each value's column name. + +![Gauge visualization with multiple rows and columns of numeric values showing all the values](/media/docs/grafana/panels-visualizations/screenshot-grafana-12.2-gauge-example7.png 'Gauge viz with multiple rows and columns of numeric values showing all the values') + +### Example - Defined min and max + +You can also define minimum and maximum values as part of the dataset. + +| Identifier | value | max | min | +| ---------- | ----- | --- | --- | +| Gauges | 5 | 10 | 2 | + +![Gauge visualization with numeric values defining max and minimum](/media/docs/grafana/panels-visualizations/screenshot-grafana-12.2-gauge-example4.png 'Gauge with numeric values defining max and minimum') + +If you don’t want to display gauges for the `min` and `max` values, you can configure only one field to be displayed as described in the [value options](#value-options) section. + +![Gauge visualization with numeric values defining max and minimum but hidden](/media/docs/grafana/panels-visualizations/screenshot-grafana-12.2-gauge-example5.png 'Gauge with numeric values defining max and minimum but hidden') + +Even when minimum and maximum values aren’t displayed, the visualization still pulls the range from them. + ## Panel options {{< docs/shared lookup="visualizations/panel-options.md" source="grafana" version="" >}} @@ -135,6 +213,10 @@ Adjust the sizes of the gauge text. {{< docs/shared lookup="visualizations/thresholds-options-2.md" source="grafana" version="" >}} +Last, gauge colors and thresholds (the outer bar markers) of the gauge can be configured as described above. + +![Gauge viz with multiple rows and columns of numeric values showing all the values and thresholds defined for 0-6-11](/media/docs/grafana/panels-visualizations/screenshot-grafana-12.2-gauge-example8.png 'Gauge viz with multiple rows and columns of numeric values showing all the values and thresholds defined for 0-6-11') + ## Field overrides {{< docs/shared lookup="visualizations/overrides-options.md" source="grafana" version="" >}} diff --git a/docs/sources/search/_index.md b/docs/sources/search/_index.md index 2d736973616..832a359ba41 100644 --- a/docs/sources/search/_index.md +++ b/docs/sources/search/_index.md @@ -1,8 +1,9 @@ --- -description: Learn how to search for Grafana dashboards +description: Learn how to search for Grafana dashboards and folders keywords: - search - dashboard + - folder labels: products: - cloud @@ -13,27 +14,43 @@ title: Search weight: 80 --- -# Search dashboards +# Search dashboards and folders -You can search for dashboards by dashboard name and by panel title. When you search for dashboards, the system returns all dashboards available within the Grafana instance, even if you do not have permission to view the contents of the dashboard. +You can search for dashboards and dashboard folders by name. -## Search dashboards using dashboard name +When you search for dashboards, you can also do it by panel title. Whether you search by name or panel title, the system returns all dashboards available within the Grafana instance, even if you do not have permission to view the contents of the dashboard. -Begin typing any part of the dashboard name in the search bar. The search returns results for any partial string match in real-time, as you type. +## Search by name -Dashboard search is: +Begin typing any part of the dashboard or folder name in the search bar. The search returns results for any partial string match in real-time, as you type. + +The search is: - Real-time - _Not_ case sensitive -- Functional across stored _and_ file based dashboards. +- Functional across stored _and_ file based dashboards and folders. {{% admonition type="note" %}} -You can use your keyboard arrow keys to navigate the results and press `Enter` to open the selected dashboard. +You can use your keyboard arrow keys to navigate the results and press `Enter` to open the selected dashboard or folder. {{% /admonition %}} -The following image shows the search results when you search using dashboard name. +The following images show: -{{< figure src="/static/img/docs/v91/dashboard-features/search-by-dashboard-name.png" width="700px" >}} +Searching by dashboard name from the **Dashboards** page. + +{{< figure src="/media/docs/grafana/dashboards/search-for-dashboard.png" width="700px" >}} + +Searching by folder name from the **Dashboards** page. + +{{< figure src="/media/docs/grafana/dashboards/search-folder.png" width="700px" >}} + +Searching by dashboard name inside a folder. + +{{< figure src="/media/docs/grafana/dashboards/search-in-folder.png" width="700px" >}} + +{{% admonition type="note" %}} +When you search within a folder, its subfolders are not part of the results returned. You need to be on the **Dashboards** page (or the root level) to search for subfolders by name. +{{% /admonition %}} ## Search dashboards using panel title diff --git a/docs/sources/setup-grafana/configure-grafana/_index.md b/docs/sources/setup-grafana/configure-grafana/_index.md index 88106a20c68..505b3b141a5 100644 --- a/docs/sources/setup-grafana/configure-grafana/_index.md +++ b/docs/sources/setup-grafana/configure-grafana/_index.md @@ -39,7 +39,7 @@ On Windows, the `sample.ini` file is located in the same directory as `defaults. ### macOS -By default, the configuration file is located at `/usr/local/etc/grafana/grafana.ini`. For a Grafana instance installed using Homebrew, edit the `grafana.ini` file directly. Otherwise, add a configuration file named `custom.ini` to the `conf` folder to override the settings defined in `conf/defaults.ini`. +By default, the configuration file is located at `/opt/homebrew/etc/grafana/grafana.ini` or `/usr/local/etc/grafana/grafana.ini`. For a Grafana instance installed using Homebrew, edit the `grafana.ini` file directly. Otherwise, add a configuration file named `custom.ini` to the `conf` folder to override the settings defined in `conf/defaults.ini`. ## Remove comments in the .ini files @@ -701,6 +701,18 @@ You can enable both policies simultaneously. Set the policy template that will be used when adding the `Content-Security-Policy-Report-Only` header to your requests. `$NONCE` in the template includes a random nonce. +### actions_allow_post_url + +Sets API paths to be accessible between plugins using the POST verb. This is a comma separated list, and uses glob matching. + +This will allow access to all plugins that have a backend: + +`actions_allow_post_url=/api/plugins/*` + +This will limit access to the backend of a single plugin: + +`actions_allow_post_url=/api/plugins/grafana-special-app` +
### angular_support_enabled @@ -1901,9 +1913,8 @@ Graphite metric prefix. Defaults to `prod.grafana.%(instance_name)s.` ## [grafana_net] -### url - -Default is https://grafana.com. +Refer to [grafana_com] config as that is the new and preferred config name. +The grafana_net config is still accepted and parsed to grafana_com config.
@@ -1912,6 +1923,7 @@ Default is https://grafana.com. ### url Default is https://grafana.com. +The default authentication identity provider for Grafana Cloud.
diff --git a/docs/sources/setup-grafana/configure-grafana/configure-custom-branding/index.md b/docs/sources/setup-grafana/configure-grafana/configure-custom-branding/index.md index 56ac8b1524c..f38a142e429 100644 --- a/docs/sources/setup-grafana/configure-grafana/configure-custom-branding/index.md +++ b/docs/sources/setup-grafana/configure-grafana/configure-custom-branding/index.md @@ -16,6 +16,10 @@ Custom branding enables you to replace the Grafana Labs brand and logo with your {{% admonition type="note" %}} Available in [Grafana Enterprise]({{< relref "../../../introduction/grafana-enterprise" >}}) and [Grafana Cloud](/docs/grafana-cloud). For Cloud Advanced and Enterprise customers, please provide custom elements and logos to our Support team. We will help you host your images and update your custom branding. + +This feature is not available for Grafana Free and Pro tiers. +For more information on feature availability across plans, refer to our [feature comparison page](/docs/grafana-cloud/cost-management-and-billing/understand-grafana-cloud-features/) + {{% /admonition %}} The `grafana.ini` file includes Grafana Enterprise custom branding. As with all configuration options, you can use environment variables to set custom branding. 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 d50f0bb7f26..3102a626929 100644 --- a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md +++ b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md @@ -29,6 +29,7 @@ Most [generally available](https://grafana.com/docs/release-life-cycle/#general- | `correlations` | Correlations page | Yes | | `autoMigrateXYChartPanel` | Migrate old XYChart panel to new XYChart2 model | Yes | | `cloudWatchCrossAccountQuerying` | Enables cross-account querying in CloudWatch datasources | Yes | +| `accessControlOnCall` | Access control primitives for OnCall | Yes | | `nestedFolders` | Enable folder nesting | Yes | | `logsContextDatasourceUi` | Allow datasource to provide custom UI for context view | Yes | | `lokiQuerySplitting` | Split large interval queries into subqueries with smaller time intervals | Yes | @@ -66,6 +67,7 @@ Most [generally available](https://grafana.com/docs/release-life-cycle/#general- | `groupToNestedTableTransformation` | Enables the group to nested table transformation | Yes | | `tlsMemcached` | Use TLS-enabled memcached in the enterprise caching feature | Yes | | `cloudWatchNewLabelParsing` | Updates CloudWatch label parsing to be more accurate | Yes | +| `newDashboardSharingComponent` | Enables the new sharing drawer design | | | `pluginProxyPreserveTrailingSlash` | Preserve plugin proxy trailing slash. | | | `openSearchBackendFlowEnabled` | Enables the backend query flow for Open Search datasource plugin | Yes | | `cloudWatchRoundUpEndTime` | Round up end time for metric queries to the next minute to avoid missing data | Yes | @@ -85,7 +87,6 @@ Most [generally available](https://grafana.com/docs/release-life-cycle/#general- | `autoMigrateStatPanel` | Migrate old stat panel to supported stat panel - broken out from autoMigrateOldPanels to enable granular tracking | | `disableAngular` | Dynamic flag to disable angular at runtime. The preferred method is to set `angular_support_enabled` to `false` in the [security] settings, which allows you to change the state at runtime. | | `grpcServer` | Run the GRPC server | -| `accessControlOnCall` | Access control primitives for OnCall | | `alertingNoNormalState` | Stop maintaining state of alerts that are not firing | | `renderAuthJWT` | Uses JWT-based auth for rendering instead of relying on remote cache | | `refactorVariablesTimeRange` | Refactor time range variables flow to reduce number of API calls made when query variables are chained | @@ -95,7 +96,7 @@ Most [generally available](https://grafana.com/docs/release-life-cycle/#general- | `reportingRetries` | Enables rendering retries for the reporting feature | | `externalServiceAccounts` | Automatic service account and token setup for plugins | | `cloudWatchBatchQueries` | Runs CloudWatch metrics queries as separate batches | -| `teamHttpHeaders` | Enables Team LBAC for datasources to apply team headers to the client requests | +| `teamHttpHeaders` | Enables LBAC for datasources to apply LogQL filtering of logs to the client requests for users in teams | | `pdfTables` | Enables generating table data as PDF in reporting | | `canvasPanelPanZoom` | Allow pan and zoom in canvas panel | | `regressionTransformation` | Enables regression analysis transformation | @@ -105,6 +106,7 @@ Most [generally available](https://grafana.com/docs/release-life-cycle/#general- | `accessActionSets` | Introduces action sets for resource permissions. Also ensures that all folder editors and admins can create subfolders without needing any additional permissions. | | `azureMonitorPrometheusExemplars` | Allows configuration of Azure Monitor as a data source that can provide Prometheus exemplars | | `cloudwatchMetricInsightsCrossAccount` | Enables cross account observability for Cloudwatch Metric Insights query builder | +| `useSessionStorageForRedirection` | Use session storage for handling the redirection after login | ## Experimental feature toggles @@ -119,6 +121,7 @@ Experimental features might be changed or removed without prior notice. | `lokiExperimentalStreaming` | Support new streaming approach for loki (prototype, needs special loki build) | | `storage` | Configurable storage for dashboards, datasources, and resources | | `canvasPanelNesting` | Allow elements nesting | +| `vizActions` | Allow actions in visualizations | | `disableSecretsCompatibility` | Disable duplicated secret storage in legacy tables | | `logRequestsInstrumentedAsUnknown` | Logs the path for requests that are instrumented as unknown | | `showDashboardValidationWarnings` | Show warnings when dashboards do not validate against the schema | @@ -144,6 +147,7 @@ Experimental features might be changed or removed without prior notice. | `awsDatasourcesTempCredentials` | Support temporary security credentials in AWS plugins for Grafana Cloud customers | | `mlExpressions` | Enable support for Machine Learning in server-side expressions | | `metricsSummary` | Enables metrics summary queries in the Tempo data source | +| `datasourceAPIServers` | Expose some datasources as apiservers. | | `permissionsFilterRemoveSubquery` | Alternative permission filter implementation that does not use subqueries for fetching the dashboard folder | | `aiGeneratedDashboardChanges` | Enable AI powered features for dashboards to auto-summary changes when saving | | `sseGroupByDatasource` | Send query to the same datasource in a single request when using server side expressions. The `cloudWatchBatchQueries` feature toggle should be enabled if this used with CloudWatch. | @@ -155,6 +159,7 @@ Experimental features might be changed or removed without prior notice. | `disableClassicHTTPHistogram` | Disables classic HTTP Histogram (use with enableNativeHTTPHistogram) | | `kubernetesSnapshots` | Routes snapshot requests from /api to the /apis endpoint | | `kubernetesDashboards` | Use the kubernetes API in the frontend for dashboards | +| `kubernetesFolders` | Use the kubernetes API in the frontend for folders, and route /api/folders requests to k8s | | `datasourceQueryTypes` | Show query type endpoints in datasource API servers (currently hardcoded for testdata, expressions, and prometheus) | | `queryService` | Register /apis/query.grafana.app/ -- will eventually replace /api/ds/query | | `queryServiceRewrite` | Rewrite requests targeting /ds/query to the query service | @@ -180,7 +185,6 @@ Experimental features might be changed or removed without prior notice. | `disableNumericMetricsSortingInExpressions` | In server-side expressions, disable the sorting of numeric-kind metrics by their metric name or labels. | | `queryLibrary` | Enables Query Library feature in Explore | | `logsExploreTableDefaultVisualization` | Sets the logs table as default visualisation in logs explore | -| `newDashboardSharingComponent` | Enables the new sharing drawer design | | `alertingListViewV2` | Enables the new alert list view design | | `notificationBanner` | Enables the notification banner UI and API | | `dashboardRestore` | Enables deleted dashboard restore feature (backend only) | @@ -190,7 +194,6 @@ Experimental features might be changed or removed without prior notice. | `databaseReadReplica` | Use a read replica for some database queries. | | `alertingApiServer` | Register Alerting APIs with the K8s API server | | `dashboardRestoreUI` | Enables the frontend to be able to restore a recently deleted dashboard | -| `backgroundPluginInstaller` | Enable background plugin installer | | `dataplaneAggregator` | Enable grafana dataplane aggregator | | `newFiltersUI` | Enables new combobox style UI for the Ad hoc filters variable in scenes architecture | | `lokiSendDashboardPanelNames` | Send dashboard and panel names to Loki when querying | @@ -204,8 +207,8 @@ Experimental features might be changed or removed without prior notice. The following toggles require explicitly setting Grafana's [app mode]({{< relref "../_index.md#app_mode" >}}) to 'development' before you can enable this feature toggle. These features tend to be experimental. -| Feature toggle name | Description | -| -------------------------------------- | -------------------------------------------------------------- | -| `grafanaAPIServerWithExperimentalAPIs` | Register experimental APIs with the k8s API server | -| `grafanaAPIServerEnsureKubectlAccess` | Start an additional https handler and write kubectl options | -| `panelTitleSearchInV1` | Enable searching for dashboards using panel title in search v1 | +| Feature toggle name | Description | +| -------------------------------------- | ----------------------------------------------------------------------------- | +| `grafanaAPIServerWithExperimentalAPIs` | Register experimental APIs with the k8s API server, including all datasources | +| `grafanaAPIServerEnsureKubectlAccess` | Start an additional https handler and write kubectl options | +| `panelTitleSearchInV1` | Enable searching for dashboards using panel title in search v1 | diff --git a/docs/sources/setup-grafana/configure-security/configure-authentication/saml/index.md b/docs/sources/setup-grafana/configure-security/configure-authentication/saml/index.md index d18be432199..bddba3548cd 100644 --- a/docs/sources/setup-grafana/configure-security/configure-authentication/saml/index.md +++ b/docs/sources/setup-grafana/configure-security/configure-authentication/saml/index.md @@ -209,11 +209,10 @@ In order to validate Azure AD users with Grafana, you need to configure the SAML 1. In the Set up **Single Sign-On with SAML** pane, select the pencil icon for **Basic SAML Configuration** to edit the settings. 1. In the **Basic SAML Configuration** pane, click on the **Edit** button and update the following fields: - In the **Identifier (Entity ID)** field, enter `https://localhost/saml/metadata`. - - In the **Identifier (Entity ID)** field, remove the default value. - In the **Reply URL (Assertion Consumer Service URL)** field, enter `https://localhost/saml/acs`. - - In the **Sign on URL** field, enter `https://localhost/saml/auth`. - - In the **Relay State** field, enter `https://localhost/saml/slo`. - - In the **Logout URL** field, enter `https://localhost/logout`. + - In the **Sign on URL** field, enter `https://localhost`. + - In the **Relay State** field, enter `https://localhost`. + - In the **Logout URL** field, enter `https://localhost/saml/slo`. 1. Select **Save**. 1. At the **SAML Certificate** section, copy the **App Federation Metadata Url**. - Use this URL in the `idp_metadata_url` field in the `custom.ini` file. @@ -329,7 +328,7 @@ The table below describes all SAML configuration options. Continue reading below | `name_id_format` | No | The Name ID Format to request within the SAML assertion | `urn:oasis:names:tc:SAML:2.0:nameid-format:transient` | | `client_id` | No | Client ID of the IdP service application used to retrieve more information about the user from the IdP. | | | `client_secret` | No | Client secret of the IdP service application used to retrieve more information about the user from the IdP. | | -| `access_token_url` | No | URL to retrieve the access token from the IdP. | | +| `token_url` | No | URL to retrieve the access token from the IdP. | | | `force_use_graph_api` | No | Whether to use the IdP service application retrieve more information about the user from the IdP. | `false` | ### Signature algorithm diff --git a/docs/sources/shared/alerts/alerting_provisioning.md b/docs/sources/shared/alerts/alerting_provisioning.md index 4dd0a656119..b5fe4c23eac 100644 --- a/docs/sources/shared/alerts/alerting_provisioning.md +++ b/docs/sources/shared/alerts/alerting_provisioning.md @@ -1652,9 +1652,9 @@ Status: Accepted ### Duration -| Name | Type | Go type | Default | Description | Example | -| -------- | ------------------------- | ------- | ------- | ----------- | ------- | -| Duration | int64 (formatted integer) | int64 | | | | +| Name | Type | Go type | Default | Description | Example | +| -------- | ------ | ------- | ------- | ----------- | ------- | +| Duration | string | int64 | | | | ### EmbeddedContactPoint diff --git a/docs/sources/whatsnew/whats-new-in-v11-2.md b/docs/sources/whatsnew/whats-new-in-v11-2.md index cc5cb186f1d..1fe71c25854 100644 --- a/docs/sources/whatsnew/whats-new-in-v11-2.md +++ b/docs/sources/whatsnew/whats-new-in-v11-2.md @@ -180,6 +180,10 @@ _Generally available in all editions of Grafana_ Explore now supports forward direction search for Loki logs searches. This allows users to seamlessly browse logs in a time range in forward chronological order (for example, tracing a specific user's actions using logs). +{{< figure src="/static/img/logs/forward_search.png" alt="Explore logs with the Direction option selected" caption-align="left" >}} + +To use this feature, select **Forward** for the **Direction** option. Note that in the screenshot above, logs are rendered beginning from the starting time period of the query, not the end. + [Documentation](https://grafana.com/docs/grafana//datasources/loki/query-editor/) ## Data sources diff --git a/e2e/scenes/dashboards-suite/dashboard-export-json.spec.ts b/e2e/scenes/dashboards-suite/dashboard-export-json.spec.ts new file mode 100644 index 00000000000..6795c5680a7 --- /dev/null +++ b/e2e/scenes/dashboards-suite/dashboard-export-json.spec.ts @@ -0,0 +1,70 @@ +import { e2e } from '../utils'; +import '../../utils/support/clipboard'; + +describe('Export as JSON', () => { + beforeEach(() => { + e2e.flows.login(Cypress.env('USERNAME'), Cypress.env('PASSWORD')); + }); + + it('Export for internal and external use', () => { + // Opening a dashboard + cy.intercept({ + pathname: '/api/ds/query', + }).as('query'); + e2e.flows.openDashboard({ + uid: 'ZqZnVvFZz', + queryParams: { '__feature.scenes': true, '__feature.newDashboardSharingComponent': true }, + }); + cy.wait('@query'); + + // cy.wrap( + // Cypress.automation('remote:debugger:protocol', { + // command: 'Browser.grantPermissions', + // params: { + // permissions: ['clipboardReadWrite', 'clipboardSanitizedWrite'], + // origin: window.location.origin, + // }, + // }) + // ); + + // Open the export drawer + e2e.pages.Dashboard.DashNav.NewExportButton.arrowMenu().click(); + e2e.pages.Dashboard.DashNav.NewExportButton.Menu.exportAsJson().click(); + + cy.url().should('include', 'shareView=export'); + + // Export as JSON + e2e.pages.ExportDashboardDrawer.ExportAsJson.container().should('be.visible'); + e2e.pages.ExportDashboardDrawer.ExportAsJson.exportExternallyToggle().should('not.be.checked'); + e2e.components.CodeEditor.container().should('exist'); + + e2e.pages.ExportDashboardDrawer.ExportAsJson.saveToFileButton().should('exist'); + e2e.pages.ExportDashboardDrawer.ExportAsJson.copyToClipboardButton().should('exist'); + e2e.pages.ExportDashboardDrawer.ExportAsJson.cancelButton().should('exist'); + + //TODO Failing in CI/CD. Fix it + // Copy link button should be visible + // e2e.pages.ExportDashboardDrawer.ExportAsJson.copyToClipboardButton() + // .click() + // .then(() => { + // cy.copyFromClipboard().then((url) => { + // cy.wrap(url).should('not.include', '__inputs'); + // }); + // }); + + e2e.pages.ExportDashboardDrawer.ExportAsJson.exportExternallyToggle().click({ force: true }); + + //TODO Failing in CI/CD. Fix it + // e2e.pages.ExportDashboardDrawer.ExportAsJson.copyToClipboardButton() + // .click() + // .then(() => { + // cy.copyFromClipboard().then((url) => { + // cy.wrap(url).should('include', '__inputs'); + // }); + // }); + + e2e.pages.ExportDashboardDrawer.ExportAsJson.cancelButton().click(); + + cy.url().should('not.include', 'shareView=export'); + }); +}); diff --git a/e2e/scenes/dashboards-suite/dashboard-share-externally-create.spec.ts b/e2e/scenes/dashboards-suite/dashboard-share-externally-create.spec.ts new file mode 100644 index 00000000000..60642f9e0ef --- /dev/null +++ b/e2e/scenes/dashboards-suite/dashboard-share-externally-create.spec.ts @@ -0,0 +1,180 @@ +import { PublicDashboard } from '../../../public/app/features/dashboard/components/ShareModal/SharePublicDashboard/SharePublicDashboardUtils'; +import { e2e } from '../utils'; +import '../../utils/support/clipboard'; + +describe('Shared dashboards', () => { + beforeEach(() => { + e2e.flows.login(Cypress.env('USERNAME'), Cypress.env('PASSWORD')); + }); + + it('Close share externally drawer', () => { + openDashboard(); + + // Open share externally drawer + e2e.pages.Dashboard.DashNav.newShareButton.arrowMenu().click(); + e2e.pages.Dashboard.DashNav.newShareButton.menu.shareExternally().click(); + + cy.url().should('include', 'shareView=public_dashboard'); + e2e.pages.ShareDashboardDrawer.ShareExternally.container().should('be.visible'); + e2e.pages.ShareDashboardDrawer.ShareExternally.Creation.PublicShare.cancelButton().click(); + + cy.url().should('not.include', 'shareView=public_dashboard'); + e2e.pages.ShareDashboardDrawer.ShareExternally.container().should('not.exist'); + }); + + it('Create a shared dashboard and check API', () => { + openDashboard(); + + // Open share externally drawer + e2e.pages.Dashboard.DashNav.newShareButton.arrowMenu().click(); + e2e.pages.Dashboard.DashNav.newShareButton.menu.shareExternally().click(); + + // Create button should be disabled + e2e.pages.ShareDashboardDrawer.ShareExternally.Creation.PublicShare.createButton().should('be.disabled'); + + // Create flow shouldn't show these elements + e2e.pages.ShareDashboardDrawer.ShareExternally.Configuration.enableTimeRangeSwitch().should('not.exist'); + e2e.pages.ShareDashboardDrawer.ShareExternally.Configuration.enableAnnotationsSwitch().should('not.exist'); + e2e.pages.ShareDashboardDrawer.ShareExternally.Configuration.copyUrlButton().should('not.exist'); + + e2e.pages.ShareDashboardDrawer.ShareExternally.Configuration.revokeAccessButton().should('not.exist'); + e2e.pages.ShareDashboardDrawer.ShareExternally.Configuration.toggleAccessButton().should('not.exist'); + + // Acknowledge checkbox + e2e.pages.ShareDashboardDrawer.ShareExternally.Creation.willBePublicCheckbox() + .should('be.enabled') + .click({ force: true }); + + // Create shared dashboard + cy.intercept('POST', '/api/dashboards/uid/edediimbjhdz4b/public-dashboards').as('create'); + e2e.pages.ShareDashboardDrawer.ShareExternally.Creation.PublicShare.createButton().should('be.enabled').click(); + cy.wait('@create') + .its('response.body') + .then((body: PublicDashboard) => { + cy.log(JSON.stringify(body)); + cy.clearCookies() + .request(getPublicDashboardAPIUrl(body.accessToken)) + .then((resp) => { + expect(resp.status).to.eq(200); + }); + }); + + // These elements shouldn't be rendered after creating public dashboard + e2e.pages.ShareDashboardDrawer.ShareExternally.Creation.willBePublicCheckbox().should('not.exist'); + e2e.pages.ShareDashboardDrawer.ShareExternally.Creation.PublicShare.createButton().should('not.exist'); + + // These elements should be rendered + e2e.pages.ShareDashboardDrawer.ShareExternally.Configuration.enableTimeRangeSwitch().should('exist'); + e2e.pages.ShareDashboardDrawer.ShareExternally.Configuration.enableAnnotationsSwitch().should('exist'); + e2e.pages.ShareDashboardDrawer.ShareExternally.Configuration.copyUrlButton().should('exist'); + e2e.pages.ShareDashboardDrawer.ShareExternally.Configuration.revokeAccessButton().should('exist'); + e2e.pages.ShareDashboardDrawer.ShareExternally.Configuration.toggleAccessButton().should('exist'); + }); + + // Skipping as clipboard permissions are failing in CI. Public dashboard creation is checked in previous test on purpose + it.skip('Open a shared dashboard', () => { + openDashboard(); + + cy.wrap( + Cypress.automation('remote:debugger:protocol', { + command: 'Browser.grantPermissions', + params: { + permissions: ['clipboardReadWrite', 'clipboardSanitizedWrite'], + origin: window.location.origin, + }, + }) + ); + + // Tag indicating a dashboard is public + e2e.pages.Dashboard.DashNav.publicDashboardTag().should('exist'); + + // Open share externally drawer + e2e.pages.Dashboard.DashNav.newShareButton.arrowMenu().click(); + e2e.pages.Dashboard.DashNav.newShareButton.menu.shareExternally().click(); + + e2e.pages.ShareDashboardDrawer.ShareExternally.Configuration.enableTimeRangeSwitch().should('exist'); + e2e.pages.ShareDashboardDrawer.ShareExternally.Configuration.enableAnnotationsSwitch().should('exist'); + e2e.pages.ShareDashboardDrawer.ShareExternally.Configuration.copyUrlButton().should('exist'); + e2e.pages.ShareDashboardDrawer.ShareExternally.Configuration.revokeAccessButton().should('exist'); + e2e.pages.ShareDashboardDrawer.ShareExternally.Configuration.toggleAccessButton().should('exist'); + + e2e.pages.ShareDashboardDrawer.ShareExternally.Configuration.copyUrlButton() + .click() + .then(() => { + cy.copyFromClipboard().then((url) => { + cy.clearCookies() + .request(getPublicDashboardAPIUrl(String(url))) + .then((resp) => { + expect(resp.status).to.eq(200); + }); + }); + }); + }); + + it('Disable a shared dashboard', () => { + openDashboard(); + + //TODO Failing in CI/CD. Fix it + // cy.wrap( + // Cypress.automation('remote:debugger:protocol', { + // command: 'Browser.grantPermissions', + // params: { + // permissions: ['clipboardReadWrite', 'clipboardSanitizedWrite'], + // origin: window.location.origin, + // }, + // }) + // ); + + // Open share externally drawer + e2e.pages.Dashboard.DashNav.newShareButton.arrowMenu().click(); + e2e.pages.Dashboard.DashNav.newShareButton.menu.shareExternally().click(); + + // Save public dashboard + cy.intercept('PATCH', '/api/dashboards/uid/edediimbjhdz4b/public-dashboards/*').as('update'); + + // Switch off enabling toggle + e2e.pages.ShareDashboardDrawer.ShareExternally.Configuration.toggleAccessButton() + .should('be.enabled') + .click({ force: true }); + + cy.wait('@update') + .its('response') + .then((rs) => { + expect(rs.statusCode).eq(200); + const publicDashboard: PublicDashboard = rs.body; + cy.clearCookies() + .request({ url: getPublicDashboardAPIUrl(publicDashboard.accessToken), failOnStatusCode: false }) + .then((resp) => { + expect(resp.status).to.eq(403); + }); + }); + // .then(() => { + // e2e.pages.ShareDashboardDrawer.ShareExternally.Configuration.toggleAccessButton().contains('Resume access'); + // e2e.pages.ShareDashboardDrawer.ShareExternally.Configuration.copyUrlButton().should('be.enabled'); + // }); + + //TODO Failing in CI/CD. Fix it + // e2e.pages.ShareDashboardDrawer.ShareExternally.Configuration.copyUrlButton() + // .click() + // .then(() => { + // cy.copyFromClipboard().then((url) => { + // cy.clearCookies() + // .request({ url: getPublicDashboardAPIUrl(String(url)), failOnStatusCode: false }) + // .then((resp) => { + // expect(resp.status).to.eq(403); + // }); + // }); + // }); + }); +}); + +const openDashboard = () => { + e2e.flows.openDashboard({ + uid: 'edediimbjhdz4b', + queryParams: { '__feature.scenes': true, '__feature.newDashboardSharingComponent': true }, + }); +}; + +const getPublicDashboardAPIUrl = (accessToken: string): string => { + return `/api/public/dashboards/${accessToken}`; +}; diff --git a/e2e/scenes/dashboards-suite/dashboard-share-internally.spec.ts b/e2e/scenes/dashboards-suite/dashboard-share-internally.spec.ts new file mode 100644 index 00000000000..96b7291d40f --- /dev/null +++ b/e2e/scenes/dashboards-suite/dashboard-share-internally.spec.ts @@ -0,0 +1,246 @@ +import { ShareLinkConfiguration } from '../../../public/app/features/dashboard-scene/sharing/ShareButton/utils'; +import { e2e } from '../utils'; +import '../../utils/support/clipboard'; + +describe('Share internally', () => { + beforeEach(() => { + e2e.flows.login(Cypress.env('USERNAME'), Cypress.env('PASSWORD')); + cy.window().then((win) => { + win.localStorage.removeItem('grafana.dashboard.link.shareConfiguration'); + }); + }); + + it('Create a locked time range short link', () => { + cy.intercept({ + pathname: '/api/ds/query', + }).as('query'); + openDashboard(); + cy.wait('@query'); + + //TODO Failing in CI/CD. Fix it + // cy.wrap( + // Cypress.automation('remote:debugger:protocol', { + // command: 'Browser.grantPermissions', + // params: { + // permissions: ['clipboardReadWrite', 'clipboardSanitizedWrite'], + // origin: window.location.origin, + // }, + // }) + // ); + + // Open share externally drawer + e2e.pages.Dashboard.DashNav.newShareButton.arrowMenu().click(); + + cy.intercept('POST', '/api/short-urls').as('create'); + e2e.pages.Dashboard.DashNav.newShareButton.menu.shareInternally().click(); + + cy.url().should('include', 'shareView=link'); + + e2e.pages.ShareDashboardDrawer.ShareInternally.lockTimeRangeSwitch().should('exist'); + e2e.pages.ShareDashboardDrawer.ShareInternally.shortenUrlSwitch().should('exist'); + e2e.pages.ShareDashboardDrawer.ShareInternally.copyUrlButton().should('exist'); + e2e.components.RadioButton.container().should('have.length', 3); + + cy.window().then((win) => { + const shareConfiguration = win.localStorage.getItem('grafana.dashboard.link.shareConfiguration'); + expect(shareConfiguration).equal(null); + }); + cy.wait('@create') + .its('response') + .then((rs) => { + expect(rs.statusCode).eq(200); + const body: { url: string; uid: string } = rs.body; + expect(body.url).contain('goto'); + + // const url = fromBaseUrl(getShortLinkUrl(body.uid)); + // cy.intercept('GET', url).as('get'); + // cy.visit(url, { retryOnNetworkFailure: true }); + // cy.wait('@get'); + // + // cy.url().should('not.include', 'from=now-6h&to=now'); + }); + }); + + it('Create a relative time range short link', () => { + cy.intercept({ + pathname: '/api/ds/query', + }).as('query'); + openDashboard(); + cy.wait('@query'); + + e2e.pages.Dashboard.DashNav.newShareButton.arrowMenu().click(); + + e2e.pages.Dashboard.DashNav.newShareButton.menu.shareInternally().click(); + + cy.intercept('POST', '/api/short-urls').as('update'); + e2e.pages.ShareDashboardDrawer.ShareInternally.lockTimeRangeSwitch().click({ force: true }); + + cy.window().then((win) => { + const shareConfiguration = win.localStorage.getItem('grafana.dashboard.link.shareConfiguration'); + const { useAbsoluteTimeRange, useShortUrl, theme }: ShareLinkConfiguration = JSON.parse(shareConfiguration); + expect(useAbsoluteTimeRange).eq(false); + expect(useShortUrl).eq(true); + expect(theme).eq('current'); + }); + + cy.wait('@update') + .its('response') + .then((rs) => { + expect(rs.statusCode).eq(200); + const body: { url: string; uid: string } = rs.body; + expect(body.url).contain('goto'); + + // const url = fromBaseUrl(getShortLinkUrl(body.uid)); + // cy.intercept('GET', url).as('get'); + // cy.visit(url, { retryOnNetworkFailure: true }); + // cy.wait('@get'); + // + // cy.url().should('include', 'from=now-6h&to=now'); + }); + + // + // e2e.pages.ShareDashboardDrawer.ShareInternally.shortenUrlSwitch().click({ force: true }); + // + // cy.window().then((win) => { + // const shareConfiguration = win.localStorage.getItem('grafana.dashboard.link.shareConfiguration'); + // const { useAbsoluteTimeRange, useShortUrl, theme }: ShareLinkConfiguration = JSON.parse(shareConfiguration); + // expect(useAbsoluteTimeRange).eq(true); + // expect(useShortUrl).eq(false); + // expect(theme).eq('current'); + // }); + + // e2e.pages.ShareDashboardDrawer.ShareInternally.copyUrlButton().should('exist'); + + // e2e.pages.ShareDashboardDrawer.ShareInternally.copyUrlButton() + // .click() + // .then(() => { + // cy.copyFromClipboard().then((url) => { + // cy.wrap(url).should('include', 'from=now-6h&to=now'); + // cy.wrap(url).should('not.include', 'goto'); + // }); + // }); + }); + + it('Create a relative time range short link', () => { + cy.intercept({ + pathname: '/api/ds/query', + }).as('query'); + openDashboard(); + cy.wait('@query'); + + e2e.pages.Dashboard.DashNav.newShareButton.arrowMenu().click(); + + e2e.pages.Dashboard.DashNav.newShareButton.menu.shareInternally().click(); + + cy.intercept('POST', '/api/short-urls').as('update'); + e2e.pages.ShareDashboardDrawer.ShareInternally.lockTimeRangeSwitch().click({ force: true }); + + cy.window().then((win) => { + const shareConfiguration = win.localStorage.getItem('grafana.dashboard.link.shareConfiguration'); + const { useAbsoluteTimeRange, useShortUrl, theme }: ShareLinkConfiguration = JSON.parse(shareConfiguration); + expect(useAbsoluteTimeRange).eq(false); + expect(useShortUrl).eq(true); + expect(theme).eq('current'); + }); + + cy.wait('@update') + .its('response') + .then((rs) => { + expect(rs.statusCode).eq(200); + const body: { url: string; uid: string } = rs.body; + expect(body.url).contain('goto'); + + // const url = fromBaseUrl(getShortLinkUrl(body.uid)); + // cy.intercept('GET', url).as('get'); + // cy.visit(url, { retryOnNetworkFailure: true }); + // cy.wait('@get'); + // + // cy.url().should('include', 'from=now-6h&to=now'); + }); + }); + + //TODO Failing in CI/CD. Fix it + it.skip('Share button gets configured link', () => { + cy.intercept({ + pathname: '/api/ds/query', + }).as('query'); + openDashboard(); + cy.wait('@query'); + + // cy.wrap( + // Cypress.automation('remote:debugger:protocol', { + // command: 'Browser.grantPermissions', + // params: { + // permissions: ['clipboardReadWrite', 'clipboardSanitizedWrite'], + // origin: window.location.origin, + // }, + // }) + // ); + + //TODO Failing in CI/CD. Fix it + // e2e.pages.Dashboard.DashNav.newShareButton + // .shareLink() + // .click() + // .then(() => { + // cy.window() + // .then((win) => { + // return win.navigator.clipboard.readText().then((url) => { + // cy.wrap(url).as('url'); + // }); + // }) + // .then(() => { + // cy.get('@url').then((url) => { + // cy.wrap(url).should('not.include', 'from=now-6h&to=now'); + // cy.wrap(url).should('include', 'goto'); + // }); + // }); + // }); + + // Open share externally drawer + e2e.pages.Dashboard.DashNav.newShareButton.arrowMenu().click(); + e2e.pages.Dashboard.DashNav.newShareButton.menu.shareInternally().click(); + + cy.window().then((win) => { + const shareConfiguration = win.localStorage.getItem('grafana.dashboard.link.shareConfiguration'); + expect(shareConfiguration).equal(null); + }); + + e2e.pages.ShareDashboardDrawer.ShareInternally.shortenUrlSwitch().click({ force: true }); + e2e.pages.ShareDashboardDrawer.ShareInternally.lockTimeRangeSwitch().click({ force: true }); + + e2e.components.Drawer.General.close().click(); + + cy.url().should('not.include', 'shareView=link'); + + //TODO Failing in CI/CD. Fix it + // e2e.pages.Dashboard.DashNav.newShareButton + // .shareLink() + // .click() + // .then(() => { + // cy.window() + // .then((win) => { + // return win.navigator.clipboard.readText().then((url) => { + // cy.wrap(url).as('url'); + // }); + // }) + // .then(() => { + // cy.get('@url').then((url) => { + // cy.wrap(url).should('include', 'from=now-6h&to=now'); + // cy.wrap(url).should('not.include', 'goto'); + // }); + // }); + // }); + }); +}); + +const openDashboard = () => { + e2e.flows.openDashboard({ + uid: 'ZqZnVvFZz', + queryParams: { '__feature.scenes': true, '__feature.newDashboardSharingComponent': true }, + timeRange: { from: 'now-6h', to: 'now' }, + }); +}; + +// const getShortLinkUrl = (uid: string): string => { +// return `/goto/${uid}`; +// }; diff --git a/e2e/scenes/dashboards-suite/dashboard-share-snapshot-create.spec.ts b/e2e/scenes/dashboards-suite/dashboard-share-snapshot-create.spec.ts new file mode 100644 index 00000000000..808ebc69ee8 --- /dev/null +++ b/e2e/scenes/dashboards-suite/dashboard-share-snapshot-create.spec.ts @@ -0,0 +1,98 @@ +import { SnapshotCreateResponse } from '../../../public/app/features/dashboard/services/SnapshotSrv'; +import { fromBaseUrl } from '../../utils/support/url'; +import { e2e } from '../utils'; +import '../../utils/support/clipboard'; + +describe('Snapshots', () => { + beforeEach(() => { + e2e.flows.login(Cypress.env('USERNAME'), Cypress.env('PASSWORD')); + }); + + it('Create a snapshot dashboard', () => { + // Opening a dashboard + cy.intercept({ + pathname: '/api/ds/query', + }).as('query'); + e2e.flows.openDashboard({ + uid: 'ZqZnVvFZz', + queryParams: { '__feature.scenes': true, '__feature.newDashboardSharingComponent': true }, + }); + cy.wait('@query'); + + //TODO Failing in CI/CD. Fix it + // cy.wrap( + // Cypress.automation('remote:debugger:protocol', { + // command: 'Browser.grantPermissions', + // params: { + // permissions: ['clipboardReadWrite', 'clipboardSanitizedWrite'], + // origin: window.location.origin, + // }, + // }) + // ); + + const panelsToCheck = [ + 'Raw Data Graph', + 'Last non-null', + 'min', + 'Max', + 'The data from graph above with seriesToColumns transform', + ]; + + // Open the sharing drawer + e2e.pages.Dashboard.DashNav.newShareButton.arrowMenu().click(); + e2e.pages.Dashboard.DashNav.newShareButton.menu.shareSnapshot().click(); + + // Publish snapshot + cy.intercept('POST', '/api/snapshots').as('create'); + e2e.pages.ShareDashboardDrawer.ShareSnapshot.publishSnapshot().click(); + cy.wait('@create') + .its('response') + .then((rs) => { + expect(rs.statusCode).eq(200); + const body: SnapshotCreateResponse = rs.body; + cy.visit(fromBaseUrl(getSnapshotUrl(body.key))); + + // Validate the dashboard controls are rendered + e2e.pages.Dashboard.Controls().should('exist'); + + // Validate the panels are rendered + for (const title of panelsToCheck) { + e2e.components.Panels.Panel.title(title).should('be.visible'); + } + }); + + // Copy link button should be visible + // e2e.pages.ShareDashboardDrawer.ShareSnapshot.copyUrlButton().should('exist'); + + //TODO Failing in CI/CD. Fix it + // Copy the snapshot URL form the clipboard and open the snapshot + // e2e.pages.ShareDashboardDrawer.ShareSnapshot.copyUrlButton() + // .click() + // .then(() => { + // cy.copyFromClipboard().then((url) => { + // cy.wrap(url).as('url'); + // }); + // }) + // .then(() => { + // cy.get('@url').then((url) => { + // e2e.pages.ShareDashboardDrawer.ShareSnapshot.visit(getSnapshotKey(String(url))); + // }); + // + // // Validate the dashboard controls are rendered + // e2e.pages.Dashboard.Controls().should('exist'); + // + // // Validate the panels are rendered + // for (const title of panelsToCheck) { + // e2e.components.Panels.Panel.title(title).should('be.visible'); + // } + // }); + }); +}); + +const getSnapshotUrl = (uid: string): string => { + return `/dashboard/snapshot/${uid}`; +}; + +// const getSnapshotKey = (url: string): string => { +// return url.split('/').pop(); +// }; diff --git a/e2e/scenes/dashboards-suite/load-options-from-url.spec.ts b/e2e/scenes/dashboards-suite/load-options-from-url.spec.ts index 04740c74e24..d674ae2a385 100644 --- a/e2e/scenes/dashboards-suite/load-options-from-url.spec.ts +++ b/e2e/scenes/dashboards-suite/load-options-from-url.spec.ts @@ -22,7 +22,9 @@ describe('Variables - Load options from Url', () => { cy.get('input').click(); }); - e2e.components.Select.option().parent().should('have.length', 9); + e2e.components.Select.option().parent().should('have.length', 10); + + e2e.components.Select.toggleAllOptions().should('have.text', 'Selected (1)'); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('All').should('be.visible'); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('A').should('be.visible'); @@ -37,7 +39,9 @@ describe('Variables - Load options from Url', () => { cy.get('input').click(); }); - e2e.components.Select.option().parent().should('have.length', 9); + e2e.components.Select.option().parent().should('have.length', 10); + + e2e.components.Select.toggleAllOptions().should('have.text', 'Selected (1)'); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('All').should('be.visible'); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('AA').should('be.visible'); @@ -52,7 +56,9 @@ describe('Variables - Load options from Url', () => { cy.get('input').click(); }); - e2e.components.Select.option().parent().should('have.length', 9); + e2e.components.Select.option().parent().should('have.length', 10); + + e2e.components.Select.toggleAllOptions().should('have.text', 'Selected (1)'); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('All').should('be.visible'); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('AAA').should('be.visible'); @@ -75,7 +81,9 @@ describe('Variables - Load options from Url', () => { cy.get('input').click(); }); - e2e.components.Select.option().parent().should('have.length', 9); + e2e.components.Select.option().parent().should('have.length', 10); + + e2e.components.Select.toggleAllOptions().should('have.text', 'Selected (1)'); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('All').should('be.visible'); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('A').should('be.visible'); @@ -90,7 +98,9 @@ describe('Variables - Load options from Url', () => { cy.get('input').click(); }); - e2e.components.Select.option().parent().should('have.length', 9); + e2e.components.Select.option().parent().should('have.length', 10); + + e2e.components.Select.toggleAllOptions().should('have.text', 'Selected (1)'); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('All').should('be.visible'); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BA').should('be.visible'); @@ -105,7 +115,9 @@ describe('Variables - Load options from Url', () => { cy.get('input').click(); }); - e2e.components.Select.option().parent().should('have.length', 9); + e2e.components.Select.option().parent().should('have.length', 10); + + e2e.components.Select.toggleAllOptions().should('have.text', 'Selected (1)'); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('All').should('be.visible'); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BBA').should('be.visible'); @@ -139,7 +151,9 @@ describe('Variables - Load options from Url', () => { cy.get('input').click(); }); - e2e.components.Select.option().parent().should('have.length', 9); + e2e.components.Select.option().parent().should('have.length', 10); + + e2e.components.Select.toggleAllOptions().should('have.text', 'Selected (1)'); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('All').should('be.visible'); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('A').should('be.visible'); diff --git a/e2e/scenes/dashboards-suite/new-query-variable.spec.ts b/e2e/scenes/dashboards-suite/new-query-variable.spec.ts index 6f63d2ac46e..d6ead2937c4 100644 --- a/e2e/scenes/dashboards-suite/new-query-variable.spec.ts +++ b/e2e/scenes/dashboards-suite/new-query-variable.spec.ts @@ -175,7 +175,10 @@ describe('Variables - Query - Add variable', () => { cy.get('input').click(); }); - e2e.components.Select.option().should('have.length', 2); + e2e.components.Select.option().should('have.length', 3); + + e2e.components.Select.toggleAllOptions().should('have.text', 'Selected (1)'); + e2e.components.Select.option().contains('All'); e2e.components.Select.option().contains('C'); }); diff --git a/e2e/scenes/dashboards-suite/set-options-from-ui.spec.ts b/e2e/scenes/dashboards-suite/set-options-from-ui.spec.ts index 7903488314a..bdcc5c6b516 100644 --- a/e2e/scenes/dashboards-suite/set-options-from-ui.spec.ts +++ b/e2e/scenes/dashboards-suite/set-options-from-ui.spec.ts @@ -19,6 +19,7 @@ describe('Variables - Set options from ui', () => { }); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('A').should('be.visible').click(); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('B').should('be.visible').click(); + cy.get('body').click(); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('B').scrollIntoView().should('be.visible'); @@ -31,7 +32,9 @@ describe('Variables - Set options from ui', () => { cy.get('input').click(); }); - e2e.components.Select.option().parent().should('have.length', 9); + e2e.components.Select.option().parent().should('have.length', 10); + + e2e.components.Select.toggleAllOptions().should('have.text', 'Selected (1)'); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('All').should('be.visible'); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BA').should('be.visible'); @@ -72,6 +75,9 @@ describe('Variables - Set options from ui', () => { cy.get('input').click(); }); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('B').should('be.visible').click(); + + e2e.components.Select.toggleAllOptions().should('have.text', 'Selected (2)'); + cy.get('body').click(); cy.wait('@query'); @@ -102,7 +108,7 @@ describe('Variables - Set options from ui', () => { cy.get('input').click(); }); - e2e.components.Select.option().should('have.length', 9); + e2e.components.Select.option().should('have.length', 10); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('All').should('be.visible'); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('AAA').should('be.visible'); @@ -142,7 +148,7 @@ describe('Variables - Set options from ui', () => { cy.get('input').click(); }); - e2e.components.Select.option().should('have.length', 9); + e2e.components.Select.option().should('have.length', 10); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('All').should('be.visible'); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BA').should('be.visible'); @@ -156,7 +162,7 @@ describe('Variables - Set options from ui', () => { .within(() => { cy.get('input').click(); }); - e2e.components.Select.option().should('have.length', 9); + e2e.components.Select.option().should('have.length', 10); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BBA').should('be.visible'); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BBB').should('be.visible'); diff --git a/e2e/utils/support/clipboard.ts b/e2e/utils/support/clipboard.ts new file mode 100644 index 00000000000..aa5841ea2d6 --- /dev/null +++ b/e2e/utils/support/clipboard.ts @@ -0,0 +1,29 @@ +declare global { + // eslint-disable-next-line @typescript-eslint/no-namespace + namespace Cypress { + interface Chainable { + copyToClipboard(): Chainable; + copyFromClipboard(): Chainable; + } + } +} + +Cypress.Commands.add('copyFromClipboard', () => { + return cy.window().then((win) => { + return cy.wrap(win.navigator.clipboard.readText()); + }); +}); + +Cypress.Commands.add( + 'copyToClipboard', + { + prevSubject: [], + }, + (subject: string) => { + return cy.window().then((win) => { + return cy.wrap(win.navigator.clipboard.writeText(subject)); + }); + } +); + +export {}; diff --git a/e2e/utils/support/url.ts b/e2e/utils/support/url.ts index 34620974b06..94e42827940 100644 --- a/e2e/utils/support/url.ts +++ b/e2e/utils/support/url.ts @@ -1,5 +1,3 @@ -import { e2e } from '../index'; - const getBaseUrl = () => Cypress.env('BASE_URL') || Cypress.config().baseUrl || 'http://localhost:3000'; export const fromBaseUrl = (url = '') => new URL(url, getBaseUrl()).href; diff --git a/go.mod b/go.mod index b2117a25bd0..9d4902f1ef9 100644 --- a/go.mod +++ b/go.mod @@ -23,7 +23,7 @@ require ( github.com/Azure/azure-storage-blob-go v0.15.0 // @grafana/grafana-backend-group github.com/Azure/go-autorest/autorest v0.11.29 // @grafana/grafana-backend-group github.com/Azure/go-autorest/autorest/adal v0.9.23 // @grafana/grafana-backend-group - github.com/BurntSushi/toml v1.3.2 // @grafana/identity-access-team + github.com/BurntSushi/toml v1.4.0 // @grafana/identity-access-team github.com/Masterminds/semver v1.5.0 // @grafana/grafana-backend-group github.com/Masterminds/semver/v3 v3.2.0 // @grafana/grafana-release-guild github.com/Masterminds/sprig/v3 v3.2.3 // @grafana/grafana-backend-group @@ -40,17 +40,16 @@ require ( github.com/blang/semver/v4 v4.0.0 // indirect; @grafana/grafana-release-guild github.com/blugelabs/bluge v0.1.9 // @grafana/grafana-backend-group github.com/blugelabs/bluge_segment_api v0.2.0 // @grafana/grafana-backend-group - github.com/bradfitz/gomemcache v0.0.0-20190913173617-a41fca850d0b // @grafana/grafana-backend-group + github.com/bradfitz/gomemcache v0.0.0-20230905024940-24af94b03874 // @grafana/grafana-backend-group github.com/bufbuild/connect-go v1.10.0 // @grafana/observability-traces-and-profiling github.com/bwmarrin/snowflake v0.3.0 // @grafan/grafana-app-platform-squad github.com/centrifugal/centrifuge v0.33.3 // @grafana/grafana-app-platform-squad github.com/crewjam/saml v0.4.13 // @grafana/identity-access-team - github.com/dave/dst v0.27.2 // @grafana/grafana-as-code + github.com/dave/dst v0.27.3 // @grafana/grafana-as-code github.com/dlmiddlecote/sqlstats v1.0.2 // @grafana/grafana-backend-group - github.com/fatih/color v1.16.0 // @grafana/grafana-backend-group + github.com/fatih/color v1.17.0 // @grafana/grafana-backend-group github.com/fullstorydev/grpchan v1.1.1 // @grafana/grafana-backend-group github.com/gchaincl/sqlhooks v1.3.0 // @grafana/grafana-search-and-storage - github.com/getkin/kin-openapi v0.125.0 // @grafana/grafana-as-code github.com/go-jose/go-jose/v3 v3.0.3 // @grafana/identity-access-team github.com/go-kit/log v0.2.1 // @grafana/grafana-backend-group github.com/go-ldap/ldap/v3 v3.4.4 // @grafana/identity-access-team @@ -74,9 +73,9 @@ require ( github.com/googleapis/gax-go/v2 v2.13.0 // @grafana/grafana-backend-group github.com/gorilla/mux v1.8.1 // @grafana/grafana-backend-group github.com/gorilla/websocket v1.5.0 // @grafana/grafana-app-platform-squad - github.com/grafana/alerting v0.0.0-20240822131459-9daa6239cc41 // @grafana/alerting-backend - github.com/grafana/authlib v0.0.0-20240906122029-0100695765b9 // @grafana/identity-access-team - github.com/grafana/authlib/claims v0.0.0-20240903121118-16441568af1e // @grafana/identity-access-team + github.com/grafana/alerting v0.0.0-20240917171353-6c25eb6eff10 // @grafana/alerting-backend + github.com/grafana/authlib v0.0.0-20240827201526-24af227df935 // @grafana/identity-access-team + github.com/grafana/authlib/claims v0.0.0-20240827210201-19d5347dd8dd // @grafana/identity-access-team github.com/grafana/codejen v0.0.3 // @grafana/dataviz-squad github.com/grafana/cuetsy v0.1.11 // @grafana/grafana-as-code github.com/grafana/dataplane/examples v0.0.1 // @grafana/observability-metrics @@ -84,12 +83,12 @@ require ( github.com/grafana/dskit v0.0.0-20240311184239-73feada6c0d7 // @grafana/grafana-backend-group 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-aws-sdk v0.30.0 // @grafana/aws-datasources + github.com/grafana/grafana-aws-sdk v0.31.2 // @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 github.com/grafana/grafana-openapi-client-go v0.0.0-20231213163343-bd475d63fb79 // @grafana/grafana-backend-group - github.com/grafana/grafana-plugin-sdk-go v0.247.0 // @grafana/plugins-platform-backend + github.com/grafana/grafana-plugin-sdk-go v0.250.0 // @grafana/plugins-platform-backend github.com/grafana/grafana/pkg/aggregator v0.0.0-20240813192817-1b0e6b5c09b2 // @grafana/grafana-app-platform-squad github.com/grafana/grafana/pkg/apimachinery v0.0.0-20240821155123-6891eb1d35da // @grafana/grafana-app-platform-squad github.com/grafana/grafana/pkg/apiserver v0.0.0-20240821155123-6891eb1d35da // @grafana/grafana-app-platform-squad @@ -166,21 +165,21 @@ require ( go.opentelemetry.io/otel v1.29.0 // @grafana/grafana-backend-group go.opentelemetry.io/otel/exporters/jaeger v1.17.0 // @grafana/grafana-backend-group go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.29.0 // @grafana/grafana-backend-group - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.28.0 // @grafana/grafana-backend-group + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.29.0 // @grafana/grafana-backend-group go.opentelemetry.io/otel/sdk v1.29.0 // @grafana/grafana-backend-group go.opentelemetry.io/otel/trace v1.29.0 // @grafana/grafana-backend-group go.uber.org/atomic v1.11.0 // @grafana/alerting-backend go.uber.org/goleak v1.3.0 // @grafana/grafana-search-and-storage gocloud.dev v0.39.0 // @grafana/grafana-app-platform-squad golang.org/x/crypto v0.27.0 // @grafana/grafana-backend-group - golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // @grafana/alerting-backend - golang.org/x/mod v0.19.0 // indirect; @grafana/grafana-backend-group + golang.org/x/exp v0.0.0-20240808152545-0cdaa3abc0fa // @grafana/alerting-backend + golang.org/x/mod v0.20.0 // indirect; @grafana/grafana-backend-group golang.org/x/net v0.29.0 // @grafana/oss-big-tent @grafana/partner-datasources - golang.org/x/oauth2 v0.22.0 // @grafana/identity-access-team + golang.org/x/oauth2 v0.23.0 // @grafana/identity-access-team golang.org/x/sync v0.8.0 // @grafana/alerting-backend golang.org/x/text v0.18.0 // @grafana/grafana-backend-group golang.org/x/time v0.6.0 // @grafana/grafana-backend-group - golang.org/x/tools v0.23.0 // @grafana/grafana-as-code + golang.org/x/tools v0.24.0 // @grafana/grafana-as-code gonum.org/v1/gonum v0.14.0 // @grafana/observability-metrics google.golang.org/api v0.191.0 // @grafana/grafana-backend-group google.golang.org/grpc v1.66.0 // @grafana/plugins-platform-backend @@ -254,7 +253,7 @@ require ( github.com/centrifugal/protocol v0.13.4 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cheekybits/genny v1.0.0 // indirect - github.com/chromedp/cdproto v0.0.0-20240426225625-909263490071 // indirect + github.com/chromedp/cdproto v0.0.0-20240810084448-b931b754e476 // indirect github.com/cloudflare/circl v1.3.7 // indirect github.com/cockroachdb/apd/v2 v2.0.2 // indirect github.com/coreos/go-semver v0.3.1 // indirect @@ -267,7 +266,7 @@ require ( github.com/docker/go-units v0.5.0 // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/edsrzf/mmap-go v1.1.0 // indirect - github.com/elazarl/goproxy v0.0.0-20231117061959-7cc037d33fb5 // indirect + github.com/elazarl/goproxy v0.0.0-20240726154733-8b0c20506380 // indirect github.com/emicklei/go-restful/v3 v3.11.0 // indirect github.com/emicklei/proto v1.10.0 // indirect github.com/envoyproxy/protoc-gen-validate v1.0.4 // indirect @@ -285,14 +284,13 @@ require ( github.com/go-openapi/spec v0.21.0 // indirect github.com/go-openapi/swag v0.23.0 // indirect github.com/go-openapi/validate v0.24.0 // indirect - github.com/goccy/go-json v0.10.2 // indirect + github.com/goccy/go-json v0.10.3 // indirect github.com/gofrs/uuid v4.4.0+incompatible // indirect github.com/gogo/googleapis v1.4.1 // indirect github.com/gogo/status v1.1.1 // indirect github.com/golang-jwt/jwt/v5 v5.2.1 // indirect github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect github.com/golang-sql/sqlexp v0.1.0 // indirect - github.com/golang/glog v1.2.1 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/google/btree v1.1.2 // indirect github.com/google/cel-go v0.20.1 // indirect @@ -304,10 +302,9 @@ require ( github.com/grafana/grafana/pkg/storage/unified/apistore v0.0.0-20240821183201-2f012860344d // @grafana/grafana-search-and-storage github.com/grafana/grafana/pkg/storage/unified/resource v0.0.0-20240821161612-71f0dae39e9d // @grafana/grafana-search-and-storage github.com/grafana/regexp v0.0.0-20221123153739-15dc172cd2db // indirect - github.com/grafana/sqlds/v3 v3.2.0 // indirect github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 // indirect github.com/grpc-ecosystem/go-grpc-prometheus v1.2.1-0.20191002090509-6af20e3a5340 // indirect; @grafana/plugins-platform-backend - github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 // @grafana/identity-access-team + github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0 // @grafana/identity-access-team github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect github.com/hashicorp/go-immutable-radix v1.3.1 // indirect @@ -343,7 +340,7 @@ require ( github.com/karlseguin/ccache/v3 v3.0.5 // indirect github.com/klauspost/asmfmt v1.3.2 // indirect github.com/klauspost/compress v1.17.9 // indirect - github.com/klauspost/cpuid/v2 v2.2.7 // indirect + github.com/klauspost/cpuid/v2 v2.2.8 // indirect github.com/kr/text v0.2.0 // indirect github.com/kylelemons/godebug v1.1.0 // indirect github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect @@ -354,7 +351,7 @@ require ( github.com/mattetti/filebuffer v1.0.1 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-ieproxy v0.0.11 // indirect - github.com/mattn/go-runewidth v0.0.15 // indirect + github.com/mattn/go-runewidth v0.0.16 // indirect github.com/mfridman/interpolate v0.0.2 // indirect github.com/miekg/dns v1.1.59 // indirect github.com/minio/asm2plan9s v0.0.0-20200509001527-cdd76441f9d8 // indirect @@ -392,7 +389,7 @@ require ( github.com/prometheus/common/sigv4 v0.1.0 // indirect github.com/prometheus/exporter-toolkit v0.11.0 // indirect github.com/prometheus/procfs v0.15.1 // indirect - github.com/protocolbuffers/txtpbfmt v0.0.0-20220428173112-74888fd59c2b // indirect + github.com/protocolbuffers/txtpbfmt v0.0.0-20230328191034-3462fbc510c0 // indirect github.com/redis/rueidis v1.0.45 // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/rivo/uniseg v0.4.7 // indirect @@ -444,7 +441,7 @@ require ( golang.org/x/xerrors v0.0.0-20240716161551-93cc26a95ae9 // indirect google.golang.org/genproto v0.0.0-20240812133136-8ffd90a71988 // indirect; @grafana/grafana-backend-group google.golang.org/genproto/googleapis/api v0.0.0-20240822170219-fc7c04adadcd // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240822170219-fc7c04adadcd // indirect gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect gopkg.in/fsnotify/fsnotify.v1 v1.4.7 // indirect gopkg.in/inf.v0 v0.9.1 // indirect @@ -476,12 +473,19 @@ require ( gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect ) +require ( + github.com/getkin/kin-openapi v0.127.0 // @grafana/grafana-app-platform-squad + github.com/grafana/grafana/apps/playlist v0.0.0-20240917082838-e2bce38a7990 // @grafana/grafana-app-platform-squad +) + require ( cloud.google.com/go/longrunning v0.5.12 // indirect github.com/at-wat/mqtt-go v0.19.4 // indirect github.com/dolthub/maphash v0.1.0 // indirect github.com/gammazero/deque v0.2.1 // indirect + github.com/grafana/grafana-app-sdk v0.19.0 // indirect github.com/grafana/grafana/pkg/semconv v0.0.0-20240808213237-f4d2e064f435 // indirect + github.com/grafana/sqlds/v4 v4.1.0 // indirect github.com/hairyhenderson/go-which v0.2.0 // indirect github.com/iancoleman/orderedmap v0.3.0 // indirect github.com/maypok86/otter v1.2.2 // indirect diff --git a/go.sum b/go.sum index d988a24a782..c9aa9b5f4e9 100644 --- a/go.sum +++ b/go.sum @@ -1420,8 +1420,9 @@ github.com/AzureAD/microsoft-authentication-library-for-go v1.1.1/go.mod h1:wP83 github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2 h1:XHOnouVk1mxXfQidrMEnLlPk9UMeRtyBTnEFtxkV0kU= github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8= github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= +github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0= +github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/Code-Hex/go-generics-cache v1.5.1 h1:6vhZGc5M7Y/YD8cIUcY8kcuQLB4cHR7U+0KMqAA0KcU= github.com/Code-Hex/go-generics-cache v1.5.1/go.mod h1:qxcC9kRVrct9rHeiYpFWSoW1vxyillCVzX13KZG8dl4= @@ -1641,8 +1642,8 @@ github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvF github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4= github.com/boombuler/barcode v1.0.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= github.com/boombuler/barcode v1.0.1/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= -github.com/bradfitz/gomemcache v0.0.0-20190913173617-a41fca850d0b h1:L/QXpzIa3pOvUGt1D1lA5KjYhPBAN/3iWdP7xeFS9F0= -github.com/bradfitz/gomemcache v0.0.0-20190913173617-a41fca850d0b/go.mod h1:H0wQNHz2YrLsuXOZozoeDmnHXkNCRmMW0gwFWDfEZDA= +github.com/bradfitz/gomemcache v0.0.0-20230905024940-24af94b03874 h1:N7oVaKyGp8bttX0bfZGmcGkjz7DLQXhAn3DNd3T0ous= +github.com/bradfitz/gomemcache v0.0.0-20230905024940-24af94b03874/go.mod h1:r5xuitiExdLAJ09PR7vBVENGvp4ZuTBeWTGtxuX3K+c= github.com/bsm/ginkgo/v2 v2.9.5 h1:rtVBYPs3+TC5iLUVOis1B9tjLTup7Cj5IfzosKtvTJ0= github.com/bsm/ginkgo/v2 v2.9.5/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= github.com/bsm/gomega v1.26.0 h1:LhQm+AFcgV2M0WyKroMASzAzCAJVpAxQXv4SaI9a69Y= @@ -1681,8 +1682,8 @@ github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XL github.com/cheekybits/genny v1.0.0 h1:uGGa4nei+j20rOSeDeP5Of12XVm7TGUd4dJA9RDitfE= github.com/cheekybits/genny v1.0.0/go.mod h1:+tQajlRqAUrPI7DOSpB0XAqZYtQakVtB7wXkRAgjxjQ= github.com/chromedp/cdproto v0.0.0-20230802225258-3cf4e6d46a89/go.mod h1:GKljq0VrfU4D5yc+2qA6OVr8pmO/MBbPEWqWQ/oqGEs= -github.com/chromedp/cdproto v0.0.0-20240426225625-909263490071 h1:RdCf9hH3xq5vJifrjGB7zQlFkdRB3pAppcX2helDq2U= -github.com/chromedp/cdproto v0.0.0-20240426225625-909263490071/go.mod h1:GKljq0VrfU4D5yc+2qA6OVr8pmO/MBbPEWqWQ/oqGEs= +github.com/chromedp/cdproto v0.0.0-20240810084448-b931b754e476 h1:VnjHsRXCRti7Av7E+j4DCha3kf68echfDzQ+wD11SBU= +github.com/chromedp/cdproto v0.0.0-20240810084448-b931b754e476/go.mod h1:GKljq0VrfU4D5yc+2qA6OVr8pmO/MBbPEWqWQ/oqGEs= github.com/chromedp/chromedp v0.9.2/go.mod h1:LkSXJKONWTCHAfQasKFUZI+mxqS4tZqhmtGzzhLsnLs= github.com/chromedp/sysutil v1.0.0/go.mod h1:kgWmDdq8fTzXYcKIBqIYvRRTnYb9aNS9moAV0xufSww= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= @@ -1769,8 +1770,8 @@ github.com/cznic/zappy v0.0.0-20160723133515-2533cb5b45cc/go.mod h1:Y1SNZ4dRUOKX github.com/dave/astrid v0.0.0-20170323122508-8c2895878b14/go.mod h1:Sth2QfxfATb/nW4EsrSi2KyJmbcniZ8TgTaji17D6ms= github.com/dave/brenda v1.1.0/go.mod h1:4wCUr6gSlu5/1Tk7akE5X7UorwiQ8Rij0SKH3/BGMOM= github.com/dave/courtney v0.3.0/go.mod h1:BAv3hA06AYfNUjfjQr+5gc6vxeBVOupLqrColj+QSD8= -github.com/dave/dst v0.27.2 h1:4Y5VFTkhGLC1oddtNwuxxe36pnyLxMFXT51FOzH8Ekc= -github.com/dave/dst v0.27.2/go.mod h1:jHh6EOibnHgcUW3WjKHisiooEkYwqpHLBSX1iOBhEyc= +github.com/dave/dst v0.27.3 h1:P1HPoMza3cMEquVf9kKy8yXsFirry4zEnWOdYPOoIzY= +github.com/dave/dst v0.27.3/go.mod h1:jHh6EOibnHgcUW3WjKHisiooEkYwqpHLBSX1iOBhEyc= github.com/dave/gopackages v0.0.0-20170318123100-46e7023ec56e/go.mod h1:i00+b/gKdIDIxuLDFob7ustLAVqhsZRk2qVZrArELGQ= github.com/dave/jennifer v1.6.0 h1:MQ/6emI2xM7wt0tJzJzyUik2Q3Tcn2eE0vtYgh4GPVI= github.com/dave/jennifer v1.6.0/go.mod h1:AxTG893FiZKqxy3FP1kL80VMshSMuz2G+EgvszgGRnk= @@ -1828,9 +1829,8 @@ github.com/edsrzf/mmap-go v1.1.0 h1:6EUwBLQ/Mcr1EYLE4Tn1VdW1A4ckqCQWZBw8Hr0kjpQ= github.com/edsrzf/mmap-go v1.1.0/go.mod h1:19H/e8pUPLicwkyNgOykDXkJ9F0MHE+Z52B8EIth78Q= github.com/efficientgo/tools/core v0.0.0-20220225185207-fe763185946b h1:ZHiD4/yE4idlbqvAO6iYCOYRzOMRpxkW+FKasRA3tsQ= github.com/efficientgo/tools/core v0.0.0-20220225185207-fe763185946b/go.mod h1:OmVcnJopJL8d3X3sSXTiypGoUSgFq1aDGmlrdi9dn/M= -github.com/elazarl/goproxy v0.0.0-20231117061959-7cc037d33fb5 h1:m62nsMU279qRD9PQSWD1l66kmkXzuYcnVJqL4XLeV2M= -github.com/elazarl/goproxy v0.0.0-20231117061959-7cc037d33fb5/go.mod h1:Ro8st/ElPeALwNFlcTpWmkr6IoMFfkjXAvTHpevnDsM= -github.com/elazarl/goproxy/ext v0.0.0-20190711103511-473e67f1d7d2/go.mod h1:gNh8nYJoAm43RfaxurUnxr+N1PwuFV3ZMl/efxlIlY8= +github.com/elazarl/goproxy v0.0.0-20240726154733-8b0c20506380 h1:1NyRx2f4W4WBRyg0Kys0ZbaNmDDzZ2R/C7DTi+bbsJ0= +github.com/elazarl/goproxy v0.0.0-20240726154733-8b0c20506380/go.mod h1:thX175TtLTzLj3p7N/Q9IiKZ7NF+p72cvL91emV0hzo= github.com/elazarl/goproxy/ext v0.0.0-20220115173737-adb46da277ac h1:9yrT5tmn9Zc0ytWPASlaPwQfQMQYnRf0RSDe1XvHw0Q= github.com/elazarl/goproxy/ext v0.0.0-20220115173737-adb46da277ac/go.mod h1:gNh8nYJoAm43RfaxurUnxr+N1PwuFV3ZMl/efxlIlY8= github.com/emicklei/go-restful/v3 v3.8.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= @@ -1875,8 +1875,8 @@ github.com/fatih/color v1.12.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGE github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= github.com/fatih/color v1.14.1/go.mod h1:2oHN61fhTpgcxD3TSWCgKDiH1+x4OiDVVGH8WlgGZGg= github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw= -github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= -github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= +github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4= +github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI= github.com/felixge/httpsnoop v1.0.3/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= @@ -1905,8 +1905,8 @@ github.com/gammazero/deque v0.2.1 h1:qSdsbG6pgp6nL7A0+K/B7s12mcCY/5l5SIUpMOl+dC0 github.com/gammazero/deque v0.2.1/go.mod h1:LFroj8x4cMYCukHJDbxFCkT+r9AndaJnFMuZDV34tuU= github.com/gchaincl/sqlhooks v1.3.0 h1:yKPXxW9a5CjXaVf2HkQn6wn7TZARvbAOAelr3H8vK2Y= github.com/gchaincl/sqlhooks v1.3.0/go.mod h1:9BypXnereMT0+Ys8WGWHqzgkkOfHIhyeUCqXC24ra34= -github.com/getkin/kin-openapi v0.125.0 h1:jyQCyf2qXS1qvs2U00xQzkGCqYPhEhZDmSmVt65fXno= -github.com/getkin/kin-openapi v0.125.0/go.mod h1:wb1aSZA/iWmorQP9KTAS/phLj/t17B5jT7+fS8ed9NM= +github.com/getkin/kin-openapi v0.127.0 h1:Mghqi3Dhryf3F8vR370nN67pAERW+3a95vomb3MAREY= +github.com/getkin/kin-openapi v0.127.0/go.mod h1:OZrfXzUfGrNbsKj+xmFBx6E5c6yH3At/tAKSc2UszXM= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/go-asn1-ber/asn1-ber v1.5.4 h1:vXT6d/FNDiELJnLb6hGNa309LMsrCoYFvpwHDF0+Y1A= github.com/go-asn1-ber/asn1-ber v1.5.4/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= @@ -2034,8 +2034,9 @@ github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u1 github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= github.com/gobwas/ws v1.2.1/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/KY= github.com/goccy/go-json v0.9.11/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= -github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA= +github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/goccy/go-yaml v1.9.5/go.mod h1:U/jl18uSupI5rdI2jmuCswEA2htH9eXfferR3KfscvA= github.com/goccy/go-yaml v1.9.8/go.mod h1:JubOolP3gh0HpiBc4BLRD4YmjEjHAmIIB2aaXKkTfoE= github.com/goccy/go-yaml v1.11.0/go.mod h1:H+mJrWtjPTJAHvRbV09MCK9xYwODM+wRTVFFTWckfng= @@ -2075,8 +2076,6 @@ github.com/golang/glog v1.0.0/go.mod h1:EWib/APOK0SL3dFbYqvxE3UYd8E6s1ouQ7iEp/0L github.com/golang/glog v1.1.0/go.mod h1:pfYeQZ3JWZoXTV5sFc986z3HTpwQs9At6P4ImfuP3NQ= github.com/golang/glog v1.1.2/go.mod h1:zR+okUeTbrL6EL3xHUDxZuEtGv04p5shwip1+mL/rLQ= github.com/golang/glog v1.2.0/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w= -github.com/golang/glog v1.2.1 h1:OptwRhECazUx5ix5TTWC3EZhsZEHWcYWY4FQHTIubm4= -github.com/golang/glog v1.2.1/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w= github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= @@ -2257,12 +2256,12 @@ github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/grafana/alerting v0.0.0-20240822131459-9daa6239cc41 h1:p+UsX43BoDH5YlG6xUd9xDS3M4sWouy8VJ+ODv5S6uE= -github.com/grafana/alerting v0.0.0-20240822131459-9daa6239cc41/go.mod h1:GMLi6d09Xqo96fCVUjNk//rcjP5NKEdjOzfWIffD5r4= -github.com/grafana/authlib v0.0.0-20240906122029-0100695765b9 h1:e+kFqd2sECBhbxOV1NoVxsudLygNQuu9bO+7FjNTkXo= -github.com/grafana/authlib v0.0.0-20240906122029-0100695765b9/go.mod h1:PFzXbCrn0GIpN4KwT6NP1l5Z1CPLfmKHnYx8rZzQcyY= -github.com/grafana/authlib/claims v0.0.0-20240903121118-16441568af1e h1:ng5SopWamGS0MHaCj2e5huWYxAfMeCrj1l/dbJnfiow= -github.com/grafana/authlib/claims v0.0.0-20240903121118-16441568af1e/go.mod h1:r+F8H6awwjNQt/KPZ2GNwjk8TvsJ7/gxzkXN26GlL/A= +github.com/grafana/alerting v0.0.0-20240917171353-6c25eb6eff10 h1:oDbLKM34O+JUF9EQFS+9aYhdYoeNfUpXqNjFCLIxwF4= +github.com/grafana/alerting v0.0.0-20240917171353-6c25eb6eff10/go.mod h1:GMLi6d09Xqo96fCVUjNk//rcjP5NKEdjOzfWIffD5r4= +github.com/grafana/authlib v0.0.0-20240827201526-24af227df935 h1:nT4UY61s2flsiLkU2jDqtqFhOLwqh355+8ZhnavKoMQ= +github.com/grafana/authlib v0.0.0-20240827201526-24af227df935/go.mod h1:ER7bMzNNWTN/5Zl3pwqfgS6XEhcanjrvL7lOp8Ow6oc= +github.com/grafana/authlib/claims v0.0.0-20240827210201-19d5347dd8dd h1:sIlR7n38/MnZvX2qxDEszywXdI5soCwQ78aTDSARvus= +github.com/grafana/authlib/claims v0.0.0-20240827210201-19d5347dd8dd/go.mod h1:r+F8H6awwjNQt/KPZ2GNwjk8TvsJ7/gxzkXN26GlL/A= github.com/grafana/codejen v0.0.3 h1:tAWxoTUuhgmEqxJPOLtJoxlPBbMULFwKFOcRsPRPXDw= github.com/grafana/codejen v0.0.3/go.mod h1:zmwwM/DRyQB7pfuBjTWII3CWtxcXh8LTwAYGfDfpR6s= github.com/grafana/cue v0.0.0-20230926092038-971951014e3f h1:TmYAMnqg3d5KYEAaT6PtTguL2GjLfvr6wnAX8Azw6tQ= @@ -2281,8 +2280,10 @@ github.com/grafana/gofpdf v0.0.0-20231002120153-857cc45be447 h1:jxJJ5z0GxqhWFbQU github.com/grafana/gofpdf v0.0.0-20231002120153-857cc45be447/go.mod h1:IxsY6mns6Q5sAnWcrptrgUrSglTZJXH/kXr9nbpb/9I= github.com/grafana/gomemcache v0.0.0-20240805133030-fdaf6a95408e h1:UlEET0InuoFautfaFp8lDrNF7rPHYXuBMrzwWx9XqFY= github.com/grafana/gomemcache v0.0.0-20240805133030-fdaf6a95408e/go.mod h1:IGRj8oOoxwJbHBYl1+OhS9UjQR0dv6SQOep7HqmtyFU= -github.com/grafana/grafana-aws-sdk v0.30.0 h1:6IIetM4s2NbvPOI4/fefsyN84BIb0/T09lHGF/pywo8= -github.com/grafana/grafana-aws-sdk v0.30.0/go.mod h1:ZSVPU7IIJSi5lEg+K3Js+EUpZLXxUaBdaQWH+As1ihI= +github.com/grafana/grafana-app-sdk v0.19.0 h1:RY9HvCFR+4WUy81n53wejZnof9qE6++pd6r24d6+JYs= +github.com/grafana/grafana-app-sdk v0.19.0/go.mod h1:y0BgzYxc+a7CwOqkwUhN9zXd5cgZJjd2zAbgHEd/xzo= +github.com/grafana/grafana-aws-sdk v0.31.2 h1:Mv01GAHcIG3S2pVtRlt1cUnnWzUAr4qr74HJvYv11JQ= +github.com/grafana/grafana-aws-sdk v0.31.2/go.mod h1:5nt5Gmp6+GyM+Jr7xsXKJtbizxbYXXLmEac6kw5paQI= 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= @@ -2292,8 +2293,10 @@ github.com/grafana/grafana-google-sdk-go v0.1.0/go.mod h1:Vo2TKWfDVmNTELBUM+3lkr github.com/grafana/grafana-openapi-client-go v0.0.0-20231213163343-bd475d63fb79 h1:r+mU5bGMzcXCRVAuOrTn54S80qbfVkvTdUJZfSfTNbs= github.com/grafana/grafana-openapi-client-go v0.0.0-20231213163343-bd475d63fb79/go.mod h1:wc6Hbh3K2TgCUSfBC/BOzabItujtHMESZeFk5ZhdxhQ= github.com/grafana/grafana-plugin-sdk-go v0.114.0/go.mod h1:D7x3ah+1d4phNXpbnOaxa/osSaZlwh9/ZUnGGzegRbk= -github.com/grafana/grafana-plugin-sdk-go v0.247.0 h1:QOxVw3sgaPaxR3odu9DSRNGdCknyAuMWl0hDhaF91OA= -github.com/grafana/grafana-plugin-sdk-go v0.247.0/go.mod h1:sEi0wRVTvpxyB0KaMNbhPM74OnDMwVpah97usm6QXEM= +github.com/grafana/grafana-plugin-sdk-go v0.250.0 h1:9EBucp9jLqMx2b8NTlOXH+4OuQWUh6L85c6EJUN8Jdo= +github.com/grafana/grafana-plugin-sdk-go v0.250.0/go.mod h1:gCGN9kHY3KeX4qyni3+Kead38Q+85pYOrsDcxZp6AIk= +github.com/grafana/grafana/apps/playlist v0.0.0-20240917082838-e2bce38a7990 h1:uQMZE/z+Y+o/U0z/g8ckAHss7U7LswedilByA2535DU= +github.com/grafana/grafana/apps/playlist v0.0.0-20240917082838-e2bce38a7990/go.mod h1:3Vi0xv/4OBkBw4R9GAERkSrBnx06qrjpmNBRisucuSM= github.com/grafana/grafana/pkg/aggregator v0.0.0-20240813192817-1b0e6b5c09b2 h1:2H9x4q53pkfUGtSNYD1qSBpNnxrFgylof/TYADb5xMI= github.com/grafana/grafana/pkg/aggregator v0.0.0-20240813192817-1b0e6b5c09b2/go.mod h1:gBLBniiSUQvyt4LRrpIeysj8Many0DV+hdUKifRE0Ec= github.com/grafana/grafana/pkg/apimachinery v0.0.0-20240821155123-6891eb1d35da h1:2E3c/I3ayAy4Z1GwIPqXNZcpUccRapE1aBXA1ho4g7o= @@ -2323,8 +2326,8 @@ github.com/grafana/regexp v0.0.0-20221123153739-15dc172cd2db h1:7aN5cccjIqCLTzed github.com/grafana/regexp v0.0.0-20221123153739-15dc172cd2db/go.mod h1:M5qHK+eWfAv8VR/265dIuEpL3fNfeC21tXXp9itM24A= github.com/grafana/saml v0.4.15-0.20240523142256-cc370b98af7c h1:SWmG1QLZ36Ay0htq4Wt3dzlNIhWvQ3GUf7mk19dR8nI= github.com/grafana/saml v0.4.15-0.20240523142256-cc370b98af7c/go.mod h1:S4+611dxnKt8z/ulbvaJzcgSHsuhjVc1QHNTcr1R7Fw= -github.com/grafana/sqlds/v3 v3.2.0 h1:WXuYEaFfiCvgm8kK2ixx44/zAEjFzCylA2+RF3GBqZA= -github.com/grafana/sqlds/v3 v3.2.0/go.mod h1:kH0WuHUR3j0Q7IEymbm2JiaPckUhRCbqjV9ajaBAnmM= +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/tempo v1.5.1-0.20240604192202-01f4bc8ac2d1 h1:cSE1u4IUQ9EPcQErMZ9YVYayJTIGgH4g2E1Rp2WmGy0= github.com/grafana/tempo v1.5.1-0.20240604192202-01f4bc8ac2d1/go.mod h1:ttAEYdYVYBNngPulKIHkmHvjXfLfX7jDWI74jzb8jh4= github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= @@ -2346,8 +2349,8 @@ github.com/grpc-ecosystem/grpc-gateway/v2 v2.7.0/go.mod h1:hgWBS7lorOAVIJEQMi4Zs github.com/grpc-ecosystem/grpc-gateway/v2 v2.11.3/go.mod h1:o//XUCC/F+yRGJoPO/VU0GSB0f8Nhgmxx0VIRUvaC0w= github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0/go.mod h1:YN5jB8ie0yfIUg6VvR9Kz84aCaG7AsGZnLjhHbUqwPg= github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0/go.mod h1:qmOFXW2epJhM0qSnUUYpldc7gVz2KMQwJ/QYCDIa7XU= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 h1:bkypFPDjIYGfCYD5mRBvpqxfYX1YCS1PXdKYWi8FsN0= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0/go.mod h1:P+Lt/0by1T8bfcF3z737NnSbmxQAppXMRziHUxPOC8k= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0 h1:asbCHRVmodnJTuQ3qamDwqVOIjwqUPTYmYuemVOx+Ys= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0/go.mod h1:ggCgvZ2r7uOoQjOyu2Y1NhHmEPPzzuhWgcza5M1Ji1I= github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed/go.mod h1:tMWxXQ9wFIaZeTI9F+hmhFiGpFmhOHzyShyFUhRm0H4= github.com/hairyhenderson/go-which v0.2.0 h1:vxoCKdgYc6+MTBzkJYhWegksHjjxuXPNiqo5G2oBM+4= github.com/hairyhenderson/go-which v0.2.0/go.mod h1:U1BQQRCjxYHfOkXDyCgst7OZVknbqI7KuGKhGnmyIik= @@ -2575,8 +2578,8 @@ github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ib github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.2.3/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY= github.com/klauspost/cpuid/v2 v2.2.5/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= -github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM= -github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= +github.com/klauspost/cpuid/v2 v2.2.8 h1:+StwCXwm9PdpiEkPyzBXIy+M9KUb4ODm0Zarf1kS5BM= +github.com/klauspost/cpuid/v2 v2.2.8/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= github.com/kolo/xmlrpc v0.0.0-20220921171641-a4b6fa1dd06b h1:udzkj9S/zlT5X367kqJis0QP7YMxobob6zhzq6Yre00= github.com/kolo/xmlrpc v0.0.0-20220921171641-a4b6fa1dd06b/go.mod h1:pcaDhQK0/NJZEvtCO0qQPPropqV0sJOJ6YW7X+9kRwM= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= @@ -2666,8 +2669,8 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= -github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= -github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= +github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= github.com/mattn/go-sqlite3 v1.14.14/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= @@ -3027,8 +3030,8 @@ github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0leargg github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= github.com/prometheus/prometheus v0.52.0 h1:f7kHJgr7+zShpWdTCeKqbCWR7nKTScgLYQwRux9h1V0= github.com/prometheus/prometheus v0.52.0/go.mod h1:3z74cVsmVH0iXOR5QBjB7Pa6A0KJeEAK5A6UsmAFb1g= -github.com/protocolbuffers/txtpbfmt v0.0.0-20220428173112-74888fd59c2b h1:zd/2RNzIRkoGGMjE+YIsZ85CnDIz672JK2F3Zl4vux4= -github.com/protocolbuffers/txtpbfmt v0.0.0-20220428173112-74888fd59c2b/go.mod h1:KjY0wibdYKc4DYkerHSbguaf3JeIPGhNJBp2BNiFH78= +github.com/protocolbuffers/txtpbfmt v0.0.0-20230328191034-3462fbc510c0 h1:sadMIsgmHpEOGbUs6VtHBXRR1OHevnj7hLx9ZcdNGW4= +github.com/protocolbuffers/txtpbfmt v0.0.0-20230328191034-3462fbc510c0/go.mod h1:jgxiZysxFPM+iWKwQwPR+y+Jvo54ARd4EisXxKYpB5c= github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= github.com/redis/go-redis/v9 v9.1.0 h1:137FnGdk+EQdCbye1FW+qOEcY5S+SpY9T0NiuqvtfMY= github.com/redis/go-redis/v9 v9.1.0/go.mod h1:urWj3He21Dj5k4TK1y59xH8Uj6ATueP8AH1cY3lZl4c= @@ -3044,7 +3047,6 @@ github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= -github.com/rogpeppe/go-charset v0.0.0-20180617210344-2471d30d28b4/go.mod h1:qgYeAmZ5ZIpBWTGllZSQnw97Dj+woV0toclVaRGI8pc= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= @@ -3366,8 +3368,8 @@ go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.25.0/go.mod h1:h95q0LBGh7hl go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.29.0 h1:dIIDULZJpgdiHz5tXrTgKIMLkus6jEFa7x5SOKcyR7E= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.29.0/go.mod h1:jlRVBe7+Z1wyxFSUs48L6OBQZ5JwH2Hg/Vbl+t9rAgI= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.25.0/go.mod h1:8GlBGcDk8KKi7n+2S4BT/CPZQYH3erLu0/k64r1MYgo= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.28.0 h1:R3X6ZXmNPRR8ul6i3WgFURCHzaXjHdm0karRG/+dj3s= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.28.0/go.mod h1:QWFXnDavXWwMx2EEcZsf3yxgEKAqsxQ+Syjp+seyInw= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.29.0 h1:nSiV3s7wiCam610XcLbYOmMfJxB9gO4uK3Xgv5gmTgg= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.29.0/go.mod h1:hKn/e/Nmd19/x1gvIHwtOwVWM+VhuITSWip3JUDghj0= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.25.0/go.mod h1:e7ciERRhZaOZXVjx5MiL8TK5+Xv7G5Gv5PA2ZDEJdL8= go.opentelemetry.io/otel/metric v1.17.0/go.mod h1:h4skoxdZI17AxwITdmdZjjYJQH5nzijUUjm+wtPph5o= go.opentelemetry.io/otel/metric v1.19.0/go.mod h1:L5rUsV9kM1IxCj1MmSdS+JQAcVm319EUrDVLrt7jqt8= @@ -3503,8 +3505,8 @@ golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1/go.mod h1:FXUEEKJgO7OQYeo8N0 golang.org/x/exp v0.0.0-20230817173708-d852ddb80c63/go.mod h1:0v4NqG35kSWCMzLaMeX+IQrlSnVE/bqGSyC2cz/9Le8= golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo= golang.org/x/exp v0.0.0-20240119083558-1b970713d09a/go.mod h1:idGWGoKP1toJGkd5/ig9ZLuPcZBC3ewk7SzmH0uou08= -golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8= -golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= +golang.org/x/exp v0.0.0-20240808152545-0cdaa3abc0fa h1:ELnwvuAXPNtPk1TJRuGkI9fDTwym6AYBu0qzT8AcHdI= +golang.org/x/exp v0.0.0-20240808152545-0cdaa3abc0fa/go.mod h1:akd2r19cwCdwSwWeIdzYQGa/EZZyqcOdwWiwj5L5eKQ= golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= @@ -3558,8 +3560,8 @@ golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.16.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= -golang.org/x/mod v0.19.0 h1:fEdghXQSo20giMthA7cd28ZC+jts4amQ3YMXiP5oMQ8= -golang.org/x/mod v0.19.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.20.0 h1:utOm6MM3R3dnawAiJgn0y+xvuYRsm1RKM/4giyfDgV0= +golang.org/x/mod v0.20.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20180530234432-1e491301e022/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -3702,8 +3704,8 @@ golang.org/x/oauth2 v0.16.0/go.mod h1:hqZ+0LWXsiVoZpeld6jVt06P3adbS2Uu911W1SsJv2 golang.org/x/oauth2 v0.17.0/go.mod h1:OzPDGQiuQMguemayvdylqddI7qcD9lnSDb+1FiwQ5HA= golang.org/x/oauth2 v0.18.0/go.mod h1:Wf7knwG0MPoWIMMBgFlEaSUDaKskp0dCfrlJRJXbBi8= golang.org/x/oauth2 v0.19.0/go.mod h1:vYi7skDa1x015PmRRYZ7+s1cWyPgrPiSYRe4rnsexc8= -golang.org/x/oauth2 v0.22.0 h1:BzDx2FehcG7jJwgWLELCdmLuxk2i+x9UDpSiss2u0ZA= -golang.org/x/oauth2 v0.22.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= +golang.org/x/oauth2 v0.23.0 h1:PbgcYx2W7i4LvjJWEbf0ngHV6qJYr86PkAV3bXdLEbs= +golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -4023,8 +4025,8 @@ golang.org/x/tools v0.17.0/go.mod h1:xsh6VxdV005rRVaS6SSAf9oiAqljS7UZUacMZ8Bnsps golang.org/x/tools v0.18.0/go.mod h1:GL7B4CwcLLeo59yx/9UWWuNOW1n3VZ4f5axWfML7Lcg= golang.org/x/tools v0.19.0/go.mod h1:qoJWxmGSIBmAeriMx19ogtrEPrGtDbPK634QFIcLAhc= golang.org/x/tools v0.20.0/go.mod h1:WvitBU7JJf6A4jOdg4S1tviW9bhUxkgeCui/0JHctQg= -golang.org/x/tools v0.23.0 h1:SGsXPZ+2l4JsgaCKkx+FQ9YZ5XEtA1GZYuoDjenLjvg= -golang.org/x/tools v0.23.0/go.mod h1:pnu6ufv6vQkll6szChhK3C3L/ruaIv5eBeztNG8wtsI= +golang.org/x/tools v0.24.0 h1:J1shsA93PJUEVaUSaay7UXAyE8aimq3GW0pjlolpa24= +golang.org/x/tools v0.24.0/go.mod h1:YhNqVBIfWHdzvTLs0d8LCuMhkKUgSUKldakyV7W/WDQ= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -4387,8 +4389,8 @@ google.golang.org/genproto/googleapis/rpc v0.0.0-20240311132316-a219d84964c2/go. google.golang.org/genproto/googleapis/rpc v0.0.0-20240401170217-c3f982113cda/go.mod h1:WtryC6hu0hhx87FDGxWCDptyssuo68sk10vYjF+T9fY= google.golang.org/genproto/googleapis/rpc v0.0.0-20240415141817-7cd4c1c1f9ec/go.mod h1:WtryC6hu0hhx87FDGxWCDptyssuo68sk10vYjF+T9fY= google.golang.org/genproto/googleapis/rpc v0.0.0-20240415180920-8c6c420018be/go.mod h1:WtryC6hu0hhx87FDGxWCDptyssuo68sk10vYjF+T9fY= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142 h1:e7S5W7MGGLaSu8j3YjdezkZ+m1/Nm0uRVRMEMGk26Xs= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240822170219-fc7c04adadcd h1:6TEm2ZxXoQmFWFlt1vNxvVOa1Q0dXFQD1m/rYjXmS0E= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240822170219-fc7c04adadcd/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU= google.golang.org/grpc v1.8.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= google.golang.org/grpc v1.12.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= diff --git a/go.work b/go.work index 265fd6b33d9..846ebb7016d 100644 --- a/go.work +++ b/go.work @@ -5,6 +5,7 @@ go 1.23.1 use ( . // skip:golangci-lint + ./apps/playlist ./pkg/aggregator ./pkg/apimachinery ./pkg/apiserver @@ -20,3 +21,5 @@ use ( // when we release xorm we would like to release it like github.com/grafana/grafana/pkg/util/xorm // but we don't want to change all the imports. so we use replace to handle this situation replace xorm.io/xorm => ./pkg/util/xorm + +replace github.com/getkin/kin-openapi => github.com/getkin/kin-openapi v0.125.0 diff --git a/go.work.sum b/go.work.sum index 62e3d98e882..39980fec8f0 100644 --- a/go.work.sum +++ b/go.work.sum @@ -403,6 +403,12 @@ github.com/cncf/udpa/go v0.0.0-20220112060539-c52dc94e7fbe h1:QQ3GSy+MqSHxm/d8nC github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I= github.com/cockroachdb/cockroach-go v0.0.0-20181001143604-e0a95dfd547c h1:2zRrJWIt/f9c9HhNHAgrRgq0San5gRRUJTBXLkchal0= github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa h1:OaNxuTZr7kxeODyLWsRMC+OD03aFUH+mW6r2d+MWa5Y= +github.com/cockroachdb/errors v1.9.1 h1:yFVvsI0VxmRShfawbt/laCIDy/mtTqqnvoNgiy5bEV8= +github.com/cockroachdb/errors v1.9.1/go.mod h1:2sxOtL2WIc096WSZqZ5h8fa17rdDq9HZOZLBCor4mBk= +github.com/cockroachdb/logtags v0.0.0-20211118104740-dabe8e521a4f h1:6jduT9Hfc0njg5jJ1DdKCFPdMBrp/mdZfCpa5h+WM74= +github.com/cockroachdb/logtags v0.0.0-20211118104740-dabe8e521a4f/go.mod h1:Vz9DsVWQQhf3vs21MhPMZpMGSht7O/2vFW2xusFUVOs= +github.com/cockroachdb/redact v1.1.3 h1:AKZds10rFSIj7qADf0g46UixK8NNLwWTNdCIGS5wfSQ= +github.com/cockroachdb/redact v1.1.3/go.mod h1:BVNblN9mBWFyMyqK1k3AAiSxhvhfK2oOZZ2lK+dpvRg= github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd h1:qMd81Ts1T2OTKmB4acZcyKaMtRnY5Y44NuXGX2GFJ1w= github.com/containerd/cgroups/v3 v3.0.3 h1:S5ByHZ/h9PMe5IOQoN7E+nMc2UcLEM/V48DGDJ9kip0= github.com/coreos/etcd v3.3.10+incompatible h1:jFneRYjIvLMLhDLCzuTuU4rSJUjRplcJQ7pD7MnhC04= @@ -432,6 +438,7 @@ github.com/dave/astrid v0.0.0-20170323122508-8c2895878b14 h1:YI1gOOdmMk3xodBao7f github.com/dave/brenda v1.1.0 h1:Sl1LlwXnbw7xMhq3y2x11McFu43AjDcwkllxxgZ3EZw= github.com/dave/courtney v0.3.0 h1:8aR1os2ImdIQf3Zj4oro+lD/L4Srb5VwGefqZ/jzz7U= github.com/dave/gopackages v0.0.0-20170318123100-46e7023ec56e h1:l99YKCdrK4Lvb/zTupt0GMPfNbncAGf8Cv/t1sYLOg0= +github.com/dave/jennifer v1.5.0/go.mod h1:4MnyiFIlZS3l5tSDn8VnzE6ffAhYBMB2SZntBsZGUok= github.com/dave/kerr v0.0.0-20170318121727-bc25dd6abe8e h1:xURkGi4RydhyaYR6PzcyHTueQudxY4LgxN1oYEPJHa0= github.com/dave/patsy v0.0.0-20210517141501-957256f50cba h1:1o36L4EKbZzazMk8iGC4kXpVnZ6TPxR2mZ9qVKjNNAs= github.com/dave/rebecca v0.9.1 h1:jxVfdOxRirbXL28vXMvUvJ1in3djwkVKXCq339qhBL0= @@ -479,7 +486,10 @@ github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbS github.com/fsouza/fake-gcs-server v1.7.0 h1:Un0BXUXrRWYSmYyC1Rqm2e2WJfTPyDy/HGMz31emTi8= github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU= github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA= -github.com/getkin/kin-openapi v0.124.0/go.mod h1:wb1aSZA/iWmorQP9KTAS/phLj/t17B5jT7+fS8ed9NM= +github.com/getkin/kin-openapi v0.125.0 h1:jyQCyf2qXS1qvs2U00xQzkGCqYPhEhZDmSmVt65fXno= +github.com/getkin/kin-openapi v0.125.0/go.mod h1:wb1aSZA/iWmorQP9KTAS/phLj/t17B5jT7+fS8ed9NM= +github.com/getsentry/sentry-go v0.12.0 h1:era7g0re5iY13bHSdN/xMkyV+5zZppjRVQhZrXCaEIk= +github.com/getsentry/sentry-go v0.12.0/go.mod h1:NSap0JBYWzHND8oMbyi0+XZhUalc1TBdRL1M71JZW2c= github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg= @@ -521,6 +531,8 @@ github.com/godbus/dbus/v5 v5.0.4 h1:9349emZab16e7zQvpmsbtjc18ykshndd8y2PG3sgJbA= github.com/gogo/status v1.1.0/go.mod h1:BFv9nrluPLmrS0EmGVvLaPNmRosr9KapBYd5/hpY1WM= github.com/golang-jwt/jwt v3.2.1+incompatible h1:73Z+4BJcrTC+KczS6WvTPvRGOp1WmfEP4Q1lOd9Z/+c= github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g= +github.com/golang/glog v1.2.1 h1:OptwRhECazUx5ix5TTWC3EZhsZEHWcYWY4FQHTIubm4= +github.com/golang/glog v1.2.1/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w= github.com/gomarkdown/markdown v0.0.0-20230922112808-5421fefb8386 h1:EcQR3gusLHN46TAD+G+EbaaqJArt5vHhNpXAa12PQf4= github.com/gomarkdown/markdown v0.0.0-20230922112808-5421fefb8386/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA= github.com/gomodule/redigo v1.8.9 h1:Sl3u+2BI/kk+VEatbj0scLdrFhjPmbxOc1myhDP41ws= @@ -547,10 +559,14 @@ github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl github.com/gorilla/handlers v1.5.1 h1:9lRY6j8DEeeBT10CvO9hGW0gmky0BprnvDI5vfhUHH4= github.com/gorilla/handlers v1.5.1/go.mod h1:t8XrUpc4KVXb7HGyJ4/cEnwQiaxrX/hz1Zv/4g96P1Q= github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= -github.com/grafana/authlib/claims v0.0.0-20240827210201-19d5347dd8dd/go.mod h1:r+F8H6awwjNQt/KPZ2GNwjk8TvsJ7/gxzkXN26GlL/A= +github.com/grafana/alerting v0.0.0-20240830172655-aa466962ea18 h1:3cQ+d+fkNL2EqpARaBVG34KlVz7flDujYfDx3njvdh8= +github.com/grafana/alerting v0.0.0-20240830172655-aa466962ea18/go.mod h1:GMLi6d09Xqo96fCVUjNk//rcjP5NKEdjOzfWIffD5r4= github.com/grafana/gomemcache v0.0.0-20240229205252-cd6a66d6fb56/go.mod h1:PGk3RjYHpxMM8HFPhKKo+vve3DdlPUELZLSDEFehPuU= github.com/grafana/pyroscope-go/godeltaprof v0.1.6/go.mod h1:Tk376Nbldo4Cha9RgiU7ik8WKFkNpfds98aUzS8omLE= +github.com/grafana/thema v0.0.0-20230511182720-3146087fcc26 h1:HX927q4X1n451pnGb8U0wq74i8PCzuxVjzv7TyD10kc= +github.com/grafana/thema v0.0.0-20230511182720-3146087fcc26/go.mod h1:Pn9nfzCk7nV0mvNgwusgCjCROZP6nm4GpwTnmEhLT24= github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7 h1:pdN6V1QBWetyv/0+wjACpqVH+eVULgEjkurDLq3goeM= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0/go.mod h1:P+Lt/0by1T8bfcF3z737NnSbmxQAppXMRziHUxPOC8k= github.com/grpc-ecosystem/grpc-opentracing v0.0.0-20180507213350-8e809c8a8645 h1:MJG/KsmcqMwFAkh8mTnAwhyKoB+sTAnY4CACC110tbU= github.com/grpc-ecosystem/grpc-opentracing v0.0.0-20180507213350-8e809c8a8645/go.mod h1:6iZfnjpejD4L/4DwD7NryNaJyCQdzwWwH2MWhCA90Kw= github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed h1:5upAirOpQc1Q53c0bnx2ufif5kANL7bfZWcc6VJWJd8= @@ -632,6 +648,8 @@ github.com/konsorten/go-windows-terminal-sequences v1.0.3 h1:CE8S1cTafDpPvMhIxNJ github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515 h1:T+h1c/A9Gawja4Y9mFVWj2vyii2bbUNDw3kt9VxK2EY= github.com/kr/pty v1.1.1 h1:VkoXIwSboBpnk99O/KFauAEILuNHv5DVFKZMBN/gUgw= +github.com/kr/pty v1.1.8 h1:AkaSdXYQOWeaO3neb8EM634ahkXXe3jYbVh/F9lq+GI= +github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= github.com/kshvakov/clickhouse v1.3.5 h1:PDTYk9VYgbjPAWry3AoDREeMgOVUFij6bh6IjlloHL0= github.com/labstack/echo/v4 v4.11.4 h1:vDZmA+qNeh1pd/cCkEicDMrjtrnMGQ1QFI9gWN1zGq8= github.com/labstack/echo/v4 v4.11.4/go.mod h1:noh7EvLwqDsmh/X/HWKPUl1AjzJrhyptRyEbQJfxen8= @@ -653,6 +671,8 @@ github.com/mailgun/raymond/v2 v2.0.48 h1:5dmlB680ZkFG2RN/0lvTAghrSxIESeu9/2aeDqA github.com/mailgun/raymond/v2 v2.0.48/go.mod h1:lsgvL50kgt1ylcFJYZiULi5fjPBkkhNfj4KA0W54Z18= github.com/matryer/moq v0.3.3 h1:pScMH9VyrdT4S93yiLpVyU8rCDqGQr24uOyBxmktG5Q= github.com/matryer/moq v0.3.3/go.mod h1:RJ75ZZZD71hejp39j4crZLsEDszGk6iH4v4YsWFKH4s= +github.com/matryer/moq v0.5.0 h1:h2PJUYjZSiyEahzVogDRmrgL9Bsx9xYAl8l+LPfmwL8= +github.com/matryer/moq v0.5.0/go.mod h1:39GTnrD0mVWHPvWdYj5ki/lxfhLQEtHcLh+tWoYF/iE= github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 h1:jWpvCLoY8Z/e3VKvlsiIGKtc+UG6U5vzxaoagmhXfyg= github.com/maxatome/go-testdeep v1.12.0 h1:Ql7Go8Tg0C1D/uMMX59LAoYK7LffeJQ6X2T04nTH68g= github.com/microcosm-cc/bluemonday v1.0.25 h1:4NEwSfiJ+Wva0VxN5B8OwMicaJvD8r9tlJWm9rtloEg= @@ -762,6 +782,8 @@ github.com/prometheus/common/assets v0.2.0 h1:0P5OrzoHrYBOSM1OigWL3mY8ZvV2N4zIE/ github.com/prometheus/exporter-toolkit v0.10.1-0.20230714054209-2f4150c63f97/go.mod h1:LoBCZeRh+5hX+fSULNyFnagYlQG/gBsyA/deNzROkq8= github.com/prometheus/statsd_exporter v0.26.0 h1:SQl3M6suC6NWQYEzOvIv+EF6dAMYEqIuZy+o4H9F5Ig= github.com/prometheus/statsd_exporter v0.26.0/go.mod h1:GXFLADOmBTVDrHc7b04nX8ooq3azG61pnECNqT7O5DM= +github.com/puzpuzpuz/xsync/v2 v2.5.1 h1:mVGYAvzDSu52+zaGyNjC+24Xw2bQi3kTr4QJ6N9pIIU= +github.com/puzpuzpuz/xsync/v2 v2.5.1/go.mod h1:gD2H2krq/w52MfPLE+Uy64TzJDVY7lP2znR9qmR35kU= github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 h1:N/ElC8H3+5XpJzTSTfLsJV/mx9Q9g7kxmchpfZyxgzM= github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= github.com/relvacode/iso8601 v1.4.0 h1:GsInVSEJfkYuirYFxa80nMLbH2aydgZpIf52gYZXUJs= @@ -782,6 +804,7 @@ github.com/segmentio/fasthash v0.0.0-20180216231524-a72b379d632e h1:uO75wNGioszj github.com/segmentio/fasthash v0.0.0-20180216231524-a72b379d632e/go.mod h1:tm/wZFQ8e24NYaBGIlnO2WGCAi67re4HHuOm0sftE/M= github.com/sercand/kuberesolver/v5 v5.1.1 h1:CYH+d67G0sGBj7q5wLK61yzqJJ8gLLC8aeprPTHb6yY= github.com/sercand/kuberesolver/v5 v5.1.1/go.mod h1:Fs1KbKhVRnB2aDWN12NjKCB+RgYMWZJ294T3BtmVCpQ= +github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= github.com/shoenig/test v1.7.1 h1:UJcjSAI3aUKx52kfcfhblgyhZceouhvvs3OYdWgn+PY= github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= github.com/sony/gobreaker v0.4.1 h1:oMnRNZXX5j85zso6xCPRNPtmAycat+WcoKbklScLDgQ= @@ -845,6 +868,8 @@ github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQ github.com/xhit/go-str2duration v1.2.0 h1:BcV5u025cITWxEQKGWr1URRzrcXtu7uk8+luz3Yuhwc= github.com/xhit/go-str2duration/v2 v2.1.0 h1:lxklc02Drh6ynqX+DdPyp5pCKLUQpRT8bp8Ydu2Bstc= github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77 h1:ESFSdwYZvkeru3RtdrYueztKhOBCSAAzS4Gf+k0tEow= +github.com/yalue/merged_fs v1.3.0 h1:qCeh9tMPNy/i8cwDsQTJ5bLr6IRxbs6meakNE5O+wyY= +github.com/yalue/merged_fs v1.3.0/go.mod h1:WqqchfVYQyclV2tnR7wtRhBddzBvLVR83Cjw9BKQw0M= github.com/ydb-platform/ydb-go-genproto v0.0.0-20240126124512-dbb0e1720dbf h1:ckwNHVo4bv2tqNkgx3W3HANh3ta1j6TR5qw08J1A7Tw= github.com/ydb-platform/ydb-go-genproto v0.0.0-20240126124512-dbb0e1720dbf/go.mod h1:Er+FePu1dNUieD+XTMDduGpQuCPssK5Q4BjF+IIXJ3I= github.com/ydb-platform/ydb-go-sdk/v3 v3.55.1 h1:Ebo6J5AMXgJ3A438ECYotA0aK7ETqjQx9WoZvVxzKBE= @@ -937,6 +962,7 @@ go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.23.1 h1:ZqR go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.23.1/go.mod h1:D7ynngPWlGJrqyGSDOdscuv7uqttfCE3jcBvffDv9y4= go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.23.1 h1:q/Nj5/2TZRIt6PderQ9oU0M00fzoe8UZuINGw6ETGTw= go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.23.1/go.mod h1:DTE9yAu6r08jU3xa68GiSeI7oRcSEQ2RpKbbQGO+dWM= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.28.0/go.mod h1:QWFXnDavXWwMx2EEcZsf3yxgEKAqsxQ+Syjp+seyInw= go.opentelemetry.io/otel/exporters/prometheus v0.46.0 h1:I8WIFXR351FoLJYuloU4EgXbtNX2URfU/85pUPheIEQ= go.opentelemetry.io/otel/exporters/prometheus v0.46.0/go.mod h1:ztwVUHe5DTR/1v7PeuGRnU5Bbd4QKYwApWmuutKsJSs= go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.23.1 h1:C8r95vDR125t815KD+b1tI0Fbc1pFnwHTBxkbIZ6Szc= @@ -958,18 +984,22 @@ golang.org/x/arch v0.4.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M= golang.org/x/exp v0.0.0-20230321023759-10a507213a29/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= +golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= golang.org/x/image v0.6.0 h1:bR8b5okrPI3g/gyZakLZHeWxAR8Dn5CyxXv1hLH5g/4= golang.org/x/image v0.6.0/go.mod h1:MXLdDR43H7cDJq5GEGXEVeeNhPgi+YYEQ2pC1byI1x0= golang.org/x/lint v0.0.0-20210508222113-6edffad5e616 h1:VLliZ0d+/avPrXXH+OakdXhpJuEoBZuwh1m2j7U6Iug= golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028 h1:4+4C/Iv2U4fMZBiMCc98MG1In4gJY5YRhtpDNeDeHWs= +golang.org/x/mod v0.19.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20190921015927-1a5e07d1ff72/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211216030914-fe4d6282115f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE= +golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg= golang.org/x/oauth2 v0.20.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/oauth2 v0.21.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= +golang.org/x/oauth2 v0.22.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/sys v0.0.0-20190204203706-41f3e6584952/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= @@ -981,8 +1011,12 @@ golang.org/x/telemetry v0.0.0-20240521205824-bda55230c457 h1:zf5N6UOrA487eEFacMe golang.org/x/telemetry v0.0.0-20240521205824-bda55230c457/go.mod h1:pRgIJT+bRLFKnoM1ldnzKoxTIn14Yxz928LQRYYgIN0= golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= +golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= +golang.org/x/tools v0.23.0/go.mod h1:pnu6ufv6vQkll6szChhK3C3L/ruaIv5eBeztNG8wtsI= +gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw= +gomodules.xyz/jsonpatch/v2 v2.4.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0 h1:OE9mWmgKkjJyEmDAAtGMPjXu+YNeGvK9VTSHY6+Qihc= gonum.org/v1/plot v0.10.1 h1:dnifSs43YJuNMDzB7v8wV64O4ABBHReuAVAoBxqBqS4= google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM= @@ -991,6 +1025,7 @@ google.golang.org/genproto/googleapis/api v0.0.0-20240528184218-531527333157/go. google.golang.org/genproto/googleapis/api v0.0.0-20240604185151-ef581f913117/go.mod h1:OimBR/bc1wPO9iV4NC2bpyjy3VnAwZh5EBPQdtaE5oo= google.golang.org/genproto/googleapis/api v0.0.0-20240725223205-93522f1f2a9f/go.mod h1:AHT0dDg3SoMOgZGnZk29b5xTbPHMoEC8qthmBLJCpys= google.golang.org/genproto/googleapis/api v0.0.0-20240730163845-b1a4ccb954bf/go.mod h1:OFMYQFHJ4TM3JRlWDZhJbZfra2uqc3WLBZiaaqP4DtU= +google.golang.org/genproto/googleapis/api v0.0.0-20240814211410-ddb44dafa142/go.mod h1:d6be+8HhtEtucleCbxpPW9PA9XwISACu8nvpPqF0BVo= google.golang.org/genproto/googleapis/bytestream v0.0.0-20240730163845-b1a4ccb954bf h1:T4tsZBlZYXK3j40sQNP5MBO32I+rn6ypV1PpklsiV8k= google.golang.org/genproto/googleapis/bytestream v0.0.0-20240730163845-b1a4ccb954bf/go.mod h1:5/MT647Cn/GGhwTpXC7QqcaR5Cnee4v4MKCU1/nwnIQ= google.golang.org/genproto/googleapis/rpc v0.0.0-20240528184218-531527333157/go.mod h1:EfXuqaE1J41VCDicxHzUDm+8rk+7ZdXzHV0IhO/I6s0= @@ -998,6 +1033,7 @@ google.golang.org/genproto/googleapis/rpc v0.0.0-20240604185151-ef581f913117/go. google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY= google.golang.org/genproto/googleapis/rpc v0.0.0-20240722135656-d784300faade/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY= google.golang.org/genproto/googleapis/rpc v0.0.0-20240730163845-b1a4ccb954bf/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU= google.golang.org/grpc v1.64.1/go.mod h1:hiQF4LFZelK2WKaP6W0L92zGHtiQdZxk8CrSdvyjeP0= google.golang.org/grpc v1.65.0/go.mod h1:WgYC2ypjlB0EiQi6wdKixMqukr6lBc0Vo+oOgjrM5ZQ= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0 h1:M1YKkFIboKNieVO5DLUEVzQfGwJD30Nv2jfUgzb5UcE= @@ -1017,6 +1053,8 @@ gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.1.3 h1:qTakTkI6ni6LFD5sBwwsdSO+AQqbSIxOauHTTQKZ/7o= howett.net/plist v1.0.0 h1:7CrbWYbPPO/PyNy38b2EB/+gYbjCe2DXBxgtOOZbSQM= howett.net/plist v1.0.0/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g= +k8s.io/apiextensions-apiserver v0.31.0 h1:fZgCVhGwsclj3qCw1buVXCV6khjRzKC5eCFt24kyLSk= +k8s.io/apiextensions-apiserver v0.31.0/go.mod h1:b9aMDEYaEe5sdK+1T0KU78ApR/5ZVp4i56VacZYEHxk= k8s.io/code-generator v0.31.0 h1:w607nrMi1KeDKB3/F/J4lIoOgAwc+gV9ZKew4XRfMp8= k8s.io/code-generator v0.31.0/go.mod h1:84y4w3es8rOJOUUP1rLsIiGlO1JuEaPFXQPA9e/K6U0= k8s.io/gengo v0.0.0-20230829151522-9cce18d56c01 h1:pWEwq4Asjm4vjW7vcsmijwBhOr1/shsbSYiWXmNGlks= diff --git a/package.json b/package.json index 757797e0993..50e6858263a 100644 --- a/package.json +++ b/package.json @@ -79,10 +79,10 @@ "@emotion/eslint-plugin": "11.11.0", "@grafana/eslint-config": "7.0.0", "@grafana/eslint-plugin": "link:./packages/grafana-eslint-rules", - "@grafana/plugin-e2e": "1.7.1", + "@grafana/plugin-e2e": "1.8.0", "@grafana/tsconfig": "^2.0.0", "@manypkg/get-packages": "^2.2.0", - "@playwright/test": "1.46.1", + "@playwright/test": "1.47.2", "@pmmmwh/react-refresh-webpack-plugin": "0.5.15", "@react-types/button": "3.9.6", "@react-types/menu": "3.9.11", @@ -115,14 +115,14 @@ "@types/gtag.js": "^0.0.20", "@types/history": "4.7.11", "@types/ini": "^4", - "@types/jest": "29.5.12", + "@types/jest": "29.5.13", "@types/jquery": "3.5.30", "@types/js-yaml": "^4.0.5", "@types/jsurl": "^1.2.28", "@types/lodash": "4.17.7", "@types/logfmt": "^1.2.3", "@types/lucene": "^2", - "@types/node": "20.16.4", + "@types/node": "20.16.5", "@types/node-forge": "^1", "@types/ol-ext": "npm:@siedlerchr/types-ol-ext@3.2.4", "@types/pluralize": "^0.0.33", @@ -146,7 +146,7 @@ "@types/slate-plain-serializer": "0.7.5", "@types/slate-react": "0.22.9", "@types/swagger-ui-react": "4.18.3", - "@types/systemjs": "6.15.0", + "@types/systemjs": "6.15.1", "@types/testing-library__jest-dom": "5.14.9", "@types/tinycolor2": "1.4.6", "@types/uuid": "9.0.8", @@ -169,19 +169,19 @@ "cypress": "13.10.0", "cypress-file-upload": "5.0.8", "cypress-recurse": "^1.35.3", - "esbuild": "0.20.2", + "esbuild": "0.24.0", "esbuild-loader": "4.2.2", "esbuild-plugin-browserslist": "^0.14.0", "eslint": "8.57.0", "eslint-config-prettier": "9.1.0", "eslint-plugin-import": "^2.26.0", - "eslint-plugin-jest": "28.8.2", + "eslint-plugin-jest": "28.8.3", "eslint-plugin-jest-dom": "^5.4.0", "eslint-plugin-jsdoc": "48.11.0", "eslint-plugin-jsx-a11y": "6.10.0", "eslint-plugin-lodash": "7.4.0", "eslint-plugin-no-barrel-files": "^1.1.0", - "eslint-plugin-react": "7.35.2", + "eslint-plugin-react": "7.36.1", "eslint-plugin-react-hooks": "4.6.0", "eslint-plugin-testing-library": "^6.2.2", "eslint-scope": "^8.0.0", @@ -205,12 +205,12 @@ "knip": "^5.10.0", "lerna": "8.1.8", "mini-css-extract-plugin": "2.9.1", - "msw": "2.3.5", + "msw": "2.4.9", "mutationobserver-shim": "0.3.7", "ngtemplate-loader": "2.1.0", "node-notifier": "10.0.1", - "nx": "19.2.0", - "postcss": "8.4.45", + "nx": "19.8.0", + "postcss": "8.4.47", "postcss-loader": "8.1.1", "postcss-reporter": "7.1.0", "postcss-scss": "4.0.9", @@ -221,8 +221,8 @@ "react-test-renderer": "18.2.0", "redux-mock-store": "1.5.4", "rimraf": "6.0.1", - "rudder-sdk-js": "2.48.17", - "sass": "1.77.8", + "rudder-sdk-js": "2.48.18", + "sass": "1.78.0", "sass-loader": "14.2.1", "smtp-tester": "^2.1.0", "style-loader": "4.0.0", @@ -252,7 +252,7 @@ "@floating-ui/react": "0.26.23", "@formatjs/intl-durationformat": "^0.2.4", "@glideapps/glide-data-grid": "^6.0.0", - "@grafana/aws-sdk": "0.4.1", + "@grafana/aws-sdk": "0.4.2", "@grafana/azure-sdk": "0.0.3", "@grafana/data": "workspace:*", "@grafana/e2e-selectors": "workspace:*", @@ -268,7 +268,7 @@ "@grafana/prometheus": "workspace:*", "@grafana/runtime": "workspace:*", "@grafana/saga-icons": "workspace:*", - "@grafana/scenes": "5.14.1", + "@grafana/scenes": "5.14.7", "@grafana/schema": "workspace:*", "@grafana/sql": "workspace:*", "@grafana/ui": "workspace:*", @@ -354,10 +354,10 @@ "ol-ext": "4.0.23", "pluralize": "^8.0.0", "prismjs": "1.29.0", - "rc-slider": "11.1.5", + "rc-slider": "11.1.6", "rc-time-picker": "3.7.3", - "rc-tree": "5.8.8", - "re-resizable": "6.9.17", + "rc-tree": "5.9.0", + "re-resizable": "6.9.18", "react": "18.2.0", "react-diff-viewer": "^3.1.1", "react-dom": "18.2.0", @@ -376,7 +376,7 @@ "react-router": "5.3.3", "react-router-dom": "5.3.3", "react-router-dom-v5-compat": "^6.26.1", - "react-select": "5.8.0", + "react-select": "5.8.1", "react-split-pane": "0.1.92", "react-table": "7.8.0", "react-transition-group": "4.4.5", diff --git a/packages/grafana-data/package.json b/packages/grafana-data/package.json index 736499dca19..3d861553fe8 100644 --- a/packages/grafana-data/package.json +++ b/packages/grafana-data/package.json @@ -62,16 +62,16 @@ }, "devDependencies": { "@grafana/tsconfig": "^2.0.0", - "@rollup/plugin-node-resolve": "15.2.3", + "@rollup/plugin-node-resolve": "15.2.4", "@types/dompurify": "^3.0.0", "@types/history": "4.7.11", "@types/lodash": "4.17.7", - "@types/node": "20.16.4", + "@types/node": "20.16.5", "@types/papaparse": "5.3.14", "@types/react": "18.3.3", "@types/react-dom": "18.2.25", "@types/tinycolor2": "1.4.6", - "esbuild": "0.20.2", + "esbuild": "0.24.0", "react": "18.2.0", "react-dom": "18.2.0", "rimraf": "6.0.1", diff --git a/packages/grafana-data/src/dataframe/dimensions.ts b/packages/grafana-data/src/dataframe/dimensions.ts index 80058770514..82222fe6ccd 100644 --- a/packages/grafana-data/src/dataframe/dimensions.ts +++ b/packages/grafana-data/src/dataframe/dimensions.ts @@ -1,7 +1,7 @@ import { KeyValue } from '../types/data'; import { Field } from '../types/dataFrame'; -export interface Dimension { +export interface Dimension { // Name of the dimension name: string; // Collection of fields representing dimension @@ -12,28 +12,28 @@ export interface Dimension { columns: Array>; } -export type Dimensions = KeyValue; +export type Dimensions = KeyValue>; -export const createDimension = (name: string, columns: Field[]): Dimension => { +export const createDimension = (name: string, columns: Array>): Dimension => { return { name, columns, }; }; -export const getColumnsFromDimension = (dimension: Dimension) => { +export const getColumnsFromDimension = (dimension: Dimension) => { return dimension.columns; }; -export const getColumnFromDimension = (dimension: Dimension, column: number) => { +export const getColumnFromDimension = (dimension: Dimension, column: number) => { return dimension.columns[column]; }; -export const getValueFromDimension = (dimension: Dimension, column: number, row: number) => { +export const getValueFromDimension = (dimension: Dimension, column: number, row: number) => { return dimension.columns[column].values[row]; }; -export const getAllValuesFromDimension = (dimension: Dimension, column: number, row: number) => { +export const getAllValuesFromDimension = (dimension: Dimension, column: number, row: number) => { return dimension.columns.map((c) => c.values[row]); }; -export const getDimensionByName = (dimensions: Dimensions, name: string) => dimensions[name]; +export const getDimensionByName = (dimensions: Dimensions, name: string) => dimensions[name]; diff --git a/packages/grafana-data/src/datetime/datemath.ts b/packages/grafana-data/src/datetime/datemath.ts index 0aedd4934b5..8ebf5c99269 100644 --- a/packages/grafana-data/src/datetime/datemath.ts +++ b/packages/grafana-data/src/datetime/datemath.ts @@ -2,7 +2,15 @@ import { includes, isDate } from 'lodash'; import { TimeZone } from '../types/time'; -import { DateTime, dateTime, dateTimeForTimeZone, DurationUnit, isDateTime, ISO_8601 } from './moment_wrapper'; +import { + DateTime, + dateTime, + dateTimeAsMoment, + dateTimeForTimeZone, + DurationUnit, + isDateTime, + ISO_8601, +} from './moment_wrapper'; const units: DurationUnit[] = ['y', 'M', 'w', 'd', 'h', 'm', 's', 'Q']; @@ -51,10 +59,10 @@ export function parse( // We got some non string which is not a moment nor Date. TS should be able to check for that but not always. return undefined; } else { - let time; + let time: DateTime; let mathString = ''; - let index; - let parseString; + let index = -1; + let parseString = ''; if (text.substring(0, 3) === 'now') { time = dateTimeForTimeZone(timezone); @@ -104,10 +112,9 @@ export function isValid(text: string | DateTime): boolean { * @param time * @param roundUp If true it will round the time to endOf time unit, otherwise to startOf time unit. */ -// TODO: Had to revert Andrejs `time: moment.Moment` to `time: any` export function parseDateMath( mathString: string, - time: any, + time: DateTime, roundUp?: boolean, fiscalYearStartMonth = 0 ): DateTime | undefined { @@ -187,21 +194,21 @@ export function parseDateMath( return result; } -export function roundToFiscal(fyStartMonth: number, dateTime: any, unit: string, roundUp: boolean | undefined) { +export function roundToFiscal(fyStartMonth: number, dateTime: DateTime, unit: string, roundUp: boolean | undefined) { switch (unit) { case 'y': if (roundUp) { - roundToFiscal(fyStartMonth, dateTime, unit, false).add(11, 'M').endOf('M'); + roundToFiscal(fyStartMonth, dateTime, unit, false)?.add(11, 'M').endOf('M'); } else { - dateTime.subtract((dateTime.month() - fyStartMonth + 12) % 12, 'M').startOf('M'); + dateTime.subtract((dateTimeAsMoment(dateTime).month() - fyStartMonth + 12) % 12, 'M').startOf('M'); } return dateTime; case 'Q': if (roundUp) { - roundToFiscal(fyStartMonth, dateTime, unit, false).add(2, 'M').endOf('M'); + roundToFiscal(fyStartMonth, dateTime, unit, false)?.add(2, 'M').endOf('M'); } else { // why + 12? to ensure this number is always a positive offset from fyStartMonth - dateTime.subtract((dateTime.month() - fyStartMonth + 12) % 3, 'M').startOf('M'); + dateTime.subtract((dateTimeAsMoment(dateTime).month() - fyStartMonth + 12) % 3, 'M').startOf('M'); } return dateTime; default: diff --git a/packages/grafana-data/src/events/EventBus.ts b/packages/grafana-data/src/events/EventBus.ts index b155319ecc1..cdcd16b2e89 100644 --- a/packages/grafana-data/src/events/EventBus.ts +++ b/packages/grafana-data/src/events/EventBus.ts @@ -1,3 +1,4 @@ +import { IScope } from 'angular'; import EventEmitter from 'eventemitter3'; import { Unsubscribable, Observable, Subscriber } from 'rxjs'; import { filter } from 'rxjs/operators'; @@ -55,7 +56,7 @@ export class EventBusSrv implements EventBus, LegacyEmitter { /** * Legacy functions */ - emit(event: AppEvent | string, payload?: T | any): void { + emit(event: AppEvent | string, payload?: T): void { // console.log(`Deprecated emitter function used (emit), use $emit`); if (typeof event === 'string') { @@ -65,7 +66,7 @@ export class EventBusSrv implements EventBus, LegacyEmitter { } } - on(event: AppEvent | string, handler: LegacyEventHandler, scope?: any) { + on(event: AppEvent | string, handler: LegacyEventHandler, scope?: IScope) { // console.log(`Deprecated emitter function used (on), use $on`); // need this wrapper to make old events compatible with old handlers diff --git a/packages/grafana-data/src/events/types.ts b/packages/grafana-data/src/events/types.ts index f5e111eb2fc..ad4fec0a025 100644 --- a/packages/grafana-data/src/events/types.ts +++ b/packages/grafana-data/src/events/types.ts @@ -1,3 +1,4 @@ +import { IScope } from 'angular'; import { Unsubscribable, Observable } from 'rxjs'; /** @@ -128,12 +129,12 @@ export interface LegacyEmitter { /** * @deprecated use $on */ - on(event: AppEvent | string, handler: LegacyEventHandler, scope?: any): void; + on(event: AppEvent | string, handler: LegacyEventHandler, scope?: IScope): void; /** * @deprecated use $on */ - off(event: AppEvent | string, handler: (payload?: T | any) => void): void; + off(event: AppEvent | string, handler: (payload?: T) => void): void; } /** @public */ diff --git a/packages/grafana-data/src/field/fieldColor.test.ts b/packages/grafana-data/src/field/fieldColor.test.ts index 6a1e0abf285..9f293aab072 100644 --- a/packages/grafana-data/src/field/fieldColor.test.ts +++ b/packages/grafana-data/src/field/fieldColor.test.ts @@ -1,8 +1,9 @@ import { createTheme } from '../themes/createTheme'; -import { Field, FieldType } from '../types/dataFrame'; +import { DataFrame, Field, FieldType } from '../types/dataFrame'; import { FieldColorModeId } from '../types/fieldColor'; import { fieldColorModeRegistry, FieldValueColorCalculator, getFieldSeriesColor } from './fieldColor'; +import { cacheFieldDisplayNames } from './fieldState'; function getTestField(mode: string, fixedColor?: string, name = 'name'): Field { return { @@ -55,6 +56,67 @@ describe('fieldColorModeRegistry', () => { expect(calcFn1(12, 34, undefined)).toEqual(calcFn2(56, 78, undefined)); }); + it('Palette uses displayName with Value fields', () => { + let frames: DataFrame[] = [ + { + length: 0, + fields: [ + { + name: 'Time', + type: FieldType.time, + values: [], + config: {}, + }, + { + name: 'Value', + labels: { foo: 'bar' }, + type: FieldType.number, + values: [], + config: { + color: { + mode: FieldColorModeId.PaletteClassicByName, + }, + }, + state: {}, + }, + ], + }, + { + length: 0, + fields: [ + { + name: 'Time', + type: FieldType.time, + values: [], + config: {}, + }, + { + name: 'Value', + labels: { foo: 'baz' }, + type: FieldType.number, + values: [], + config: { + color: { + mode: FieldColorModeId.PaletteClassicByName, + }, + }, + state: {}, + }, + ], + }, + ]; + + cacheFieldDisplayNames(frames); + + const mode = fieldColorModeRegistry.get(FieldColorModeId.PaletteClassicByName); + + const calcFn1 = mode.getCalculator(frames[0].fields[1], createTheme()); + const calcFn2 = mode.getCalculator(frames[1].fields[1], createTheme()); + + expect(calcFn1(0, 0)).toEqual('#82B5D8'); + expect(calcFn2(0, 0)).toEqual('#FCE2DE'); + }); + it('When color.seriesBy is set to last use that instead of v', () => { const field = getTestField('continuous-GrYlRd'); diff --git a/packages/grafana-data/src/field/fieldColor.ts b/packages/grafana-data/src/field/fieldColor.ts index 93818c8a3af..8c7702c2bb0 100644 --- a/packages/grafana-data/src/field/fieldColor.ts +++ b/packages/grafana-data/src/field/fieldColor.ts @@ -218,7 +218,7 @@ export class FieldColorSchemeMode implements FieldColorMode { } } else if (this.useSeriesName) { return (_: number, _percent: number, _threshold?: Threshold) => { - return colors[Math.abs(stringHash(field.name)) % colors.length]; + return colors[Math.abs(stringHash(field.state?.displayName ?? field.name)) % colors.length]; }; } else { return (_: number, _percent: number, _threshold?: Threshold) => { diff --git a/packages/grafana-data/src/field/fieldOverrides.ts b/packages/grafana-data/src/field/fieldOverrides.ts index 549599d81b4..a1cf083cbea 100644 --- a/packages/grafana-data/src/field/fieldOverrides.ts +++ b/packages/grafana-data/src/field/fieldOverrides.ts @@ -607,7 +607,7 @@ export function useFieldOverrides( /** * Clones the existing dataContext or creates a new one */ -function getFieldDataContextClone(frame: DataFrame, field: Field, fieldScopedVars: ScopedVars) { +export function getFieldDataContextClone(frame: DataFrame, field: Field, fieldScopedVars: ScopedVars) { if (fieldScopedVars?.__dataContext) { return { value: { diff --git a/packages/grafana-data/src/field/fieldState.ts b/packages/grafana-data/src/field/fieldState.ts index 2ec8942fcfe..3ddaebbda63 100644 --- a/packages/grafana-data/src/field/fieldState.ts +++ b/packages/grafana-data/src/field/fieldState.ts @@ -64,7 +64,7 @@ export function cacheFieldDisplayNames(frames: DataFrame[]) { * moves each field's config.custom.hideFrom to field.state.hideFrom * and mutates orgiginal field.config.custom.hideFrom to one with explicit overrides only, (without the ad-hoc stateful __system override from legend toggle) */ -export function decoupleHideFromState(frames: DataFrame[], fieldConfig: FieldConfigSource) { +export function decoupleHideFromState(frames: DataFrame[], fieldConfig: FieldConfigSource) { frames.forEach((frame) => { frame.fields.forEach((field) => { const hideFrom = { diff --git a/packages/grafana-data/src/field/overrides/processors.ts b/packages/grafana-data/src/field/overrides/processors.ts index ee396ebba01..9846c1c3ef3 100644 --- a/packages/grafana-data/src/field/overrides/processors.ts +++ b/packages/grafana-data/src/field/overrides/processors.ts @@ -19,7 +19,7 @@ export interface NumberFieldConfigSettings { } export const numberOverrideProcessor = ( - value: any, + value: unknown, context: FieldOverrideContext, settings?: NumberFieldConfigSettings ) => { @@ -27,7 +27,7 @@ export const numberOverrideProcessor = ( return undefined; } - return parseFloat(value); + return parseFloat(String(value)); }; export const displayNameOverrideProcessor = ( diff --git a/packages/grafana-data/src/geo/layer.ts b/packages/grafana-data/src/geo/layer.ts index 0c4f4ac7799..18ed9b6a6df 100644 --- a/packages/grafana-data/src/geo/layer.ts +++ b/packages/grafana-data/src/geo/layer.ts @@ -42,7 +42,7 @@ export interface MapLayerHandler { */ registerOptionsUI?: ( builder: PanelOptionsEditorBuilder>, - context: StandardEditorContext + context: StandardEditorContext> ) => void; } diff --git a/packages/grafana-data/src/index.ts b/packages/grafana-data/src/index.ts index f7b3fa21399..890123bcd90 100644 --- a/packages/grafana-data/src/index.ts +++ b/packages/grafana-data/src/index.ts @@ -145,6 +145,7 @@ export { validateFieldConfig, applyRawFieldOverrides, useFieldOverrides, + getFieldDataContextClone, } from './field/fieldOverrides'; export { getFieldDisplayValuesProxy } from './field/getFieldDisplayValuesProxy'; export { @@ -800,6 +801,14 @@ export { VariableSuggestionsScope, OneClickMode, } from './types/dataLink'; +export { + type Action, + type ActionModel, + HttpRequestMethod, + defaultActionConfig, + contentTypeOptions, + httpMethodOptions, +} from './types/action'; export { DataFrameType } from './types/dataFrameTypes'; export { FieldType, diff --git a/packages/grafana-data/src/transformations/fieldReducer.ts b/packages/grafana-data/src/transformations/fieldReducer.ts index 0205ce30dee..a032619aeb3 100644 --- a/packages/grafana-data/src/transformations/fieldReducer.ts +++ b/packages/grafana-data/src/transformations/fieldReducer.ts @@ -423,7 +423,7 @@ const buildPercentileReducers = (percentiles = [...Array.from({ length: 99 }, (_ percentiles.forEach((p) => { const percentile = p / 100; - const id = `p${p}` as ReducerID; + const id = `p${p}`; const name = `${p}${nth(p)} %`; const description = `${p}${nth(p)} percentile value`; diff --git a/packages/grafana-data/src/transformations/transformers/calculateField.test.ts b/packages/grafana-data/src/transformations/transformers/calculateField.test.ts index 154083e3169..3091be651bd 100644 --- a/packages/grafana-data/src/transformations/transformers/calculateField.test.ts +++ b/packages/grafana-data/src/transformations/transformers/calculateField.test.ts @@ -7,6 +7,7 @@ import { BinaryOperationID } from '../../utils/binaryOperators'; import { mockTransformationsRegistry } from '../../utils/tests/mockTransformationsRegistry'; import { UnaryOperationID } from '../../utils/unaryOperators'; import { ReducerID } from '../fieldReducer'; +import { FieldMatcherID } from '../matchers/ids'; import { transformDataFrame } from '../transformDataFrame'; import { @@ -197,6 +198,72 @@ describe('calculateField transformer w/ timeseries', () => { }); }); + it('all numbers + static number', async () => { + const cfg = { + id: DataTransformerID.calculateField, + options: { + mode: CalculateFieldMode.BinaryOperation, + binary: { + left: { matcher: { id: FieldMatcherID.byType, options: FieldType.number } }, + operator: BinaryOperationID.Add, + right: '2', + }, + replaceFields: true, + }, + }; + + await expect(transformDataFrame([cfg], [seriesBC])).toEmitValuesWith((received) => { + const data = received[0]; + const filtered = data[0]; + const rows = new DataFrameView(filtered).toArray(); + expect(rows).toEqual([ + { + 'B + 2': 4, + 'C + 2': 5, + TheTime: 1000, + }, + { + 'B + 2': 202, + 'C + 2': 302, + TheTime: 2000, + }, + ]); + }); + }); + + it('all numbers + field number', async () => { + const cfg = { + id: DataTransformerID.calculateField, + options: { + mode: CalculateFieldMode.BinaryOperation, + binary: { + left: { matcher: { id: FieldMatcherID.byType, options: FieldType.number } }, + operator: BinaryOperationID.Add, + right: 'C', + }, + replaceFields: true, + }, + }; + + await expect(transformDataFrame([cfg], [seriesBC])).toEmitValuesWith((received) => { + const data = received[0]; + const filtered = data[0]; + const rows = new DataFrameView(filtered).toArray(); + expect(rows).toEqual([ + { + 'B + C': 5, + 'C + C': 6, + TheTime: 1000, + }, + { + 'B + C': 500, + 'C + C': 600, + TheTime: 2000, + }, + ]); + }); + }); + it('unary math', async () => { const unarySeries = toDataFrame({ fields: [ diff --git a/packages/grafana-data/src/transformations/transformers/calculateField.ts b/packages/grafana-data/src/transformations/transformers/calculateField.ts index 820ecdec7cf..72a136c3980 100644 --- a/packages/grafana-data/src/transformations/transformers/calculateField.ts +++ b/packages/grafana-data/src/transformations/transformers/calculateField.ts @@ -5,7 +5,7 @@ import { getTimeField } from '../../dataframe/processDataFrame'; import { getFieldDisplayName } from '../../field/fieldState'; import { NullValueMode } from '../../types/data'; import { DataFrame, FieldType, Field } from '../../types/dataFrame'; -import { DataTransformerInfo } from '../../types/transformations'; +import { DataTransformContext, DataTransformerInfo } from '../../types/transformations'; import { BinaryOperationID, binaryOperators } from '../../utils/binaryOperators'; import { UnaryOperationID, unaryOperators } from '../../utils/unaryOperators'; import { doStandardCalcs, fieldReducers, ReducerID } from '../fieldReducer'; @@ -58,9 +58,14 @@ export interface UnaryOptions { } export interface BinaryOptions { - left: string; + left: BinaryValue; operator: BinaryOperationID; - right: string; + right: BinaryValue; +} + +export interface BinaryValue { + fixed?: string; + matcher?: { id?: FieldMatcherID; options?: string }; } interface IndexOptions { @@ -79,9 +84,9 @@ export const defaultWindowOptions: WindowOptions = { }; const defaultBinaryOptions: BinaryOptions = { - left: '', + left: { fixed: '' }, operator: BinaryOperationID.Add, - right: '', + right: { fixed: '' }, }; const defaultUnaryOptions: UnaryOptions = { @@ -152,13 +157,62 @@ export const calculateFieldTransformer: DataTransformerInfo { + frame.fields.map((field) => { + fieldNames.push(field.name); + }); + }); const binaryOptions = { - ...options.binary, - left: ctx.interpolate(options.binary?.left!), - right: ctx.interpolate(options.binary?.right!), + left: checkBinaryValueType(options.binary?.left ?? '', fieldNames), + operator: options.binary?.operator ?? defaultBinaryOptions.operator, + right: checkBinaryValueType(options.binary?.right ?? '', fieldNames), }; + options.binary = binaryOptions; + if (binaryOptions.left?.matcher?.id && binaryOptions.left?.matcher.id === FieldMatcherID.byType) { + const fieldType = binaryOptions.left.matcher.options; + const operator = binaryOperators.getIfExists(binaryOptions.operator); + return data.map((frame) => { + const { timeField } = getTimeField(frame); + const newFields: Field[] = []; + if (timeField && options.timeSeries !== false) { + newFields.push(timeField); + } + // For each field of type match, apply operator + frame.fields.map((field, index) => { + if (!options.replaceFields) { + newFields.push(field); + } + if (field.type === fieldType) { + const left = field.values; + // TODO consolidate common creator logic + const right = findFieldValuesWithNameOrConstant( + frame, + binaryOptions.right ?? defaultBinaryOptions.right, + data, + ctx + ); + if (!left || !right || !operator) { + return undefined; + } - creator = getBinaryCreator(defaults(binaryOptions, defaultBinaryOptions), data); + const arr = new Array(left.length); + for (let i = 0; i < arr.length; i++) { + arr[i] = operator.operation(left[i], right[i]); + } + const newField = { + ...field, + name: `${field.name} ${options.binary?.operator ?? ''} ${options.binary?.right.matcher?.options ?? options.binary?.right.fixed}`, + values: arr, + }; + newFields.push(newField); + } + }); + return { ...frame, fields: newFields }; + }); + } else { + creator = getBinaryCreator(defaults(binaryOptions, defaultBinaryOptions), data, ctx); + } break; case CalculateFieldMode.Index: return data.map((frame) => { @@ -488,23 +542,27 @@ function getReduceRowCreator(options: ReduceOptions, allFrames: DataFrame[]): Va function findFieldValuesWithNameOrConstant( frame: DataFrame, - name: string, - allFrames: DataFrame[] + value: BinaryValue, + allFrames: DataFrame[], + ctx: DataTransformContext ): number[] | undefined { - if (!name) { + if (!value) { return undefined; } - for (const f of frame.fields) { - if (name === getFieldDisplayName(f, frame, allFrames)) { - if (f.type === FieldType.boolean) { - return f.values.map((v) => (v ? 1 : 0)); + if (value.matcher && value.matcher.id === FieldMatcherID.byName) { + const name = ctx.interpolate(value.matcher.options ?? ''); + for (const f of frame.fields) { + if (name === getFieldDisplayName(f, frame, allFrames)) { + if (f.type === FieldType.boolean) { + return f.values.map((v) => (v ? 1 : 0)); + } + return f.values; } - return f.values; } } - const v = parseFloat(name); + const v = parseFloat(value.fixed ?? ctx.interpolate(value.matcher?.options ?? '')); if (!isNaN(v)) { return new Array(frame.length).fill(v); } @@ -512,12 +570,12 @@ function findFieldValuesWithNameOrConstant( return undefined; } -function getBinaryCreator(options: BinaryOptions, allFrames: DataFrame[]): ValuesCreator { +function getBinaryCreator(options: BinaryOptions, allFrames: DataFrame[], ctx: DataTransformContext): ValuesCreator { const operator = binaryOperators.getIfExists(options.operator); return (frame: DataFrame) => { - const left = findFieldValuesWithNameOrConstant(frame, options.left, allFrames); - const right = findFieldValuesWithNameOrConstant(frame, options.right, allFrames); + const left = findFieldValuesWithNameOrConstant(frame, options.left, allFrames, ctx); + const right = findFieldValuesWithNameOrConstant(frame, options.right, allFrames, ctx); if (!left || !right || !operator) { return undefined; } @@ -530,6 +588,24 @@ function getBinaryCreator(options: BinaryOptions, allFrames: DataFrame[]): Value }; } +export function checkBinaryValueType(value: BinaryValue | string, names: string[]): BinaryValue { + // Support old binary value structure + if (typeof value === 'string') { + if (isNaN(Number(value))) { + return { matcher: { id: FieldMatcherID.byName, options: value } }; + } else { + // If it's a number, check if matches name, otherwise store as fixed number value + if (names.includes(value)) { + return { matcher: { id: FieldMatcherID.byName, options: value } }; + } else { + return { fixed: value }; + } + } + } + // Pass through new BinaryValue structure + return value; +} + function getUnaryCreator(options: UnaryOptions, allFrames: DataFrame[]): ValuesCreator { const operator = unaryOperators.getIfExists(options.operator); @@ -577,7 +653,7 @@ export function getNameFromOptions(options: CalculateFieldTransformerOptions) { } case CalculateFieldMode.BinaryOperation: { const { binary } = options; - const alias = `${binary?.left ?? ''} ${binary?.operator ?? ''} ${binary?.right ?? ''}`; + const alias = `${binary?.left?.matcher?.options ?? binary?.left?.fixed ?? ''} ${binary?.operator ?? ''} ${binary?.right?.matcher?.options ?? binary?.right?.fixed ?? ''}`; //Remove $ signs as they will be interpolated and cause issues. Variables can still be used //in alias but shouldn't in the autogenerated name diff --git a/packages/grafana-data/src/transformations/transformers/joinDataFrames.test.ts b/packages/grafana-data/src/transformations/transformers/joinDataFrames.test.ts index 976ea8fd731..0515131be30 100644 --- a/packages/grafana-data/src/transformations/transformers/joinDataFrames.test.ts +++ b/packages/grafana-data/src/transformations/transformers/joinDataFrames.test.ts @@ -280,6 +280,65 @@ describe('align frames', () => { ] `); }); + + it('should perform an inner join with empty values', () => { + const out = joinDataFrames({ + frames: [ + toDataFrame({ + fields: [ + { + name: 'A', + type: FieldType.string, + values: [], + }, + { + name: 'B', + type: FieldType.string, + values: [], + }, + ], + }), + toDataFrame({ + fields: [ + { + name: 'A', + type: FieldType.string, + values: [], + }, + { + name: 'C', + type: FieldType.string, + values: [], + }, + ], + }), + ], + joinBy: fieldMatchers.get(FieldMatcherID.byName).get('A'), + mode: JoinMode.inner, + })!; + + expect( + out.fields.map((f) => ({ + name: f.name, + values: f.values, + })) + ).toMatchInlineSnapshot(` + [ + { + "name": "A", + "values": [], + }, + { + "name": "B", + "values": [], + }, + { + "name": "C", + "values": [], + }, + ] + `); + }); }); it('unsorted input keep indexes', () => { diff --git a/packages/grafana-data/src/transformations/transformers/joinDataFrames.ts b/packages/grafana-data/src/transformations/transformers/joinDataFrames.ts index a2e5f0ad080..98f28ff9fce 100644 --- a/packages/grafana-data/src/transformations/transformers/joinDataFrames.ts +++ b/packages/grafana-data/src/transformations/transformers/joinDataFrames.ts @@ -424,7 +424,8 @@ function joinInner(tables: AlignedData[]): Array count + (table.length - 1), 1); + return Array.from({ length: fieldCount }, () => []); } // Transpose the joined tables to get the desired output format. @@ -545,7 +546,7 @@ export function join(tables: AlignedData[], nullModes?: number[][], mode: JoinMo // Test a few samples to see if the values are ascending // Only exported for tests -export function isLikelyAscendingVector(data: any[], samples = 50) { +export function isLikelyAscendingVector(data: unknown[], samples = 50) { const len = data.length; // empty or single value @@ -575,7 +576,7 @@ export function isLikelyAscendingVector(data: any[], samples = 50) { for (let prevVal = data[firstIdx], i = firstIdx + stride; i <= lastIdx; i += stride) { const v = data[i]; - if (v != null) { + if (v != null && prevVal != null) { if (v <= prevVal) { return false; } diff --git a/packages/grafana-data/src/types/action.ts b/packages/grafana-data/src/types/action.ts new file mode 100644 index 00000000000..a3cb099ebe8 --- /dev/null +++ b/packages/grafana-data/src/types/action.ts @@ -0,0 +1,74 @@ +import { ScopedVars } from './ScopedVars'; +import { DataFrame, Field, ValueLinkConfig } from './dataFrame'; +import { InterpolateFunction } from './panel'; +import { SelectableValue } from './select'; + +export enum ActionType { + Fetch = 'fetch', +} + +export interface Action { + type: ActionType; + title: string; + + // Options for the selected type + // Currently this is required because there is only one valid type (fetch) + // once multiple types are valid, usage of this will need to be optional + [ActionType.Fetch]: FetchOptions; +} + +/** + * Processed Action Model. The values are ready to use + */ +export interface ActionModel { + title: string; + onClick: (event: any, origin?: any) => void; +} + +interface FetchOptions { + method: HttpRequestMethod; + url: string; + body?: string; + queryParams?: Array<[string, string]>; + headers?: Array<[string, string]>; +} + +export enum HttpRequestMethod { + POST = 'POST', + PUT = 'PUT', + GET = 'GET', +} + +export const httpMethodOptions: SelectableValue[] = [ + { label: HttpRequestMethod.POST, value: HttpRequestMethod.POST }, + { label: HttpRequestMethod.PUT, value: HttpRequestMethod.PUT }, + { label: HttpRequestMethod.GET, value: HttpRequestMethod.GET }, +]; + +export const contentTypeOptions: SelectableValue[] = [ + { label: 'application/json', value: 'application/json' }, + { label: 'text/plain', value: 'text/plain' }, + { label: 'application/xml', value: 'application/xml' }, + { label: 'application/x-www-form-urlencoded', value: 'application/x-www-form-urlencoded' }, +]; + +export const defaultActionConfig: Action = { + type: ActionType.Fetch, + title: '', + fetch: { + url: '', + method: HttpRequestMethod.POST, + body: '{}', + queryParams: [], + headers: [['Content-Type', 'application/json']], + }, +}; + +export type ActionsArgs = { + frame: DataFrame; + field: Field; + fieldScopedVars: ScopedVars; + replaceVariables: InterpolateFunction; + actions: Action[]; + config: ValueLinkConfig; +}; diff --git a/packages/grafana-data/src/types/dataLink.ts b/packages/grafana-data/src/types/dataLink.ts index 47805993095..47cba535029 100644 --- a/packages/grafana-data/src/types/dataLink.ts +++ b/packages/grafana-data/src/types/dataLink.ts @@ -51,7 +51,6 @@ export interface DataLink { internal?: InternalDataLink; origin?: DataLinkConfigOrigin; - sortIndex?: number; } /** @@ -131,6 +130,7 @@ export enum VariableSuggestionsScope { } export enum OneClickMode { + Action = 'action', Link = 'link', Off = 'off', } diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index 27eeb7769cc..d9997e00dc7 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -38,6 +38,7 @@ export interface FeatureToggles { autoMigrateXYChartPanel?: boolean; disableAngular?: boolean; canvasPanelNesting?: boolean; + vizActions?: boolean; disableSecretsCompatibility?: boolean; logRequestsInstrumentedAsUnknown?: boolean; topnav?: boolean; @@ -86,6 +87,7 @@ export interface FeatureToggles { mlExpressions?: boolean; traceQLStreaming?: boolean; metricsSummary?: boolean; + datasourceAPIServers?: boolean; grafanaAPIServerWithExperimentalAPIs?: boolean; grafanaAPIServerEnsureKubectlAccess?: boolean; featureToggleAdminPage?: boolean; @@ -114,6 +116,7 @@ export interface FeatureToggles { kubernetesPlaylists?: boolean; kubernetesSnapshots?: boolean; kubernetesDashboards?: boolean; + kubernetesFolders?: boolean; datasourceQueryTypes?: boolean; queryService?: boolean; queryServiceRewrite?: boolean; @@ -199,7 +202,7 @@ export interface FeatureToggles { bodyScrolling?: boolean; cloudwatchMetricInsightsCrossAccount?: boolean; prometheusAzureOverrideAudience?: boolean; - backgroundPluginInstaller?: boolean; + alertingFilterV2?: boolean; dataplaneAggregator?: boolean; newFiltersUI?: boolean; lokiSendDashboardPanelNames?: boolean; @@ -209,4 +212,7 @@ export interface FeatureToggles { exploreLogsLimitedTimeRange?: boolean; appPlatformAccessTokens?: boolean; appSidecar?: boolean; + groupAttributeSync?: boolean; + improvedExternalSessionHandling?: boolean; + useSessionStorageForRedirection?: boolean; } diff --git a/packages/grafana-data/src/types/trace.ts b/packages/grafana-data/src/types/trace.ts index 8346f07dfb9..a63852685e6 100644 --- a/packages/grafana-data/src/types/trace.ts +++ b/packages/grafana-data/src/types/trace.ts @@ -4,6 +4,7 @@ export type TraceKeyValuePair = { key: string; value: T; + type?: string; }; /** diff --git a/packages/grafana-data/src/valueFormats/categories.ts b/packages/grafana-data/src/valueFormats/categories.ts index 22e48bd04f4..e176dd65a4d 100644 --- a/packages/grafana-data/src/valueFormats/categories.ts +++ b/packages/grafana-data/src/valueFormats/categories.ts @@ -148,6 +148,7 @@ export const getCategories = (): ValueFormatCategory[] => [ { name: 'Bulgarian Lev (BGN)', id: 'currencyBGN', fn: currency('BGN') }, { name: 'Guaraní (₲)', id: 'currencyPYG', fn: currency('₲') }, { name: 'Uruguay Peso (UYU)', id: 'currencyUYU', fn: currency('UYU') }, + { name: 'Israeli New Shekels (₪)', id: 'currencyILS', fn: currency('₪') }, ], }, { diff --git a/packages/grafana-data/src/valueFormats/dateTimeFormatters.ts b/packages/grafana-data/src/valueFormats/dateTimeFormatters.ts index 2d2db131ebc..11468be5fe4 100644 --- a/packages/grafana-data/src/valueFormats/dateTimeFormatters.ts +++ b/packages/grafana-data/src/valueFormats/dateTimeFormatters.ts @@ -185,7 +185,7 @@ export function toDays(size: number, decimals?: DecimalCount): FormattedValue { } if (Math.abs(size) < 7) { - return { text: toFixed(size, decimals), suffix: ' day' }; + return toFixedScaled(size, decimals, ' day'); } else if (Math.abs(size) < 365) { return toFixedScaled(size / 7, decimals, ' week'); } else { diff --git a/packages/grafana-e2e-selectors/package.json b/packages/grafana-e2e-selectors/package.json index 2cdb068e4b1..a00a4f6e143 100644 --- a/packages/grafana-e2e-selectors/package.json +++ b/packages/grafana-e2e-selectors/package.json @@ -39,9 +39,9 @@ "postpack": "mv package.json.bak package.json" }, "devDependencies": { - "@rollup/plugin-node-resolve": "15.2.3", - "@types/node": "20.16.4", - "esbuild": "0.20.2", + "@rollup/plugin-node-resolve": "15.2.4", + "@types/node": "20.16.5", + "esbuild": "0.24.0", "rimraf": "6.0.1", "rollup": "2.79.1", "rollup-plugin-dts": "^5.0.0", diff --git a/packages/grafana-e2e-selectors/src/selectors/components.ts b/packages/grafana-e2e-selectors/src/selectors/components.ts index d0c9cd1f8f3..bb6bd78a7b9 100644 --- a/packages/grafana-e2e-selectors/src/selectors/components.ts +++ b/packages/grafana-e2e-selectors/src/selectors/components.ts @@ -422,6 +422,7 @@ export const Components = { }, Select: { option: 'data-testid Select option', + toggleAllOptions: 'data-testid toggle all options', input: () => 'input[id*="time-options-input"]', singleValue: () => 'div[class*="-singleValue"]', }, diff --git a/packages/grafana-e2e-selectors/src/selectors/pages.ts b/packages/grafana-e2e-selectors/src/selectors/pages.ts index 45c453130fa..be04b0e8296 100644 --- a/packages/grafana-e2e-selectors/src/selectors/pages.ts +++ b/packages/grafana-e2e-selectors/src/selectors/pages.ts @@ -286,23 +286,51 @@ export const Pages = { }, }, ShareDashboardDrawer: { + ShareInternally: { + container: 'data-testid share internally drawer container', + lockTimeRangeSwitch: 'data-testid share internally lock time range switch', + shortenUrlSwitch: 'data-testid share internally shorten url switch', + copyUrlButton: 'data-testid share internally copy url button', + }, ShareExternally: { container: 'data-testid share externally drawer container', - copyUrlButton: 'data-testid share externally copy url button', + publicAlert: 'data-testid public share alert', + emailSharingAlert: 'data-testid email share alert', shareTypeSelect: 'data-testid share externally share type select', + Creation: { + PublicShare: { + createButton: 'data-testid public share dashboard create button', + cancelButton: 'data-testid public share dashboard cancel button', + }, + EmailShare: { + createButton: 'data-testid email share dashboard create button', + cancelButton: 'data-testid email share dashboard cancel button', + }, + willBePublicCheckbox: 'data-testid share dashboard will be public checkbox', + }, + Configuration: { + enableTimeRangeSwitch: 'data-testid share externally enable time range switch', + enableAnnotationsSwitch: 'data-testid share externally enable annotations switch', + copyUrlButton: 'data-testid share externally copy url button', + revokeAccessButton: 'data-testid share externally revoke access button', + toggleAccessButton: 'data-testid share externally pause or resume access button', + }, }, ShareSnapshot: { + url: (key: string) => `/dashboard/snapshot/${key}`, container: 'data-testid share snapshot drawer container', + publishSnapshot: 'data-testid share snapshot publish button', + copyUrlButton: 'data-testid share snapshot copy url button', }, }, ExportDashboardDrawer: { ExportAsJson: { - container: 'data-testid export as Json drawer container', - codeEditor: 'data-testid export as Json code editor', - exportExternallyToggle: 'data-testid export externally toggle type select', - saveToFileButton: 'data-testid save to file button', - copyToClipboardButton: 'data-testid copy to clipboard button', - cancelButton: 'data-testid cancel button', + container: 'data-testid export as json drawer container', + codeEditor: 'data-testid export as json code editor', + exportExternallyToggle: 'data-testid export as json externally switch', + saveToFileButton: 'data-testid export as json save to file button', + copyToClipboardButton: 'data-testid export as json copy to clipboard button', + cancelButton: 'data-testid export as json cancel button', }, }, PublicDashboard: { diff --git a/packages/grafana-flamegraph/package.json b/packages/grafana-flamegraph/package.json index 54d88edad67..0453af5c93e 100644 --- a/packages/grafana-flamegraph/package.json +++ b/packages/grafana-flamegraph/package.json @@ -60,7 +60,7 @@ "@babel/preset-env": "7.25.4", "@babel/preset-react": "7.24.7", "@grafana/tsconfig": "^2.0.0", - "@rollup/plugin-node-resolve": "15.2.3", + "@rollup/plugin-node-resolve": "15.2.4", "@testing-library/dom": "10.0.0", "@testing-library/jest-dom": "^6.1.2", "@testing-library/react": "15.0.2", @@ -68,12 +68,12 @@ "@types/d3": "^7", "@types/jest": "^29.5.4", "@types/lodash": "4.17.7", - "@types/node": "20.16.4", + "@types/node": "20.16.5", "@types/react": "18.3.3", "@types/react-virtualized-auto-sizer": "1.0.4", "@types/tinycolor2": "1.4.6", "babel-jest": "29.7.0", - "esbuild": "0.20.2", + "esbuild": "0.24.0", "jest": "^29.6.4", "jest-canvas-mock": "2.5.2", "rollup": "2.79.1", diff --git a/packages/grafana-icons/package.json b/packages/grafana-icons/package.json index f0d8270af4c..1c9d1deeab3 100644 --- a/packages/grafana-icons/package.json +++ b/packages/grafana-icons/package.json @@ -45,10 +45,10 @@ "@svgr/plugin-prettier": "^8.1.0", "@svgr/plugin-svgo": "^8.1.0", "@types/babel__core": "^7", - "@types/node": "20.16.4", + "@types/node": "20.16.5", "@types/react": "18.3.3", "@types/react-dom": "18.2.25", - "esbuild": "0.20.2", + "esbuild": "0.24.0", "prettier": "3.3.3", "react": "18.2.0", "react-dom": "18.2.0", diff --git a/packages/grafana-o11y-ds-frontend/package.json b/packages/grafana-o11y-ds-frontend/package.json index a6d517d8883..b198623bee1 100644 --- a/packages/grafana-o11y-ds-frontend/package.json +++ b/packages/grafana-o11y-ds-frontend/package.json @@ -24,7 +24,7 @@ "@grafana/runtime": "11.3.0-pre", "@grafana/schema": "11.3.0-pre", "@grafana/ui": "11.3.0-pre", - "react-select": "5.8.0", + "react-select": "5.8.1", "react-use": "17.5.1", "rxjs": "7.8.1", "tslib": "2.7.0" @@ -36,9 +36,9 @@ "@testing-library/react": "15.0.2", "@testing-library/user-event": "14.5.2", "@types/jest": "^29.5.4", - "@types/node": "20.16.4", + "@types/node": "20.16.5", "@types/react": "18.3.3", - "@types/systemjs": "6.15.0", + "@types/systemjs": "6.15.1", "@types/testing-library__jest-dom": "5.14.9", "jest": "^29.6.4", "react": "18.2.0", diff --git a/packages/grafana-prometheus/package.json b/packages/grafana-prometheus/package.json index bd99b7403b2..70d54c9a693 100644 --- a/packages/grafana-prometheus/package.json +++ b/packages/grafana-prometheus/package.json @@ -40,7 +40,7 @@ "@floating-ui/react": "0.26.23", "@grafana/data": "11.3.0-pre", "@grafana/experimental": "1.8.0", - "@grafana/faro-web-sdk": "1.9.1", + "@grafana/faro-web-sdk": "1.10.0", "@grafana/runtime": "11.3.0-pre", "@grafana/schema": "11.3.0-pre", "@grafana/ui": "11.3.0-pre", @@ -65,7 +65,7 @@ "pluralize": "8.0.0", "prismjs": "1.29.0", "react-highlight-words": "0.20.0", - "react-select": "5.8.0", + "react-select": "5.8.1", "react-use": "17.5.1", "react-window": "1.8.10", "rxjs": "7.8.1", @@ -79,7 +79,7 @@ "@grafana/e2e-selectors": "11.3.0-pre", "@grafana/tsconfig": "^2.0.0", "@rollup/plugin-image": "3.0.3", - "@rollup/plugin-node-resolve": "15.2.3", + "@rollup/plugin-node-resolve": "15.2.4", "@swc/core": "1.4.2", "@swc/helpers": "0.5.13", "@testing-library/dom": "10.0.0", @@ -89,10 +89,10 @@ "@types/d3": "7.4.3", "@types/debounce-promise": "3.1.9", "@types/eslint": "8.56.10", - "@types/jest": "29.5.12", + "@types/jest": "29.5.13", "@types/jquery": "3.5.30", "@types/lodash": "4.17.7", - "@types/node": "20.16.4", + "@types/node": "20.16.5", "@types/pluralize": "^0.0.33", "@types/prismjs": "1.26.4", "@types/react": "18.3.3", @@ -106,15 +106,15 @@ "@typescript-eslint/parser": "6.21.0", "copy-webpack-plugin": "12.0.2", "css-loader": "7.1.2", - "esbuild": "0.20.2", + "esbuild": "0.24.0", "eslint": "8.57.0", "eslint-config-prettier": "9.1.0", "eslint-plugin-import": "^2.26.0", - "eslint-plugin-jest": "28.8.2", + "eslint-plugin-jest": "28.8.3", "eslint-plugin-jsdoc": "48.11.0", "eslint-plugin-jsx-a11y": "6.10.0", "eslint-plugin-lodash": "7.4.0", - "eslint-plugin-react": "7.35.2", + "eslint-plugin-react": "7.36.1", "eslint-plugin-react-hooks": "4.6.0", "eslint-webpack-plugin": "4.2.0", "fork-ts-checker-webpack-plugin": "9.0.2", @@ -131,7 +131,7 @@ "rollup-plugin-dts": "^5.0.0", "rollup-plugin-esbuild": "5.0.0", "rollup-plugin-node-externals": "^5.0.0", - "sass": "1.77.8", + "sass": "1.78.0", "sass-loader": "14.2.1", "style-loader": "4.0.0", "testing-library-selector": "0.3.1", diff --git a/packages/grafana-prometheus/src/querycache/QueryCache.test.ts b/packages/grafana-prometheus/src/querycache/QueryCache.test.ts index 9efe6a73288..8d6fd349a84 100644 --- a/packages/grafana-prometheus/src/querycache/QueryCache.test.ts +++ b/packages/grafana-prometheus/src/querycache/QueryCache.test.ts @@ -6,8 +6,8 @@ import { DataFrame, DataQueryRequest, DateTime, dateTime, TimeRange } from '@gra import { QueryEditorMode } from '../querybuilder/shared/types'; import { PromQuery } from '../types'; -import { QueryCache } from './QueryCache'; -import { IncrementalStorageDataFrameScenarios } from './QueryCacheTestData'; +import { CacheRequestInfo, QueryCache } from './QueryCache'; +import { IncrementalStorageDataFrameScenarios, trimmedFirstPointInPromFrames } from './QueryCacheTestData'; // Will not interpolate vars! const interpolateStringTest = (query: PromQuery) => { @@ -415,10 +415,10 @@ describe('QueryCache: Prometheus', function () { const secondMergedLength = secondQueryResult[0].fields[0].values.length; - // Since the step is 15s, and the request was 30 seconds later, we should have 2 extra frames, but we should evict the first two, so we should get the same length - expect(firstMergedLength).toEqual(secondMergedLength); - expect(firstQueryResult[0].fields[0].values[2]).toEqual(secondQueryResult[0].fields[0].values[0]); - expect(firstQueryResult[0].fields[0].values[0] + 30000).toEqual(secondQueryResult[0].fields[0].values[0]); + // Since the step is 15s, and the request was 30 seconds later, we should have 2 extra frames, but we should evict the first one so we keep one datapoint before the new from so the first datapoint in view connects to the y-axis + expect(firstMergedLength + 1).toEqual(secondMergedLength); + expect(firstQueryResult[0].fields[0].values[1]).toEqual(secondQueryResult[0].fields[0].values[0]); + expect(firstQueryResult[0].fields[0].values[0] + 30000).toEqual(secondQueryResult[0].fields[0].values[1]); cache.set(targetIdentity, `'1=1'|${interval}|${JSON.stringify(thirdRange.raw)}`); @@ -438,7 +438,9 @@ describe('QueryCache: Prometheus', function () { const cachedAfterThird = storage.cache.get(targetIdentity); const storageLengthAfterThirdQuery = cachedAfterThird?.frames[0].fields[0].values.length; - expect(storageLengthAfterThirdQuery).toEqual(20); + + // Should have the 20 data points in the viz, plus one extra + expect(storageLengthAfterThirdQuery).toEqual(21); }); it('Will build signature using target overrides', () => { @@ -496,6 +498,56 @@ describe('QueryCache: Prometheus', function () { expect(cacheRequest.shouldCache).toBe(true); }); + it('should not modify the initial request', () => { + const storage = new QueryCache({ + getTargetSignature: getPrometheusTargetSignature, + overlapString: '10m', + }); + + const firstFrames = trimmedFirstPointInPromFrames as unknown as DataFrame[]; + // There are 6 values + expect(firstFrames[0].fields[1].values.length).toBe(6); + const expectedValueLength = firstFrames[0].fields[1].values.length; + + const cache = new Map(); + const interval = 15000; + // start time of scenario + const firstFrom = dateTime(new Date(1726835104488)); + const firstTo = dateTime(new Date(1726836004488)); + const firstRange: TimeRange = { + from: firstFrom, + to: firstTo, + raw: { + from: 'now-15m', + to: 'now', + }, + }; + // Signifier definition + const dashboardId = `dashid`; + const panelId = 200; + const targetIdentity = `${dashboardId}|${panelId}|A`; + + const request = mockPromRequest({ + range: firstRange, + dashboardUID: dashboardId, + panelId: panelId, + }); + + // we set a bigger interval than query interval + request.targets[0].interval = '1m'; + const requestInfo: CacheRequestInfo = { + requests: [], // unused + targSigs: cache, + shouldCache: true, + }; + const targetSignature = `1=1|${interval}|${JSON.stringify(request.rangeRaw ?? '')}`; + cache.set(targetIdentity, targetSignature); + + const firstQueryResult = storage.procFrames(request, requestInfo, firstFrames); + + expect(firstQueryResult[0].fields[1].values.length).toBe(expectedValueLength); + }); + it('Should modify request', () => { const request = mockPromRequest(); const storage = new QueryCache({ diff --git a/packages/grafana-prometheus/src/querycache/QueryCache.ts b/packages/grafana-prometheus/src/querycache/QueryCache.ts index 4758e4cff58..9b9c308e022 100644 --- a/packages/grafana-prometheus/src/querycache/QueryCache.ts +++ b/packages/grafana-prometheus/src/querycache/QueryCache.ts @@ -9,6 +9,7 @@ import { incrRoundDn, isValidDuration, parseDuration, + rangeUtil, Table, trimTable, } from '@grafana/data'; @@ -220,7 +221,10 @@ export class QueryCache { // eslint-disable-next-line @typescript-eslint/consistent-type-assertions let table: Table = frame.fields.map((field) => field.values) as Table; - let trimmed = trimTable(table, newFrom, newTo); + const dataPointStep = findDatapointStep(request, respFrames); + + // query interval is greater than request.intervalMs, use query interval to make sure we've always got one datapoint outside the panel viewport + let trimmed = trimTable(table, newFrom - dataPointStep, newTo); if (trimmed[0].length > 0) { for (let i = 0; i < trimmed.length; i++) { @@ -255,3 +259,21 @@ export class QueryCache { return respFrames; } } + +function findDatapointStep(request: DataQueryRequest, respFrames: DataFrame[]): number { + // Prometheus specific logic below + if (request.targets[0].datasource?.type !== 'prometheus') { + return 0; + } + + const target = request.targets.find((t) => t.refId === respFrames[0].refId); + + let dataPointStep = request.intervalMs; + if (target?.interval) { + const minStepMs = rangeUtil.intervalToMs(target.interval); + if (minStepMs > request.intervalMs) { + dataPointStep = minStepMs; + } + } + return dataPointStep; +} diff --git a/packages/grafana-prometheus/src/querycache/QueryCacheTestData.ts b/packages/grafana-prometheus/src/querycache/QueryCacheTestData.ts index a066f3107e4..91aca344f2f 100644 --- a/packages/grafana-prometheus/src/querycache/QueryCacheTestData.ts +++ b/packages/grafana-prometheus/src/querycache/QueryCacheTestData.ts @@ -258,6 +258,151 @@ const twoRequestsOneCachedMissingData = { }, }; +export const trimmedFirstPointInPromFrames = [ + { + refId: 'A', + meta: { + type: 'timeseries-multi', + typeVersion: [0, 1], + custom: { + resultType: 'matrix', + }, + executedQueryString: 'Expr: ecs_cpu_seconds_total\nStep: 1m0s', + }, + fields: [ + { + name: 'Time', + type: 'time', + typeInfo: { + frame: 'time.Time', + }, + config: { + interval: 60000, + }, + values: [1726835100000, 1726835160000, 1726835220000, 1726835280000, 1726835340000, 1726835400000], + entities: {}, + }, + { + name: 'ecs_cpu_seconds_total', + type: 'number', + typeInfo: { + frame: 'float64', + }, + labels: { + __name__: 'ecs_cpu_seconds_total', + container: 'browser-container', + cpu: '0', + environment: 'staging', + instance: 'localhost:9779', + job: 'node', + task_id: '7eaae23357564449bbde3d6b8aa3d171', + test_run_id: '178196', + }, + config: {}, + values: [148.528672986, 148.535654343, 148.535654343, 148.535654343, 148.535654343, 148.535654343], + entities: {}, + }, + ], + length: 6, + }, + { + refId: 'A', + meta: { + type: 'timeseries-multi', + typeVersion: [0, 1], + custom: { + resultType: 'matrix', + }, + }, + fields: [ + { + name: 'Time', + type: 'time', + typeInfo: { + frame: 'time.Time', + }, + config: { + interval: 60000, + }, + values: [ + 1726835100000, 1726835160000, 1726835220000, 1726835280000, 1726835340000, 1726835400000, 1726835460000, + 1726835520000, 1726835580000, 1726835640000, 1726835700000, 1726835760000, 1726835820000, 1726835880000, + 1726835940000, 1726836000000, + ], + entities: {}, + }, + { + name: 'ecs_cpu_seconds_total', + type: 'number', + typeInfo: { + frame: 'float64', + }, + labels: { + __name__: 'ecs_cpu_seconds_total', + container: 'browser-container', + cpu: '0', + environment: 'staging', + instance: 'localhost:9779', + job: 'node', + task_id: '800bedb7d7434ee69251e1d72aa24ee4', + }, + config: {}, + values: [ + 18.273081476, 18.27823287, 18.28002373, 18.281447944, 18.282133248, 18.283555666, 18.28503474, 18.287278624, + 18.290889095, 18.295363816, 18.29912598, 18.301647198, 18.305721365, 18.313378915, 18.31617255, 18.32104371, + ], + entities: {}, + }, + ], + length: 16, + }, + { + refId: 'A', + meta: { + type: 'timeseries-multi', + typeVersion: [0, 1], + custom: { + resultType: 'matrix', + }, + }, + fields: [ + { + name: 'Time', + type: 'time', + typeInfo: { + frame: 'time.Time', + }, + config: { + interval: 60000, + }, + values: [1726835100000, 1726835160000, 1726835220000, 1726835280000, 1726835340000, 1726835400000], + entities: {}, + }, + { + name: 'ecs_cpu_seconds_total', + type: 'number', + typeInfo: { + frame: 'float64', + }, + labels: { + __name__: 'ecs_cpu_seconds_total', + container: 'browser-container', + cpu: '1', + environment: 'staging', + instance: 'localhost:9779', + job: 'node', + task_id: '7eaae23357564449bbde3d6b8aa3d171', + test_run_id: '178196', + }, + config: {}, + values: [147.884430886, 147.893771728, 147.893771728, 147.893771728, 147.893771728, 147.893771728], + entities: {}, + }, + ], + length: 6, + }, +]; + export const IncrementalStorageDataFrameScenarios = { histogram: { // 3 requests, one 30 seconds after the first, and then the user waits a minute and shortens to a 5 minute query window from 1 hour to force frames to get evicted diff --git a/packages/grafana-runtime/package.json b/packages/grafana-runtime/package.json index 420229b4510..3ff4fb44d61 100644 --- a/packages/grafana-runtime/package.json +++ b/packages/grafana-runtime/package.json @@ -49,19 +49,19 @@ }, "devDependencies": { "@grafana/tsconfig": "^2.0.0", - "@rollup/plugin-node-resolve": "15.2.3", + "@rollup/plugin-node-resolve": "15.2.4", "@rollup/plugin-terser": "0.4.4", "@testing-library/dom": "10.0.0", "@testing-library/react": "15.0.2", "@testing-library/user-event": "14.5.2", "@types/angular": "1.8.9", "@types/history": "4.7.11", - "@types/jest": "29.5.12", + "@types/jest": "29.5.13", "@types/lodash": "4.17.7", "@types/react": "18.3.3", "@types/react-dom": "18.2.25", - "@types/systemjs": "6.15.0", - "esbuild": "0.20.2", + "@types/systemjs": "6.15.1", + "esbuild": "0.24.0", "lodash": "4.17.21", "react": "18.2.0", "react-dom": "18.2.0", diff --git a/packages/grafana-schema/package.json b/packages/grafana-schema/package.json index fe52af2d1fa..1e5338c51b5 100644 --- a/packages/grafana-schema/package.json +++ b/packages/grafana-schema/package.json @@ -37,8 +37,8 @@ }, "devDependencies": { "@grafana/tsconfig": "^2.0.0", - "@rollup/plugin-node-resolve": "15.2.3", - "esbuild": "0.20.2", + "@rollup/plugin-node-resolve": "15.2.4", + "esbuild": "0.24.0", "glob": "^11.0.0", "rimraf": "6.0.1", "rollup": "2.79.1", diff --git a/packages/grafana-sql/package.json b/packages/grafana-sql/package.json index fea6724e138..a73286bc3d2 100644 --- a/packages/grafana-sql/package.json +++ b/packages/grafana-sql/package.json @@ -25,7 +25,7 @@ "lodash": "4.17.21", "react": "18.2.0", "react-dom": "18.2.0", - "react-select": "5.8.0", + "react-select": "5.8.1", "react-use": "17.5.1", "react-virtualized-auto-sizer": "1.0.24", "rxjs": "7.8.1", @@ -42,11 +42,11 @@ "@testing-library/user-event": "14.5.2", "@types/jest": "^29.5.4", "@types/lodash": "4.17.7", - "@types/node": "20.16.4", + "@types/node": "20.16.5", "@types/react": "18.3.3", "@types/react-dom": "18.2.25", "@types/react-virtualized-auto-sizer": "1.0.4", - "@types/systemjs": "6.15.0", + "@types/systemjs": "6.15.1", "@types/testing-library__jest-dom": "5.14.9", "@types/uuid": "9.0.8", "jest": "^29.6.4", diff --git a/packages/grafana-ui/package.json b/packages/grafana-ui/package.json index c127aa3f076..b2e2a7b1b98 100644 --- a/packages/grafana-ui/package.json +++ b/packages/grafana-ui/package.json @@ -86,9 +86,9 @@ "prismjs": "1.29.0", "rc-cascader": "3.28.1", "rc-drawer": "7.2.0", - "rc-slider": "11.1.5", + "rc-slider": "11.1.6", "rc-time-picker": "^3.7.3", - "rc-tooltip": "6.2.0", + "rc-tooltip": "6.2.1", "react-calendar": "5.0.0", "react-colorful": "5.6.1", "react-custom-scrollbars-2": "4.5.0", @@ -99,7 +99,7 @@ "react-inlinesvg": "3.0.2", "react-loading-skeleton": "3.4.0", "react-router-dom-v5-compat": "^6.26.1", - "react-select": "5.8.0", + "react-select": "5.8.1", "react-table": "7.8.0", "react-transition-group": "4.4.5", "react-use": "17.5.1", @@ -117,7 +117,7 @@ "@babel/core": "7.25.2", "@faker-js/faker": "^8.4.1", "@grafana/tsconfig": "^2.0.0", - "@rollup/plugin-node-resolve": "15.2.3", + "@rollup/plugin-node-resolve": "15.2.4", "@storybook/addon-a11y": "^8.1.6", "@storybook/addon-actions": "^8.1.6", "@storybook/addon-docs": "^8.1.6", @@ -143,9 +143,9 @@ "@types/d3": "7.4.3", "@types/hoist-non-react-statics": "3.3.5", "@types/is-hotkey": "0.1.10", - "@types/jest": "29.5.12", + "@types/jest": "29.5.13", "@types/mock-raf": "1.0.6", - "@types/node": "20.16.4", + "@types/node": "20.16.5", "@types/prismjs": "1.26.4", "@types/react": "18.3.3", "@types/react-color": "3.0.12", @@ -165,7 +165,7 @@ "core-js": "3.38.1", "css-loader": "7.1.2", "csstype": "3.1.3", - "esbuild": "0.20.2", + "esbuild": "0.24.0", "expose-loader": "5.0.0", "mock-raf": "1.0.1", "process": "^0.11.10", diff --git a/packages/grafana-ui/src/components/Actions/ActionButton.tsx b/packages/grafana-ui/src/components/Actions/ActionButton.tsx new file mode 100644 index 00000000000..49b3c82c5c9 --- /dev/null +++ b/packages/grafana-ui/src/components/Actions/ActionButton.tsx @@ -0,0 +1,18 @@ +import { ActionModel, Field } from '@grafana/data'; + +import { Button, ButtonProps } from '../Button'; + +type ActionButtonProps = ButtonProps & { + action: ActionModel; +}; + +/** + * @internal + */ +export function ActionButton({ action, ...buttonProps }: ActionButtonProps) { + return ( + + ); +} diff --git a/packages/grafana-ui/src/components/Combobox/Combobox.story.tsx b/packages/grafana-ui/src/components/Combobox/Combobox.story.tsx index 20889e6ed1c..d5846b3025a 100644 --- a/packages/grafana-ui/src/components/Combobox/Combobox.story.tsx +++ b/packages/grafana-ui/src/components/Combobox/Combobox.story.tsx @@ -6,7 +6,7 @@ import React, { ComponentProps, useEffect, useState } from 'react'; import { Alert } from '../Alert/Alert'; import { Field } from '../Forms/Field'; -import { Combobox, Option, Value } from './Combobox'; +import { Combobox, ComboboxOption } from './Combobox'; const chance = new Chance(); @@ -69,7 +69,7 @@ type Story = StoryObj; export const Basic: Story = {}; -async function generateOptions(amount: number): Promise { +async function generateOptions(amount: number): Promise { return Array.from({ length: amount }, (_, index) => ({ label: chance.sentence({ words: index % 5 }), value: chance.guid(), @@ -78,8 +78,8 @@ async function generateOptions(amount: number): Promise { } const ManyOptionsStory: StoryFn = ({ numberOfOptions, ...args }) => { - const [value, setValue] = useState(null); - const [options, setOptions] = useState([]); + const [value, setValue] = useState(null); + const [options, setOptions] = useState([]); const [isLoading, setIsLoading] = useState(true); useEffect(() => { diff --git a/packages/grafana-ui/src/components/Combobox/Combobox.test.tsx b/packages/grafana-ui/src/components/Combobox/Combobox.test.tsx index 2263618d601..46b7d48a404 100644 --- a/packages/grafana-ui/src/components/Combobox/Combobox.test.tsx +++ b/packages/grafana-ui/src/components/Combobox/Combobox.test.tsx @@ -1,10 +1,10 @@ import { render, screen, fireEvent } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; -import { Combobox, Option } from './Combobox'; +import { Combobox, ComboboxOption } from './Combobox'; // Mock data for the Combobox options -const options: Option[] = [ +const options: ComboboxOption[] = [ { label: 'Option 1', value: '1' }, { label: 'Option 2', value: '2' }, { label: 'Option 3', value: '3', description: 'This is option 3' }, @@ -73,9 +73,10 @@ describe('Combobox', () => { const input = screen.getByRole('combobox'); await userEvent.click(input); - await userEvent.keyboard('{ArrowDown}{ArrowDown}{Enter}'); - expect(onChangeHandler).toHaveBeenCalledWith(options[1]); - expect(screen.queryByDisplayValue('Option 2')).toBeInTheDocument(); + await userEvent.keyboard('{ArrowDown}{ArrowDown}{Enter}'); // Focus is at index 0 to start with + + expect(onChangeHandler).toHaveBeenCalledWith(options[2]); + expect(screen.queryByDisplayValue('Option 3')).toBeInTheDocument(); }); it('clears selected value', async () => { @@ -91,4 +92,44 @@ describe('Combobox', () => { expect(onChangeHandler).toHaveBeenCalledWith(null); expect(screen.queryByDisplayValue('Option 2')).not.toBeInTheDocument(); }); + + describe('create custom value', () => { + it('should allow creating a custom value', async () => { + const onChangeHandler = jest.fn(); + render(); + const input = screen.getByRole('combobox'); + await userEvent.type(input, 'custom value'); + await userEvent.keyboard('{Enter}'); + + expect(screen.getByDisplayValue('custom value')).toBeInTheDocument(); + expect(onChangeHandler).toHaveBeenCalledWith( + expect.objectContaining({ label: 'custom value', value: 'custom value' }) + ); + }); + + it('should proivde custom string when all options are numbers', async () => { + const options = [ + { label: '1', value: 1 }, + { label: '2', value: 2 }, + { label: '3', value: 3 }, + ]; + + const onChangeHandler = jest.fn(); + + render(); + const input = screen.getByRole('combobox'); + + await userEvent.type(input, 'custom value'); + await userEvent.keyboard('{Enter}'); + + expect(screen.getByDisplayValue('custom value')).toBeInTheDocument(); + expect(typeof onChangeHandler.mock.calls[0][0].value === 'string').toBeTruthy(); + expect(typeof onChangeHandler.mock.calls[0][0].value === 'number').toBeFalsy(); + + await userEvent.click(input); + await userEvent.keyboard('{Enter}'); // Select 1 as the first option + expect(typeof onChangeHandler.mock.calls[1][0].value === 'string').toBeFalsy(); + expect(typeof onChangeHandler.mock.calls[1][0].value === 'number').toBeTruthy(); + }); + }); }); diff --git a/packages/grafana-ui/src/components/Combobox/Combobox.tsx b/packages/grafana-ui/src/components/Combobox/Combobox.tsx index 3fe19ab9ac7..cb8a5f7bbba 100644 --- a/packages/grafana-ui/src/components/Combobox/Combobox.tsx +++ b/packages/grafana-ui/src/components/Combobox/Combobox.tsx @@ -2,7 +2,7 @@ import { cx } from '@emotion/css'; import { autoUpdate, flip, size, useFloating } from '@floating-ui/react'; import { useVirtualizer } from '@tanstack/react-virtual'; import { useCombobox } from 'downshift'; -import { SetStateAction, useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { SetStateAction, useCallback, useEffect, useId, useMemo, useRef, useState } from 'react'; import { useStyles2 } from '../../themes'; import { t } from '../../utils/i18n'; @@ -11,30 +11,29 @@ import { Input, Props as InputProps } from '../Input/Input'; import { getComboboxStyles } from './getComboboxStyles'; -export type Value = string | number; -export type Option = { +export type ComboboxOption = { label: string; - value: Value; + value: T; description?: string; }; -interface ComboboxProps +interface ComboboxProps extends Omit { - onChange: (val: Option | null) => void; - value: Value | null; - options: Option[]; isClearable?: boolean; createCustomValue?: boolean; + options: Array>; + onChange: (option: ComboboxOption | null) => void; + value: T | null; } -function itemToString(item: Option | null) { - return item?.label ?? item?.value?.toString() ?? ''; +function itemToString(item: ComboboxOption | null) { + return item?.label ?? item?.value.toString() ?? ''; } -function itemFilter(inputValue: string) { +function itemFilter(inputValue: string) { const lowerCasedInputValue = inputValue.toLowerCase(); - return (item: Option) => { + return (item: ComboboxOption) => { return ( !inputValue || item?.label?.toLowerCase().includes(lowerCasedInputValue) || @@ -53,15 +52,21 @@ const INDEX_WIDTH_CALCULATION = 100; // A multiplier guesstimate times the amount of characters. If any padding or image support etc. is added this will need to be updated. const WIDTH_MULTIPLIER = 7.3; -export const Combobox = ({ +/** + * A performant Select replacement. + * + * @alpha + */ +export const Combobox = ({ options, onChange, value, isClearable = false, createCustomValue = false, id, + 'aria-labelledby': ariaLabelledBy, ...restProps -}: ComboboxProps) => { +}: ComboboxProps) => { const [items, setItems] = useState(options); const selectedItemIndex = useMemo(() => { @@ -78,7 +83,7 @@ export const Combobox = ({ }, [options, value]); const selectedItem = useMemo(() => { - if (selectedItemIndex) { + if (selectedItemIndex !== null) { return options[selectedItemIndex]; } @@ -95,6 +100,9 @@ export const Combobox = ({ const inputRef = useRef(null); const floatingRef = useRef(null); + const menuId = `downshift-${useId().replace(/:/g, '--')}-menu`; + const labelId = `downshift-${useId().replace(/:/g, '--')}-label`; + const styles = useStyles2(getComboboxStyles); const [popoverMaxWidth, setPopoverMaxWidth] = useState(undefined); const [popoverWidth, setPopoverWidth] = useState(undefined); @@ -119,26 +127,28 @@ export const Combobox = ({ closeMenu, selectItem, } = useCombobox({ + menuId, + labelId, inputId: id, items, itemToString, selectedItem, - onSelectedItemChange: ({ selectedItem, inputValue }) => { + onSelectedItemChange: ({ selectedItem }) => { onChange(selectedItem); }, - defaultHighlightedIndex: selectedItemIndex ?? undefined, + defaultHighlightedIndex: selectedItemIndex ?? 0, scrollIntoView: () => {}, onInputValueChange: ({ inputValue }) => { const filteredItems = options.filter(itemFilter(inputValue)); if (createCustomValue && inputValue && filteredItems.findIndex((opt) => opt.label === inputValue) === -1) { - setItems([ - ...filteredItems, - { - label: inputValue, - value: inputValue, - description: t('combobox.custom-value.create', 'Create custom value'), - }, - ]); + const customValueOption: ComboboxOption = { + label: inputValue, + // @ts-ignore Type casting needed to make this work when T is a number + value: inputValue as unknown as T, + description: t('combobox.custom-value.create', 'Create custom value'), + }; + + setItems([...filteredItems, customValueOption]); return; } else { setItems(filteredItems); @@ -177,6 +187,7 @@ export const Combobox = ({ ]; const elements = { reference: inputRef.current, floating: floatingRef.current }; const { floatingStyles } = useFloating({ + strategy: 'fixed', open: isOpen, placement: 'bottom-start', middleware, @@ -231,17 +242,21 @@ export const Combobox = ({ */ onChange: () => {}, onBlur, + 'aria-labelledby': ariaLabelledBy, // Label should be handled with the Field component })} />
{isOpen && (
    @@ -259,7 +274,10 @@ export const Combobox = ({ height: virtualRow.size, transform: `translateY(${virtualRow.start}px)`, }} - {...getItemProps({ item: items[virtualRow.index], index: virtualRow.index })} + {...getItemProps({ + item: items[virtualRow.index], + index: virtualRow.index, + })} >
    {items[virtualRow.index].label} @@ -278,7 +296,7 @@ export const Combobox = ({ }; const useDynamicWidth = ( - items: Option[], + items: Array>, range: { startIndex: number; endIndex: number } | null, setPopoverWidth: { (value: SetStateAction): void } ) => { diff --git a/packages/grafana-ui/src/components/Combobox/getComboboxStyles.ts b/packages/grafana-ui/src/components/Combobox/getComboboxStyles.ts index 06fa657821c..ae5c94bc409 100644 --- a/packages/grafana-ui/src/components/Combobox/getComboboxStyles.ts +++ b/packages/grafana-ui/src/components/Combobox/getComboboxStyles.ts @@ -4,12 +4,14 @@ import { GrafanaTheme2 } from '@grafana/data'; export const getComboboxStyles = (theme: GrafanaTheme2) => { return { + menuClosed: css({ + display: 'none', + }), menu: css({ label: 'grafana-select-menu', background: theme.components.dropdown.background, boxShadow: theme.shadows.z3, - position: 'relative', - zIndex: 1, + zIndex: theme.zIndex.dropdown, }), menuHeight: css({ height: 400, diff --git a/packages/grafana-ui/src/components/DateTimePickers/TimeRangePicker/CalendarBody.tsx b/packages/grafana-ui/src/components/DateTimePickers/TimeRangePicker/CalendarBody.tsx index a3f988cb3a4..37da44b515f 100644 --- a/packages/grafana-ui/src/components/DateTimePickers/TimeRangePicker/CalendarBody.tsx +++ b/packages/grafana-ui/src/components/DateTimePickers/TimeRangePicker/CalendarBody.tsx @@ -99,7 +99,7 @@ export const getBodyStyles = (theme: GrafanaTheme2) => { fontSize: theme.typography.size.md, border: '1px solid transparent', - '&:hover': { + '&:hover, &:focus': { position: 'relative', }, @@ -157,7 +157,6 @@ export const getBodyStyles = (theme: GrafanaTheme2) => { color: theme.colors.primary.contrastText, fontWeight: theme.typography.fontWeightMedium, background: theme.colors.primary.main, - boxShadow: 'none', border: '0px', }, diff --git a/packages/grafana-ui/src/components/DateTimePickers/TimeRangePicker/TimePickerFooter.tsx b/packages/grafana-ui/src/components/DateTimePickers/TimeRangePicker/TimePickerFooter.tsx index 28b328ed5c3..7ac73a45cb4 100644 --- a/packages/grafana-ui/src/components/DateTimePickers/TimeRangePicker/TimePickerFooter.tsx +++ b/packages/grafana-ui/src/components/DateTimePickers/TimeRangePicker/TimePickerFooter.tsx @@ -9,8 +9,8 @@ import { selectors } from '@grafana/e2e-selectors'; import { useStyles2 } from '../../../themes'; import { t, Trans } from '../../../utils/i18n'; import { Button } from '../../Button'; +import { Combobox } from '../../Combobox/Combobox'; import { Field } from '../../Forms/Field'; -import { Select } from '../../Select/Select'; import { Tab, TabContent, TabsBar } from '../../Tabs'; import { TimeZonePicker } from '../TimeZonePicker'; import { TimeZoneDescription } from '../TimeZonePicker/TimeZoneDescription'; @@ -142,13 +142,12 @@ export const TimePickerFooter = (props: Props) => { className={style.fiscalYearField} label={t('time-picker.footer.fiscal-year-start', 'Fiscal year start month')} > - item.value === value)?.value} + > = [ +export const monthOptions: Array> = [ { label: 'January', value: 0 }, { label: 'February', value: 1 }, { label: 'March', value: 2 }, diff --git a/packages/grafana-ui/src/components/Portal/Portal.tsx b/packages/grafana-ui/src/components/Portal/Portal.tsx index 1dd8e29b374..df94e4b9d92 100644 --- a/packages/grafana-ui/src/components/Portal/Portal.tsx +++ b/packages/grafana-ui/src/components/Portal/Portal.tsx @@ -1,4 +1,4 @@ -import { css, cx } from '@emotion/css'; +import { css } from '@emotion/css'; import { PropsWithChildren, useLayoutEffect, useRef } from 'react'; import * as React from 'react'; import ReactDOM from 'react-dom'; @@ -51,25 +51,27 @@ export function getPortalContainer() { /** @internal */ export function PortalContainer() { const styles = useStyles2(getStyles); - const isBodyScrolling = window.grafanaBootData?.settings.featureToggles.bodyScrolling; - return ( -
    - ); + return
    ; } -const getStyles = (theme: GrafanaTheme2) => ({ - grafanaPortalContainer: css({ - position: 'fixed', - top: 0, - width: '100%', - zIndex: theme.zIndex.portal, - }), -}); +const getStyles = (theme: GrafanaTheme2) => { + const isBodyScrolling = window.grafanaBootData?.settings.featureToggles.bodyScrolling; + return { + grafanaPortalContainer: css( + isBodyScrolling + ? { + position: 'fixed', + top: 0, + width: '100%', + zIndex: theme.zIndex.portal, + } + : { + position: 'absolute', + width: '100%', + } + ), + }; +}; export const RefForwardingPortal = React.forwardRef((props, ref) => { return ; diff --git a/packages/grafana-ui/src/components/Select/Select.story.tsx b/packages/grafana-ui/src/components/Select/Select.story.tsx index ac0a7277c40..975d81d9ba2 100644 --- a/packages/grafana-ui/src/components/Select/Select.story.tsx +++ b/packages/grafana-ui/src/components/Select/Select.story.tsx @@ -311,6 +311,33 @@ MultiSelectBasic.args = { noMultiValueWrap: false, }; +export const MultiSelectBasicWithSelectAll: StoryFn = (args) => { + const [value, setValue] = useState>>([]); + + return ( +
    + { + setValue(v); + action('onChange')(v); + }} + prefix={getPrefix(args.icon)} + {...args} + /> +
    + ); +}; + +MultiSelectBasicWithSelectAll.args = { + isClearable: false, + closeMenuOnSelect: false, + maxVisibleValues: 5, + noMultiValueWrap: false, +}; + export const MultiSelectAsync: StoryFn = (args) => { const [value, setValue] = useState>>(); diff --git a/packages/grafana-ui/src/components/Select/SelectBase.test.tsx b/packages/grafana-ui/src/components/Select/SelectBase.test.tsx index 895e0d055b1..ac41aa47c03 100644 --- a/packages/grafana-ui/src/components/Select/SelectBase.test.tsx +++ b/packages/grafana-ui/src/components/Select/SelectBase.test.tsx @@ -244,6 +244,7 @@ describe('SelectBase', () => { removedValue: { label: 'Option 1', value: 1 }, }); }); + it('does not allow deleting selected values when disabled', async () => { const value = [ { @@ -264,5 +265,96 @@ describe('SelectBase', () => { expect(screen.queryByLabelText('Remove Option 1')).not.toBeInTheDocument(); }); + + describe('toggle all', () => { + beforeEach(() => { + jest.resetAllMocks(); + }); + it('renders menu with select all toggle', async () => { + render( + + ); + await userEvent.click(screen.getByText(/choose/i)); + const toggleAllOptions = screen.getByTestId(selectors.components.Select.toggleAllOptions); + expect(toggleAllOptions).toBeInTheDocument(); + }); + + it('correctly displays the number of selected items', async () => { + render( + + ); + await userEvent.click(screen.getByText(/Option 1/i)); + const toggleAllOptions = screen.getByTestId(selectors.components.Select.toggleAllOptions); + expect(toggleAllOptions.textContent).toBe('Selected (1)'); + }); + + it('correctly removes all selected options when in indeterminate state', async () => { + render( + + ); + await userEvent.click(screen.getByText(/Option 1/i)); + let toggleAllOptions = screen.getByTestId(selectors.components.Select.toggleAllOptions); + expect(toggleAllOptions.textContent).toBe('Selected (1)'); + + // Toggle all unselected when in indeterminate state + await userEvent.click(toggleAllOptions); + expect(onChangeHandler).toHaveBeenCalledWith([], expect.anything()); + }); + + it('correctly removes all selected options when all options are selected', async () => { + render( + + ); + await userEvent.click(screen.getByText(/Option 1/i)); + let toggleAllOptions = screen.getByTestId(selectors.components.Select.toggleAllOptions); + expect(toggleAllOptions.textContent).toBe('Selected (2)'); + + // Toggle all unselected when in indeterminate state + await userEvent.click(toggleAllOptions); + expect(onChangeHandler).toHaveBeenCalledWith([], expect.anything()); + }); + + it('correctly selects all values when none are selected', async () => { + render( + + ); + await userEvent.click(screen.getByText(/Choose/i)); + let toggleAllOptions = screen.getByTestId(selectors.components.Select.toggleAllOptions); + expect(toggleAllOptions.textContent).toBe('Selected (0)'); + + // Toggle all unselected when in indeterminate state + await userEvent.click(toggleAllOptions); + expect(onChangeHandler).toHaveBeenCalledWith(options, expect.anything()); + }); + }); }); }); diff --git a/packages/grafana-ui/src/components/Select/SelectBase.tsx b/packages/grafana-ui/src/components/Select/SelectBase.tsx index a37634653d6..0fe0dea431e 100644 --- a/packages/grafana-ui/src/components/Select/SelectBase.tsx +++ b/packages/grafana-ui/src/components/Select/SelectBase.tsx @@ -1,4 +1,5 @@ import { t } from 'i18next'; +import { isArray, negate } from 'lodash'; import { ComponentProps, useCallback, useEffect, useRef, useState } from 'react'; import * as React from 'react'; import { @@ -30,7 +31,7 @@ import { Props, SingleValue } from './SingleValue'; import { ValueContainer } from './ValueContainer'; import { getSelectStyles } from './getSelectStyles'; import { useCustomSelectStyles } from './resetSelectStyles'; -import { ActionMeta, InputActionMeta, SelectBaseProps } from './types'; +import { ActionMeta, InputActionMeta, SelectBaseProps, ToggleAllState } from './types'; import { cleanValue, findSelectedValue, omitDescriptions } from './utils'; const CustomControl = (props: any) => { @@ -77,6 +78,16 @@ interface SelectPropsWithExtras extends ReactSelectProps { noMultiValueWrap?: boolean; } +function determineToggleAllState(selectedValue: SelectableValue[], options: SelectableValue[]) { + if (options.length === selectedValue.length) { + return ToggleAllState.allSelected; + } else if (selectedValue.length === 0) { + return ToggleAllState.noneSelected; + } else { + return ToggleAllState.indeterminate; + } +} + export function SelectBase({ allowCustomValue = false, allowCreateWhileLoading = false, @@ -126,6 +137,7 @@ export function SelectBase({ onMenuScrollToTop, onOpenMenu, onFocus, + toggleAllOptions, openMenuOnFocus = false, options = [], placeholder = t('grafana-ui.select.placeholder', 'Choose'), @@ -295,6 +307,30 @@ export function SelectBase({ const SelectMenuComponent = virtualized ? VirtualizedSelectMenu : SelectMenu; + let toggleAllState = ToggleAllState.noneSelected; + if (toggleAllOptions?.enabled && isArray(selectedValue)) { + if (toggleAllOptions?.determineToggleAllState) { + toggleAllState = toggleAllOptions.determineToggleAllState(selectedValue, options); + } else { + toggleAllState = determineToggleAllState(selectedValue, options); + } + } + + const toggleAll = useCallback(() => { + let toSelect = toggleAllState === ToggleAllState.noneSelected ? options : []; + if (toggleAllOptions?.optionsFilter) { + toSelect = + toggleAllState === ToggleAllState.noneSelected + ? options.filter(toggleAllOptions.optionsFilter) + : options.filter(negate(toggleAllOptions.optionsFilter)); + } + + onChange(toSelect, { + action: 'select-option', + option: {}, + }); + }, [options, toggleAllOptions, onChange, toggleAllState]); + return ( <> ({ Input: CustomInput, ...components, }} + toggleAllOptions={ + toggleAllOptions?.enabled && { + state: toggleAllState, + selectAllClicked: toggleAll, + selectedCount: isArray(selectedValue) ? selectedValue.length : undefined, + } + } styles={selectStyles} className={className} {...commonSelectProps} diff --git a/packages/grafana-ui/src/components/Select/SelectMenu.tsx b/packages/grafana-ui/src/components/Select/SelectMenu.tsx index 161345219f2..7a0e854126f 100644 --- a/packages/grafana-ui/src/components/Select/SelectMenu.tsx +++ b/packages/grafana-ui/src/components/Select/SelectMenu.tsx @@ -1,32 +1,62 @@ -import { cx } from '@emotion/css'; +import { css, cx } from '@emotion/css'; import { max } from 'lodash'; import { RefCallback, useLayoutEffect, useMemo, useRef } from 'react'; import * as React from 'react'; -import { MenuListProps } from 'react-select'; import { FixedSizeList as List } from 'react-window'; import { SelectableValue, toIconName } from '@grafana/data'; import { selectors } from '@grafana/e2e-selectors'; import { useTheme2 } from '../../themes/ThemeContext'; +import { Trans } from '../../utils/i18n'; +import { clearButtonStyles } from '../Button'; import { CustomScrollbar } from '../CustomScrollbar/CustomScrollbar'; import { Icon } from '../Icon/Icon'; import { getSelectStyles } from './getSelectStyles'; +import { ToggleAllState } from './types'; + +export interface ToggleAllOptions { + state: ToggleAllState; + selectAllClicked: () => void; + selectedCount?: number; +} interface SelectMenuProps { maxHeight: number; innerRef: RefCallback; innerProps: {}; + selectProps: { + toggleAllOptions?: ToggleAllOptions; + components?: { Option?: (props: React.PropsWithChildren>) => JSX.Element }; + }; } -export const SelectMenu = ({ children, maxHeight, innerRef, innerProps }: React.PropsWithChildren) => { +export const SelectMenu = ({ + children, + maxHeight, + innerRef, + innerProps, + selectProps, +}: React.PropsWithChildren) => { const theme = useTheme2(); const styles = getSelectStyles(theme); + const { toggleAllOptions, components } = selectProps; + + const optionsElement = components?.Option ?? SelectMenuOptions; + return (
    + {toggleAllOptions && ( + + )} {children}
    @@ -49,16 +79,33 @@ const VIRTUAL_LIST_WIDTH_EXTRA = 58; // // VIRTUAL_LIST_ITEM_HEIGHT and WIDTH_ESTIMATE_MULTIPLIER are both magic numbers. // Some characters (such as emojis and other unicode characters) may consist of multiple code points in which case the width would be inaccurate (but larger than needed). +interface VirtualSelectMenuProps { + children: React.ReactNode; + innerRef: React.Ref; + focusedOption: T; + innerProps: JSX.IntrinsicElements['div']; + options: T[]; + maxHeight: number; + selectProps: { + toggleAllOptions?: ToggleAllOptions; + components?: { Option?: (props: React.PropsWithChildren>) => JSX.Element }; + }; +} + export const VirtualizedSelectMenu = ({ children, maxHeight, innerRef: scrollRef, options, + selectProps, focusedOption, -}: MenuListProps) => { +}: VirtualSelectMenuProps) => { const theme = useTheme2(); const styles = getSelectStyles(theme); const listRef = useRef(null); + const { toggleAllOptions, components } = selectProps; + + const optionComponent = components?.Option ?? SelectMenuOptions; // we need to check for option groups (categories) // these are top level options with child options @@ -94,6 +141,7 @@ export const VirtualizedSelectMenu = ({ // add a bottom divider to the last item in the category React.cloneElement(child.props.children.at(-1), { innerProps: { + ...child.props.children.at(-1).props.innerProps, style: { borderBottom: `1px solid ${theme.colors.border.weak}`, height: VIRTUAL_LIST_ITEM_HEIGHT, @@ -105,7 +153,21 @@ export const VirtualizedSelectMenu = ({ return [child]; }); - const longestOption = max(flattenedOptions.map((option) => option.label?.length)) ?? 0; + if (toggleAllOptions) { + flattenedChildren.unshift( + + ); + } + + let longestOption = max(flattenedOptions.map((option) => option.label?.length)) ?? 0; + if (toggleAllOptions && longestOption < 12) { + longestOption = 12; + } const widthEstimate = longestOption * VIRTUAL_LIST_WIDTH_ESTIMATE_MULTIPLIER + VIRTUAL_LIST_PADDING * 2 + VIRTUAL_LIST_WIDTH_EXTRA; const heightEstimate = Math.min(flattenedChildren.length * VIRTUAL_LIST_ITEM_HEIGHT, maxHeight); @@ -138,12 +200,54 @@ interface SelectMenuOptionProps { isDisabled: boolean; isFocused: boolean; isSelected: boolean; + indeterminate?: boolean; innerProps: JSX.IntrinsicElements['div']; innerRef: RefCallback; renderOptionLabel?: (value: SelectableValue) => JSX.Element; data: SelectableValue; } +const ToggleAllOption = ({ + state, + onClick, + selectedCount, + optionComponent, +}: { + state: ToggleAllState; + onClick: () => void; + selectedCount?: number; + optionComponent: (props: React.PropsWithChildren>) => JSX.Element; +}) => { + const theme = useTheme2(); + const styles = getSelectStyles(theme); + + return ( + + ); +}; + export const SelectMenuOptions = ({ children, data, diff --git a/packages/grafana-ui/src/components/Select/getSelectStyles.ts b/packages/grafana-ui/src/components/Select/getSelectStyles.ts index a75b79417a3..32e30c1da0e 100644 --- a/packages/grafana-ui/src/components/Select/getSelectStyles.ts +++ b/packages/grafana-ui/src/components/Select/getSelectStyles.ts @@ -163,5 +163,11 @@ export const getSelectStyles = stylesFactory((theme: GrafanaTheme2) => { borderBottom: `1px solid ${theme.colors.border.weak}`, }, }), + toggleAllButton: css({ + width: '100%', + border: 0, + padding: 0, + textAlign: 'left', + }), }; }); diff --git a/packages/grafana-ui/src/components/Select/types.ts b/packages/grafana-ui/src/components/Select/types.ts index ae97e63adfd..b5183d97ea3 100644 --- a/packages/grafana-ui/src/components/Select/types.ts +++ b/packages/grafana-ui/src/components/Select/types.ts @@ -15,6 +15,12 @@ export type InputActionMeta = { }; export type LoadOptionsCallback = (options: Array>) => void; +export enum ToggleAllState { + allSelected = 'allSelected', + indeterminate = 'indeterminate', + noneSelected = 'noneSelected', +} + export interface SelectCommonProps { /** Aria label applied to the input field */ ['aria-label']?: string; @@ -78,6 +84,14 @@ export interface SelectCommonProps { onMenuScrollToTop?: (event: WheelEvent | TouchEvent) => void; onOpenMenu?: () => void; onFocus?: () => void; + toggleAllOptions?: { + enabled: boolean; + optionsFilter?: (v: SelectableValue) => boolean; + determineToggleAllState?: ( + selectedValues: Array>, + options: Array> + ) => ToggleAllState; + }; openMenuOnFocus?: boolean; options?: Array>; placeholder?: string; diff --git a/packages/grafana-ui/src/components/Switch/Switch.tsx b/packages/grafana-ui/src/components/Switch/Switch.tsx index 53414c56aa6..269463c2126 100644 --- a/packages/grafana-ui/src/components/Switch/Switch.tsx +++ b/packages/grafana-ui/src/components/Switch/Switch.tsx @@ -85,8 +85,9 @@ const getSwitchStyles = (theme: GrafanaTheme2, transparent?: boolean) => ({ lineHeight: 1, input: { + height: '100%', + width: '100% !important', opacity: 0, - left: '-100vw', zIndex: -1000, position: 'absolute', diff --git a/packages/grafana-ui/src/components/Table/TableCellInspector.tsx b/packages/grafana-ui/src/components/Table/TableCellInspector.tsx index 24d6bcc649a..c9a78b51d53 100644 --- a/packages/grafana-ui/src/components/Table/TableCellInspector.tsx +++ b/packages/grafana-ui/src/components/Table/TableCellInspector.tsx @@ -71,7 +71,7 @@ export function TableCellInspector({ value, onDismiss, mode }: TableCellInspecto text} style={{ marginLeft: 'auto', width: '200px' }}> - Copy to Clipboard + Copy to Clipboard {currentMode === 'code' ? ( { }) ), rightWrapper: css({ - paddingLeft: theme.spacing(0.5), + padding: theme.spacing(0.5), }), bottomWrapper: css({ display: 'flex', flexWrap: 'wrap', justifyContent: 'space-between', width: '100%', - paddingLeft: theme.spacing(0.5), + padding: theme.spacing(0.5), gap: '15px 25px', }), section: css({ diff --git a/packages/grafana-ui/src/components/VizTooltip/VizTooltipFooter.tsx b/packages/grafana-ui/src/components/VizTooltip/VizTooltipFooter.tsx index e455f8591aa..ca4ef351874 100644 --- a/packages/grafana-ui/src/components/VizTooltip/VizTooltipFooter.tsx +++ b/packages/grafana-ui/src/components/VizTooltip/VizTooltipFooter.tsx @@ -1,37 +1,50 @@ import { css } from '@emotion/css'; -import { Field, GrafanaTheme2, LinkModel } from '@grafana/data'; +import { ActionModel, Field, GrafanaTheme2, LinkModel } from '@grafana/data'; import { Button, ButtonProps, DataLinkButton, Stack } from '..'; import { useStyles2 } from '../../themes'; +import { ActionButton } from '../Actions/ActionButton'; interface VizTooltipFooterProps { dataLinks: Array>; + actions?: Array>; annotate?: () => void; } export const ADD_ANNOTATION_ID = 'add-annotation-button'; -export const VizTooltipFooter = ({ dataLinks, annotate }: VizTooltipFooterProps) => { - const styles = useStyles2(getStyles); - - const renderDataLinks = () => { - const buttonProps: ButtonProps = { - variant: 'secondary', - }; - - return ( - - {dataLinks.map((link, i) => ( - - ))} - - ); +const renderDataLinks = (dataLinks: LinkModel[]) => { + const buttonProps: ButtonProps = { + variant: 'secondary', }; + return ( + + {dataLinks.map((link, i) => ( + + ))} + + ); +}; + +const renderActions = (actions: ActionModel[]) => { + return ( + + {actions.map((action, i) => ( + + ))} + + ); +}; + +export const VizTooltipFooter = ({ dataLinks, actions, annotate }: VizTooltipFooterProps) => { + const styles = useStyles2(getStyles); + return (
    - {dataLinks.length > 0 &&
    {renderDataLinks()}
    } + {dataLinks.length > 0 &&
    {renderDataLinks(dataLinks)}
    } + {actions && actions.length > 0 &&
    {renderActions(actions)}
    } {annotate != null && (
    + + + + ); +}; diff --git a/public/app/features/actions/ActionsInlineEditor.tsx b/public/app/features/actions/ActionsInlineEditor.tsx new file mode 100644 index 00000000000..04a59f0714e --- /dev/null +++ b/public/app/features/actions/ActionsInlineEditor.tsx @@ -0,0 +1,192 @@ +import { css } from '@emotion/css'; +import { DragDropContext, Droppable, DropResult } from '@hello-pangea/dnd'; +import { cloneDeep } from 'lodash'; +import { ReactNode, useEffect, useState } from 'react'; + +import { Action, DataFrame, GrafanaTheme2, defaultActionConfig, VariableSuggestion } from '@grafana/data'; +import { Button } from '@grafana/ui/src/components/Button'; +import { Modal } from '@grafana/ui/src/components/Modal/Modal'; +import { useStyles2 } from '@grafana/ui/src/themes'; + +import { ActionEditorModalContent } from './ActionEditorModalContent'; +import { ActionListItem } from './ActionsListItem'; + +interface ActionsInlineEditorProps { + actions?: Action[]; + onChange: (actions: Action[]) => void; + data: DataFrame[]; + getSuggestions: () => VariableSuggestion[]; + showOneClick?: boolean; +} + +export const ActionsInlineEditor = ({ + actions, + onChange, + data, + getSuggestions, + showOneClick = false, +}: ActionsInlineEditorProps) => { + const [editIndex, setEditIndex] = useState(null); + const [isNew, setIsNew] = useState(false); + + const [actionsSafe, setActionsSafe] = useState([]); + + useEffect(() => { + setActionsSafe(actions ?? []); + }, [actions]); + + const styles = useStyles2(getActionsInlineEditorStyle); + const isEditing = editIndex !== null; + + const onActionChange = (index: number, action: Action) => { + if (isNew) { + if (action.title.trim() === '') { + setIsNew(false); + setEditIndex(null); + return; + } else { + setEditIndex(null); + setIsNew(false); + } + } + const update = cloneDeep(actionsSafe); + update[index] = action; + onChange(update); + + setEditIndex(null); + }; + + const onActionAdd = () => { + let update = cloneDeep(actionsSafe); + setEditIndex(update.length); + setIsNew(true); + }; + + const onActionCancel = (index: number) => { + if (isNew) { + setIsNew(false); + } + setEditIndex(null); + }; + + const onActionRemove = (index: number) => { + const update = cloneDeep(actionsSafe); + update.splice(index, 1); + onChange(update); + }; + + const onDragEnd = (result: DropResult) => { + if (!actions || !result.destination) { + return; + } + + const update = cloneDeep(actionsSafe); + const action = update[result.source.index]; + + update.splice(result.source.index, 1); + update.splice(result.destination.index, 0, action); + + setActionsSafe(update); + onChange(update); + }; + + const renderFirstAction = (actionsJSX: ReactNode, key: string) => { + if (showOneClick) { + return ( +
    + One-click action {actionsJSX} +
    + ); + } + return actionsJSX; + }; + + return ( + <> + + + {(provided) => ( +
    + {actionsSafe.map((action, idx) => { + const key = `${action.title}/${idx}`; + + const actionsJSX = ( +
    + setEditIndex(idx)} + onRemove={() => onActionRemove(idx)} + data={data} + itemKey={key} + /> +
    + ); + + if (idx === 0) { + return renderFirstAction(actionsJSX, key); + } + + return actionsJSX; + })} + {provided.placeholder} +
    + )} +
    +
    + + {isEditing && editIndex !== null && ( + { + onActionCancel(editIndex); + }} + > + + + )} + + + + ); +}; + +const getActionsInlineEditorStyle = (theme: GrafanaTheme2) => ({ + wrapper: css({ + marginBottom: theme.spacing(2), + display: 'flex', + flexDirection: 'column', + }), + oneClickOverlay: css({ + height: 'auto', + border: `2px dashed ${theme.colors.text.link}`, + fontSize: 10, + color: theme.colors.text.primary, + marginBottom: theme.spacing(1), + }), + oneClickSpan: css({ + padding: 10, + // Negates the padding on the span from moving the underlying link + marginBottom: -10, + display: 'inline-block', + }), + itemWrapper: css({ + padding: '4px 8px 8px 8px', + }), + button: css({ + marginLeft: theme.spacing(1), + }), +}); diff --git a/public/app/features/actions/ActionsListItem.tsx b/public/app/features/actions/ActionsListItem.tsx new file mode 100644 index 00000000000..5d69abfeeb7 --- /dev/null +++ b/public/app/features/actions/ActionsListItem.tsx @@ -0,0 +1,111 @@ +import { css, cx } from '@emotion/css'; +import { Draggable } from '@hello-pangea/dnd'; + +import { Action, DataFrame, GrafanaTheme2 } from '@grafana/data'; +import { Icon } from '@grafana/ui/src/components/Icon/Icon'; +import { IconButton } from '@grafana/ui/src/components/IconButton/IconButton'; +import { useStyles2 } from '@grafana/ui/src/themes'; + +export interface ActionsListItemProps { + index: number; + action: Action; + data: DataFrame[]; + onChange: (index: number, action: Action) => void; + onEdit: () => void; + onRemove: () => void; + isEditing?: boolean; + itemKey: string; +} + +export const ActionListItem = ({ action, onEdit, onRemove, index, itemKey }: ActionsListItemProps) => { + const styles = useStyles2(getActionListItemStyles); + const { title = '' } = action; + + const hasTitle = title.trim() !== ''; + + return ( + + {(provided) => ( + <> +
    +
    +
    + {hasTitle ? title : 'Action title not provided'} +
    +
    +
    + + +
    + +
    +
    +
    + + )} +
    + ); +}; + +const getActionListItemStyles = (theme: GrafanaTheme2) => { + return { + wrapper: css({ + display: 'flex', + flexGrow: 1, + alignItems: 'center', + justifyContent: 'space-between', + width: '100%', + padding: '5px 0 5px 10px', + borderRadius: theme.shape.radius.default, + background: theme.colors.background.secondary, + gap: 8, + }), + linkDetails: css({ + display: 'flex', + flexDirection: 'column', + flexGrow: 1, + }), + errored: css({ + color: theme.colors.error.text, + fontStyle: 'italic', + }), + notConfigured: css({ + fontStyle: 'italic', + }), + title: css({ + color: theme.colors.text.primary, + fontSize: theme.typography.size.sm, + fontWeight: theme.typography.fontWeightMedium, + }), + url: css({ + color: theme.colors.text.secondary, + fontSize: theme.typography.size.sm, + whiteSpace: 'nowrap', + overflow: 'hidden', + textOverflow: 'ellipsis', + maxWidth: `calc(100% - 100px)`, + }), + dragIcon: css({ + cursor: 'grab', + color: theme.colors.text.secondary, + margin: theme.spacing(0, 0.5), + }), + icon: css({ + color: theme.colors.text.secondary, + }), + dragRow: css({ + position: 'relative', + }), + icons: css({ + display: 'flex', + padding: 6, + alignItems: 'center', + gap: 8, + }), + }; +}; diff --git a/public/app/features/actions/ParamsEditor.tsx b/public/app/features/actions/ParamsEditor.tsx new file mode 100644 index 00000000000..46287cadf89 --- /dev/null +++ b/public/app/features/actions/ParamsEditor.tsx @@ -0,0 +1,130 @@ +import { css } from '@emotion/css'; +import { useEffect, useState } from 'react'; + +import { contentTypeOptions, GrafanaTheme2, VariableSuggestion } from '@grafana/data'; +import { IconButton } from '@grafana/ui/src/components/IconButton/IconButton'; +import { Input } from '@grafana/ui/src/components/Input/Input'; +import { Stack } from '@grafana/ui/src/components/Layout/Stack/Stack'; +import { Select } from '@grafana/ui/src/components/Select/Select'; +import { useStyles2 } from '@grafana/ui/src/themes'; + +import { SuggestionsInput } from '../transformers/suggestionsInput/SuggestionsInput'; + +interface Props { + onChange: (v: Array<[string, string]>) => void; + value: Array<[string, string]>; + suggestions: VariableSuggestion[]; + contentTypeHeader?: boolean; +} + +export const ParamsEditor = ({ value, onChange, suggestions, contentTypeHeader = false }: Props) => { + const styles = useStyles2(getStyles); + + const headersContentType = value.find(([key, value]) => key === 'Content-Type'); + + const [paramName, setParamName] = useState(''); + const [paramValue, setParamValue] = useState(''); + const [contentTypeParamValue, setContentTypeParamValue] = useState(''); + + useEffect(() => { + if (contentTypeParamValue !== '') { + setContentTypeParamValue(contentTypeParamValue); + } else if (headersContentType) { + setContentTypeParamValue(headersContentType[1]); + } + }, [contentTypeParamValue, headersContentType]); + + // forces re-init of first SuggestionsInput(s), since they are stateful and don't respond to 'value' prop changes to be able to clear them :( + const [entryKey, setEntryKey] = useState(Math.random().toString()); + + const changeParamValue = (paramValue: string) => { + setParamValue(paramValue); + }; + + const changeParamName = (paramName: string) => { + setParamName(paramName); + }; + + const removeParam = (key: string) => () => { + const updatedParams = value.filter((param) => param[0] !== key); + onChange(updatedParams); + }; + + const addParam = (contentType?: [string, string]) => { + let newParams: Array<[string, string]>; + + if (value) { + newParams = value.filter((e) => e[0] !== (contentType ? contentType[0] : paramName)); + } else { + newParams = []; + } + + newParams.push(contentType ?? [paramName, paramValue]); + newParams.sort((a, b) => a[0].localeCompare(b[0])); + onChange(newParams); + + setParamName(''); + setParamValue(''); + setEntryKey(Math.random().toString()); + }; + + const changeContentTypeParamValue = (value: string) => { + setContentTypeParamValue(value); + addParam(['Content-Type', value]); + }; + + const isAddParamsDisabled = paramName === '' || paramValue === ''; + + return ( +
    + + + + addParam()} disabled={isAddParamsDisabled} /> + + + + {Array.from(value.filter((param) => param[0] !== 'Content-Type') || []).map((entry) => ( + + + + + + ))} + + + {contentTypeHeader && ( +
    + + + { id="search_filter" placeholder={t('ldap-settings-page.search_filter.placeholder', 'example: cn=%s')} type="text" - {...register('settings.config.servers.0.search_filter', { required: true })} + {...register(`${serverConfig}.search_filter`, { required: true })} /> - + ( + onChange(v.map(({ value }) => String(value)))} + /> + )} + > @@ -319,17 +350,34 @@ export const LdapSettingsPage = () => { - - - - + + + + + } + placement="bottom-start" + > + diff --git a/public/app/features/alerting/unified/PanelAlertTabContent.test.tsx b/public/app/features/alerting/unified/PanelAlertTabContent.test.tsx index 339c30c2ecf..0e3b84c1c79 100644 --- a/public/app/features/alerting/unified/PanelAlertTabContent.test.tsx +++ b/public/app/features/alerting/unified/PanelAlertTabContent.test.tsx @@ -3,7 +3,7 @@ import { byTestId, byText } from 'testing-library-selector'; import { DataSourceApi } from '@grafana/data'; import { PromOptions, PrometheusDatasource } from '@grafana/prometheus'; -import { setDataSourceSrv, setPluginExtensionsHook } from '@grafana/runtime'; +import { setDataSourceSrv, setPluginLinksHook } from '@grafana/runtime'; import * as ruleActionButtons from 'app/features/alerting/unified/components/rules/RuleActionsButtons'; import { DashboardModel, PanelModel } from 'app/features/dashboard/state'; import { getDatasourceSrv } from 'app/features/plugins/datasource_srv'; @@ -191,8 +191,8 @@ describe('PanelAlertTabContent', () => { AccessControlAction.AlertingRuleExternalWrite, ]); - setPluginExtensionsHook(() => ({ - extensions: [], + setPluginLinksHook(() => ({ + links: [], isLoading: false, })); diff --git a/public/app/features/alerting/unified/RuleList.test.tsx b/public/app/features/alerting/unified/RuleList.test.tsx index f57adfc8c30..5ed14c39266 100644 --- a/public/app/features/alerting/unified/RuleList.test.tsx +++ b/public/app/features/alerting/unified/RuleList.test.tsx @@ -13,7 +13,7 @@ import { locationService, setAppEvents, setDataSourceSrv, - usePluginLinkExtensions, + usePluginLinks, } from '@grafana/runtime'; import appEvents from 'app/core/app_events'; import * as ruleActionButtons from 'app/features/alerting/unified/components/rules/RuleActionsButtons'; @@ -52,7 +52,7 @@ import { DataSourceType, GRAFANA_RULES_SOURCE_NAME } from './utils/datasource'; jest.mock('@grafana/runtime', () => ({ ...jest.requireActual('@grafana/runtime'), getPluginLinkExtensions: jest.fn(), - usePluginLinkExtensions: jest.fn(), + usePluginLinks: jest.fn(), useReturnToPrevious: jest.fn(), })); jest.mock('./api/buildInfo'); @@ -72,7 +72,7 @@ setupPluginsExtensionsHook(); const mocks = { getAllDataSourcesMock: jest.mocked(config.getAllDataSources), getPluginLinkExtensionsMock: jest.mocked(getPluginLinkExtensions), - usePluginLinkExtensionsMock: jest.mocked(usePluginLinkExtensions), + usePluginLinksMock: jest.mocked(usePluginLinks), rulesInSameGroupHaveInvalidForMock: jest.mocked(actions.rulesInSameGroupHaveInvalidFor), api: { @@ -168,8 +168,8 @@ describe('RuleList', () => { AccessControlAction.AlertingRuleExternalWrite, ]); mocks.rulesInSameGroupHaveInvalidForMock.mockReturnValue([]); - mocks.usePluginLinkExtensionsMock.mockReturnValue({ - extensions: [ + mocks.usePluginLinksMock.mockReturnValue({ + links: [ { pluginId: 'grafana-ml-app', id: '1', @@ -812,6 +812,7 @@ describe('RuleList', () => { expect(ui.exportButton.get()).toBeInTheDocument(); }); }); + describe('Grafana Managed Alerts', () => { it('New alert button should be visible when the user has alert rule create and folder read permissions and no rules exists', async () => { grantUserPermissions([ @@ -828,6 +829,7 @@ describe('RuleList', () => { renderRuleList(); await waitFor(() => expect(mocks.api.fetchRules).toHaveBeenCalledTimes(1)); + expect(ui.newRuleButton.get()).toBeInTheDocument(); }); @@ -903,33 +905,4 @@ describe('RuleList', () => { }); }); }); - - describe('Analytics', () => { - it('Sends log info when creating an alert rule from a scratch', async () => { - grantUserPermissions([ - AccessControlAction.FoldersRead, - AccessControlAction.AlertingRuleCreate, - AccessControlAction.AlertingRuleRead, - ]); - - mocks.getAllDataSourcesMock.mockReturnValue([]); - setDataSourceSrv(new MockDataSourceSrv({})); - mocks.api.fetchRules.mockResolvedValue([]); - mocks.api.fetchRulerRules.mockResolvedValue({}); - - renderRuleList(); - - await waitFor(() => expect(mocks.api.fetchRules).toHaveBeenCalledTimes(1)); - - const button = screen.getByText('New alert rule'); - - button.addEventListener('click', (event) => event.preventDefault(), false); - - expect(button).toBeEnabled(); - - await userEvent.click(button); - - expect(analytics.logInfo).toHaveBeenCalledWith(analytics.LogMessages.alertRuleFromScratch); - }); - }); }); diff --git a/public/app/features/alerting/unified/components/HoverCard.tsx b/public/app/features/alerting/unified/components/HoverCard.tsx index 0572cfa7794..9df1f2a5066 100644 --- a/public/app/features/alerting/unified/components/HoverCard.tsx +++ b/public/app/features/alerting/unified/components/HoverCard.tsx @@ -6,7 +6,7 @@ import { cloneElement, ReactElement, ReactNode, useRef } from 'react'; import { GrafanaTheme2 } from '@grafana/data'; import { Popover as GrafanaPopover, PopoverController, useStyles2, Stack } from '@grafana/ui'; -export interface HoverCardProps { +export interface PopupCardProps { children: ReactElement; header?: ReactNode; content: ReactElement; @@ -16,9 +16,10 @@ export interface HoverCardProps { disabled?: boolean; showAfter?: number; arrow?: boolean; + showOn?: 'click' | 'hover'; } -export const HoverCard = ({ +export const PopupCard = ({ children, header, content, @@ -27,8 +28,9 @@ export const HoverCard = ({ showAfter = 300, wrapperClassName, disabled = false, + showOn = 'hover', ...rest -}: HoverCardProps) => { +}: PopupCardProps) => { const popoverRef = useRef(null); const styles = useStyles2(getStyles); @@ -36,6 +38,9 @@ export const HoverCard = ({ return children; } + const showOnHover = showOn === 'hover'; + const showOnClick = showOn === 'click'; + const body = ( {header &&
    {header}
    } @@ -47,6 +52,21 @@ export const HoverCard = ({ return ( {(showPopper, hidePopper, popperProps) => { + // support hover and click interaction + const onClickProps = { + onClick: showPopper, + }; + + const onHoverProps = { + onMouseLeave: hidePopper, + onMouseEnter: showPopper, + }; + + const blurFocusProps = { + onBlur: hidePopper, + onFocus: showPopper, + }; + return ( <> {popoverRef.current && ( @@ -54,22 +74,24 @@ export const HoverCard = ({ {...popperProps} {...rest} wrapperClassName={classnames(styles.popover, wrapperClassName)} - onMouseLeave={hidePopper} - onMouseEnter={showPopper} - onFocus={showPopper} - onBlur={hidePopper} referenceElement={popoverRef.current} renderArrow={arrow} + // @TODO + // if we want interaction with the content we should not pass blur / focus handlers but then clicking outside doesn't close the popper + {...blurFocusProps} + // if we want hover interaction we have to make sure we add the leave / enter handlers + {...(showOnHover ? onHoverProps : {})} /> )} {cloneElement(children, { ref: popoverRef, - onMouseEnter: showPopper, - onMouseLeave: hidePopper, onFocus: showPopper, onBlur: hidePopper, tabIndex: 0, + // make sure we pass the correct interaction handlers here to the element we want to interact with + ...(showOnHover ? onHoverProps : {}), + ...(showOnClick ? onClickProps : {}), })} ); @@ -83,7 +105,7 @@ const getStyles = (theme: GrafanaTheme2) => ({ borderRadius: theme.shape.radius.default, boxShadow: theme.shadows.z3, background: theme.colors.background.primary, - border: `1px solid ${theme.colors.border.medium}`, + border: `1px solid ${theme.colors.border.weak}`, }), card: { body: css({ diff --git a/public/app/features/alerting/unified/components/Tokenize.tsx b/public/app/features/alerting/unified/components/Tokenize.tsx index 7802f2d3c26..c1ecc6d42a6 100644 --- a/public/app/features/alerting/unified/components/Tokenize.tsx +++ b/public/app/features/alerting/unified/components/Tokenize.tsx @@ -4,7 +4,7 @@ import * as React from 'react'; import { GrafanaTheme2 } from '@grafana/data'; import { Badge, useStyles2 } from '@grafana/ui'; -import { HoverCard } from './HoverCard'; +import { PopupCard } from './HoverCard'; import { keywords as KEYWORDS, builtinFunctions as FUNCTIONS } from './receivers/editor/language'; const VARIABLES = ['$', '.', '"']; @@ -83,7 +83,7 @@ function Token({ content, description, type }: TokenProps) { const disableCard = Boolean(type) === false; return ( - - + ); } diff --git a/public/app/features/alerting/unified/components/contact-points/ContactPoint.tsx b/public/app/features/alerting/unified/components/contact-points/ContactPoint.tsx index dbe35ffa19d..b53b0ee7032 100644 --- a/public/app/features/alerting/unified/components/contact-points/ContactPoint.tsx +++ b/public/app/features/alerting/unified/components/contact-points/ContactPoint.tsx @@ -49,6 +49,13 @@ export const ContactPoint = ({ disabled = false, contactPoint }: ContactPointPro }) } /> + {receivers.length === 0 && ( +
    + + No integrations configured + +
    + )} {showFullMetadata ? (
    {receivers.map((receiver, index) => { @@ -286,4 +293,8 @@ const getStyles = (theme: GrafanaTheme2) => ({ borderBottomLeftRadius: `${theme.shape.radius.default}`, borderBottomRightRadius: `${theme.shape.radius.default}`, }), + noIntegrationsContainer: css({ + paddingTop: `${theme.spacing(1.5)}`, + paddingLeft: `${theme.spacing(1.5)}`, + }), }); 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 6faa94018e0..e1599527caa 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 @@ -19,7 +19,8 @@ import setupMimirFlavoredServer, { MIMIR_DATASOURCE_UID } from './__mocks__/mimi import setupVanillaAlertmanagerFlavoredServer, { VANILLA_ALERTMANAGER_DATASOURCE_UID, } from './__mocks__/vanillaAlertmanagerServer'; -import { ContactPointWithMetadata, RouteReference } from './utils'; +import { RECEIVER_META_KEY } from './constants'; +import { ContactPointWithMetadata, ReceiverConfigWithMetadata, RouteReference } from './utils'; /** * There are lots of ways in which we test our pages and components. Here's my opinionated approach to testing them. @@ -197,6 +198,30 @@ describe('contact points', () => { expect(editAction).toHaveAttribute('aria-disabled', 'true'); }); + it('should show warning when no receivers are configured', async () => { + renderWithProvider(); + + expect(screen.getByText(/No integrations configured/i)).toBeInTheDocument(); + }); + + it('should not show warning when at least one receiver is configured', async () => { + const receiver: ReceiverConfigWithMetadata = { + name: 'email', + provenance: undefined, + type: 'email', + disableResolveMessage: false, + settings: { addresses: 'test1@test.com,test2@test.com,test3@test.com,test4@test.com' }, + [RECEIVER_META_KEY]: { + name: 'Email', + description: 'The email receiver', + }, + }; + renderWithProvider( + + ); + expect(screen.queryByText(/No integrations configured/i)).not.toBeInTheDocument(); + }); + it('should disable buttons when provisioned', async () => { const { user } = renderWithProvider(); diff --git a/public/app/features/alerting/unified/components/contact-points/useContactPoints.tsx b/public/app/features/alerting/unified/components/contact-points/useContactPoints.tsx index ef7bb63bbff..10d56e2ac5f 100644 --- a/public/app/features/alerting/unified/components/contact-points/useContactPoints.tsx +++ b/public/app/features/alerting/unified/components/contact-points/useContactPoints.tsx @@ -4,7 +4,7 @@ */ import { produce } from 'immer'; -import { remove } from 'lodash'; +import { merge, remove, set } from 'lodash'; import { useMemo } from 'react'; import { alertingApi } from 'app/features/alerting/unified/api/alertingApi'; @@ -340,10 +340,45 @@ export function useDeleteContactPoint({ alertmanager }: BaseAlertmanagerArgs) { }; } -const mapIntegrationSettings = (integration: GrafanaManagedReceiverConfig): GrafanaManagedReceiverConfig => { +/** + * Turns a Grafana Managed receiver config into a format that can be sent to the k8s API + * + * When updating secure settings, we need to send a value of `true` for any secure setting that we want to keep the same. + * + * Any other setting that has a value in `secureSettings` will correspond to a new value for that setting - + * so we should not tell the API that we want to preserve it. Those values will instead be sent within `settings` + */ +const mapIntegrationSettingsForK8s = (integration: GrafanaManagedReceiverConfig): GrafanaManagedReceiverConfig => { + const { secureSettings, settings, ...restOfIntegration } = integration; + const secureFields = Object.entries(secureSettings || {}).reduce((acc, [key, value]) => { + // If a secure field has no (changed) value, then we tell the backend to persist it + if (value === undefined) { + return { + ...acc, + [key]: true, + }; + } + return acc; + }, {}); + + const mappedSecureSettings = Object.entries(secureSettings || {}).reduce((acc, [key, value]) => { + // If the value is an empty string/falsy value, then we need to omit it from the payload + // so the backend knows to remove it + if (!value) { + return acc; + } + + // Otherwise, we send the value of the secure field + return set(acc, key, value); + }, {}); + + // Merge settings properly with lodash so we don't lose any information from nested keys/secure settings + const mergedSettings = merge({}, settings, mappedSecureSettings); + return { - ...integration, - settings: { ...integration.settings, ...integration.secureSettings }, + ...restOfIntegration, + secureFields, + settings: mergedSettings, }; }; const grafanaContactPointToK8sReceiver = ( @@ -358,7 +393,7 @@ const grafanaContactPointToK8sReceiver = ( }, spec: { title: contactPoint.name, - integrations: (contactPoint.grafana_managed_receiver_configs || []).map(mapIntegrationSettings), + integrations: (contactPoint.grafana_managed_receiver_configs || []).map(mapIntegrationSettingsForK8s), }, }; }; diff --git a/public/app/features/alerting/unified/components/expressions/Expression.tsx b/public/app/features/alerting/unified/components/expressions/Expression.tsx index c9d7d8e2e17..9110dbbf8fa 100644 --- a/public/app/features/alerting/unified/components/expressions/Expression.tsx +++ b/public/app/features/alerting/unified/components/expressions/Expression.tsx @@ -20,7 +20,7 @@ import { import { AlertQuery, PromAlertingRuleState } from 'app/types/unified-alerting-dto'; import { usePagination } from '../../hooks/usePagination'; -import { HoverCard } from '../HoverCard'; +import { PopupCard } from '../HoverCard'; import { Spacer } from '../Spacer'; import { AlertStateTag } from '../rules/AlertStateTag'; @@ -424,7 +424,7 @@ const TimeseriesRow: FC = ({ frame, index }) => {name}
    - = ({ frame, index }) => } > Time series data - +
    diff --git a/public/app/features/alerting/unified/components/extensions/AlertInstanceExtensionPoint.tsx b/public/app/features/alerting/unified/components/extensions/AlertInstanceExtensionPoint.tsx index ffb42258520..5f64ef13e70 100644 --- a/public/app/features/alerting/unified/components/extensions/AlertInstanceExtensionPoint.tsx +++ b/public/app/features/alerting/unified/components/extensions/AlertInstanceExtensionPoint.tsx @@ -1,7 +1,7 @@ import { ReactElement, useMemo, useState } from 'react'; import { PluginExtensionLink, PluginExtensionPoints } from '@grafana/data'; -import { usePluginLinkExtensions } from '@grafana/runtime'; +import { usePluginLinks } from '@grafana/runtime'; import { Dropdown, IconButton } from '@grafana/ui'; import { ConfirmNavigationModal } from 'app/features/explore/extensions/ConfirmNavigationModal'; import { Alert, CombinedRule } from 'app/types/unified-alerting'; @@ -21,13 +21,13 @@ export const AlertInstanceExtensionPoint = ({ }: AlertInstanceExtensionPointProps): ReactElement | null => { const [selectedExtension, setSelectedExtension] = useState(); const context = useMemo(() => ({ instance, rule }), [instance, rule]); - const { extensions } = usePluginLinkExtensions({ context, extensionPointId, limitPerPlugin: 3 }); + const { links } = usePluginLinks({ context, extensionPointId, limitPerPlugin: 3 }); - if (extensions.length === 0) { + if (links.length === 0) { return null; } - const menu = ; + const menu = ; return ( <> diff --git a/public/app/features/alerting/unified/components/notification-policies/Matchers.tsx b/public/app/features/alerting/unified/components/notification-policies/Matchers.tsx index 914ba8067de..87f81464a00 100644 --- a/public/app/features/alerting/unified/components/notification-policies/Matchers.tsx +++ b/public/app/features/alerting/unified/components/notification-policies/Matchers.tsx @@ -7,7 +7,7 @@ import { getTagColorsFromName, useStyles2, Stack } from '@grafana/ui'; import { ObjectMatcher } from 'app/plugins/datasource/alertmanager/types'; import { MatcherFormatter, matcherFormatter } from '../../utils/matchers'; -import { HoverCard } from '../HoverCard'; +import { PopupCard } from '../HoverCard'; type MatchersProps = { matchers: ObjectMatcher[]; formatter?: MatcherFormatter }; @@ -29,7 +29,7 @@ const Matchers: FC = ({ matchers, formatter = 'default' }) => { ))} {/* TODO hover state to show all matchers we're not showing */} {hasMoreMatchers && ( - = ({ matchers, formatter = 'default' }) => {
    {`and ${rest.length} more`}
    -
    + )}
    diff --git a/public/app/features/alerting/unified/components/notification-policies/Policy.tsx b/public/app/features/alerting/unified/components/notification-policies/Policy.tsx index d90c5307532..0dbbc61072e 100644 --- a/public/app/features/alerting/unified/components/notification-policies/Policy.tsx +++ b/public/app/features/alerting/unified/components/notification-policies/Policy.tsx @@ -42,7 +42,7 @@ import { createContactPointLink, createMuteTimingLink } from '../../utils/misc'; import { InheritableProperties, getInheritedProperties } from '../../utils/notification-policies'; import { InsertPosition } from '../../utils/routeTree'; import { Authorize } from '../Authorize'; -import { HoverCard } from '../HoverCard'; +import { PopupCard } from '../HoverCard'; import { Label } from '../Label'; import { MetaText } from '../MetaText'; import { ProvisioningBadge } from '../Provisioning'; @@ -633,7 +633,7 @@ const ProvisionedTooltip = (children: ReactNode) => ( ); const Errors: FC<{ errors: React.ReactNode[] }> = ({ errors }) => ( - = ({ errors }) => ( - + ); const ContinueMatchingIndicator: FC = () => { @@ -697,7 +697,7 @@ function AutogeneratedRootIndicator() { } const InheritedProperties: FC<{ properties: InheritableProperties }> = ({ properties }) => ( - = ({ proper
    {pluralize('property', Object.keys(properties).length, true)}
    -
    + ); const TimeIntervals: FC<{ timings: string[]; alertManagerSourceName: string }> = ({ @@ -884,7 +884,7 @@ const ContactPointsHoverDetails: FC = ({ const groupedIntegrations = groupBy(details.grafana_managed_receiver_configs, (config) => config.type); return ( - = ({ > {contactPoint} - + ); }; diff --git a/public/app/features/alerting/unified/components/notification-policies/PromDurationInput.tsx b/public/app/features/alerting/unified/components/notification-policies/PromDurationInput.tsx index 697f49531b0..e90c9aba186 100644 --- a/public/app/features/alerting/unified/components/notification-policies/PromDurationInput.tsx +++ b/public/app/features/alerting/unified/components/notification-policies/PromDurationInput.tsx @@ -2,7 +2,7 @@ import { forwardRef } from 'react'; import { Icon, Input } from '@grafana/ui'; -import { HoverCard } from '../HoverCard'; +import { PopupCard } from '../HoverCard'; import { PromDurationDocs } from './PromDurationDocs'; @@ -10,9 +10,9 @@ export const PromDurationInput = forwardRef} disabled={false}> + } disabled={false}> - + } {...props} ref={ref} diff --git a/public/app/features/alerting/unified/components/receivers/TemplateDataDocs.tsx b/public/app/features/alerting/unified/components/receivers/TemplateDataDocs.tsx index cb6caa193b3..fd3db87ce62 100644 --- a/public/app/features/alerting/unified/components/receivers/TemplateDataDocs.tsx +++ b/public/app/features/alerting/unified/components/receivers/TemplateDataDocs.tsx @@ -4,7 +4,7 @@ import * as React from 'react'; import { GrafanaTheme2 } from '@grafana/data'; import { useStyles2, Stack } from '@grafana/ui'; -import { HoverCard } from '../HoverCard'; +import { PopupCard } from '../HoverCard'; import { AlertTemplateData, @@ -35,13 +35,13 @@ export function TemplateDataDocs() { dataItems={GlobalTemplateData} typeRenderer={(type) => type === '[]Alert' ? ( - +
    {type}
    -
    + ) : type === 'KeyValue' ? ( - }> + }>
    {type}
    -
    + ) : ( type ) diff --git a/public/app/features/alerting/unified/components/receivers/form/ChannelOptions.tsx b/public/app/features/alerting/unified/components/receivers/form/ChannelOptions.tsx index eb57f14c55c..1c99524f434 100644 --- a/public/app/features/alerting/unified/components/receivers/form/ChannelOptions.tsx +++ b/public/app/features/alerting/unified/components/receivers/form/ChannelOptions.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; -import { useFormContext, FieldError, FieldErrors, DeepMap } from 'react-hook-form'; +import { DeepMap, FieldError, FieldErrors, useFormContext } from 'react-hook-form'; -import { Button, Field, Input } from '@grafana/ui'; +import { Field, SecretInput } from '@grafana/ui'; import { NotificationChannelOption, NotificationChannelSecureFields } from 'app/types'; import { ChannelValues, ReceiverFormValues } from '../../../types/receiver-form'; @@ -33,6 +33,7 @@ export function ChannelOptions({ }: Props): JSX.Element { const { watch } = useFormContext>(); const currentFormValues = watch(); // react hook form types ARE LYING! + return ( <> {selectedChannelOptions.map((option: NotificationChannelOption, index: number) => { @@ -50,18 +51,8 @@ export function ChannelOptions({ if (secureFields && secureFields[option.propertyName]) { return ( - - onResetSecureField(option.propertyName)} fill="text" type="button" size="sm"> - Clear - - ) - } - /> + + onResetSecureField(option.propertyName)} isConfigured /> ); } @@ -74,6 +65,8 @@ export function ChannelOptions({ return ( ({ return () => subscription.unsubscribe(); }, [selectedType, initialValues, setValue, fieldName, watch]); - const [_secureFields, setSecureFields] = useState(secureFields ?? {}); + const [_secureFields, setSecureFields] = useState>(secureFields ?? {}); const onResetSecureField = (key: string) => { if (_secureFields[key]) { - const updatedSecureFields = { ...secureFields }; - delete updatedSecureFields[key]; + const updatedSecureFields = { ..._secureFields }; + updatedSecureFields[key] = ''; setSecureFields(updatedSecureFields); setValue(`${pathPrefix}.secureFields`, updatedSecureFields); } diff --git a/public/app/features/alerting/unified/components/receivers/form/GrafanaReceiverForm.test.tsx b/public/app/features/alerting/unified/components/receivers/form/GrafanaReceiverForm.test.tsx index 9cbf672221e..23e2d50466f 100644 --- a/public/app/features/alerting/unified/components/receivers/form/GrafanaReceiverForm.test.tsx +++ b/public/app/features/alerting/unified/components/receivers/form/GrafanaReceiverForm.test.tsx @@ -1,8 +1,10 @@ import { clickSelectOption } from 'test/helpers/selectOptionInTest'; -import { render, waitFor } from 'test/test-utils'; +import { render, waitFor, screen } from 'test/test-utils'; import { byLabelText, byRole, byTestId, byText } from 'testing-library-selector'; +import { config } from '@grafana/runtime'; import { disablePlugin } from 'app/features/alerting/unified/mocks/server/configure'; +import { captureRequests } from 'app/features/alerting/unified/mocks/server/events'; import { setOnCallFeatures, setOnCallIntegrations, @@ -33,6 +35,51 @@ const ui = { }; describe('GrafanaReceiverForm', () => { + describe('alertingApiServer', () => { + beforeEach(() => { + config.featureToggles.alertingApiServer = true; + }); + afterEach(() => { + config.featureToggles.alertingApiServer = false; + }); + + it('handles nested secure fields correctly', async () => { + const capturedRequests = captureRequests( + (req) => req.url.includes('/v0alpha1/namespaces/default/receivers') && req.method === 'POST' + ); + const { user } = render(); + const { type, click } = user; + + await waitFor(() => expect(ui.loadingIndicator.query()).not.toBeInTheDocument()); + + // Select MQTT receiver and fill out basic required fields for contact point + await clickSelectOption(await byTestId('items.0.type').find(), 'MQTT'); + await type(screen.getByLabelText(/^name/i), 'mqtt contact point'); + await type(screen.getByLabelText(/broker url/i), 'broker url'); + await type(screen.getByLabelText(/topic/i), 'topic'); + + // Fill out fields that we know will be nested secure fields + await click(screen.getByText(/optional mqtt settings/i)); + await click(screen.getByRole('button', { name: /^Add$/i })); + await type(screen.getByLabelText(/ca certificate/i), 'some cert'); + + await click(screen.getByRole('button', { name: /save contact point/i })); + + const [request] = await capturedRequests; + const postRequestbody = await request.clone().json(); + + const integrationPayload = postRequestbody.spec.integrations[0]; + expect(integrationPayload.settings.tlsConfig).toEqual({ + // Expect the payload to have included the value of a secret field + caCertificate: 'some cert', + // And to not have removed other values (which would happen if we incorrectly merged settings together) + insecureSkipVerify: false, + }); + + expect(postRequestbody).toMatchSnapshot(); + }); + }); + describe('OnCall contact point', () => { it('OnCall contact point should be disabled if OnCall integration is not enabled', async () => { disablePlugin(SupportedPlugin.OnCall); diff --git a/public/app/features/alerting/unified/components/receivers/form/GrafanaReceiverForm.tsx b/public/app/features/alerting/unified/components/receivers/form/GrafanaReceiverForm.tsx index ade39241b34..a7d26ed25cd 100644 --- a/public/app/features/alerting/unified/components/receivers/form/GrafanaReceiverForm.tsx +++ b/public/app/features/alerting/unified/components/receivers/form/GrafanaReceiverForm.tsx @@ -7,7 +7,6 @@ import { useUpdateContactPoint, } from 'app/features/alerting/unified/components/contact-points/useContactPoints'; import { GRAFANA_RULES_SOURCE_NAME } from 'app/features/alerting/unified/utils/datasource'; -import { shouldUseK8sApi } from 'app/features/alerting/unified/utils/k8s/utils'; import { GrafanaManagedContactPoint, GrafanaManagedReceiverConfig, @@ -139,7 +138,6 @@ export const GrafanaReceiverForm = ({ contactPoint, readOnly = false, editMode } return { dto: n }; }); - const disableEditTitle = editMode && shouldUseK8sApi(GRAFANA_RULES_SOURCE_NAME); return ( <> {hasOnCallError && ( @@ -152,7 +150,6 @@ export const GrafanaReceiverForm = ({ contactPoint, readOnly = false, editMode } {contactPoint?.provisioned && } - disableEditTitle={disableEditTitle} isEditable={isEditable} isTestable={isTestable} onSubmit={onSubmit} diff --git a/public/app/features/alerting/unified/components/receivers/form/ReceiverForm.tsx b/public/app/features/alerting/unified/components/receivers/form/ReceiverForm.tsx index 0d1e6b74f15..b1d5d9005a0 100644 --- a/public/app/features/alerting/unified/components/receivers/form/ReceiverForm.tsx +++ b/public/app/features/alerting/unified/components/receivers/form/ReceiverForm.tsx @@ -38,11 +38,6 @@ interface Props { * and that contact point being created will be set as the default? */ showDefaultRouteWarning?: boolean; - /** - * Should we disable the title editing? Required when we're using the API server, - * as at the time of writing this is not possible - */ - disableEditTitle?: boolean; } export function ReceiverForm({ @@ -57,7 +52,6 @@ export function ReceiverForm({ isTestable, customValidators, showDefaultRouteWarning, - disableEditTitle, }: Props) { const notifyApp = useAppNotification(); const styles = useStyles2(getStyles); @@ -93,6 +87,17 @@ export function ReceiverForm({ const { fields, append, remove } = useControlledFieldArray({ name: 'items', formAPI, softDelete: true }); const submitCallback = async (values: ReceiverFormValues) => { + values.items.forEach((item) => { + if (item.secureFields) { + // omit secure fields with boolean value as BE expects not touched fields to be omitted: https://github.com/grafana/grafana/pull/71307 + Object.keys(item.secureFields).forEach((key) => { + if (item.secureFields[key] === true || item.secureFields[key] === false) { + delete item.secureFields[key]; + } + }); + } + }); + try { await onSubmit({ ...values, @@ -127,7 +132,6 @@ export function ReceiverForm({ ; readOnly?: boolean; customValidator?: (value: string) => boolean | string | Promise; + onResetSecureField?: (propertyName: string) => void; + secureFields?: NotificationChannelSecureFields; } export const OptionField: FC = ({ option, + parentOption, invalid, pathPrefix, pathSuffix = '', @@ -33,13 +48,16 @@ export const OptionField: FC = ({ defaultValue, readOnly = false, customValidator, + onResetSecureField, + secureFields = {}, }) => { const optionPath = `${pathPrefix}${pathSuffix}`; - const isSecure = pathSuffix === 'secureSettings.'; if (option.element === 'subform') { return ( = ({ pathPrefix={optionPath} readOnly={readOnly} pathIndex={pathPrefix} + parentOption={parentOption} customValidator={customValidator} - isSecure={isSecure} + onResetSecureField={onResetSecureField} + secureFields={secureFields} /> ); }; -const OptionInput: FC = ({ +const OptionInput: FC = ({ option, invalid, id, @@ -90,18 +110,17 @@ const OptionInput: FC { const styles = useStyles2(getStyles); const { control, register, unregister, getValues, setValue } = useFormContext(); - const name = `${pathPrefix}${option.propertyName}`; - useEffect(() => { - // Remove the value of secure fields so it doesn't show the incorrect value when clearing the field - if (isSecure) { - setValue(name, null); - } - }, [isSecure, name, setValue]); + const name = `${pathPrefix}${option.propertyName}`; + const nestedKey = parentOption ? `${parentOption.propertyName}.${option.propertyName}` : option.propertyName; + + const isEncryptedInput = secureFields?.[nestedKey]; // workaround for https://github.com/react-hook-form/react-hook-form/issues/4993#issuecomment-829012506 useEffect( @@ -138,22 +157,26 @@ const OptionInput: FC - - option.validationRule ? validateOption(v, option.validationRule, option.required) : true, - customValidator: (v) => (customValidator ? customValidator(v) : true), - }, - setValueAs: option.setValueAs, - })} - placeholder={option.placeholder} - /> + {isEncryptedInput ? ( + onResetSecureField?.(nestedKey)} isConfigured /> + ) : ( + + option.validationRule ? validateOption(v, option.validationRule, option.required) : true, + customValidator: (v) => (customValidator ? customValidator(v) : true), + }, + setValueAs: option.setValueAs, + })} + placeholder={option.placeholder} + /> + )} ); @@ -209,17 +232,21 @@ const OptionInput: FC -