diff --git a/.betterer.results b/.betterer.results index 89141673931..a9aa3e106a9 100644 --- a/.betterer.results +++ b/.betterer.results @@ -454,46 +454,6 @@ exports[`better eslint`] = { "packages/grafana-data/test/__mocks__/pluginMocks.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"] ], - "packages/grafana-flamegraph/src/FlameGraph/FlameGraph.tsx: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"] - ], - "packages/grafana-flamegraph/src/FlameGraph/FlameGraphMetadata.tsx: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"] - ], - "packages/grafana-flamegraph/src/FlameGraph/FlameGraphTooltip.tsx: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"] - ], - "packages/grafana-flamegraph/src/FlameGraphHeader.tsx: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"], - [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.", "11"], - [0, 0, 0, "Styles should be written using objects.", "12"] - ], - "packages/grafana-flamegraph/src/TopTable/FlameGraphTopTableContainer.tsx:5381": [ - [0, 0, 0, "Styles should be written using objects.", "0"], - [0, 0, 0, "Styles should be written using objects.", "1"] - ], "packages/grafana-o11y-ds-frontend/src/utils.ts:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"] ], @@ -740,6 +700,9 @@ exports[`better eslint`] = { "packages/grafana-runtime/src/services/pluginExtensions/getPluginExtensions.ts:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"] ], + "packages/grafana-runtime/src/services/pluginExtensions/usePluginExtensions.ts:5381": [ + [0, 0, 0, "Do not use any type assertions.", "0"] + ], "packages/grafana-runtime/src/utils/DataSourceWithBackend.ts:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"], [0, 0, 0, "Unexpected any. Specify a different type.", "1"] @@ -760,21 +723,7 @@ exports[`better eslint`] = { [0, 0, 0, "Do not use any type assertions.", "6"] ], "packages/grafana-sql/src/components/query-editor-raw/QueryToolbox.tsx:5381": [ - [0, 0, 0, "\'HorizontalGroup\' import from \'@grafana/ui\' is restricted from being used by a pattern. Use Stack component instead.", "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"] - ], - "packages/grafana-sql/src/components/query-editor-raw/QueryValidator.tsx: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"] - ], - "packages/grafana-sql/src/components/query-editor-raw/RawEditor.tsx:5381": [ - [0, 0, 0, "Styles should be written using objects.", "0"], - [0, 0, 0, "Styles should be written using objects.", "1"] + [0, 0, 0, "\'HorizontalGroup\' import from \'@grafana/ui\' is restricted from being used by a pattern. Use Stack component instead.", "0"] ], "packages/grafana-sql/src/components/visual-query-builder/AwesomeQueryBuilder.tsx:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"] @@ -1242,10 +1191,6 @@ exports[`better eslint`] = { [0, 0, 0, "Styles should be written using objects.", "16"], [0, 0, 0, "Styles should be written using objects.", "17"] ], - "public/app/core/components/connectWithCleanUp.tsx:5381": [ - [0, 0, 0, "Unexpected any. Specify a different type.", "0"], - [0, 0, 0, "Do not use any type assertions.", "1"] - ], "public/app/core/config.ts:5381": [ [0, 0, 0, "Do not re-export imported variable (\`config\`)", "0"] ], @@ -1396,11 +1341,8 @@ exports[`better eslint`] = { [0, 0, 0, "Styles should be written using objects.", "0"], [0, 0, 0, "Styles should be written using objects.", "1"] ], - "public/app/features/admin/UserListPublicDashboardPage/UserListPublicDashboardPage.tsx:5381": [ - [0, 0, 0, "\'HorizontalGroup\' import from \'@grafana/ui/src\' is restricted from being used by a pattern. Use Stack component instead.", "0"] - ], "public/app/features/admin/UserOrgs.tsx:5381": [ - [0, 0, 0, "\'HorizontalGroup\' import from \'@grafana/ui\' is restricted from being used by a pattern. Use Stack component instead.", "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"], @@ -1416,8 +1358,7 @@ exports[`better eslint`] = { [0, 0, 0, "Styles should be written using objects.", "13"], [0, 0, 0, "Styles should be written using objects.", "14"], [0, 0, 0, "Styles should be written using objects.", "15"], - [0, 0, 0, "Styles should be written using objects.", "16"], - [0, 0, 0, "Styles should be written using objects.", "17"] + [0, 0, 0, "Styles should be written using objects.", "16"] ], "public/app/features/admin/UserPermissions.tsx:5381": [ [0, 0, 0, "Styles should be written using objects.", "0"] @@ -1749,9 +1690,6 @@ exports[`better eslint`] = { [0, 0, 0, "Styles should be written using objects.", "16"], [0, 0, 0, "Styles should be written using objects.", "17"] ], - "public/app/features/alerting/unified/components/receivers/GlobalConfigForm.tsx:5381": [ - [0, 0, 0, "\'HorizontalGroup\' import from \'@grafana/ui\' is restricted from being used by a pattern. Use Stack component instead.", "0"] - ], "public/app/features/alerting/unified/components/receivers/PayloadEditor.tsx:5381": [ [0, 0, 0, "Styles should be written using objects.", "0"], [0, 0, 0, "Styles should be written using objects.", "1"], @@ -1930,9 +1868,8 @@ exports[`better eslint`] = { [0, 0, 0, "Styles should be written using objects.", "1"] ], "public/app/features/alerting/unified/components/rule-editor/PreviewRule.tsx:5381": [ - [0, 0, 0, "\'HorizontalGroup\' import from \'@grafana/ui\' is restricted from being used by a pattern. Use Stack component instead.", "0"], - [0, 0, 0, "Unexpected any. Specify a different type.", "1"], - [0, 0, 0, "Styles should be written using objects.", "2"] + [0, 0, 0, "Unexpected any. Specify a different type.", "0"], + [0, 0, 0, "Styles should be written using objects.", "1"] ], "public/app/features/alerting/unified/components/rule-editor/PreviewRuleResult.tsx:5381": [ [0, 0, 0, "Styles should be written using objects.", "0"], @@ -1972,9 +1909,6 @@ exports[`better eslint`] = { [0, 0, 0, "Styles should be written using objects.", "0"], [0, 0, 0, "Styles should be written using objects.", "1"] ], - "public/app/features/alerting/unified/components/rule-editor/alert-rule-form/AlertRuleForm.tsx:5381": [ - [0, 0, 0, "\'HorizontalGroup\' import from \'@grafana/ui\' is restricted from being used by a pattern. Use Stack component instead.", "0"] - ], "public/app/features/alerting/unified/components/rule-editor/notificaton-preview/NotificationPolicyMatchers.tsx:5381": [ [0, 0, 0, "Styles should be written using objects.", "0"], [0, 0, 0, "Styles should be written using objects.", "1"] @@ -2065,9 +1999,6 @@ exports[`better eslint`] = { [0, 0, 0, "Styles should be written using objects.", "2"], [0, 0, 0, "Styles should be written using objects.", "3"] ], - "public/app/features/alerting/unified/components/rules/MultipleDataSourcePicker.tsx:5381": [ - [0, 0, 0, "\'HorizontalGroup\' import from \'@grafana/ui\' is restricted from being used by a pattern. Use Stack component instead.", "0"] - ], "public/app/features/alerting/unified/components/rules/NoRulesCTA.tsx:5381": [ [0, 0, 0, "Styles should be written using objects.", "0"] ], @@ -2080,22 +2011,10 @@ exports[`better eslint`] = { [0, 0, 0, "Styles should be written using objects.", "5"], [0, 0, 0, "Styles should be written using objects.", "6"] ], - "public/app/features/alerting/unified/components/rules/RuleActionsButtons.tsx:5381": [ - [0, 0, 0, "Styles should be written using objects.", "0"] - ], "public/app/features/alerting/unified/components/rules/RuleConfigStatus.tsx:5381": [ [0, 0, 0, "Styles should be written using objects.", "0"], [0, 0, 0, "Styles should be written using objects.", "1"] ], - "public/app/features/alerting/unified/components/rules/RuleDetails.tsx: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"] - ], - "public/app/features/alerting/unified/components/rules/RuleDetailsActionButtons.tsx:5381": [ - [0, 0, 0, "\'HorizontalGroup\' import from \'@grafana/ui\' is restricted from being used by a pattern. Use Stack component instead.", "0"], - [0, 0, 0, "Styles should be written using objects.", "1"] - ], "public/app/features/alerting/unified/components/rules/RuleDetailsAnnotations.tsx:5381": [ [0, 0, 0, "Styles should be written using objects.", "0"] ], @@ -2129,7 +2048,7 @@ exports[`better eslint`] = { [0, 0, 0, "Styles should be written using objects.", "0"] ], "public/app/features/alerting/unified/components/rules/RulesGroup.tsx:5381": [ - [0, 0, 0, "\'HorizontalGroup\' import from \'@grafana/ui\' is restricted from being used by a pattern. Use Stack component instead.", "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"], @@ -2140,8 +2059,7 @@ exports[`better eslint`] = { [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.", "11"], - [0, 0, 0, "Styles should be written using objects.", "12"] + [0, 0, 0, "Styles should be written using objects.", "11"] ], "public/app/features/alerting/unified/components/rules/RulesTable.tsx:5381": [ [0, 0, 0, "Styles should be written using objects.", "0"], @@ -2214,13 +2132,6 @@ exports[`better eslint`] = { [0, 0, 0, "Styles should be written using objects.", "3"], [0, 0, 0, "Styles should be written using objects.", "4"] ], - "public/app/features/alerting/unified/components/silences/SilencesTable.tsx: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"] - ], "public/app/features/alerting/unified/hooks/useAlertmanagerConfig.ts:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"] ], @@ -2304,9 +2215,8 @@ exports[`better eslint`] = { [0, 0, 0, "Unexpected any. Specify a different type.", "2"] ], "public/app/features/api-keys/ApiKeysTable.tsx:5381": [ - [0, 0, 0, "\'HorizontalGroup\' import from \'@grafana/ui\' is restricted from being used by a pattern. Use Stack component instead.", "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/features/api-keys/MigrateToServiceAccountsCard.tsx:5381": [ [0, 0, 0, "Styles should be written using objects.", "0"], @@ -2432,9 +2342,6 @@ exports[`better eslint`] = { [0, 0, 0, "Styles should be written using objects.", "0"], [0, 0, 0, "Styles should be written using objects.", "1"] ], - "public/app/features/correlations/Forms/CorrelationFormNavigation.tsx:5381": [ - [0, 0, 0, "\'HorizontalGroup\' import from \'@grafana/ui\' is restricted from being used by a pattern. Use Stack component instead.", "0"] - ], "public/app/features/correlations/components/Wizard/index.ts:5381": [ [0, 0, 0, "Do not use export all (\`export * from ...\`)", "0"], [0, 0, 0, "Do not use export all (\`export * from ...\`)", "1"] @@ -2445,9 +2352,6 @@ exports[`better eslint`] = { "public/app/features/dashboard-scene/inspect/HelpWizard/utils.ts:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"] ], - "public/app/features/dashboard-scene/inspect/InspectJsonTab.tsx:5381": [ - [0, 0, 0, "Use data-testid for E2E selectors instead of aria-label", "0"] - ], "public/app/features/dashboard-scene/pages/DashboardScenePage.tsx:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"], [0, 0, 0, "Unexpected any. Specify a different type.", "1"], @@ -2464,8 +2368,9 @@ exports[`better eslint`] = { [0, 0, 0, "Unexpected any. Specify a different type.", "1"] ], "public/app/features/dashboard-scene/saving/getDashboardChanges.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, "Unexpected any. Specify a different type.", "0"], + [0, 0, 0, "Do not use any type assertions.", "1"], + [0, 0, 0, "Do not use any type assertions.", "2"] ], "public/app/features/dashboard-scene/scene/PanelMenuBehavior.tsx:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"], @@ -2513,16 +2418,10 @@ exports[`better eslint`] = { "public/app/features/dashboard-scene/settings/annotations/AnnotationSettingsEdit.tsx:5381": [ [0, 0, 0, "\'HorizontalGroup\' import from \'@grafana/ui\' is restricted from being used by a pattern. Use Stack component instead.", "0"] ], - "public/app/features/dashboard-scene/settings/annotations/AnnotationSettingsList.tsx:5381": [ - [0, 0, 0, "\'VerticalGroup\' import from \'@grafana/ui\' is restricted from being used by a pattern. Use Stack component instead.", "0"] - ], "public/app/features/dashboard-scene/settings/annotations/index.tsx:5381": [ [0, 0, 0, "Do not re-export imported variable (\`./AnnotationSettingsEdit\`)", "0"], [0, 0, 0, "Do not re-export imported variable (\`./AnnotationSettingsList\`)", "1"] ], - "public/app/features/dashboard-scene/settings/links/DashboardLinkList.tsx:5381": [ - [0, 0, 0, "\'HorizontalGroup\' import from \'@grafana/ui\' is restricted from being used by a pattern. Use Stack component instead.", "0"] - ], "public/app/features/dashboard-scene/settings/variables/components/SelectionOptionsForm.tsx:5381": [ [0, 0, 0, "\'VerticalGroup\' import from \'@grafana/ui\' is restricted from being used by a pattern. Use Stack component instead.", "0"] ], @@ -2551,22 +2450,8 @@ exports[`better eslint`] = { "public/app/features/dashboard/components/AddLibraryPanelWidget/index.ts:5381": [ [0, 0, 0, "Do not re-export imported variable (\`./AddLibraryPanelWidget\`)", "0"] ], - "public/app/features/dashboard/components/AddWidgetModal/AddWidgetModal.tsx: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"] - ], "public/app/features/dashboard/components/AnnotationSettings/AnnotationSettingsEdit.tsx:5381": [ - [0, 0, 0, "\'HorizontalGroup\' import from \'@grafana/ui\' is restricted from being used by a pattern. Use Stack component instead.", "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, "Use data-testid for E2E selectors instead of aria-label", "3"], - [0, 0, 0, "Use data-testid for E2E selectors instead of aria-label", "4"], - [0, 0, 0, "Styles should be written using objects.", "5"] - ], - "public/app/features/dashboard/components/AnnotationSettings/AnnotationSettingsList.tsx:5381": [ - [0, 0, 0, "\'VerticalGroup\' import from \'@grafana/ui\' is restricted from being used by a pattern. Use Stack component instead.", "0"], - [0, 0, 0, "Styles should be written using objects.", "1"] + [0, 0, 0, "\'HorizontalGroup\' import from \'@grafana/ui\' is restricted from being used by a pattern. Use Stack component instead.", "0"] ], "public/app/features/dashboard/components/AnnotationSettings/index.tsx:5381": [ [0, 0, 0, "Do not re-export imported variable (\`./AnnotationSettingsEdit\`)", "0"], @@ -2595,20 +2480,12 @@ exports[`better eslint`] = { "public/app/features/dashboard/components/DashExportModal/index.ts:5381": [ [0, 0, 0, "Do not re-export imported variable (\`./DashboardExporter\`)", "0"] ], - "public/app/features/dashboard/components/DashNav/DashNavButton.tsx:5381": [ - [0, 0, 0, "Styles should be written using objects.", "0"] - ], "public/app/features/dashboard/components/DashNav/index.ts:5381": [ [0, 0, 0, "Do not re-export imported variable (\`DashNav\`)", "0"] ], - "public/app/features/dashboard/components/DashboardLoading/DashboardFailed.tsx:5381": [ - [0, 0, 0, "Styles should be written using objects.", "0"] - ], "public/app/features/dashboard/components/DashboardLoading/DashboardLoading.tsx:5381": [ [0, 0, 0, "\'HorizontalGroup\' import from \'@grafana/ui\' is restricted from being used by a pattern. Use Stack component instead.", "0"], - [0, 0, 0, "\'VerticalGroup\' import from \'@grafana/ui\' is restricted from being used by a pattern. Use Stack component instead.", "1"], - [0, 0, 0, "Styles should be written using objects.", "2"], - [0, 0, 0, "Styles should be written using objects.", "3"] + [0, 0, 0, "\'VerticalGroup\' import from \'@grafana/ui\' is restricted from being used by a pattern. Use Stack component instead.", "1"] ], "public/app/features/dashboard/components/DashboardPrompt/DashboardPrompt.test.tsx:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"] @@ -2630,23 +2507,14 @@ exports[`better eslint`] = { "public/app/features/dashboard/components/DashboardRow/index.ts:5381": [ [0, 0, 0, "Do not re-export imported variable (\`./DashboardRow\`)", "0"] ], - "public/app/features/dashboard/components/DashboardSettings/ListNewButton.tsx:5381": [ - [0, 0, 0, "Styles should be written using objects.", "0"] - ], "public/app/features/dashboard/components/DashboardSettings/VersionsSettings.tsx:5381": [ [0, 0, 0, "\'HorizontalGroup\' import from \'@grafana/ui\' is restricted from being used by a pattern. Use Stack component instead.", "0"] ], "public/app/features/dashboard/components/DashboardSettings/index.ts:5381": [ [0, 0, 0, "Do not re-export imported variable (\`./DashboardSettings\`)", "0"] ], - "public/app/features/dashboard/components/DeleteDashboard/DeleteDashboardModal.tsx:5381": [ - [0, 0, 0, "Styles should be written using objects.", "0"] - ], "public/app/features/dashboard/components/HelpWizard/HelpWizard.tsx:5381": [ - [0, 0, 0, "\'HorizontalGroup\' import from \'@grafana/ui\' is restricted from being used by a pattern. Use Stack component instead.", "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, "\'HorizontalGroup\' import from \'@grafana/ui\' is restricted from being used by a pattern. Use Stack component instead.", "0"] ], "public/app/features/dashboard/components/Inspector/PanelInspector.tsx:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"] @@ -2656,89 +2524,26 @@ exports[`better eslint`] = { [0, 0, 0, "Do not re-export imported variable (\`./LinkSettingsList\`)", "1"] ], "public/app/features/dashboard/components/PanelEditor/DynamicConfigValueEditor.tsx:5381": [ - [0, 0, 0, "\'HorizontalGroup\' import from \'@grafana/ui\' is restricted from being used by a pattern. Use Stack component instead.", "0"], - [0, 0, 0, "Styles should be written using objects.", "1"], - [0, 0, 0, "Styles should be written using objects.", "2"] - ], - "public/app/features/dashboard/components/PanelEditor/OptionsPane.tsx:5381": [ - [0, 0, 0, "Use data-testid for E2E selectors instead of aria-label", "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"] - ], - "public/app/features/dashboard/components/PanelEditor/OptionsPaneCategory.tsx:5381": [ - [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, "\'HorizontalGroup\' import from \'@grafana/ui\' is restricted from being used by a pattern. Use Stack component instead.", "0"] ], "public/app/features/dashboard/components/PanelEditor/OptionsPaneItemDescriptor.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", "1"], - [0, 0, 0, "Styles should be written using objects.", "2"] - ], - "public/app/features/dashboard/components/PanelEditor/OptionsPaneOptions.tsx: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"], - [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, "Use data-testid for E2E selectors instead of aria-label", "1"] ], "public/app/features/dashboard/components/PanelEditor/OverrideCategoryTitle.tsx:5381": [ [0, 0, 0, "\'HorizontalGroup\' import from \'@grafana/ui\' is restricted from being used by a pattern. Use Stack component instead.", "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.", "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"] + [0, 0, 0, "Unexpected any. Specify a different type.", "1"] ], "public/app/features/dashboard/components/PanelEditor/PanelEditor.tsx:5381": [ [0, 0, 0, "\'HorizontalGroup\' import from \'@grafana/ui\' is restricted from being used by a pattern. Use Stack component instead.", "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, "Use data-testid for E2E selectors instead of aria-label", "3"], - [0, 0, 0, "Do not use any type assertions.", "4"], - [0, 0, 0, "Styles should be written using objects.", "5"], - [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.", "11"], - [0, 0, 0, "Styles should be written using objects.", "12"], - [0, 0, 0, "Styles should be written using objects.", "13"], - [0, 0, 0, "Styles should be written using objects.", "14"] - ], - "public/app/features/dashboard/components/PanelEditor/PanelEditorTabs.tsx: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, "Do not use any type assertions.", "1"] ], "public/app/features/dashboard/components/PanelEditor/PanelNotSupported.tsx:5381": [ [0, 0, 0, "\'VerticalGroup\' import from \'@grafana/ui\' is restricted from being used by a pattern. Use Stack component instead.", "0"], [0, 0, 0, "\'Layout\' import from \'@grafana/ui/src/components/Layout/Layout\' is restricted from being used by a pattern. Use Stack component instead.", "1"] ], - "public/app/features/dashboard/components/PanelEditor/VisualizationButton.tsx:5381": [ - [0, 0, 0, "Styles should be written using objects.", "0"], - [0, 0, 0, "Styles should be written using objects.", "1"] - ], "public/app/features/dashboard/components/PanelEditor/VisualizationSelectPane.tsx:5381": [ - [0, 0, 0, "Use data-testid for E2E selectors instead of aria-label", "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"], - [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, "Use data-testid for E2E selectors instead of aria-label", "0"] ], "public/app/features/dashboard/components/PanelEditor/getFieldOverrideElements.tsx:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"] @@ -2756,27 +2561,10 @@ exports[`better eslint`] = { [0, 0, 0, "Do not use any type assertions.", "4"], [0, 0, 0, "Unexpected any. Specify a different type.", "5"] ], - "public/app/features/dashboard/components/PublicDashboardNotAvailable/PublicDashboardNotAvailable.tsx: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"] - ], - "public/app/features/dashboard/components/RowOptions/RowOptionsModal.tsx:5381": [ - [0, 0, 0, "Styles should be written using objects.", "0"] - ], "public/app/features/dashboard/components/SaveDashboard/SaveDashboardButton.tsx:5381": [ [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"] ], - "public/app/features/dashboard/components/SaveDashboard/SaveDashboardErrorProxy.tsx: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"] - ], - "public/app/features/dashboard/components/SaveDashboard/UnsavedChangesModal.tsx:5381": [ - [0, 0, 0, "Styles should be written using objects.", "0"] - ], "public/app/features/dashboard/components/SaveDashboard/forms/SaveDashboardAsForm.test.tsx:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"] ], @@ -2784,12 +2572,10 @@ exports[`better eslint`] = { [0, 0, 0, "Unexpected any. Specify a different type.", "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, "Use data-testid for E2E selectors instead of aria-label", "3"], - [0, 0, 0, "Styles should be written using objects.", "4"] + [0, 0, 0, "Use data-testid for E2E selectors instead of aria-label", "3"] ], "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"], - [0, 0, 0, "Styles should be written using objects.", "1"] + [0, 0, 0, "\'HorizontalGroup\' import from \'@grafana/ui\' is restricted from being used by a pattern. Use Stack component instead.", "0"] ], "public/app/features/dashboard/components/SaveDashboard/types.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"] @@ -2802,70 +2588,26 @@ exports[`better eslint`] = { ], "public/app/features/dashboard/components/ShareModal/SharePublicDashboard/ConfigPublicDashboard/ConfigPublicDashboard.tsx:5381": [ [0, 0, 0, "\'HorizontalGroup\' import from \'@grafana/ui/src\' is restricted from being used by a pattern. Use Stack component instead.", "0"], - [0, 0, 0, "\'Layout\' import from \'@grafana/ui/src/components/Layout/Layout\' is restricted from being used by a pattern. Use Stack component instead.", "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, "\'Layout\' import from \'@grafana/ui/src/components/Layout/Layout\' is restricted from being used by a pattern. Use Stack component instead.", "1"] ], "public/app/features/dashboard/components/ShareModal/SharePublicDashboard/ConfigPublicDashboard/Configuration.tsx:5381": [ [0, 0, 0, "\'VerticalGroup\' import from \'@grafana/ui/src\' is restricted from being used by a pattern. Use Stack component instead.", "0"], [0, 0, 0, "\'Layout\' import from \'@grafana/ui/src/components/Layout/Layout\' is restricted from being used by a pattern. Use Stack component instead.", "1"] ], - "public/app/features/dashboard/components/ShareModal/SharePublicDashboard/ConfigPublicDashboard/EmailSharingConfiguration.tsx: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"] - ], - "public/app/features/dashboard/components/ShareModal/SharePublicDashboard/ConfigPublicDashboard/SettingsSummary.tsx:5381": [ - [0, 0, 0, "Styles should be written using objects.", "0"] - ], "public/app/features/dashboard/components/ShareModal/SharePublicDashboard/CreatePublicDashboard/AcknowledgeCheckboxes.tsx:5381": [ [0, 0, 0, "\'HorizontalGroup\' import from \'@grafana/ui/src\' is restricted from being used by a pattern. Use Stack component instead.", "0"], - [0, 0, 0, "\'VerticalGroup\' import from \'@grafana/ui/src\' is restricted from being used by a pattern. Use Stack component instead.", "1"], - [0, 0, 0, "Styles should be written using objects.", "2"] - ], - "public/app/features/dashboard/components/ShareModal/SharePublicDashboard/CreatePublicDashboard/CreatePublicDashboard.tsx: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"] - ], - "public/app/features/dashboard/components/ShareModal/SharePublicDashboard/ModalAlerts/UnsupportedDataSourcesAlert.tsx:5381": [ - [0, 0, 0, "Styles should be written using objects.", "0"] + [0, 0, 0, "\'VerticalGroup\' import from \'@grafana/ui/src\' is restricted from being used by a pattern. Use Stack component instead.", "1"] ], "public/app/features/dashboard/components/ShareModal/index.ts:5381": [ [0, 0, 0, "Do not re-export imported variable (\`./ShareModal\`)", "0"], [0, 0, 0, "Do not use export all (\`export * from ...\`)", "1"] ], - "public/app/features/dashboard/components/SubMenu/AnnotationPicker.tsx:5381": [ - [0, 0, 0, "Styles should be written using objects.", "0"], - [0, 0, 0, "Styles should be written using objects.", "1"] - ], "public/app/features/dashboard/components/SubMenu/DashboardLinksDashboard.tsx:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"], [0, 0, 0, "Unexpected any. Specify a different type.", "1"] ], - "public/app/features/dashboard/components/SubMenu/SubMenu.tsx:5381": [ - [0, 0, 0, "Styles should be written using objects.", "0"], - [0, 0, 0, "Styles should be written using objects.", "1"] - ], "public/app/features/dashboard/components/TransformationsEditor/TransformationEditor.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.", "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"], - [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, "Unexpected any. Specify a different type.", "0"] ], "public/app/features/dashboard/components/TransformationsEditor/TransformationPicker.tsx:5381": [ [0, 0, 0, "\'VerticalGroup\' import from \'@grafana/ui\' is restricted from being used by a pattern. Use Stack component instead.", "0"] @@ -3229,9 +2971,6 @@ exports[`better eslint`] = { "public/app/features/explore/ContentOutline/ContentOutline.tsx:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"] ], - "public/app/features/explore/CorrelationEditorModeBar.tsx:5381": [ - [0, 0, 0, "\'HorizontalGroup\' import from \'@grafana/ui\' is restricted from being used by a pattern. Use Stack component instead.", "0"] - ], "public/app/features/explore/Logs/LiveLogs.tsx:5381": [ [0, 0, 0, "Styles should be written using objects.", "0"], [0, 0, 0, "Styles should be written using objects.", "1"], @@ -3663,9 +3402,6 @@ 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/explore/extensions/ConfirmNavigationModal.tsx:5381": [ - [0, 0, 0, "\'VerticalGroup\' import from \'@grafana/ui\' is restricted from being used by a pattern. Use Stack component instead.", "0"] - ], "public/app/features/explore/hooks/useStateSync/index.ts:5381": [ [0, 0, 0, "Do not re-export imported variable (\`./external.utils\`)", "0"] ], @@ -3732,9 +3468,6 @@ exports[`better eslint`] = { "public/app/features/inspector/InspectDataTab.tsx:5381": [ [0, 0, 0, "Use data-testid for E2E selectors instead of aria-label", "0"] ], - "public/app/features/inspector/InspectJSONTab.tsx:5381": [ - [0, 0, 0, "Use data-testid for E2E selectors instead of aria-label", "0"] - ], "public/app/features/inspector/InspectStatsTab.tsx:5381": [ [0, 0, 0, "Use data-testid for E2E selectors instead of aria-label", "0"], [0, 0, 0, "Styles should be written using objects.", "1"] @@ -3923,7 +3656,7 @@ exports[`better eslint`] = { [0, 0, 0, "Styles should be written using objects.", "1"] ], "public/app/features/manage-dashboards/components/PublicDashboardListTable/PublicDashboardListTable.tsx:5381": [ - [0, 0, 0, "\'HorizontalGroup\' import from \'@grafana/ui/src\' is restricted from being used by a pattern. Use Stack component instead.", "0"], + [0, 0, 0, "\'HorizontalGroup\' import from \'@grafana/ui\' is restricted from being used by a pattern. Use Stack component instead.", "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"], @@ -4116,6 +3849,9 @@ exports[`better eslint`] = { [0, 0, 0, "Unexpected any. Specify a different type.", "11"], [0, 0, 0, "Do not use any type assertions.", "12"] ], + "public/app/features/plugins/extensions/getPluginExtensions.test.tsx:5381": [ + [0, 0, 0, "Unexpected any. Specify a different type.", "0"] + ], "public/app/features/plugins/loader/sharedDependencies.ts:5381": [ [0, 0, 0, "* import is invalid because \'Layout,HorizontalGroup,VerticalGroup\' from \'@grafana/ui\' is restricted from being used by a pattern. Use Stack component instead.", "0"] ], @@ -4256,9 +3992,6 @@ exports[`better eslint`] = { "public/app/features/search/utils.ts:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"] ], - "public/app/features/serviceaccounts/ServiceAccountPage.tsx:5381": [ - [0, 0, 0, "\'HorizontalGroup\' import from \'@grafana/ui\' is restricted from being used by a pattern. Use Stack component instead.", "0"] - ], "public/app/features/serviceaccounts/components/ServiceAccountProfile.tsx:5381": [ [0, 0, 0, "Styles should be written using objects.", "0"] ], @@ -4297,19 +4030,16 @@ exports[`better eslint`] = { [0, 0, 0, "Styles should be written using objects.", "4"] ], "public/app/features/storage/RootView.tsx:5381": [ - [0, 0, 0, "\'HorizontalGroup\' import from \'@grafana/ui\' is restricted from being used by a pattern. Use Stack component instead.", "0"], - [0, 0, 0, "\'VerticalGroup\' import from \'@grafana/ui\' is restricted from being used by a pattern. Use Stack component instead.", "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.", "0"], + [0, 0, 0, "Styles should be written using objects.", "1"] ], "public/app/features/storage/StoragePage.tsx:5381": [ - [0, 0, 0, "\'HorizontalGroup\' import from \'@grafana/ui\' is restricted from being used by a pattern. Use Stack component instead.", "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"], [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"] + [0, 0, 0, "Styles should be written using objects.", "5"] ], "public/app/features/storage/UploadButton.tsx:5381": [ [0, 0, 0, "Styles should be written using objects.", "0"] @@ -5412,12 +5142,6 @@ exports[`better eslint`] = { "public/app/plugins/datasource/influxdb/migrations.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"] ], - "public/app/plugins/datasource/influxdb/mocks.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"], - [0, 0, 0, "Do not use any type assertions.", "3"] - ], "public/app/plugins/datasource/influxdb/query_part.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"], [0, 0, 0, "Unexpected any. Specify a different type.", "1"], @@ -5486,7 +5210,7 @@ exports[`better eslint`] = { [0, 0, 0, "Styles should be written using objects.", "10"] ], "public/app/plugins/datasource/loki/components/LokiLabelBrowser.tsx:5381": [ - [0, 0, 0, "\'HorizontalGroup\' import from \'@grafana/ui\' is restricted from being used by a pattern. Use Stack component instead.", "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"], @@ -5499,8 +5223,7 @@ exports[`better eslint`] = { [0, 0, 0, "Styles should be written using objects.", "10"], [0, 0, 0, "Styles should be written using objects.", "11"], [0, 0, 0, "Styles should be written using objects.", "12"], - [0, 0, 0, "Styles should be written using objects.", "13"], - [0, 0, 0, "Styles should be written using objects.", "14"] + [0, 0, 0, "Styles should be written using objects.", "13"] ], "public/app/plugins/datasource/loki/components/monaco-query-field/MonacoQueryField.tsx:5381": [ [0, 0, 0, "Styles should be written using objects.", "0"], @@ -5537,10 +5260,9 @@ exports[`better eslint`] = { [0, 0, 0, "Do not use any type assertions.", "0"] ], "public/app/plugins/datasource/loki/querybuilder/components/LokiQueryCodeEditor.tsx:5381": [ - [0, 0, 0, "\'HorizontalGroup\' import from \'@grafana/ui\' is restricted from being used by a pattern. Use Stack component instead.", "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"] + [0, 0, 0, "Styles should be written using objects.", "2"] ], "public/app/plugins/datasource/loki/querybuilder/components/QueryPattern.tsx:5381": [ [0, 0, 0, "Styles should be written using objects.", "0"], @@ -6409,9 +6131,6 @@ exports[`no gf-form usage`] = { "e2e/utils/flows/addDataSource.ts:5381": [ [0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"] ], - "packages/grafana-e2e/src/flows/addDataSource.ts:5381": [ - [0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"] - ], "packages/grafana-prometheus/src/components/PromExploreExtraField.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.results.json b/.betterer.results.json index 5b11f01cfca..7ee6947ecb6 100644 --- a/.betterer.results.json +++ b/.betterer.results.json @@ -7005,10 +7005,6 @@ "path": "/e2e/utils/flows/addDataSource.ts", "count": 1 }, - { - "path": "/packages/grafana-e2e/src/flows/addDataSource.ts", - "count": 1 - }, { "path": "/packages/grafana-prometheus/src/components/PromExploreExtraField.tsx", "count": 4 diff --git a/.betterer.ts b/.betterer.ts index 44267d58a5a..82e1a09cfe3 100644 --- a/.betterer.ts +++ b/.betterer.ts @@ -7,7 +7,6 @@ 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 = [ - 'packages/grafana-e2e', // deprecated. '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 diff --git a/.bingo/Variables.mk b/.bingo/Variables.mk index c6a5d32a428..0a476ee23a0 100644 --- a/.bingo/Variables.mk +++ b/.bingo/Variables.mk @@ -1,4 +1,4 @@ -# Auto generated binary variables helper managed by https://github.com/bwplotka/bingo v0.8. DO NOT EDIT. +# Auto generated binary variables helper managed by https://github.com/bwplotka/bingo v0.9. DO NOT EDIT. # All tools are designed to be build inside $GOBIN. BINGO_DIR := $(dir $(lastword $(MAKEFILE_LIST))) GOPATH ?= $(shell go env GOPATH) @@ -53,9 +53,11 @@ $(LEFTHOOK): $(BINGO_DIR)/lefthook.mod @echo "(re)installing $(GOBIN)/lefthook-v1.4.8" @cd $(BINGO_DIR) && GOWORK=off $(GO) build -mod=mod -modfile=lefthook.mod -o=$(GOBIN)/lefthook-v1.4.8 "github.com/evilmartians/lefthook" -SWAGGER := $(GOBIN)/swagger-v0.30.2 +# swagger 0.30.5 isn't compatibile with go 1.22 yet so pinning to a specific commit until there's a new release +# https://github.com/go-swagger/go-swagger/issues/3070 +SWAGGER := $(GOBIN)/swagger-db51e79a0e37c572d8b59ae0c58bf2bbbbe53285 $(SWAGGER): $(BINGO_DIR)/swagger.mod @# Install binary/ries using Go 1.14+ build command. This is using bwplotka/bingo-controlled, separate go module with pinned dependencies. - @echo "(re)installing $(GOBIN)/swagger-v0.30.2" - @cd $(BINGO_DIR) && GOWORK=off $(GO) build -mod=mod -modfile=swagger.mod -o=$(GOBIN)/swagger-v0.30.2 "github.com/go-swagger/go-swagger/cmd/swagger" + @echo "(re)installing $(GOBIN)/swagger-db51e79a0e37c572d8b59ae0c58bf2bbbbe53285" + @cd $(BINGO_DIR) && GOWORK=off $(GO) build -mod=mod -modfile=swagger.mod -o=$(GOBIN)/swagger-db51e79a0e37c572d8b59ae0c58bf2bbbbe53285 "github.com/go-swagger/go-swagger/cmd/swagger" diff --git a/.bingo/swagger.mod b/.bingo/swagger.mod index 95c38246f20..ef5b4b6e2c6 100644 --- a/.bingo/swagger.mod +++ b/.bingo/swagger.mod @@ -2,4 +2,4 @@ module _ // Auto generated by https://github.com/bwplotka/bingo. DO NOT EDIT go 1.18 -require github.com/go-swagger/go-swagger v0.30.2 // cmd/swagger +require github.com/go-swagger/go-swagger v0.30.6-0.20240310114303-db51e79a0e37 // cmd/swagger diff --git a/.bingo/swagger.sum b/.bingo/swagger.sum index a03ffd86341..0dc152e6e87 100644 --- a/.bingo/swagger.sum +++ b/.bingo/swagger.sum @@ -42,8 +42,13 @@ github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJ github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= github.com/Masterminds/semver/v3 v3.1.1 h1:hLg3sBzpNErnxhQtUy/mmLR2I9foDujNK030IGemrRc= github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= +github.com/Masterminds/semver/v3 v3.2.0/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ= +github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0= +github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ= github.com/Masterminds/sprig/v3 v3.2.2 h1:17jRggJu518dr3QaafizSXOjKYp94wKfABxUmyxvxX8= github.com/Masterminds/sprig/v3 v3.2.2/go.mod h1:UoaO7Yp8KlPnJIYWTFkMaqPUYKTfGFPhxNuwnnxkKlk= +github.com/Masterminds/sprig/v3 v3.2.3 h1:eL2fZNezLomi0uOLqjQoN6BfsDD+fyLtgbJMAj9n6YA= +github.com/Masterminds/sprig/v3 v3.2.3/go.mod h1:rXcFaZ2zZbLRJv/xSysmlgIM1u11eBaRMhvYXJNkGuM= github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI= github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M= @@ -51,6 +56,8 @@ github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdko github.com/asaskevich/govalidator v0.0.0-20200907205600-7a23bdc65eef/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d h1:Byv0BzEl3/e6D5CLfI0j/7hiIEtvGVFPCZ7Ei2oq8iQ= github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= +github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so= +github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= @@ -73,9 +80,13 @@ github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSw github.com/felixge/httpsnoop v1.0.2 h1:+nS9g82KMXccJ/wp0zyRW9ZBHFETmMGtkk+2CTTrW4o= github.com/felixge/httpsnoop v1.0.3 h1:s/nj+GCswXYzN5v2DpNMuMQYe+0DDwt5WVCU6CWBdXk= 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= github.com/fsnotify/fsnotify v1.5.1 h1:mZcQUHVQUQWoPXXtuf9yuEXKudkV2sx1E06UadKWpgI= github.com/fsnotify/fsnotify v1.5.4 h1:jRbGcIw6P2Meqdwuo0H1p6JVLbL5DHKAKlYndzMwVZI= github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU= +github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= +github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= @@ -83,49 +94,71 @@ github.com/go-openapi/analysis v0.21.2 h1:hXFrOYFHUAMQdu6zwAiKKJHJQ8kqZs1ux/ru1P github.com/go-openapi/analysis v0.21.2/go.mod h1:HZwRk4RRisyG8vx2Oe6aqeSQcoxRp47Xkp3+K6q+LdY= github.com/go-openapi/analysis v0.21.4 h1:ZDFLvSNxpDaomuCueM0BlSXxpANBlFYiBvr+GXrvIHc= github.com/go-openapi/analysis v0.21.4/go.mod h1:4zQ35W4neeZTqh3ol0rv/O8JBbka9QyAgQRPp9y3pfo= +github.com/go-openapi/analysis v0.23.0 h1:aGday7OWupfMs+LbmLZG4k0MYXIANxcuBTYUC03zFCU= +github.com/go-openapi/analysis v0.23.0/go.mod h1:9mz9ZWaSlV8TvjQHLl2mUW2PbZtemkE8yA5v22ohupo= github.com/go-openapi/errors v0.19.8/go.mod h1:cM//ZKUKyO06HSwqAelJ5NsEMMcpa6VpXe8DOa1Mi1M= github.com/go-openapi/errors v0.19.9/go.mod h1:cM//ZKUKyO06HSwqAelJ5NsEMMcpa6VpXe8DOa1Mi1M= github.com/go-openapi/errors v0.20.2 h1:dxy7PGTqEh94zj2E3h1cUmQQWiM1+aeCROfAr02EmK8= github.com/go-openapi/errors v0.20.2/go.mod h1:cM//ZKUKyO06HSwqAelJ5NsEMMcpa6VpXe8DOa1Mi1M= github.com/go-openapi/errors v0.20.3 h1:rz6kiC84sqNQoqrtulzaL/VERgkoCyB6WdEkc2ujzUc= github.com/go-openapi/errors v0.20.3/go.mod h1:Z3FlZ4I8jEGxjUK+bugx3on2mIAk4txuAOhlsB1FSgk= +github.com/go-openapi/errors v0.22.0 h1:c4xY/OLxUBSTiepAg3j/MHuAv5mJhnf53LLMWFB+u/w= +github.com/go-openapi/errors v0.22.0/go.mod h1:J3DmZScxCDufmIMsdOuDHxJbdOGC0xtUynjIx092vXE= github.com/go-openapi/inflect v0.19.0 h1:9jCH9scKIbHeV9m12SmPilScz6krDxKRasNNSNPXu/4= github.com/go-openapi/inflect v0.19.0/go.mod h1:lHpZVlpIQqLyKwJ4N+YSc9hchQy/i12fJykb83CRBH4= +github.com/go-openapi/inflect v0.21.0 h1:FoBjBTQEcbg2cJUWX6uwL9OyIW8eqc9k4KhN4lfbeYk= +github.com/go-openapi/inflect v0.21.0/go.mod h1:INezMuUu7SJQc2AyR3WO0DqqYUJSj8Kb4hBd7WtjlAw= github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY= github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +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.19.6 h1:UBIxjkht+AWIgYzCDSv2GN+E/togfwXUJFRTWhl2Jjs= github.com/go-openapi/jsonreference v0.19.6/go.mod h1:diGHMEHg2IqXZGKxqyvWdfWU/aim5Dprw5bqpKkTvns= github.com/go-openapi/jsonreference v0.20.0 h1:MYlu0sBgChmCfJxxUKZ8g1cPWFOB37YSZqewK7OKeyA= github.com/go-openapi/jsonreference v0.20.0/go.mod h1:Ag74Ico3lPc+zR+qjn4XBUmXymS4zJbYVCZmcgkasdo= +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/loads v0.21.0 h1:jYtUO4wwP7psAweisP/MDoOpdzsYEESdoPcsWjHDR68= github.com/go-openapi/loads v0.21.1/go.mod h1:/DtAMXXneXFjbQMGEtbamCZb+4x7eGwkvZCvBmwUG+g= github.com/go-openapi/loads v0.21.2 h1:r2a/xFIYeZ4Qd2TnGpWDIQNcP80dIaZgf704za8enro= github.com/go-openapi/loads v0.21.2/go.mod h1:Jq58Os6SSGz0rzh62ptiu8Z31I+OTHqmULx5e/gJbNw= +github.com/go-openapi/loads v0.22.0 h1:ECPGd4jX1U6NApCGG1We+uEozOAvXvJSF4nnwHZ8Aco= +github.com/go-openapi/loads v0.22.0/go.mod h1:yLsaTCS92mnSAZX5WWoxszLj0u+Ojl+Zs5Stn1oF+rs= github.com/go-openapi/runtime v0.21.1 h1:/KIG00BzA2x2HRStX2tnhbqbQdPcFlkgsYCiNY20FZs= github.com/go-openapi/runtime v0.24.1 h1:Sml5cgQKGYQHF+M7yYSHaH1eOjvTykrddTE/KtQVjqo= github.com/go-openapi/runtime v0.24.1/go.mod h1:AKurw9fNre+h3ELZfk6ILsfvPN+bvvlaU/M9q/r9hpk= +github.com/go-openapi/runtime v0.28.0 h1:gpPPmWSNGo214l6n8hzdXYhPuJcGtziTOgUpvsFWGIQ= +github.com/go-openapi/runtime v0.28.0/go.mod h1:QN7OzcS+XuYmkQLw05akXk0jRH/eZ3kb18+1KwW9gyc= github.com/go-openapi/spec v0.20.4 h1:O8hJrt0UMnhHcluhIdUgCLRWyM2x7QkBXRvOs7m+O1M= github.com/go-openapi/spec v0.20.4/go.mod h1:faYFR1CvsJZ0mNsmsphTMSoRrNV3TEDoAM7FOEWeq8I= github.com/go-openapi/spec v0.20.6/go.mod h1:2OpW+JddWPrpXSCIX8eOx7lZ5iyuWj3RYR6VaaBKcWA= github.com/go-openapi/spec v0.20.7 h1:1Rlu/ZrOCCob0n+JKKJAWhNWMPW8bOZRg8FJaY+0SKI= github.com/go-openapi/spec v0.20.7/go.mod h1:2OpW+JddWPrpXSCIX8eOx7lZ5iyuWj3RYR6VaaBKcWA= +github.com/go-openapi/spec v0.21.0 h1:LTVzPc3p/RzRnkQqLRndbAzjY0d0BCL72A6j3CdL9ZY= +github.com/go-openapi/spec v0.21.0/go.mod h1:78u6VdPw81XU44qEWGhtr982gJ5BWg2c0I5XwVMotYk= github.com/go-openapi/strfmt v0.21.0/go.mod h1:ZRQ409bWMj+SOgXofQAGTIo2Ebu72Gs+WaRADcS5iNg= github.com/go-openapi/strfmt v0.21.1 h1:G6s2t5V5kGCHLVbSdZ/6lI8Wm4OzoPFkc3/cjAsKQrM= github.com/go-openapi/strfmt v0.21.1/go.mod h1:I/XVKeLc5+MM5oPNN7P6urMOpuLXEcNrCX/rPGuWb0k= github.com/go-openapi/strfmt v0.21.2/go.mod h1:I/XVKeLc5+MM5oPNN7P6urMOpuLXEcNrCX/rPGuWb0k= github.com/go-openapi/strfmt v0.21.3 h1:xwhj5X6CjXEZZHMWy1zKJxvW9AfHC9pkyUjLvHtKG7o= github.com/go-openapi/strfmt v0.21.3/go.mod h1:k+RzNO0Da+k3FrrynSNN8F7n/peCmQQqbbXjtDfvmGg= +github.com/go-openapi/strfmt v0.23.0 h1:nlUS6BCqcnAk0pyhi9Y+kdDVZdZMHfEKQiS4HaMgO/c= +github.com/go-openapi/strfmt v0.23.0/go.mod h1:NrtIpfKtWIygRkKVsxh7XQMDQW5HKQl6S5ik2elW+K4= github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM= github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= github.com/go-openapi/swag v0.21.1/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= github.com/go-openapi/swag v0.22.3 h1:yMBqmnQ0gyZvEb/+KzuWZOXgllrXT4SADYbvDaXHv/g= github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= +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/go-openapi/validate v0.20.3 h1:GZPPhhKSZrE8HjB4eEkoYAZmoWA4+tCemSgINH1/vKw= github.com/go-openapi/validate v0.21.0/go.mod h1:rjnrwK57VJ7A8xqfpAOEKRH8yQSGUriMu5/zuPSQ1hg= github.com/go-openapi/validate v0.22.0 h1:b0QecH6VslW/TxtpKgzpO1SNG7GU2FsaqKdP1E2T50Y= github.com/go-openapi/validate v0.22.0/go.mod h1:rjnrwK57VJ7A8xqfpAOEKRH8yQSGUriMu5/zuPSQ1hg= +github.com/go-openapi/validate v0.24.0 h1:LdfDKwNbpB6Vn40xhTdNZAnfLECL81w+VX3BumrGD58= +github.com/go-openapi/validate v0.24.0/go.mod h1:iyeX1sEufmv3nPbBdX3ieNviWnOZaJ1+zquzJEf2BAQ= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-stack/stack v1.8.1 h1:ntEHSVwIt7PNXNpgPmVfMrNhLtgjlmnZha2kOpuRiDw= github.com/go-stack/stack v1.8.1/go.mod h1:dcoOX6HbPZSZptuspn9bctJ+N/CnF5gGygcUP3XYfe4= @@ -133,6 +166,10 @@ github.com/go-swagger/go-swagger v0.29.0 h1:z3YoZtLvS1Y8TE/PCat1VypcZxM0IgKLt0Nv github.com/go-swagger/go-swagger v0.29.0/go.mod h1:Z4GJzI+bHKKkGB2Ji1rawpi3/ldXX8CkzGIa9HAC5EE= github.com/go-swagger/go-swagger v0.30.2 h1:23odPUyQZdkNFZZSBJ3mqYYcdh+LnuReEbdWN18OMRo= github.com/go-swagger/go-swagger v0.30.2/go.mod h1:neDPes8r8PCz2JPvHRDj8BTULLh4VJUt7n6MpQqxhHM= +github.com/go-swagger/go-swagger v0.30.6-0.20240310114303-db51e79a0e37 h1:KFcZmKdZmapAog2+eL1buervAYrYolBZk7fMecPPDmo= +github.com/go-swagger/go-swagger v0.30.6-0.20240310114303-db51e79a0e37/go.mod h1:i1/E+d8iPNReSE7y04FaVu5OPKB3il5cn+T1Egogg3I= +github.com/go-swagger/go-swagger v0.30.6-0.20240418033037-c46c303aaa02 h1:J6YiT/eg3gAfKMdVCkWXe6khsO+nxa8W4URZ4AUqzbA= +github.com/go-swagger/go-swagger v0.30.6-0.20240418033037-c46c303aaa02/go.mod h1:i1/E+d8iPNReSE7y04FaVu5OPKB3il5cn+T1Egogg3I= github.com/gobuffalo/attrs v0.0.0-20190224210810-a9411de4debd/go.mod h1:4duuawTqi2wkkpB4ePgWMaai6/Kc6WEz83bhFwpHzj0= github.com/gobuffalo/depgen v0.0.0-20190329151759-d478694a28d3/go.mod h1:3STtPUQYuzV0gBVOY3vy6CfMm/ljR4pABfrTeHNLHUY= github.com/gobuffalo/depgen v0.1.0/go.mod h1:+ifsuy7fhi15RWncXQQKjWS9JPkdah5sZvtHc2RXGlg= @@ -211,11 +248,15 @@ github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm4 github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.2 h1:EVhdT+1Kseyi1/pUmXKaFxYsDNy9RQYkMWRH68J/W7Y= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= 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/handlers v1.5.2 h1:cLTUSsNkgcwhgRqvCNmdbRWG0A3N4F+M2nWKdScwyEE= +github.com/gorilla/handlers v1.5.2/go.mod h1:dX+xVpaxdSw+q0Qek8SSsl3dfMk3jNddUkMzo0GtH0w= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= @@ -223,11 +264,16 @@ github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T github.com/huandu/xstrings v1.3.1/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= github.com/huandu/xstrings v1.3.2 h1:L18LIDzqlW6xN2rEkpdV8+oL/IXWJ1APd+vsdYy4Wdw= github.com/huandu/xstrings v1.3.2/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= +github.com/huandu/xstrings v1.3.3/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= +github.com/huandu/xstrings v1.4.0 h1:D17IlohoQq4UcpqD7fDk80P7l+lwAmlFaBHgOipl2FU= +github.com/huandu/xstrings v1.4.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU= github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= +github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4= +github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/jessevdk/go-flags v1.5.0 h1:1jKYvbxEjfUl0fmqTCOfonvskHHXMjBySTLW4y9LFvc= github.com/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4= @@ -246,6 +292,8 @@ github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= @@ -253,6 +301,8 @@ github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/magiconair/properties v1.8.5 h1:b6kJs+EmPFMYGkow9GiUyCyOvIwYetYJ3fSaWak/Gls= github.com/magiconair/properties v1.8.6 h1:5ibWZ6iY0NctNGWo87LalDlEZ6R41TqbbDamhfG/Qzo= github.com/magiconair/properties v1.8.6/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= +github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= +github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= @@ -283,6 +333,9 @@ github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3v github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= github.com/pelletier/go-toml/v2 v2.0.1 h1:8e3L2cCQzLFi2CR4g7vGFuFxX7Jl1kKX8gW+iV0GUKU= github.com/pelletier/go-toml/v2 v2.0.1/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZOjgMj2KwnJFUo= +github.com/pelletier/go-toml/v2 v2.1.1 h1:LWAJwfNvjQZCFIDKWYQaM62NcYeYViCmWIwmOStowAI= +github.com/pelletier/go-toml/v2 v2.1.1/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= @@ -297,18 +350,32 @@ github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTE github.com/rogpeppe/go-internal v1.8.1 h1:geMPLpDpQOgVyCg5z5GoRwLHepNdb71NXb67XFkP+Eg= github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +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/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ= +github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4= +github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= +github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ= github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= +github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8= +github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= github.com/sirupsen/logrus v1.4.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= +github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= github.com/spf13/afero v1.8.0 h1:5MmtuhAgYeU6qpa7w7bP0dv6MBYuup0vekhSpSkoq60= github.com/spf13/afero v1.8.2 h1:xehSyVa0YnHWsJ49JFljMpg1HX19V6NDZ1fkm1Xznbo= github.com/spf13/afero v1.8.2/go.mod h1:CtAatgMJh6bJEIs48Ay/FOnkljP3WeGUG0MC1RfAqwo= +github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= +github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/spf13/cast v1.4.1 h1:s0hze+J0196ZfEMTs80N7UlFt0BDuQ7Q+JDnHiMWKdA= github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w= github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU= +github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0= +github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= @@ -318,9 +385,12 @@ github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An github.com/spf13/viper v1.10.1 h1:nuJZuYpG7gTj/XqiUwg8bA0cp1+M2mC3J4g5luUYBKk= github.com/spf13/viper v1.12.0 h1:CZ7eSOd3kZoaYDLbXnmzgQI5RlciuXBMA+18HwHRfZQ= github.com/spf13/viper v1.12.0/go.mod h1:b6COn30jlNxbm/V2IqWiNWkJ+vZNiMNksliPCiuKtSI= +github.com/spf13/viper v1.18.2 h1:LUXCnvUvSM6FXAsj6nnfc8Q2tp1dIgUfY9Kc8GsSOiQ= +github.com/spf13/viper v1.18.2/go.mod h1:EKmWIqdnk5lOcmR72yw6hS+8OPYcwD0jteitLMVB+yk= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= @@ -329,9 +399,12 @@ github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s= github.com/subosito/gotenv v1.3.0 h1:mjC+YW8QpAdXibNi+vNWgzmgBH4+5l5dCXv8cNysBLI= github.com/subosito/gotenv v1.3.0/go.mod h1:YzJjq/33h7nrwdY+iHMhEOEEbW0ovIz0tB6t6PwAXzs= +github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= +github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= github.com/toqueteos/webbrowser v1.2.0 h1:tVP/gpK69Fx+qMJKsLE7TD8LuGWPnEV71wBN9rrstGQ= github.com/toqueteos/webbrowser v1.2.0/go.mod h1:XWoZq4cyp9WeUeak7w7LXRUQf1F1ATJMir8RTqb4ayM= @@ -345,6 +418,7 @@ github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= go.mongodb.org/mongo-driver v1.7.3/go.mod h1:NqaYOwnXWr5Pm7AOpO5QFxKJ503nbMse/R79oO62zWg= go.mongodb.org/mongo-driver v1.7.5/go.mod h1:VXEWRZ6URJIkUq2SCAyapmhH0ZLRBP+FT4xhp5Zvxng= go.mongodb.org/mongo-driver v1.8.2 h1:8ssUXufb90ujcIvR6MyE1SchaNj0SFxsakiZgxIyrMk= @@ -352,12 +426,16 @@ go.mongodb.org/mongo-driver v1.8.3/go.mod h1:0sQWfOeY63QTntERDJJ/0SuKK0T1uVSgKCu go.mongodb.org/mongo-driver v1.10.0/go.mod h1:wsihk0Kdgv8Kqu1Anit4sfK+22vSFbUrAVEYRhCXrA8= go.mongodb.org/mongo-driver v1.10.1 h1:NujsPveKwHaWuKUer/ceo9DzEe7HIj1SlJ6uvXZG0S4= go.mongodb.org/mongo-driver v1.10.1/go.mod h1:z4XpeoU6w+9Vht+jAFyLgVrD+jGSQQe0+CBWFHNiHt8= +go.mongodb.org/mongo-driver v1.14.0 h1:P98w8egYRjYe3XDjxhYJagTokP/H6HzlsnojRgZRd80= +go.mongodb.org/mongo-driver v1.14.0/go.mod h1:Vzb0Mk/pa7e6cWw85R4F/endUC3u0U9jGcNU603k65c= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190422162423-af44ce270edf/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= @@ -369,10 +447,14 @@ golang.org/x/crypto v0.0.0-20200414173820-0848c9571904/go.mod h1:LzIPMQfyMNhhGPh golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20201216223049-8b5274cf687f/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90 h1:Y/gsMcFOcR+6S6f3YeMKl5g+dZMEWqcz5Czj/GWYbkM= golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= +golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA= +golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -383,6 +465,8 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0 golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= +golang.org/x/exp v0.0.0-20240222234643-814bf88cf225 h1:LfspQV/FYTatPTr/3HzIcmiUFH7PGP+OQ6mgDYo3yuQ= +golang.org/x/exp v0.0.0-20240222234643-814bf88cf225/go.mod h1:CxmFvTBINI24O/j8iY7H1xHzx2i4OsyguNBmN/uPtqc= 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= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= @@ -409,6 +493,8 @@ golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.5.1 h1:OJxoQ/rynoF0dcCdI7cLPktw/hR2cueqYfjm43oqK38= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 h1:6zppjxzCulZykYSLyVDYbneBfbaBIQPYMevg0bEwv2s= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.16.0 h1:QX4fJ0Rr5cPQCF7O9lh9Se4pmwfwskqZfq5moyldzic= +golang.org/x/mod v0.16.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= 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= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -444,6 +530,8 @@ golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1 golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220121210141-e204ce36a2ba h1:6u6sik+bn/y7vILcYkK3iwTBWN7WtBvB0+SZswQnbf8= golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -465,6 +553,9 @@ golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ= +golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -511,11 +602,17 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220114195835-da31bd327af9 h1:XfKQ4OlFl8okEOr5UvAqFRVj8pY/4yfcXrddB8qAbU0= golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220829200755-d48e67d00261 h1:v6hYoSR9T5oet+pMXwUWkbiVqx/63mlHjefrHmxwfeY= golang.org/x/sys v0.0.0-20220829200755-d48e67d00261/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= +golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -526,6 +623,9 @@ golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -583,6 +683,8 @@ golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/tools v0.1.8 h1:P1HhGGuLW4aAclzjtmJdf0mJOjVUZUzOTqkAkWL+l6w= golang.org/x/tools v0.1.12 h1:VveCTK38A2rkS8ZqFY25HIDFscX5X9OoEhJd3quQmXU= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.19.0 h1:tfGCXNR1OsFG+sVdLAitlpjAvD/I6dHDKnYrpEZUHkw= +golang.org/x/tools v0.19.0/go.mod h1:qoJWxmGSIBmAeriMx19ogtrEPrGtDbPK634QFIcLAhc= 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= @@ -683,6 +785,8 @@ gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/ini.v1 v1.66.3 h1:jRskFVxYaMGAMUbN0UZ7niA9gzL9B49DOqE78vg0k3w= gopkg.in/ini.v1 v1.66.4 h1:SsAcf+mM7mRZo2nJNGt8mZCjG8ZRaNGMURJw7BsIST4= gopkg.in/ini.v1 v1.66.4/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= +gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/.bingo/variables.env b/.bingo/variables.env index c48e144db22..ab73dbb12d1 100644 --- a/.bingo/variables.env +++ b/.bingo/variables.env @@ -1,4 +1,4 @@ -# Auto generated binary variables helper managed by https://github.com/bwplotka/bingo v0.8. DO NOT EDIT. +# Auto generated binary variables helper managed by https://github.com/bwplotka/bingo v0.9. DO NOT EDIT. # All tools are designed to be build inside $GOBIN. # Those variables will work only until 'bingo get' was invoked, or if tools were installed via Makefile's Variables.mk. GOBIN=${GOBIN:=$(go env GOBIN)} @@ -20,5 +20,5 @@ JB="${GOBIN}/jb-v0.5.1" LEFTHOOK="${GOBIN}/lefthook-v1.4.8" -SWAGGER="${GOBIN}/swagger-v0.30.2" +SWAGGER="${GOBIN}/swagger-v0.30.6-0.20240310114303-db51e79a0e37" diff --git a/.drone.yml b/.drone.yml index b6399fe0c64..3ea9437dc00 100644 --- a/.drone.yml +++ b/.drone.yml @@ -25,7 +25,7 @@ steps: depends_on: [] environment: CGO_ENABLED: 0 - image: golang:1.21.9-alpine + image: golang:1.22.2-alpine name: compile-build-cmd - commands: - ./bin/build verify-drone @@ -76,14 +76,14 @@ steps: depends_on: [] environment: CGO_ENABLED: 0 - image: golang:1.21.9-alpine + image: golang:1.22.2-alpine name: compile-build-cmd - commands: - go install github.com/bazelbuild/buildtools/buildifier@latest - buildifier --lint=warn -mode=check -r . depends_on: - compile-build-cmd - image: golang:1.21.9-alpine + image: golang:1.22.2-alpine name: lint-starlark trigger: event: @@ -323,7 +323,7 @@ steps: - apk add --update make - CODEGEN_VERIFY=1 make gen-cue depends_on: [] - image: golang:1.21.9-alpine + image: golang:1.22.2-alpine name: verify-gen-cue - commands: - '# It is required that generated jsonnet is committed and in sync with its inputs.' @@ -332,14 +332,14 @@ steps: - apk add --update make - CODEGEN_VERIFY=1 make gen-jsonnet depends_on: [] - image: golang:1.21.9-alpine + image: golang:1.22.2-alpine name: verify-gen-jsonnet - commands: - apk add --update make - make gen-go depends_on: - verify-gen-cue - image: golang:1.21.9-alpine + image: golang:1.22.2-alpine name: wire-install - commands: - apk add --update build-base shared-mime-info shared-mime-info-lang @@ -347,7 +347,7 @@ steps: -timeout=5m depends_on: - wire-install - image: golang:1.21.9-alpine + image: golang:1.22.2-alpine name: test-backend - commands: - apk add --update build-base @@ -356,7 +356,7 @@ steps: | grep -o '\(.*\)/' | sort -u) depends_on: - wire-install - image: golang:1.21.9-alpine + image: golang:1.22.2-alpine name: test-backend-integration trigger: event: @@ -408,7 +408,7 @@ steps: depends_on: [] environment: CGO_ENABLED: 0 - image: golang:1.21.9-alpine + image: golang:1.22.2-alpine name: compile-build-cmd - commands: - apk add --update curl jq bash @@ -435,7 +435,7 @@ steps: - apk add --update make - make gen-go depends_on: [] - image: golang:1.21.9-alpine + image: golang:1.22.2-alpine name: wire-install - commands: - apk add --update make build-base @@ -444,16 +444,16 @@ steps: - wire-install environment: CGO_ENABLED: "1" - image: golang:1.21.9-alpine + image: golang:1.22.2-alpine name: lint-backend - commands: - go run scripts/modowners/modowners.go check go.mod - image: golang:1.21.9-alpine + image: golang:1.22.2-alpine name: validate-modfile - commands: - apk add --update make - make swagger-validate - image: golang:1.21.9-alpine + image: golang:1.22.2-alpine name: validate-openapi-spec trigger: event: @@ -512,7 +512,7 @@ steps: depends_on: [] environment: CGO_ENABLED: 0 - image: golang:1.21.9-alpine + image: golang:1.22.2-alpine name: compile-build-cmd - commands: - '# It is required that code generated from Thema/CUE be committed and in sync @@ -522,7 +522,7 @@ steps: - apk add --update make - CODEGEN_VERIFY=1 make gen-cue depends_on: [] - image: golang:1.21.9-alpine + image: golang:1.22.2-alpine name: verify-gen-cue - commands: - '# It is required that generated jsonnet is committed and in sync with its inputs.' @@ -531,14 +531,14 @@ steps: - apk add --update make - CODEGEN_VERIFY=1 make gen-jsonnet depends_on: [] - image: golang:1.21.9-alpine + image: golang:1.22.2-alpine name: verify-gen-jsonnet - commands: - apk add --update make - make gen-go depends_on: - verify-gen-cue - image: golang:1.21.9-alpine + image: golang:1.22.2-alpine name: wire-install - commands: - apk add --update g++ make python3 && ln -sf /usr/bin/python3 /usr/bin/python @@ -572,7 +572,7 @@ steps: from_secret: drone_token - commands: - /src/grafana-build artifacts -a targz:grafana:linux/amd64 -a targz:grafana:linux/arm64 - -a targz:grafana:linux/arm/v7 --go-version=1.21.9 --yarn-cache=$$YARN_CACHE_FOLDER + -a targz:grafana:linux/arm/v7 --go-version=1.22.2 --yarn-cache=$$YARN_CACHE_FOLDER --build-id=$$DRONE_BUILD_NUMBER --grafana-dir=$$PWD > packages.txt depends_on: - yarn-install @@ -776,7 +776,7 @@ steps: - /src/grafana-build artifacts -a docker:grafana:linux/amd64 -a docker:grafana:linux/amd64:ubuntu -a docker:grafana:linux/arm64 -a docker:grafana:linux/arm64:ubuntu -a docker:grafana:linux/arm/v7 -a docker:grafana:linux/arm/v7:ubuntu --yarn-cache=$$YARN_CACHE_FOLDER --build-id=$$DRONE_BUILD_NUMBER - --go-version=1.21.9 --ubuntu-base=ubuntu:22.04 --alpine-base=alpine:3.19.1 --tag-format='{{ + --go-version=1.22.2 --ubuntu-base=ubuntu:22.04 --alpine-base=alpine:3.19.1 --tag-format='{{ .version_base }}-{{ .buildID }}-{{ .arch }}' --grafana-dir=$$PWD --ubuntu-tag-format='{{ .version_base }}-{{ .buildID }}-ubuntu-{{ .arch }}' > docker.txt - find ./dist -name '*docker*.tar.gz' -type f | xargs -n1 docker load -i @@ -920,7 +920,7 @@ steps: depends_on: [] environment: CGO_ENABLED: 0 - image: golang:1.21.9-alpine + image: golang:1.22.2-alpine name: compile-build-cmd - commands: - echo $DRONE_RUNNER_NAME @@ -934,7 +934,7 @@ steps: - apk add --update make - CODEGEN_VERIFY=1 make gen-cue depends_on: [] - image: golang:1.21.9-alpine + image: golang:1.22.2-alpine name: verify-gen-cue - commands: - '# It is required that generated jsonnet is committed and in sync with its inputs.' @@ -943,14 +943,14 @@ steps: - apk add --update make - CODEGEN_VERIFY=1 make gen-jsonnet depends_on: [] - image: golang:1.21.9-alpine + image: golang:1.22.2-alpine name: verify-gen-jsonnet - commands: - apk add --update make - make gen-go depends_on: - verify-gen-cue - image: golang:1.21.9-alpine + image: golang:1.22.2-alpine name: wire-install - commands: - dockerize -wait tcp://postgres:5432 -timeout 120s @@ -971,7 +971,7 @@ steps: GRAFANA_TEST_DB: postgres PGPASSWORD: grafanatest POSTGRES_HOST: postgres - image: golang:1.21.9-alpine + image: golang:1.22.2-alpine name: postgres-integration-tests - commands: - dockerize -wait tcp://mysql57:3306 -timeout 120s @@ -992,7 +992,7 @@ steps: environment: GRAFANA_TEST_DB: mysql MYSQL_HOST: mysql57 - image: golang:1.21.9-alpine + image: golang:1.22.2-alpine name: mysql-5.7-integration-tests - commands: - dockerize -wait tcp://mysql80:3306 -timeout 120s @@ -1013,7 +1013,7 @@ steps: environment: GRAFANA_TEST_DB: mysql MYSQL_HOST: mysql80 - image: golang:1.21.9-alpine + image: golang:1.22.2-alpine name: mysql-8.0-integration-tests - commands: - dockerize -wait tcp://redis:6379 -timeout 120s @@ -1029,7 +1029,7 @@ steps: - wait-for-redis environment: REDIS_URL: redis://redis:6379/0 - image: golang:1.21.9-alpine + image: golang:1.22.2-alpine name: redis-integration-tests - commands: - dockerize -wait tcp://memcached:11211 -timeout 120s @@ -1045,7 +1045,7 @@ steps: - wait-for-memcached environment: MEMCACHED_HOSTS: memcached:11211 - image: golang:1.21.9-alpine + image: golang:1.22.2-alpine name: memcached-integration-tests - commands: - dockerize -wait tcp://mimir_backend:8080 -timeout 120s @@ -1061,7 +1061,7 @@ steps: environment: AM_TENANT_ID: test AM_URL: http://mimir_backend:8080 - image: golang:1.21.9-alpine + image: golang:1.22.2-alpine name: remote-alertmanager-integration-tests trigger: event: @@ -1150,7 +1150,7 @@ steps: - apk add --update make - CODEGEN_VERIFY=1 make gen-cue depends_on: [] - image: golang:1.21.9-alpine + image: golang:1.22.2-alpine name: verify-gen-cue trigger: event: @@ -1191,7 +1191,7 @@ steps: depends_on: [] environment: CGO_ENABLED: 0 - image: golang:1.21.9-alpine + image: golang:1.22.2-alpine name: compile-build-cmd - commands: - apt-get update -yq && apt-get install shellcheck @@ -1263,7 +1263,7 @@ steps: environment: GITHUB_TOKEN: from_secret: github_token - image: golang:1.21.9-alpine + image: golang:1.22.2-alpine name: swagger-gen trigger: event: @@ -1359,7 +1359,7 @@ steps: depends_on: [] environment: CGO_ENABLED: 0 - image: golang:1.21.9-alpine + image: golang:1.22.2-alpine name: compile-build-cmd - commands: - '# It is required that code generated from Thema/CUE be committed and in sync @@ -1370,7 +1370,7 @@ steps: - CODEGEN_VERIFY=1 make gen-cue depends_on: - clone-enterprise - image: golang:1.21.9-alpine + image: golang:1.22.2-alpine name: verify-gen-cue - commands: - '# It is required that generated jsonnet is committed and in sync with its inputs.' @@ -1380,14 +1380,14 @@ steps: - CODEGEN_VERIFY=1 make gen-jsonnet depends_on: - clone-enterprise - image: golang:1.21.9-alpine + image: golang:1.22.2-alpine name: verify-gen-jsonnet - commands: - apk add --update make - make gen-go depends_on: - verify-gen-cue - image: golang:1.21.9-alpine + image: golang:1.22.2-alpine name: wire-install - commands: - apk add --update build-base @@ -1395,7 +1395,7 @@ steps: - go test -v -run=^$ -benchmem -timeout=1h -count=8 -bench=. ${GO_PACKAGES} depends_on: - wire-install - image: golang:1.21.9-alpine + image: golang:1.22.2-alpine name: sqlite-benchmark-integration-tests - commands: - apk add --update build-base @@ -1407,7 +1407,7 @@ steps: GRAFANA_TEST_DB: postgres PGPASSWORD: grafanatest POSTGRES_HOST: postgres - image: golang:1.21.9-alpine + image: golang:1.22.2-alpine name: postgres-benchmark-integration-tests - commands: - apk add --update build-base @@ -1418,7 +1418,7 @@ steps: environment: GRAFANA_TEST_DB: mysql MYSQL_HOST: mysql57 - image: golang:1.21.9-alpine + image: golang:1.22.2-alpine name: mysql-5.7-benchmark-integration-tests - commands: - apk add --update build-base @@ -1429,7 +1429,7 @@ steps: environment: GRAFANA_TEST_DB: mysql MYSQL_HOST: mysql80 - image: golang:1.21.9-alpine + image: golang:1.22.2-alpine name: mysql-8.0-benchmark-integration-tests trigger: event: @@ -1508,7 +1508,7 @@ steps: - apk add --update make - CODEGEN_VERIFY=1 make gen-cue depends_on: [] - image: golang:1.21.9-alpine + image: golang:1.22.2-alpine name: verify-gen-cue trigger: branch: main @@ -1685,7 +1685,7 @@ steps: - apk add --update make - CODEGEN_VERIFY=1 make gen-cue depends_on: [] - image: golang:1.21.9-alpine + image: golang:1.22.2-alpine name: verify-gen-cue - commands: - '# It is required that generated jsonnet is committed and in sync with its inputs.' @@ -1694,14 +1694,14 @@ steps: - apk add --update make - CODEGEN_VERIFY=1 make gen-jsonnet depends_on: [] - image: golang:1.21.9-alpine + image: golang:1.22.2-alpine name: verify-gen-jsonnet - commands: - apk add --update make - make gen-go depends_on: - verify-gen-cue - image: golang:1.21.9-alpine + image: golang:1.22.2-alpine name: wire-install - commands: - apk add --update build-base shared-mime-info shared-mime-info-lang @@ -1709,7 +1709,7 @@ steps: -timeout=5m depends_on: - wire-install - image: golang:1.21.9-alpine + image: golang:1.22.2-alpine name: test-backend - commands: - apk add --update build-base @@ -1718,7 +1718,7 @@ steps: | grep -o '\(.*\)/' | sort -u) depends_on: - wire-install - image: golang:1.21.9-alpine + image: golang:1.22.2-alpine name: test-backend-integration trigger: branch: main @@ -1763,13 +1763,13 @@ steps: depends_on: [] environment: CGO_ENABLED: 0 - image: golang:1.21.9-alpine + image: golang:1.22.2-alpine name: compile-build-cmd - commands: - apk add --update make - make gen-go depends_on: [] - image: golang:1.21.9-alpine + image: golang:1.22.2-alpine name: wire-install - commands: - apk add --update make build-base @@ -1778,16 +1778,16 @@ steps: - wire-install environment: CGO_ENABLED: "1" - image: golang:1.21.9-alpine + image: golang:1.22.2-alpine name: lint-backend - commands: - go run scripts/modowners/modowners.go check go.mod - image: golang:1.21.9-alpine + image: golang:1.22.2-alpine name: validate-modfile - commands: - apk add --update make - make swagger-validate - image: golang:1.21.9-alpine + image: golang:1.22.2-alpine name: validate-openapi-spec - commands: - ./bin/build verify-drone @@ -1844,7 +1844,7 @@ steps: depends_on: [] environment: CGO_ENABLED: 0 - image: golang:1.21.9-alpine + image: golang:1.22.2-alpine name: compile-build-cmd - commands: - '# It is required that code generated from Thema/CUE be committed and in sync @@ -1854,7 +1854,7 @@ steps: - apk add --update make - CODEGEN_VERIFY=1 make gen-cue depends_on: [] - image: golang:1.21.9-alpine + image: golang:1.22.2-alpine name: verify-gen-cue - commands: - '# It is required that generated jsonnet is committed and in sync with its inputs.' @@ -1863,14 +1863,14 @@ steps: - apk add --update make - CODEGEN_VERIFY=1 make gen-jsonnet depends_on: [] - image: golang:1.21.9-alpine + image: golang:1.22.2-alpine name: verify-gen-jsonnet - commands: - apk add --update make - make gen-go depends_on: - verify-gen-cue - image: golang:1.21.9-alpine + image: golang:1.22.2-alpine name: wire-install - commands: - apk add --update g++ make python3 && ln -sf /usr/bin/python3 /usr/bin/python @@ -1903,7 +1903,7 @@ steps: name: build-frontend-packages - commands: - /src/grafana-build artifacts -a targz:grafana:linux/amd64 -a targz:grafana:linux/arm64 - -a targz:grafana:linux/arm/v7 --go-version=1.21.9 --yarn-cache=$$YARN_CACHE_FOLDER + -a targz:grafana:linux/arm/v7 --go-version=1.22.2 --yarn-cache=$$YARN_CACHE_FOLDER --build-id=$$DRONE_BUILD_NUMBER --grafana-dir=$$PWD > packages.txt depends_on: - update-package-json-version @@ -2143,7 +2143,7 @@ steps: - /src/grafana-build artifacts -a docker:grafana:linux/amd64 -a docker:grafana:linux/amd64:ubuntu -a docker:grafana:linux/arm64 -a docker:grafana:linux/arm64:ubuntu -a docker:grafana:linux/arm/v7 -a docker:grafana:linux/arm/v7:ubuntu --yarn-cache=$$YARN_CACHE_FOLDER --build-id=$$DRONE_BUILD_NUMBER - --go-version=1.21.9 --ubuntu-base=ubuntu:22.04 --alpine-base=alpine:3.19.1 --tag-format='{{ + --go-version=1.22.2 --ubuntu-base=ubuntu:22.04 --alpine-base=alpine:3.19.1 --tag-format='{{ .version_base }}-{{ .buildID }}-{{ .arch }}' --grafana-dir=$$PWD --ubuntu-tag-format='{{ .version_base }}-{{ .buildID }}-ubuntu-{{ .arch }}' > docker.txt - find ./dist -name '*docker*.tar.gz' -type f | xargs -n1 docker load -i @@ -2349,7 +2349,7 @@ steps: depends_on: [] environment: CGO_ENABLED: 0 - image: golang:1.21.9-alpine + image: golang:1.22.2-alpine name: compile-build-cmd - commands: - echo $DRONE_RUNNER_NAME @@ -2363,7 +2363,7 @@ steps: - apk add --update make - CODEGEN_VERIFY=1 make gen-cue depends_on: [] - image: golang:1.21.9-alpine + image: golang:1.22.2-alpine name: verify-gen-cue - commands: - '# It is required that generated jsonnet is committed and in sync with its inputs.' @@ -2372,14 +2372,14 @@ steps: - apk add --update make - CODEGEN_VERIFY=1 make gen-jsonnet depends_on: [] - image: golang:1.21.9-alpine + image: golang:1.22.2-alpine name: verify-gen-jsonnet - commands: - apk add --update make - make gen-go depends_on: - verify-gen-cue - image: golang:1.21.9-alpine + image: golang:1.22.2-alpine name: wire-install - commands: - dockerize -wait tcp://postgres:5432 -timeout 120s @@ -2400,7 +2400,7 @@ steps: GRAFANA_TEST_DB: postgres PGPASSWORD: grafanatest POSTGRES_HOST: postgres - image: golang:1.21.9-alpine + image: golang:1.22.2-alpine name: postgres-integration-tests - commands: - dockerize -wait tcp://mysql57:3306 -timeout 120s @@ -2421,7 +2421,7 @@ steps: environment: GRAFANA_TEST_DB: mysql MYSQL_HOST: mysql57 - image: golang:1.21.9-alpine + image: golang:1.22.2-alpine name: mysql-5.7-integration-tests - commands: - dockerize -wait tcp://mysql80:3306 -timeout 120s @@ -2442,7 +2442,7 @@ steps: environment: GRAFANA_TEST_DB: mysql MYSQL_HOST: mysql80 - image: golang:1.21.9-alpine + image: golang:1.22.2-alpine name: mysql-8.0-integration-tests - commands: - dockerize -wait tcp://redis:6379 -timeout 120s @@ -2458,7 +2458,7 @@ steps: - wait-for-redis environment: REDIS_URL: redis://redis:6379/0 - image: golang:1.21.9-alpine + image: golang:1.22.2-alpine name: redis-integration-tests - commands: - dockerize -wait tcp://memcached:11211 -timeout 120s @@ -2474,7 +2474,7 @@ steps: - wait-for-memcached environment: MEMCACHED_HOSTS: memcached:11211 - image: golang:1.21.9-alpine + image: golang:1.22.2-alpine name: memcached-integration-tests - commands: - dockerize -wait tcp://mimir_backend:8080 -timeout 120s @@ -2490,7 +2490,7 @@ steps: environment: AM_TENANT_ID: test AM_URL: http://mimir_backend:8080 - image: golang:1.21.9-alpine + image: golang:1.22.2-alpine name: remote-alertmanager-integration-tests trigger: branch: main @@ -2683,7 +2683,7 @@ steps: depends_on: [] environment: CGO_ENABLED: 0 - image: golang:1.21.9-alpine + image: golang:1.22.2-alpine name: compile-build-cmd - commands: - ./bin/build artifacts docker fetch --edition oss @@ -2780,7 +2780,7 @@ steps: depends_on: [] environment: CGO_ENABLED: 0 - image: golang:1.21.9-alpine + image: golang:1.22.2-alpine name: compile-build-cmd - commands: - ./bin/build artifacts packages --tag $${DRONE_TAG} --src-bucket $${PRERELEASE_BUCKET} @@ -2850,7 +2850,7 @@ steps: depends_on: [] environment: CGO_ENABLED: 0 - image: golang:1.21.9-alpine + image: golang:1.22.2-alpine name: compile-build-cmd - commands: - apk add --update g++ make python3 && ln -sf /usr/bin/python3 /usr/bin/python @@ -2917,7 +2917,7 @@ steps: depends_on: [] environment: CGO_ENABLED: 0 - image: golang:1.21.9-alpine + image: golang:1.22.2-alpine name: compile-build-cmd - depends_on: - compile-build-cmd @@ -3024,7 +3024,7 @@ steps: from_secret: gcp_key_base64 GITHUB_TOKEN: from_secret: github_token - GO_VERSION: 1.21.9 + GO_VERSION: 1.22.2 GPG_PASSPHRASE: from_secret: packages_gpg_passphrase GPG_PRIVATE_KEY: @@ -3082,13 +3082,13 @@ steps: depends_on: [] environment: CGO_ENABLED: 0 - image: golang:1.21.9-alpine + image: golang:1.22.2-alpine name: compile-build-cmd - commands: - ./bin/build whatsnew-checker depends_on: - compile-build-cmd - image: golang:1.21.9-alpine + image: golang:1.22.2-alpine name: whats-new-checker trigger: event: @@ -3191,7 +3191,7 @@ steps: - apk add --update make - CODEGEN_VERIFY=1 make gen-cue depends_on: [] - image: golang:1.21.9-alpine + image: golang:1.22.2-alpine name: verify-gen-cue - commands: - '# It is required that generated jsonnet is committed and in sync with its inputs.' @@ -3200,14 +3200,14 @@ steps: - apk add --update make - CODEGEN_VERIFY=1 make gen-jsonnet depends_on: [] - image: golang:1.21.9-alpine + image: golang:1.22.2-alpine name: verify-gen-jsonnet - commands: - apk add --update make - make gen-go depends_on: - verify-gen-cue - image: golang:1.21.9-alpine + image: golang:1.22.2-alpine name: wire-install - commands: - apk add --update build-base shared-mime-info shared-mime-info-lang @@ -3215,7 +3215,7 @@ steps: -timeout=5m depends_on: - wire-install - image: golang:1.21.9-alpine + image: golang:1.22.2-alpine name: test-backend - commands: - apk add --update build-base @@ -3224,7 +3224,7 @@ steps: | grep -o '\(.*\)/' | sort -u) depends_on: - wire-install - image: golang:1.21.9-alpine + image: golang:1.22.2-alpine name: test-backend-integration trigger: event: @@ -3281,7 +3281,7 @@ steps: from_secret: gcp_key_base64 GITHUB_TOKEN: from_secret: github_token - GO_VERSION: 1.21.9 + GO_VERSION: 1.22.2 GPG_PASSPHRASE: from_secret: packages_gpg_passphrase GPG_PRIVATE_KEY: @@ -3464,7 +3464,7 @@ steps: from_secret: gcp_key_base64 GITHUB_TOKEN: from_secret: github_token - GO_VERSION: 1.21.9 + GO_VERSION: 1.22.2 GPG_PASSPHRASE: from_secret: packages_gpg_passphrase GPG_PRIVATE_KEY: @@ -3614,7 +3614,7 @@ steps: - apk add --update make - CODEGEN_VERIFY=1 make gen-cue depends_on: [] - image: golang:1.21.9-alpine + image: golang:1.22.2-alpine name: verify-gen-cue - commands: - '# It is required that generated jsonnet is committed and in sync with its inputs.' @@ -3623,14 +3623,14 @@ steps: - apk add --update make - CODEGEN_VERIFY=1 make gen-jsonnet depends_on: [] - image: golang:1.21.9-alpine + image: golang:1.22.2-alpine name: verify-gen-jsonnet - commands: - apk add --update make - make gen-go depends_on: - verify-gen-cue - image: golang:1.21.9-alpine + image: golang:1.22.2-alpine name: wire-install - commands: - apk add --update build-base shared-mime-info shared-mime-info-lang @@ -3638,7 +3638,7 @@ steps: -timeout=5m depends_on: - wire-install - image: golang:1.21.9-alpine + image: golang:1.22.2-alpine name: test-backend - commands: - apk add --update build-base @@ -3647,7 +3647,7 @@ steps: | grep -o '\(.*\)/' | sort -u) depends_on: - wire-install - image: golang:1.21.9-alpine + image: golang:1.22.2-alpine name: test-backend-integration trigger: cron: @@ -3702,7 +3702,7 @@ steps: from_secret: gcp_key_base64 GITHUB_TOKEN: from_secret: github_token - GO_VERSION: 1.21.9 + GO_VERSION: 1.22.2 GPG_PASSPHRASE: from_secret: packages_gpg_passphrase GPG_PRIVATE_KEY: @@ -3849,7 +3849,7 @@ steps: from_secret: gcp_key_base64 GITHUB_TOKEN: from_secret: github_token - GO_VERSION: 1.21.9 + GO_VERSION: 1.22.2 GPG_PASSPHRASE: from_secret: packages_gpg_passphrase GPG_PRIVATE_KEY: @@ -3958,7 +3958,7 @@ steps: from_secret: gcp_key_base64 GITHUB_TOKEN: from_secret: github_token - GO_VERSION: 1.21.9 + GO_VERSION: 1.22.2 GPG_PASSPHRASE: from_secret: packages_gpg_passphrase GPG_PRIVATE_KEY: @@ -4048,20 +4048,20 @@ steps: - commands: [] depends_on: - clone - image: golang:1.21.9-windowsservercore-1809 + image: golang:1.22.2-windowsservercore-1809 name: windows-init - commands: - go install github.com/google/wire/cmd/wire@v0.5.0 - wire gen -tags oss ./pkg/server depends_on: - windows-init - image: golang:1.21.9-windowsservercore-1809 + image: golang:1.22.2-windowsservercore-1809 name: wire-install - commands: - go test -tags requires_buildifer -short -covermode=atomic -timeout=5m ./pkg/... depends_on: - wire-install - image: golang:1.21.9-windowsservercore-1809 + image: golang:1.22.2-windowsservercore-1809 name: test-backend trigger: event: @@ -4154,7 +4154,7 @@ steps: - apk add --update make - CODEGEN_VERIFY=1 make gen-cue depends_on: [] - image: golang:1.21.9-alpine + image: golang:1.22.2-alpine name: verify-gen-cue - commands: - '# It is required that generated jsonnet is committed and in sync with its inputs.' @@ -4163,14 +4163,14 @@ steps: - apk add --update make - CODEGEN_VERIFY=1 make gen-jsonnet depends_on: [] - image: golang:1.21.9-alpine + image: golang:1.22.2-alpine name: verify-gen-jsonnet - commands: - apk add --update make - make gen-go depends_on: - verify-gen-cue - image: golang:1.21.9-alpine + image: golang:1.22.2-alpine name: wire-install - commands: - dockerize -wait tcp://postgres:5432 -timeout 120s @@ -4191,7 +4191,7 @@ steps: GRAFANA_TEST_DB: postgres PGPASSWORD: grafanatest POSTGRES_HOST: postgres - image: golang:1.21.9-alpine + image: golang:1.22.2-alpine name: postgres-integration-tests - commands: - dockerize -wait tcp://mysql57:3306 -timeout 120s @@ -4212,7 +4212,7 @@ steps: environment: GRAFANA_TEST_DB: mysql MYSQL_HOST: mysql57 - image: golang:1.21.9-alpine + image: golang:1.22.2-alpine name: mysql-5.7-integration-tests - commands: - dockerize -wait tcp://mysql80:3306 -timeout 120s @@ -4233,7 +4233,7 @@ steps: environment: GRAFANA_TEST_DB: mysql MYSQL_HOST: mysql80 - image: golang:1.21.9-alpine + image: golang:1.22.2-alpine name: mysql-8.0-integration-tests - commands: - dockerize -wait tcp://redis:6379 -timeout 120s @@ -4249,7 +4249,7 @@ steps: - wait-for-redis environment: REDIS_URL: redis://redis:6379/0 - image: golang:1.21.9-alpine + image: golang:1.22.2-alpine name: redis-integration-tests - commands: - dockerize -wait tcp://memcached:11211 -timeout 120s @@ -4265,7 +4265,7 @@ steps: - wait-for-memcached environment: MEMCACHED_HOSTS: memcached:11211 - image: golang:1.21.9-alpine + image: golang:1.22.2-alpine name: memcached-integration-tests - commands: - dockerize -wait tcp://mimir_backend:8080 -timeout 120s @@ -4281,7 +4281,7 @@ steps: environment: AM_TENANT_ID: test AM_URL: http://mimir_backend:8080 - image: golang:1.21.9-alpine + image: golang:1.22.2-alpine name: remote-alertmanager-integration-tests trigger: event: @@ -4635,7 +4635,7 @@ steps: path: /root/.docker/ - commands: - trivy --exit-code 0 --severity UNKNOWN,LOW,MEDIUM alpine/git:2.40.1 - - trivy --exit-code 0 --severity UNKNOWN,LOW,MEDIUM golang:1.21.9-alpine + - trivy --exit-code 0 --severity UNKNOWN,LOW,MEDIUM golang:1.22.2-alpine - trivy --exit-code 0 --severity UNKNOWN,LOW,MEDIUM node:20.9.0-alpine - trivy --exit-code 0 --severity UNKNOWN,LOW,MEDIUM node:20-bookworm - trivy --exit-code 0 --severity UNKNOWN,LOW,MEDIUM google/cloud-sdk:431.0.0 @@ -4670,7 +4670,7 @@ steps: path: /root/.docker/ - commands: - trivy --exit-code 1 --severity HIGH,CRITICAL alpine/git:2.40.1 - - trivy --exit-code 1 --severity HIGH,CRITICAL golang:1.21.9-alpine + - trivy --exit-code 1 --severity HIGH,CRITICAL golang:1.22.2-alpine - trivy --exit-code 1 --severity HIGH,CRITICAL node:20.9.0-alpine - trivy --exit-code 1 --severity HIGH,CRITICAL node:20-bookworm - trivy --exit-code 1 --severity HIGH,CRITICAL google/cloud-sdk:431.0.0 @@ -4925,6 +4925,6 @@ kind: secret name: gcr_credentials --- kind: signature -hmac: e67367689de11270bb3139f25a7cabf54e2b980460fd55ad410c590722d122b8 +hmac: 165260ab07061f24b2b206251d8d2ae11eadfc90155af178b0ac8982782c4520 ... diff --git a/.eslintrc b/.eslintrc index dac72ca97e0..71d0b6fbcac 100644 --- a/.eslintrc +++ b/.eslintrc @@ -4,10 +4,11 @@ "plugins": ["@emotion", "lodash", "jest", "import", "jsx-a11y", "@grafana", "no-barrel-files"], "settings": { "import/internal-regex": "^(app/)|(@grafana)", - "import/external-module-folders": ["node_modules", ".yarn"] + "import/external-module-folders": ["node_modules", ".yarn"], }, "rules": { "@grafana/no-border-radius-literal": "error", + "@grafana/no-unreduced-motion": "error", "react/prop-types": "off", // need to ignore emotion's `css` prop, see https://github.com/jsx-eslint/eslint-plugin-react/blob/master/docs/rules/no-unknown-property.md#rule-options "react/no-unknown-property": ["error", { "ignore": ["css"] }], @@ -19,8 +20,8 @@ { "groups": [["builtin", "external"], "internal", "parent", "sibling", "index"], "newlines-between": "always", - "alphabetize": { "order": "asc" } - } + "alphabetize": { "order": "asc" }, + }, ], "no-restricted-imports": [ "error", @@ -29,47 +30,43 @@ { "name": "react-redux", "importNames": ["useDispatch", "useSelector"], - "message": "Please import from app/types instead." + "message": "Please import from app/types instead.", }, { "name": "react-i18next", "importNames": ["Trans", "t"], - "message": "Please import from app/core/internationalization instead" + "message": "Please import from app/core/internationalization instead", }, - { - "name": "@grafana/e2e", - "message": "@grafana/e2e is deprecated. Please import from ./e2e/utils instead" - } - ] - } + ], + }, ], // Use typescript's no-redeclare for compatibility with overrides "no-redeclare": "off", - "@typescript-eslint/no-redeclare": ["error"] + "@typescript-eslint/no-redeclare": ["error"], }, "overrides": [ { "files": ["packages/grafana-ui/src/components/uPlot/**/*.{ts,tsx}"], "rules": { "react-hooks/rules-of-hooks": "off", - "react-hooks/exhaustive-deps": "off" - } + "react-hooks/exhaustive-deps": "off", + }, }, { "files": ["packages/grafana-ui/src/components/ThemeDemos/**/*.{ts,tsx}"], "rules": { "@emotion/jsx-import": "off", "react/jsx-uses-react": "off", - "react/react-in-jsx-scope": "off" - } + "react/react-in-jsx-scope": "off", + }, }, { "files": ["public/dashboards/scripted*.js"], "rules": { "no-redeclare": "error", - "@typescript-eslint/no-redeclare": "off" - } + "@typescript-eslint/no-redeclare": "off", + }, }, { "extends": ["plugin:jsx-a11y/recommended"], @@ -82,17 +79,17 @@ "jsx-a11y/no-autofocus": [ "error", { - "ignoreNonDOM": true - } + "ignoreNonDOM": true, + }, ], "jsx-a11y/label-has-associated-control": [ "error", { "controlComponents": ["NumberInput"], - "depth": 2 - } - ] - } + "depth": 2, + }, + ], + }, }, { "files": [ @@ -125,14 +122,14 @@ "public/app/plugins/datasource/cloudwatch/*.{ts,tsx}", "public/app/plugins/datasource/cloudwatch/**/*.{ts,tsx}", "public/app/plugins/datasource/zipkin/*.{ts,tsx}", - "public/app/plugins/datasource/zipkin/**/*.{ts,tsx}" + "public/app/plugins/datasource/zipkin/**/*.{ts,tsx}", ], "settings": { "import/resolver": { "node": { - "extensions": [".ts", ".tsx"] - } - } + "extensions": [".ts", ".tsx"], + }, + }, }, "rules": { "import/no-restricted-paths": [ @@ -143,12 +140,12 @@ "target": "./public/app/plugins", "from": "./public", "except": ["./app/plugins"], - "message": "Core plugins are not allowed to depend on Grafana core packages" - } - ] - } - ] - } - } - ] + "message": "Core plugins are not allowed to depend on Grafana core packages", + }, + ], + }, + ], + }, + }, + ], } diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index f6ba469e722..b7ec17d0319 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -313,7 +313,6 @@ /e2e/plugin-e2e/plugin-e2e-api-tests/ @grafana/plugins-platform-frontend /packages/ @grafana/grafana-frontend-platform @grafana/plugins-platform-frontend /packages/grafana-e2e-selectors/ @grafana/grafana-frontend-platform -/packages/grafana-e2e/ @grafana/grafana-frontend-platform /packages/grafana-ui/.storybook/ @grafana/plugins-platform-frontend /packages/grafana-ui/src/components/ @grafana/grafana-frontend-platform /packages/grafana-ui/src/components/DateTimePickers/ @grafana/grafana-frontend-platform diff --git a/.github/pr-checks.json b/.github/pr-checks.json index 3566f6a65df..b97dbc137ec 100644 --- a/.github/pr-checks.json +++ b/.github/pr-checks.json @@ -1,4 +1,11 @@ [ + { + "type": "check-milestone", + "title": "Milestone Check", + "targetUrl": "https://github.com/grafana/grafana/blob/main/contribute/merge-pull-request.md#assign-a-milestone", + "success": "Milestone set", + "failure": "Milestone not set" + }, { "type": "check-changelog", "title": "Changelog Check", diff --git a/.github/renovate.json5 b/.github/renovate.json5 index b78d893ab3c..25f18c606db 100644 --- a/.github/renovate.json5 +++ b/.github/renovate.json5 @@ -13,7 +13,7 @@ "@swc/core", // versions ~1.4.5 contain multiple bugs related to baseUrl resolution breaking builds. ], "includePaths": ["package.json", "packages/**", "public/app/plugins/**"], - "ignorePaths": ["emails/**", "plugins-bundled/**", "**/mocks/**", "packages/grafana-e2e/**"], + "ignorePaths": ["emails/**", "plugins-bundled/**", "**/mocks/**"], "labels": ["area/frontend", "dependencies", "no-changelog"], "postUpdateOptions": ["yarnDedupeHighest"], "packageRules": [ diff --git a/.github/workflows/alerting-swagger-gen.yml b/.github/workflows/alerting-swagger-gen.yml index 196ce184e27..aadc1ab2fb9 100644 --- a/.github/workflows/alerting-swagger-gen.yml +++ b/.github/workflows/alerting-swagger-gen.yml @@ -16,7 +16,7 @@ jobs: - name: Set go version uses: actions/setup-go@v4 with: - go-version: '1.21.9' + go-version: '1.22.2' - name: Build swagger run: | make -C pkg/services/ngalert/api/tooling post.json api.json diff --git a/.github/workflows/bump-version.yml b/.github/workflows/bump-version.yml index a80003e94b5..7db051c03ea 100644 --- a/.github/workflows/bump-version.yml +++ b/.github/workflows/bump-version.yml @@ -58,7 +58,7 @@ jobs: # Go is required for also updating the schema versions as part of the precommit hook: - uses: actions/setup-go@v4 with: - go-version: '1.21.9' + go-version: '1.22.2' - uses: actions/setup-node@v4 with: node-version: '18' diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index d768f6c3a5b..094b0eb3213 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -48,7 +48,7 @@ jobs: name: Set go version uses: actions/setup-go@v4 with: - go-version: '1.21.9' + go-version: '1.22.2' # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL diff --git a/.github/workflows/pr-codeql-analysis-go.yml b/.github/workflows/pr-codeql-analysis-go.yml index 324c6254ce9..216aea74dab 100644 --- a/.github/workflows/pr-codeql-analysis-go.yml +++ b/.github/workflows/pr-codeql-analysis-go.yml @@ -36,7 +36,7 @@ jobs: - name: Set go version uses: actions/setup-go@v4 with: - go-version: '1.21.9' + go-version: '1.22.2' # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL diff --git a/.github/workflows/pr-go-workspace-check.yml b/.github/workflows/pr-go-workspace-check.yml index b6b9669a745..655a27c92af 100644 --- a/.github/workflows/pr-go-workspace-check.yml +++ b/.github/workflows/pr-go-workspace-check.yml @@ -22,7 +22,7 @@ jobs: - name: Set go version uses: actions/setup-go@v4 with: - go-version: '1.21.9' + go-version: '1.22.2' - name: Workspace Sync run: go work sync diff --git a/.github/workflows/publish-kinds-next.yml b/.github/workflows/publish-kinds-next.yml index bf3f9f790e8..af33517b338 100644 --- a/.github/workflows/publish-kinds-next.yml +++ b/.github/workflows/publish-kinds-next.yml @@ -36,7 +36,7 @@ jobs: - name: "Setup Go" uses: "actions/setup-go@v4" with: - go-version: '1.21.9' + go-version: '1.22.2' - name: "Verify kinds" run: go run .github/workflows/scripts/kinds/verify-kinds.go diff --git a/.github/workflows/publish-kinds-release.yml b/.github/workflows/publish-kinds-release.yml index 5a7bb9e0428..c7d6eff062c 100644 --- a/.github/workflows/publish-kinds-release.yml +++ b/.github/workflows/publish-kinds-release.yml @@ -39,7 +39,7 @@ jobs: - name: "Setup Go" uses: "actions/setup-go@v4" with: - go-version: '1.21.9' + go-version: '1.22.2' - name: "Verify kinds" run: go run .github/workflows/scripts/kinds/verify-kinds.go diff --git a/.github/workflows/verify-kinds.yml b/.github/workflows/verify-kinds.yml index 33d10b14419..0c335f6f3a4 100644 --- a/.github/workflows/verify-kinds.yml +++ b/.github/workflows/verify-kinds.yml @@ -18,7 +18,7 @@ jobs: - name: "Setup Go" uses: "actions/setup-go@v4" with: - go-version: '1.21.9' + go-version: '1.22.2' - name: "Verify kinds" run: go run .github/workflows/scripts/kinds/verify-kinds.go diff --git a/.golangci.toml b/.golangci.toml index fd4fb00286f..acc7e3eaede 100644 --- a/.golangci.toml +++ b/.golangci.toml @@ -182,6 +182,13 @@ text = "SA1019: http.CloseNotifier" linters = ["staticcheck"] text = "SA1019: strings.Title" +# go.opentelemetry.io/otel/exporters/jaeger" is deprecated: This module is no longer supported. OpenTelemetry dropped support for Jaeger exporter in July 2023. +# Jaeger officially accepts and recommends using OTLP. +# Use [go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp] or [go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc] instead. +[[issues.exclude-rules]] +linters = ["staticcheck"] +text = "SA1019: \"go.opentelemetry.io/otel/exporters/jaeger\"" + [[issues.exclude-rules]] linters = ["staticcheck"] text = "use fake service and real access control evaluator instead" diff --git a/.vscode/launch.json b/.vscode/launch.json index 99a3f849ca9..81486b93841 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -19,9 +19,7 @@ "program": "${workspaceFolder}/pkg/cmd/grafana/", "env": {}, "cwd": "${workspaceFolder}", - "args": ["apiserver", - "--secure-port=8443", - "--runtime-config=testdata.datasource.grafana.app/v0alpha1=true"] + "args": ["apiserver", "--secure-port=8443", "--runtime-config=testdata.datasource.grafana.app/v0alpha1=true"] }, { "name": "Run API Server (query-localhost)", @@ -31,12 +29,14 @@ "program": "${workspaceFolder}/pkg/cmd/grafana/", "env": {}, "cwd": "${workspaceFolder}", - "args": ["apiserver", - "--secure-port=8443", - "--runtime-config=query.grafana.app/v0alpha1=true", - "--grafana.authn.signing-keys-url=http://localhost:3000/api/signing-keys/keys", - "--hg-url=http://localhost:3000", - "--hg-key=$HGAPIKEY"] + "args": [ + "apiserver", + "--secure-port=8443", + "--runtime-config=query.grafana.app/v0alpha1=true", + "--grafana.authn.signing-keys-url=http://localhost:3000/api/signing-keys/keys", + "--hg-url=http://localhost:3000", + "--hg-key=$HGAPIKEY" + ] }, { "name": "Attach to Chrome", @@ -46,14 +46,16 @@ "webRoot": "${workspaceFolder}" }, { - "name": "Debug Jest test", + "name": "Debug UI test", "type": "node", "request": "launch", "runtimeExecutable": "yarn", "runtimeArgs": ["run", "jest", "--runInBand", "${file}"], "console": "integratedTerminal", "internalConsoleOptions": "neverOpen", - "port": 9229 + "env": { + "NODE_ENV": "test" + } }, { "name": "Debug Go test", diff --git a/.yarnrc.yml b/.yarnrc.yml index 37fa44f6405..0a455c6d94a 100644 --- a/.yarnrc.yml +++ b/.yarnrc.yml @@ -24,19 +24,10 @@ packageExtensions: peerDependencies: react: 17.0.1 react-dom: 17.0.1 - react-compat-css-styled@1.0.8: - dependencies: - react-simple-compat: 1.2.2 - react-icons@2.2.7: - peerDependencies: - prop-types: '*' - react-resizable@3.0.4: - peerDependencies: - react-dom: 17.0.1 '@msagl/drawing@*': dependencies: - queue-typescript: "^1.0.1" - "@esfx/collections-sortedmap": "^1.0.0" + queue-typescript: '^1.0.1' + '@esfx/collections-sortedmap': '^1.0.0' plugins: - path: .yarn/plugins/@yarnpkg/plugin-outdated.cjs diff --git a/Dockerfile b/Dockerfile index 0696e3f5b13..04991627ceb 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,7 +3,7 @@ ARG BASE_IMAGE=alpine:3.19.1 ARG JS_IMAGE=node:20-alpine ARG JS_PLATFORM=linux/amd64 -ARG GO_IMAGE=golang:1.21.9-alpine +ARG GO_IMAGE=golang:1.22.2-alpine ARG GO_SRC=go-builder ARG JS_SRC=js-builder @@ -14,18 +14,18 @@ ENV NODE_OPTIONS=--max_old_space_size=8000 WORKDIR /tmp/grafana -COPY package.json yarn.lock .yarnrc.yml ./ +COPY package.json project.json nx.json yarn.lock .yarnrc.yml ./ COPY .yarn .yarn COPY packages packages COPY plugins-bundled plugins-bundled COPY public public +COPY LICENSE ./ RUN apk add --no-cache make build-base python3 RUN yarn install --immutable COPY tsconfig.json .eslintrc .editorconfig .browserslistrc .prettierrc.js ./ -COPY public public COPY scripts scripts COPY emails emails @@ -77,7 +77,6 @@ COPY pkg pkg COPY scripts scripts COPY conf conf COPY .github .github -COPY LICENSE ./ ENV COMMIT_SHA=${COMMIT_SHA} ENV BUILD_BRANCH=${BUILD_BRANCH} @@ -179,7 +178,7 @@ RUN if [ ! $(getent group "$GF_GID") ]; then \ COPY --from=go-src /tmp/grafana/bin/grafana* /tmp/grafana/bin/*/grafana* ./bin/ COPY --from=js-src /tmp/grafana/public ./public -COPY --from=go-src /tmp/grafana/LICENSE ./ +COPY --from=js-src /tmp/grafana/LICENSE ./ EXPOSE 3000 diff --git a/LICENSING.md b/LICENSING.md index 97f2c53d8e7..5239154d161 100644 --- a/LICENSING.md +++ b/LICENSING.md @@ -10,7 +10,6 @@ The following directories and their subdirectories are licensed under Apache-2.0 ``` packages/grafana-data/ -packages/grafana-e2e/ packages/grafana-e2e-selectors/ packages/grafana-runtime/ packages/grafana-ui/ diff --git a/Makefile b/Makefile index 0d47a3d0215..f3eeedbbe89 100644 --- a/Makefile +++ b/Makefile @@ -9,6 +9,7 @@ include .bingo/Variables.mk GO = go +GO_VERSION = 1.22.2 GO_FILES ?= ./pkg/... ./pkg/apiserver/... ./pkg/apimachinery/... ./pkg/promlib/... SH_FILES ?= $(shell find ./scripts -name *.sh) GO_BUILD_FLAGS += $(if $(GO_BUILD_DEV),-dev) @@ -316,7 +317,7 @@ build-docker-full-ubuntu: ## Build Docker image based on Ubuntu for development. --build-arg COMMIT_SHA=$$(git rev-parse HEAD) \ --build-arg BUILD_BRANCH=$$(git rev-parse --abbrev-ref HEAD) \ --build-arg BASE_IMAGE=ubuntu:22.04 \ - --build-arg GO_IMAGE=golang:1.21.9 \ + --build-arg GO_IMAGE=golang:$(GO_VERSION) \ --tag grafana/grafana$(TAG_SUFFIX):dev-ubuntu \ $(DOCKER_BUILD_ARGS) diff --git a/conf/defaults.ini b/conf/defaults.ini index e1acf689bb5..401a65c6314 100644 --- a/conf/defaults.ini +++ b/conf/defaults.ini @@ -107,6 +107,12 @@ key_file = # this will log the request and response for each unary gRPC call enable_logging = false +# Maximum size of a message that can be received in bytes. If not set, uses the gRPC default (4MiB). +max_recv_msg_size = + +# Maximum size of a message that can be sent in bytes. If not set, uses the gRPC default (unlimited). +max_send_msg_size = + #################################### Database ############################ [database] # You can configure the database connection by specifying type, host, name, user and password @@ -1422,6 +1428,11 @@ concurrent_query_limit = # Enable the Query history enabled = true +#################################### Short Links ############################# +[short_links] +# Short links which are never accessed will be deleted as cleanup. Time is in days. Default is 7 days. Max is 365. 0 means they will be deleted approximately every 10 minutes. +expire_time = 7 + #################################### Internal Grafana Metrics ############ # Metrics available at HTTP URL /metrics and /metrics/plugins/:pluginId [metrics] diff --git a/conf/sample.ini b/conf/sample.ini index 2cfef5081a6..c9d5b89e50a 100644 --- a/conf/sample.ini +++ b/conf/sample.ini @@ -101,6 +101,8 @@ ;use_tls = false ;cert_file = ;key_file = +;max_recv_msg_size = +;max_send_msg_size = #################################### Database #################################### [database] diff --git a/devenv/docker/blocks/auth/README.md b/devenv/docker/blocks/auth/README.md index 2046804fb9a..7ebd69161fd 100644 --- a/devenv/docker/blocks/auth/README.md +++ b/devenv/docker/blocks/auth/README.md @@ -28,4 +28,3 @@ by the `devenv` target. - [openldap](./openldap) - [openldap-multiple](./openldap-multiple) - [prometheus_basic_auth_proxy](./prometheus_basic_auth_proxy) -- [saml](./saml) diff --git a/docs/make-docs b/docs/make-docs index 43efdb5faad..fc4f3d7a148 100755 --- a/docs/make-docs +++ b/docs/make-docs @@ -6,6 +6,22 @@ # [Semantic versioning](https://semver.org/) is used to help the reader identify the significance of changes. # Changes are relevant to this script and the support docs.mk GNU Make interface. # +# ## 6.1.0 (2024-04-22) +# +# ### Changed +# +# - Mount volumes with SELinux labels. +# +# https://docs.docker.com/storage/bind-mounts/#configure-the-selinux-label +# +# ## 6.1.0 (2024-04-22) +# +# ### Added +# +# - Pseudo project for including only website resources and no website content. +# +# Facilitates testing shortcodes and layout changes with a small documentation set instead of Grafana Cloud or the entire website. +# # ## 6.0.1 (2024-02-28) # # ### Added @@ -300,6 +316,7 @@ SOURCES_helm_charts_tempo_distributed='tempo' SOURCES_opentelemetry='opentelemetry-docs' SOURCES_plugins_grafana_datadog_datasource='datadog-datasource' SOURCES_plugins_grafana_oracle_datasource='oracle-datasource' +SOURCES_resources='website' VERSIONS_as_code='UNVERSIONED' VERSIONS_grafana_cloud='UNVERSIONED' @@ -311,6 +328,7 @@ VERSIONS_grafana_cloud_frontend_observability_faro_web_sdk='UNVERSIONED' VERSIONS_opentelemetry='UNVERSIONED' VERSIONS_plugins_grafana_datadog_datasource='latest' VERSIONS_plugins_grafana_oracle_datasource='latest' +VERSIONS_resources='UNVERSIONED' VERSIONS_technical_documentation='UNVERSIONED' VERSIONS_website='UNVERSIONED' VERSIONS_writers_toolkit='UNVERSIONED' @@ -321,6 +339,7 @@ PATHS_helm_charts_tempo_distributed='docs/sources/helm-charts/tempo-distributed' PATHS_mimir='docs/sources/mimir' PATHS_plugins_grafana_datadog_datasource='docs/sources' PATHS_plugins_grafana_oracle_datasource='docs/sources' +PATHS_resources='content' PATHS_tempo='docs/sources/tempo' PATHS_website='content' @@ -584,6 +603,11 @@ POSIX_HERESTRING proj_to_url_src_dst_ver "$(new_proj helm-charts/mimir-distributed "${_version}")" proj_to_url_src_dst_ver "$(new_proj enterprise-metrics "${_version}")" ;; + resources) + _repo="$(repo_path website)" + echo "arbitrary^${_repo}/config^/hugo/config" "arbitrary^${_repo}/layouts^/hugo/layouts" "arbitrary^${_repo}/scripts^/hugo/scripts" + unset _repo + ;; traces) proj_to_url_src_dst_ver "$(new_proj tempo "${_version}")" proj_to_url_src_dst_ver "$(new_proj enterprise-traces "${_version}")" @@ -617,7 +641,7 @@ $x POSIX_HERESTRING if [ -n "${url}" ]; then - if [ "${_url}" != "arbitrary" ]; then + if [ "${url}" != arbitrary ]; then printf '\r %s\r\n' "${url}" fi fi @@ -670,9 +694,9 @@ POSIX_HERESTRING fi _repo="$(repo_path website)" - volumes="--volume=${_repo}/config:/hugo/config" - volumes="${volumes} --volume=${_repo}/layouts:/hugo/layouts" - volumes="${volumes} --volume=${_repo}/scripts:/hugo/scripts" + volumes="--volume=${_repo}/config:/hugo/config:z" + volumes="${volumes} --volume=${_repo}/layouts:/hugo/layouts:z" + volumes="${volumes} --volume=${_repo}/scripts:/hugo/scripts:z" fi unset _project _repo done @@ -682,7 +706,7 @@ for x in ${url_src_dst_vers}; do $x POSIX_HERESTRING - if [ "${_url}" != "arbitrary" ]; then + if [ "${_url}" != arbitrary ]; then if [ ! -f "${_src}/_index.md" ]; then errr "Index file '${_src}/_index.md' does not exist." note "Is '${_src}' the correct source directory?" @@ -693,9 +717,9 @@ POSIX_HERESTRING debg "Mounting '${_src}' at container path '${_dst}'" if [ -z "${volumes}" ]; then - volumes="--volume=${_src}:${_dst}" + volumes="--volume=${_src}:${_dst}:z" else - volumes="${volumes} --volume=${_src}:${_dst}" + volumes="${volumes} --volume=${_src}:${_dst}:z" fi if [ -n "${_ver}" ] && [ "${_ver}" != 'UNVERSIONED' ]; then @@ -789,7 +813,7 @@ fi ${WEBSITE_EXEC} EOF chmod +x "${tempfile}" - volumes="${volumes} --volume=${tempfile}:/entrypoint" + volumes="${volumes} --volume=${tempfile}:/entrypoint:z" readonly volumes IFS='' read -r cmd <}}) 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. @@ -104,7 +106,7 @@ Create a custom role when basic roles and fixed roles do not meet your permissio ### Create custom roles using provisioning -File-based provisioning is one method you can use to create custom roles. +[File-based provisioning]({{< relref "./rbac-grafana-provisioning" >}}) is one method you can use to create custom roles. 1. Open the YAML configuration file and locate the `roles` section. diff --git a/docs/sources/administration/user-management/_index.md b/docs/sources/administration/user-management/_index.md index 27ca8ec0673..ad3be03f7cb 100644 --- a/docs/sources/administration/user-management/_index.md +++ b/docs/sources/administration/user-management/_index.md @@ -13,6 +13,10 @@ weight: 200 A _user_ is defined as any individual who can log in to Grafana. Each user is associated with a _role_ that includes _permissions_. Permissions determine the tasks a user can perform in the system. For example, the **Admin** role includes permissions for an administrator to create and delete users. +Watch the following video to learn how to manage users and permissions in Grafana OSS and Grafana Cloud: + +{{< youtube id="59uCGJN5hPI" >}} + The following topics describe how to use permissions to control user access to data sources, dashboards, users, and teams. {{< section >}} diff --git a/docs/sources/alerting/alerting-rules/templating-labels-annotations.md b/docs/sources/alerting/alerting-rules/templating-labels-annotations.md index 5177d6dad6f..38f08f8084b 100644 --- a/docs/sources/alerting/alerting-rules/templating-labels-annotations.md +++ b/docs/sources/alerting/alerting-rules/templating-labels-annotations.md @@ -30,7 +30,7 @@ Each template is evaluated whenever the alert rule is evaluated, and is evaluate ## Examples -Rather than write a complete tutorial on text/template, the following examples attempt to show the most common use-cases we have seen for templates. You can use these examples verbatim, or adapt them as necessary for your use case. For more information on how to write text/template refer to the [text/template](https://pkg.go.dev/text/template) documentation. +The following examples attempt to show the most common use-cases we have seen for templates. You can use these examples verbatim, or adapt them as necessary for your use case. For more information on how to write text/template refer see [the beginner's guide to alert notification templates in Grafana](https://grafana.com/blog/2023/04/05/grafana-alerting-a-beginners-guide-to-templating-alert-notifications/). ### Print all labels, comma separated diff --git a/docs/sources/alerting/configure-notifications/_index.md b/docs/sources/alerting/configure-notifications/_index.md index f709efed8fa..996b5ee29af 100644 --- a/docs/sources/alerting/configure-notifications/_index.md +++ b/docs/sources/alerting/configure-notifications/_index.md @@ -19,8 +19,19 @@ weight: 125 Choose how, when, and where to send your alert notifications. -As a first step, define your contact points; where to send your alert notifications to. A contact point is a set of one or more integrations that are used to deliver notifications. +As a first step, define your [contact points][contact-points] where to send your alert notifications to. A contact point is a set of one or more integrations that are used to deliver notifications. -Next, create a notification policy which is a set of rules for where, when and how your alerts are routed to contact points. In a notification policy, you define where to send your alert notifications by choosing one of the contact points you created. +Next, create a [notification policy][notification-policies] which is a set of rules for where, when and how your alerts are routed to contact points. In a notification policy, you define where to send your alert notifications by choosing one of the contact points you created. -Optionally, you can add notification templates to contact points for reuse and consistent messaging in your notifications. +Optionally, you can add [notification templates][templates-page] to contact points for reuse and consistent messaging in your notifications. + +{{% docs/reference %}} +[notification-policies]: "/docs/grafana/ -> /docs/grafana//alerting/fundamentals/notifications/notification-policies" +[notification-policies]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/alerting/fundamentals/notifications/notification-policies" + +[contact-points]: "/docs/grafana/ -> /docs/grafana//alerting/fundamentals/notifications/contact-points" +[contact-points]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/alerting/fundamentals/notifications/contact-points" + +[templates-page]: "/docs/grafana/ -> /docs/grafana//alerting/fundamentals/notifications/templates/" +[templates-page]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/alerting/fundamentals/notifications/templates/" +{{% /docs/reference %}} diff --git a/docs/sources/alerting/configure-notifications/create-notification-policy.md b/docs/sources/alerting/configure-notifications/create-notification-policy.md index bcd9d8a8064..3381cbd3137 100644 --- a/docs/sources/alerting/configure-notifications/create-notification-policy.md +++ b/docs/sources/alerting/configure-notifications/create-notification-policy.md @@ -97,6 +97,10 @@ An example of a valid matchers search input is: > All matched policies will be **exact** matches, we currently do not support regex-style or partial matching. +## Caveat + +Mute timings are not inherited from a parent notification policy, they have to be configured in full on each level. + ## Example An example of an alert configuration. diff --git a/docs/sources/alerting/configure-notifications/template-notifications/reference.md b/docs/sources/alerting/configure-notifications/template-notifications/reference.md index d38e517a4d2..3bb4fe3f216 100644 --- a/docs/sources/alerting/configure-notifications/template-notifications/reference.md +++ b/docs/sources/alerting/configure-notifications/template-notifications/reference.md @@ -74,10 +74,19 @@ In addition to iterating over each key value pair, you can sort the pairs, remov ### Time -Time is from the Go [`time`](https://pkg.go.dev/time#Time) package. You can print a time in a number of different formats. For example, to print the time that an alert fired in the format `Monday, 1st January 2022 at 10:00AM` you would write the following template: +Time is from the Go [`time`](https://pkg.go.dev/time#Time) package. + +You can format a time in a number of different formats using the `date` function. +For example, to print the time that an alert fired in the format `15:04:05 MST`: ``` -{{ .StartsAt.Format "Monday, 2 January 2006 at 3:04PM" }} +{{ .StartsAt | date "15:04:05 MST" }} +``` + +You can also use the `tz` function to change the timezone from UTC to a local time. For example: + +``` +{{ .StartsAt | tz "Europe/Paris" | date "15:04:05 MST" }} ``` You can find a reference for Go's time format [here](https://pkg.go.dev/time#pkg-constants). diff --git a/docs/sources/alerting/fundamentals/_index.md b/docs/sources/alerting/fundamentals/_index.md index ce5946e948d..e6030f9e1b7 100644 --- a/docs/sources/alerting/fundamentals/_index.md +++ b/docs/sources/alerting/fundamentals/_index.md @@ -35,15 +35,15 @@ The following concepts are key to your understanding of how Grafana Alerting wor ### Alert rules -An [alert rule][alert-rules] consists of one or more queries and expressions that select the data you want to measure. It also contains a condition, which is the threshold that an alert rule must meet or exceed in order to fire. +An [alert rule][alert-rules] consists of one or more queries and expressions that select the data you want to measure. It also contains a condition, which is the threshold that an alert rule must meet or exceed to fire. Add labels to uniquely identify your alert rule and configure alert routing. Labels link alert rules to notification policies, so you can easily manage which policy should handle which alerts and who gets notified. -Once alert rules are created, they go through various states and transitions. +After alert rules are created, they go through various states and transitions. ### Alert instances -Each alert rule can produce multiple alert instances (also known as alerts) - one alert instance for each time series. This is exceptionally powerful as it allows us to observe multiple series in a single expression. +Each alert rule can produce multiple alert instances (also known as alerts) - one alert instance for each time series. This is exceptionally powerful as it allows you to observe multiple series in a single expression. ```promql sum by(cpu) ( @@ -51,7 +51,7 @@ sum by(cpu) ( ) ``` -A rule using the PromQL expression above creates as many alert instances as the amount of CPUs we are observing after the first evaluation, enabling a single rule to report the status of each CPU. +A rule using the PromQL expression above creates as many alert instances as the amount of CPUs after the first evaluation, enabling a single rule to report the status of each CPU. {{< figure src="/static/img/docs/alerting/unified/multi-dimensional-alert.png" caption="Multiple alert instances from a single alert rule" >}} diff --git a/docs/sources/alerting/fundamentals/alert-rules/_index.md b/docs/sources/alerting/fundamentals/alert-rules/_index.md index 8edc3eac916..43cbd746922 100644 --- a/docs/sources/alerting/fundamentals/alert-rules/_index.md +++ b/docs/sources/alerting/fundamentals/alert-rules/_index.md @@ -32,7 +32,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 our [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 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. Additionally, you can also add [expressions to transform your data][expression-queries], set custom alert conditions, and include [images in alert notifications][notification-images]. @@ -77,7 +77,7 @@ When choosing which alert rule type to use, consider the following comparison be |
Feature
|
Grafana-managed alert rule
|
Data source-managed alert rule | | ------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Create alert rules based on data from any of our supported data sources | Yes | No. You can only create alert rules that are based on Prometheus-based data. | +| 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 | | Add expressions to transform your data and set alert conditions | Yes | No | diff --git a/docs/sources/alerting/fundamentals/alert-rules/annotation-label.md b/docs/sources/alerting/fundamentals/alert-rules/annotation-label.md index 5317dd7b3b1..26b989efa61 100644 --- a/docs/sources/alerting/fundamentals/alert-rules/annotation-label.md +++ b/docs/sources/alerting/fundamentals/alert-rules/annotation-label.md @@ -26,11 +26,15 @@ weight: 105 Labels and annotations contain information about an alert. Labels are used to differentiate an alert from all other alerts, while annotations are used to add additional information to an existing alert. +When creating alert rules, you can also template labels and annotations to optimize and customize your alerts. + ## Labels -Labels contain information that identifies an alert. An example of a label might be `server=server1` or `team=backend`. Each alert can have more than one label, and the complete set of labels for an alert is called its label set. It is this label set that identifies the alert. +**Labels** are unique identifiers of an alert. You can use them for searching, silencing, and routing notifications. -For example, an alert might have the label set `{alertname="High CPU usage",server="server1"}` while another alert might have the label set `{alertname="High CPU usage",server="server2"}`. These are two separate alerts because although their `alertname` labels are the same, their `server` labels are different. +Examples of labels are `server=server1` or `team=backend`. Each alert rule can have more than one label and the complete set of labels for an alert rule is called its label set. It is this label set that identifies the alert. + +For example, an alert rule might have the label set `{alertname="High CPU usage",server="server1"}` while another alert rule might have the label set `{alertname="High CPU usage",server="server2"}`. These are two separate alert rules because although their `alertname` labels are the same, their `server` labels are different. Labels are a fundamental component of alerting: @@ -94,15 +98,16 @@ Here is an example that shows how to exclude the label `Team`. You can choose be An alert's label set can contain three types of labels: -- Labels from the datasource, -- Custom labels specified in the alert rule, -- A series of reserved labels, such as `alertname` or `grafana_folder`. +- Data source query labels. For example, if you are monitoring temperature readings and each time series for these readings has a sensor_id, and a location label. These labels are used to provide additional context or dimensions to the metric data, helping to differentiate between different time series. -### Custom Labels +- Labels that are automatically added by Grafana (i.e. alertname and grafana_folder). These are Grafana reserved labels. -Custom labels are additional labels configured manually in the alert rule. +- Labels that you define yourself to help filter data in your alert rules. + You can also template labels. For example in your alert rule, you could add a label that uses templating to create more dynamic and customizable alerting. E.g. `environment` `=` `{{ your text/template }}`. -Ensure the label set for an alert does not have two or more labels with the same name. If a custom label has the same name as a label from the datasource then it will replace that label. However, should a custom label have the same name as a reserved label then the custom label will be omitted from the alert. +{{}} +Ensure the label set for an alert does not have two or more labels with the same name. If a label has the same name as a label from the data source then it will replace that label. However, should a label have the same name as a reserved label then the label will be omitted from the alert. +{{}} {{< collapse title="Key format" >}} @@ -122,7 +127,7 @@ If multiple label keys are sanitized to the same value, the duplicates will have ### Reserved labels -Reserved labels can be used in the same way as manually configured custom labels. The current list of available reserved labels are: +Reserved labels can be used in the same way as manually configured labels. The current list of available reserved labels are: | Label | Description | | -------------- | ----------------------------------------- | @@ -135,7 +140,7 @@ Labels prefixed with `grafana_` are reserved by Grafana for special use. To stop Both labels and annotations have the same structure: a set of named values; however their intended uses are different. The purpose of annotations is to add additional information to existing alerts. -There are a number of suggested annotations in Grafana such as `description`, `summary`, `runbook_url`, `dashboardUId` and `panelId`. Like custom labels, annotations must have a name, and their value can contain a combination of text and template code that is evaluated when an alert is fired. +There are a number of suggested annotations in Grafana such as `description`, `summary`, `runbook_url`, `dashboardUId` and `panelId`. Like labels, annotations must have a name, and their value can contain a combination of text and template code that is evaluated when an alert is fired. {{% docs/reference %}} [variables-label-annotation]: "/docs/grafana/ -> /docs/grafana//alerting/alerting-rules/templating-labels-annotations" diff --git a/docs/sources/alerting/fundamentals/alert-rules/organising-alerts.md b/docs/sources/alerting/fundamentals/alert-rules/organising-alerts.md index 09113a1622e..1d7c73834c6 100644 --- a/docs/sources/alerting/fundamentals/alert-rules/organising-alerts.md +++ b/docs/sources/alerting/fundamentals/alert-rules/organising-alerts.md @@ -29,7 +29,7 @@ A namespace contains one or more groups. The rules within a group are run sequen ### Groups -The rules within a group are run sequentially at a regular interval, meaning no rules will be evaluated at the same time and in order of appearance. The default interval is one (1) minute. You can rename Grafana Mimir or Loki rule namespaces and groups, and edit group evaluation intervals. +The rules within a group are run sequentially at a regular interval, meaning no rules are evaluated at the same time and in order of appearance. The default interval is one (1) minute. You can rename Grafana Mimir or Loki rule namespaces and groups, and edit group evaluation intervals. > **Note** If you want rules to be evaluated concurrently and with different intervals, consider storing them in different groups. diff --git a/docs/sources/alerting/fundamentals/alert-rules/queries-conditions.md b/docs/sources/alerting/fundamentals/alert-rules/queries-conditions.md index 41e0f2d2101..762120f9e2b 100644 --- a/docs/sources/alerting/fundamentals/alert-rules/queries-conditions.md +++ b/docs/sources/alerting/fundamentals/alert-rules/queries-conditions.md @@ -20,17 +20,17 @@ weight: 104 # Queries and conditions -In Grafana, queries play a vital role in fetching and transforming data from supported data sources, which include databases like MySQL and PostgreSQL, time series databases like Prometheus, InfluxDB and Graphite, and services like Elasticsearch, AWS CloudWatch, Azure Monitor and Google Cloud Monitoring. +In Grafana, queries play a vital role in fetching and transforming data from supported data sources, which include databases like MySQL and PostgreSQL, time series databases like Prometheus, InfluxDB and Graphite, and services like Elasticsearch, Amazon CloudWatch, Azure Monitor, and Google Cloud Monitoring. -For more information on supported data sources, see [Data sources][data-source-alerting]. +For more information on supported data sources, refer to [Data sources][data-source-alerting]. -The process of executing a query involves defining the data source, specifying the desired data to retrieve, and applying relevant filters or transformations. Query languages or syntaxes specific to the chosen data source are utilized for constructing these queries. +The process of executing a query involves defining the data source, specifying the desired data to retrieve, and applying relevant filters or transformations. Query languages or syntax specific to the chosen data source are utilized for constructing these queries. In Alerting, you define a query to get the data you want to measure and a condition that needs to be met before an alert rule fires. An alert rule consists of one or more queries and expressions that select the data you want to measure. -For more information on queries and expressions, see [Query and transform data][query-transform-data]. +For more information on queries and expressions, refer to [Query and transform data][query-transform-data]. ## Data source queries @@ -123,7 +123,7 @@ These functions are available for **Reduce** and **Classic condition** expressio ## Alert condition -An alert condition is the query or expression that determines whether the alert will fire or not depending on the value it yields. There can be only one condition which will determine the triggering of the alert. +An alert condition is the query or expression that determines whether the alert fires or not depending on the value it yields. There can be only one condition which determines the triggering of the alert. After you have defined your queries and/or expressions, choose one of them as the alert rule condition. @@ -145,11 +145,11 @@ Grafana-managed alert rules are evaluated for a specific interval of time. Durin It can be tricky to create an alert rule for a noisy metric. That is, when the value of a metric continually goes above and below a threshold. This is called flapping and results in a series of firing - resolved - firing notifications and a noisy alert state history. -For example, if you have an alert for latency with a threshold of 1000ms and the number fluctuates around 1000 (say 980 ->1010 -> 990 -> 1020, and so on) then each of those will trigger a notification. +For example, if you have an alert for latency with a threshold of 1000ms and the number fluctuates around 1000 (say 980 ->1010 -> 990 -> 1020, and so on) then each of those triggers a notification. To solve this problem, you can set a (custom) recovery threshold, which basically means having two thresholds instead of one. An alert is triggered when the first threshold is crossed and is resolved only when the second threshold is crossed. -For example, you could set a threshold of 1000ms and a recovery threshold of 900ms. This way, an alert rule will only stop firing when it goes under 900ms and flapping is reduced. +For example, you could set a threshold of 1000ms and a recovery threshold of 900ms. This way, an alert rule only stops firing when it goes under 900ms and flapping is reduced. ## Alert on numeric data @@ -179,7 +179,6 @@ For a MySQL table called "DiskSpace": | 2021-June-7 | web1 | /etc | 3 | | 2021-June-7 | web2 | /var | 4 | | 2021-June-7 | web3 | /var | 8 | -| ... | ... | ... | ... | You can query the data filtering on time, but without returning the time series to Grafana. For example, an alert that would trigger per Host, Disk when there is less than 5% free space: @@ -204,7 +203,7 @@ This query returns the following Table response to Grafana: | web2 | /var | 4 | | web3 | /var | 0 | -When this query is used as the **condition** in an alert rule, then the non-zero will be alerting. As a result, three alert instances are produced: +When this query is used as the **condition** in an alert rule, then the non-zero is alerting. As a result, three alert instances are produced: | Labels | Status | | --------------------- | -------- | diff --git a/docs/sources/alerting/fundamentals/alert-rules/rule-evaluation.md b/docs/sources/alerting/fundamentals/alert-rules/rule-evaluation.md index a34a508601c..36f51fa4574 100644 --- a/docs/sources/alerting/fundamentals/alert-rules/rule-evaluation.md +++ b/docs/sources/alerting/fundamentals/alert-rules/rule-evaluation.md @@ -42,7 +42,7 @@ In the pending period, you select the period in which an alert rule can be in br Imagine you have an alert rule evaluation interval set at every 30 seconds and the pending period to 90 seconds. -Evaluation will occur as follows: +Evaluation occurs as follows: [00:30] First evaluation - condition not met. @@ -61,12 +61,12 @@ If the alert rule has a condition that needs to be in breach for a certain amoun - The rule stays in the "pending" state until the condition has been broken for the required amount of time - pending period. -- Once the required time has passed, the rule goes into a "firing" state. +- After the required time has passed, the rule goes into a "firing" state. - If the condition is no longer broken during the pending period, the rule goes back to its normal state. **Note:** -If you want to skip the pending state, you can simply set the pending period to 0. This effectively skips the pending period and your alert rule will start firing as soon as the condition is breached. +If you want to skip the pending state, you can simply set the pending period to 0. This effectively skips the pending period and your alert rule starts firing as soon as the condition is breached. When an alert rule fires, alert instances are produced, which are then sent to the Alertmanager. diff --git a/docs/sources/alerting/fundamentals/alert-rules/state-and-health.md b/docs/sources/alerting/fundamentals/alert-rules/state-and-health.md index 06ab9b8f2f0..f03a70d86e7 100644 --- a/docs/sources/alerting/fundamentals/alert-rules/state-and-health.md +++ b/docs/sources/alerting/fundamentals/alert-rules/state-and-health.md @@ -35,10 +35,10 @@ An alert rule can be in either of the following states: | **Pending** | At least one alert instances returned by the evaluation engine is `Pending`. | | **Firing** | At least one alert instances returned by the evaluation engine is `Firing`. | -The alert rule state is determined by the “worst case” state of the alert instances produced. For example, if one alert instance is firing, the alert rule state will also be firing. +The alert rule state is determined by the “worst case” state of the alert instances produced. For example, if one alert instance is firing, the alert rule state is also firing. {{% admonition type="note" %}} -Alerts will transition first to `pending` and then `firing`, thus it will take at least two evaluation cycles before an alert is fired. +Alerts transition first to `pending` and then `firing`, thus it takes at least two evaluation cycles before an alert is fired. {{% /admonition %}} ## Alert instance state @@ -55,11 +55,11 @@ An alert instance can be in either of the following states: ## Keep last state -An alert rule can be configured to keep the last state when a `NoData` and/or `Error` state is encountered. This will both prevent alerts from firing, and from resolving and re-firing. Just like normal evaluation, the alert rule will transition from `Pending` to `Firing` after the pending period has elapsed. +An alert rule can be configured to keep the last state when a `NoData` and/or `Error` state is encountered. This both prevents alerts from firing, and from resolving and re-firing. Just like normal evaluation, the alert rule transitions from `Pending` to `Firing` after the pending period has elapsed. ## Alert rule health -An alert rule can have one the following health statuses: +An alert rule can have one of the following health statuses: | State | Description | | ---------------------- | -------------------------------------------------------------------------------------------------------- | @@ -70,7 +70,7 @@ An alert rule can have one the following health statuses: ## Special alerts for `NoData` and `Error` -When evaluation of an alert rule produces state `NoData` or `Error`, Grafana Alerting will generate alert instances that have the following additional labels: +When evaluation of an alert rule produces state `NoData` or `Error`, Grafana Alerting generates alert instances that have the following additional labels: | Label | Description | | ------------------ | ---------------------------------------------------------------------- | diff --git a/docs/sources/alerting/fundamentals/notifications/_index.md b/docs/sources/alerting/fundamentals/notifications/_index.md index 693c673beda..9e4a7affb0d 100644 --- a/docs/sources/alerting/fundamentals/notifications/_index.md +++ b/docs/sources/alerting/fundamentals/notifications/_index.md @@ -18,7 +18,7 @@ weight: 110 # Notifications -Choosing how, when, and where to send your alert notifications is an important part of setting up your alerting system. These decisions will have a direct impact on your ability to resolve issues quickly and not miss anything important. +Choosing how, when, and where to send your alert notifications is an important part of setting up your alerting system. These decisions have a direct impact on your ability to resolve issues quickly and not miss anything important. As a first step, define your contact points; where to send your alert notifications to. A contact point is a set of one or more integrations that are used to deliver notifications. Add notification templates to contact points for reuse and consistent messaging in your notifications. @@ -26,17 +26,17 @@ Next, create a notification policy which is a set of rules for where, when and h ## Alertmanagers -Grafana uses Alertmanagers to send notifications for firing and resolved alerts. Grafana has its own Alertmanager, referred to as "Grafana" in the user interface, but also supports sending notifications from other Alertmanagers too, such as the [Prometheus Alertmanager](https://prometheus.io/docs/alerting/latest/alertmanager/). The Grafana Alertmanager uses notification policies and contact points to configure how and where a notification is sent; how often a notification should be sent; and whether alerts should all be sent in the same notification, sent in grouped notifications based on a set of labels, or as separate notifications. +Grafana uses [Alertmanagers](https://grafana.com/docs/grafana/latest/alerting/fundamentals/alertmanager/) to send notifications for firing and resolved alerts. Grafana has its own Alertmanager, referred to as "Grafana" in the user interface, but also supports sending notifications from other Alertmanagers too, such as the [Prometheus Alertmanager](https://prometheus.io/docs/alerting/latest/alertmanager/). The Grafana Alertmanager uses notification policies and contact points to configure how and where a notification is sent; how often a notification should be sent; and whether alerts should all be sent in the same notification, sent in grouped notifications based on a set of labels, or as separate notifications. ## Contact points -Contact points contain the configuration for sending alert notifications, specifying destinations like email, Slack, OnCall, webhooks, and their notification messages. They allow the customization of notification messages and the use of notification templates. +[Contact points][contact-points] contain the configuration for sending alert notifications, specifying destinations like email, Slack, OnCall, webhooks, and their notification messages. They allow the customization of notification messages and the use of notification templates. A contact point is a list of integrations, each sending a message to a specific destination. You can configure them via notification policies or alert rules. ## Notification policies -Notification policies control when and where notifications are sent. A notification policy can choose to send all alerts together in the same notification, send alerts in grouped notifications based on a set of labels, or send alerts as separate notifications. You can configure each notification policy to control how often notifications should be sent as well as having one or more mute timings to inhibit notifications at certain times of the day and on certain days of the week. +[Notification policies][notification-policies] control when and where notifications are sent. A notification policy can choose to send all alerts together in the same notification, send alerts in grouped notifications based on a set of labels, or send alerts as separate notifications. You can configure each notification policy to control how often notifications should be sent as well as having one or more mute timings to inhibit notifications at certain times of the day and on certain days of the week. Notification policies are organized in a tree structure where at the root of the tree there is a notification policy called the default policy. There can be only one default policy and the default policy cannot be deleted. @@ -50,7 +50,7 @@ All alerts, irrespective of their labels, match the default policy. However, whe ## Notification templates -You can customize notifications with templates. For example, templates can be used to change the subject and message of an email, or the title and message of notifications sent to Slack. +You can customize notifications with [templates][templates-page]. For example, templates can be used to change the subject and message of an email, or the title and message of notifications sent to Slack. Templates are not limited to an individual integration or contact point, but instead can be used in a number of integrations in the same contact point and even integrations across different contact points. For example, a Grafana user can create a template called `custom_subject_or_title` and use it for both templating subjects in emails and titles of Slack messages without having to create two separate templates. @@ -58,4 +58,15 @@ All notifications templates are written in [Go's templating language](https://pk ## Silences -You can use silences to mute notifications from one or more firing rules. Silences do not stop alerts from firing or being resolved, or hide firing alerts in the user interface. A silence lasts as long as its duration which can be configured in minutes, hours, days, months or years. +You can use [silences](https://grafana.com/docs/grafana/latest/alerting/manage-notifications/create-silence/) to mute notifications from one or more firing rules. Silences do not stop alerts from firing or being resolved, or hide firing alerts in the user interface. A silence lasts as long as its duration, which can be configured in minutes, hours, days, months, or years. + +{{% docs/reference %}} +[notification-policies]: "/docs/grafana/ -> /docs/grafana//alerting/fundamentals/notifications/notification-policies" +[notification-policies]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/alerting/fundamentals/notifications/notification-policies" + +[contact-points]: "/docs/grafana/ -> /docs/grafana//alerting/fundamentals/notifications/contact-points" +[contact-points]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/alerting/fundamentals/notifications/contact-points" + +[templates-page]: "/docs/grafana/ -> /docs/grafana//alerting/fundamentals/notifications/templates/" +[templates-page]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/alerting/fundamentals/notifications/templates/" +{{% /docs/reference %}} diff --git a/docs/sources/alerting/fundamentals/notifications/alertmanager.md b/docs/sources/alerting/fundamentals/notifications/alertmanager.md index a7a55219947..15cd39ffeca 100644 --- a/docs/sources/alerting/fundamentals/notifications/alertmanager.md +++ b/docs/sources/alerting/fundamentals/notifications/alertmanager.md @@ -26,7 +26,7 @@ Alertmanagers are visible from the drop-down menu on the Alerting Contact Points In Grafana, you can use the Cloud Alertmanager, Grafana Alertmanager, or an external Alertmanager. You can also run multiple Alertmanagers; your decision depends on your set up and where your alerts are being generated. -- **Grafana Alertmanager** is an internal Alertmanager that is pre-configured and available for selection by default if you run Grafana on-premises or open-source. +- **Grafana Alertmanager** is an internal Alertmanager that is pre-configured and available for selection by default if you run Grafana on-premises or open source. The Grafana Alertmanager can receive alerts from Grafana, but it cannot receive alerts from outside Grafana, for example, from Mimir or Loki. Note that inhibition rules are not supported. @@ -34,7 +34,7 @@ In Grafana, you can use the Cloud Alertmanager, Grafana Alertmanager, or an exte - **External Alertmanager** can receive all your Grafana, Loki, Mimir, and Prometheus alerts. External Alertmanagers can be configured and administered from within Grafana itself. -Here are two examples of when you may want to [add your own external alertmanager][configure-alertmanager] and send your alerts there instead of the Grafana Alertmanager: +Here are two examples of when you may want to [add your own external Alertmanager][configure-alertmanager] and send your alerts there instead of the Grafana Alertmanager: 1. You may already have Alertmanagers on-premises in your own Cloud infrastructure that you have set up and still want to use, because you have other alert generators, such as Prometheus. diff --git a/docs/sources/alerting/fundamentals/notifications/contact-points.md b/docs/sources/alerting/fundamentals/notifications/contact-points.md index 98aa75a0f8c..b88e449147a 100644 --- a/docs/sources/alerting/fundamentals/notifications/contact-points.md +++ b/docs/sources/alerting/fundamentals/notifications/contact-points.md @@ -24,7 +24,7 @@ weight: 112 # Contact points -Contact points contain the configuration for sending notifications. A contact point is a list of integrations, each of which sends a notification to a particular email address, service or URL. Contact points can have multiple integrations of the same kind, or a combination of integrations of different kinds. For example, a contact point could contain a Pagerduty integration; an email and Slack integration; or a Pagerduty integration, a Slack integration, and two email integrations. You can also configure a contact point with no integrations; in which case no notifications are sent. +Contact points contain the configuration for sending notifications. A contact point is a list of integrations, each of which sends a notification to a particular email address, service, or URL. Contact points can have multiple integrations of the same kind, or a combination of integrations of different kinds. For example, a contact point could contain a Pagerduty integration; an email and Slack integration; or a Pagerduty integration, a Slack integration, and two email integrations. You can also configure a contact point with no integrations; in which case no notifications are sent. A contact point cannot send notifications until it has been added to a notification policy. A notification policy can only send alerts to one contact point, but a contact point can be added to a number of notification policies at the same time. When an alert matches a notification policy, the alert is sent to the contact point in that notification policy, which then sends a notification to each integration in its configuration. diff --git a/docs/sources/alerting/fundamentals/notifications/message-templating.md b/docs/sources/alerting/fundamentals/notifications/message-templating.md deleted file mode 100644 index 9e6d33eae91..00000000000 --- a/docs/sources/alerting/fundamentals/notifications/message-templating.md +++ /dev/null @@ -1,72 +0,0 @@ ---- -aliases: - - ../../contact-points/message-templating/ # /docs/grafana//alerting/contact-points/message-templating/ - - ../../alert-rules/message-templating/ # /docs/grafana//alerting/alert-rules/message-templating/ - - ../../unified-alerting/message-templating/ # /docs/grafana//alerting/unified-alerting/message-templating/ -canonical: https://grafana.com/docs/grafana/latest/alerting/fundamentals/notifications/message-templating/ -description: Learn about notification templating -keywords: - - grafana - - alerting - - guide - - contact point - - templating -labels: - products: - - cloud - - enterprise - - oss -title: Notification templates -weight: 114 ---- - -# Notification templates - -Notifications sent via contact points are built using notification templates. Grafana's default templates are based on the [Go templating system](https://golang.org/pkg/text/template) where some fields are evaluated as text, while others are evaluated as HTML (which can affect escaping). - -The default template [default_template.go](https://github.com/grafana/alerting/blob/main/templates/default_template.go) is a useful reference for custom templates. - -Since most of the contact point fields can be templated, you can create reusable custom templates and use them in multiple contact points. - -### Using templates - -The following example shows how to use default templates to render an alert message in Slack. The message title contains a count of alerts that are firing or were resolved. The message body lists the alerts and their status. - -{{< figure src="/static/img/docs/alerting/unified/contact-points-template-fields-8-0.png" class="docs-image--no-shadow" max-width= "550px" caption="Default template" >}} - -The following example shows the use of a custom template within one of the contact point fields. - -{{< figure src="/static/img/docs/alerting/unified/contact-points-use-template-8-0.png" class="docs-image--no-shadow" max-width= "550px" caption="Default template" >}} - -### Nested templates - -You can embed templates within other templates. - -For example, you can define a template fragment using the `define` keyword: - -``` -{{ define "mytemplate" }} - {{ len .Alerts.Firing }} firing. {{ len .Alerts.Resolved }} resolved. -{{ end }} -``` - -You can then embed custom templates within this fragment using the `template` keyword. For example: - -``` -Alert summary: -{{ template "mytemplate" . }} -``` - -You can use any of the following built-in template options to embed custom templates. - -| Name | Notes | -| ----------------------- | ------------------------------------------------------------ | -| `default.title` | Displays high-level status information. | -| `default.message` | Provides a formatted summary of firing and resolved alerts. | -| `teams.default.message` | Similar to `default.message`, formatted for Microsoft Teams. | - -### HTML in notification templates - -HTML in alerting notification templates is escaped. We do not support rendering of HTML in the resulting notification. - -Some notifiers support alternative methods of changing the look and feel of the resulting notification. For example, Grafana installs the base template for alerting emails to `/public/emails/ng_alert_notification.html`. You can edit this file to change the appearance of all alerting emails. diff --git a/docs/sources/alerting/fundamentals/notifications/notification-policies.md b/docs/sources/alerting/fundamentals/notifications/notification-policies.md index 1e4d1d2dcf6..15883ab6f80 100644 --- a/docs/sources/alerting/fundamentals/notifications/notification-policies.md +++ b/docs/sources/alerting/fundamentals/notifications/notification-policies.md @@ -31,21 +31,21 @@ Notification policies are _not_ a list, but rather are structured according to a Each policy consists of a set of label matchers (0 or more) that specify which labels they are or aren't interested in handling. -For more information on label matching, see [how label matching works][labels-and-label-matchers]. +For more information on label matching, refer to [how label matching works][labels-and-label-matchers]. {{% admonition type="note" %}} -If you haven't configured any label matchers for your notification policy, your notification policy will match _all_ alert instances. This may prevent child policies from being evaluated unless you have enabled **Continue matching siblings** on the notification policy. +If you haven't configured any label matchers for your notification policy, your notification policy matches _all_ alert instances. This may prevent child policies from being evaluated unless you have enabled **Continue matching siblings** on the notification policy. {{% /admonition %}} ## Routing -To determine which notification policy will handle which alert instances, you have to start by looking at the existing set of notification policies, starting with the default notification policy. +To determine which notification policy handles which alert instances, you have to start by looking at the existing set of notification policies, starting with the default notification policy. -If no policies other than the default policy are configured, the default policy will handle the alert instance. +If no policies other than the default policy are configured, the default policy handles the alert instance. -If policies other than the default policy are defined, it will evaluate those notification policies in the order they are displayed. +If policies other than the default policy are defined, it evaluates those notification policies in the order they are displayed. -If a notification policy has label matchers that match the labels of the alert instance, it will descend in to its child policies and, if there are any, will continue to look for any child policies that might have label matchers that further narrow down the set of labels, and so forth until no more child policies have been found. +If a notification policy has label matchers that match the labels of the alert instance, it descends in to its child policies and, if there are any, continues to look for any child policies that might have label matchers that further narrow down the set of labels, and so forth until no more child policies have been found. If no child policies are defined in a notification policy or if none of the child policies have any label matchers that match the alert instance's labels, the default notification policy is used. @@ -63,7 +63,7 @@ Here's a breakdown of how these policies are selected: **Pod stuck in CrashLoop** does not have a `severity` label, so none of its child policies are matched. It does have a `team=operations` label, so the first policy is matched. -The `team=security` policy is not evaluated since we already found a match and **Continue matching siblings** was not configured for that policy. +The `team=security` policy is not evaluated a match was already found and **Continue matching siblings** was not configured for that policy. **Disk Usage – 80%** has both a `team` and `severity` label, and matches a child policy of the operations team. @@ -80,15 +80,15 @@ The following properties are inherited by child policies: - Timing options - Mute timings -Each of these properties can be overwritten by an individual policy should you wish to override the inherited properties. +Each of these properties can be overwritten by an individual policy if you want to override the inherited properties. To inherit a contact point from the parent policy, leave it blank. To override the inherited grouping options, enable **Override grouping**. To override the inherited timing options, enable **Override general timings**. ### Inheritance example -The example below shows how the notification policy tree from our previous example allows the child policies of the `team=operations` to inherit its contact point. +The example below shows how the notification policy tree from the previous example allows the child policies of the `team=operations` to inherit its contact point. -In this way, we can avoid having to specify the same contact point multiple times for each child policy. +In this way, you can avoid having to specify the same contact point multiple times for each child policy. {{< figure src="/media/docs/alerting/notification-inheritance.png" max-width="750px" caption="Notification policy inheritance" >}} @@ -98,15 +98,15 @@ In this way, we can avoid having to specify the same contact point multiple time Grouping is an important feature of Grafana Alerting as it allows you to batch relevant alerts together into a smaller number of notifications. This is particularly important if notifications are delivered to first-responders, such as engineers on-call, where receiving lots of notifications in a short period of time can be overwhelming and in some cases can negatively impact a first-responders ability to respond to an incident. For example, consider a large outage where many of your systems are down. In this case, grouping can be the difference between receiving 1 phone call and 100 phone calls. -You choose how alerts are grouped together using the Group by option in a notification policy. By default, notification policies in Grafana group alerts together by alert rule using the `alertname` and `grafana_folder` labels (since alert names are not unique across multiple folders). Should you wish to group alerts by something other than the alert rule, change the grouping to any other combination of labels. +Choose how alerts are grouped together using the Group by option in a notification policy. By default, notification policies in Grafana group alerts together by alert rule using the `alertname` and `grafana_folder` labels (since alert names are not unique across multiple folders). If you want to group alerts by something other than the alert rule, change the grouping to any other combination of labels. #### Disable grouping -Should you wish to receive every alert as a separate notification, you can do so by grouping by a special label called `...`. This is useful when your alerts are being delivered to an automated system instead of a first-responder. +If you want to receive every alert as a separate notification, you can do so by grouping by a special label called `...`. This is useful when your alerts are being delivered to an automated system instead of a first-responder. #### A single group for all alerts -Should you wish to receive all alerts together in a single notification, you can do so by leaving Group by empty. +If you want to receive all alerts together in a single notification, you can do so by leaving Group by empty. ### Timing options @@ -114,19 +114,19 @@ The timing options decide how often notifications are sent for each group of ale #### Group wait -Group wait is the amount of time Grafana waits before sending the first notification for a new group of alerts. The longer Group wait is the more time you have for other alerts to arrive. The shorter Group wait is the earlier the first notification will be sent, but at the risk of sending incomplete notifications. You should always choose a Group wait that makes the most sense for your use case. +Group wait is the amount of time Grafana waits before sending the first notification for a new group of alerts. The longer Group wait is the more time you have for other alerts to arrive. The shorter Group wait is the earlier the first notification is sent, but at the risk of sending incomplete notifications. You should always choose a Group wait that makes the most sense for your use case. **Default** 30 seconds #### Group interval -Once the first notification has been sent for a new group of alerts, Grafana starts the Group interval timer. This is the amount of time Grafana waits before sending notifications about changes to the group. For example, another firing alert might have just been added to the group while an existing alert might have resolved. If an alert was too late to be included in the first notification due to Group wait, it will be included in subsequent notifications after Group interval. Once Group interval has elapsed, Grafana resets the Group interval timer. This repeats until there are no more alerts in the group after which the group is deleted. +Once the first notification has been sent for a new group of alerts, the Group interval timer starts. This is the amount of wait time before notifications about changes to the group are sent. For example, another firing alert might have just been added to the group while an existing alert might have resolved. If an alert was too late to be included in the first notification due to Group wait, it is included in subsequent notifications after Group interval. Once Group interval has elapsed, Grafana resets the Group interval timer. This repeats until there are no more alerts in the group after which the group is deleted. **Default** 5 minutes #### Repeat interval -Repeat interval decides how often notifications are repeated if the group has not changed since the last notification. You can think of these as reminders that some alerts are still firing. Repeat interval is closely related to Group interval, which means your Repeat interval must not only be greater than or equal to Group interval, but also must be a multiple of Group interval. If Repeat interval is not a multiple of Group interval it will be coerced into one. For example, if your Group interval is 5 minutes, and your Repeat interval is 9 minutes, the Repeat interval will be rounded up to the nearest multiple of 5 which is 10 minutes. +Repeat interval decides how often notifications are repeated if the group has not changed since the last notification. You can think of these as reminders that some alerts are still firing. Repeat interval is closely related to Group interval, which means your Repeat interval must not only be greater than or equal to Group interval, but also must be a multiple of Group interval. If Repeat interval is not a multiple of Group interval it is coerced into one. For example, if your Group interval is 5 minutes, and your Repeat interval is 9 minutes, the Repeat interval is rounded up to the nearest multiple of 5 which is 10 minutes. **Default** 4 hours diff --git a/docs/sources/alerting/fundamentals/notifications/templates.md b/docs/sources/alerting/fundamentals/notifications/templates.md new file mode 100644 index 00000000000..dacd9a5310f --- /dev/null +++ b/docs/sources/alerting/fundamentals/notifications/templates.md @@ -0,0 +1,159 @@ +--- +aliases: + - ../../contact-points/message-templating/ # /docs/grafana//alerting/contact-points/message-templating/ + - ../../alert-rules/message-templating/ # /docs/grafana//alerting/alert-rules/message-templating/ + - ../../unified-alerting/message-templating/ # /docs/grafana//alerting/unified-alerting/message-templating/ +canonical: https://grafana.com/docs/grafana/latest/alerting/fundamentals/notifications/templates/ +description: Learn about templates +keywords: + - grafana + - alerting + - guide + - contact point + - templating +labels: + products: + - cloud + - enterprise + - oss +title: Templates +weight: 114 +--- + +# Templates + +Use templating to customize, format, and reuse alert notification messages. Create more flexible and informative alert notification messages by incorporating dynamic content, such as metric values, labels, and other contextual information. + +In Grafana, there are two ways to template your alert notification messages: + +1. Labels and annotations + + - Template labels and annotations in alert rules. + - Labels and annotations contain information about an alert. + - Labels are used to differentiate an alert from all other alerts, while annotations are used to add additional information to an existing alert. + +2. Notification templates + + - Template notifications in contact points. + - Add notification templates to contact points for reuse and consistent messaging in your notifications. + - Use notification templates to change the title, message, and format of the message in your notifications. + +This diagram illustrates the entire process of templating, from the creation of labels and annotations in alert rules or notification templates in contact points, to what they look like when exported and applied in your alert notification messages. + +{{< figure src="/media/docs/alerting/grafana-templating-diagram-2.jpg" max-width="1200px" caption="How Templating works" >}} + +In this diagram: + +- **Monitored Application**: A web server, database, or any other service generating metrics. For example, it could be an NGINX server providing metrics about request rates, response times, and so on. +- **Prometheus**: Prometheus collects metrics from the monitored application. For example, it might scrape metrics from the NGINX server, including labels like instance (the server hostname) and job (the service name). +- **Grafana**: Grafana queries Prometheus to retrieve metrics data. For example, you might create an alert rule to monitor NGINX request rates over time, and template labels or annotations based on the instance label. +- **Alertmanager**: Part of the Prometheus ecosystem, Alertmanager handles alert notifications. For example, if the request rate exceeds a certain threshold on a particular NGINX server, Alertmanager can send an alert notification to, for example, Slack or email, including the server name and the exceeded threshold (the instance label will be interpolated, and the actual server name will appear in the alert notification). +- **Alert notification**: When an alert rule condition is met, Alertmanager sends a notification to various channels such as Slack, Grafana OnCall, etc. These notifications can include information from the labels associated with the alerting rule. For example, if an alert triggers due to high CPU usage on a specific server, the notification message can include details like server name (instance label), disk usage percentage, and the threshold that was exceeded. + +## Labels and annotations + +Labels and annotations contain information about an alert. Labels are used to differentiate an alert from all other alerts, while annotations are used to add additional information to an existing alert. + +### Template labels + +Label templates are applied in the alert rule itself (i.e. in the Configure labels and notifications section of an alert). + +{{}} +Think about templating labels when you need to improve or change how alerts are uniquely identified. This is especially helpful if the labels you get from your query aren't detailed enough. Keep in mind that it's better to keep long sentences for summaries and descriptions. Also, avoid using the query's value in labels because it may result in the creation of many alerts when you actually only need one. +{{}} + +Templating can be applied by using variables and functions. These variables can represent dynamic values retrieved from your data queries. + +{{}} +In Grafana templating, the $ and . symbols are used to reference variables and their properties. You can reference variables directly in your alert rule definitions using the $ symbol followed by the variable name. Similarly, you can access properties of variables using the dot (.) notation within alert rule definitions. +{{}} + +Here are some commonly used built-in [variables][variables-label-annotation] to interact with the name and value of labels in Grafana alerting: + +- The `$labels` variable, which contains all labels from the query. + + For example, let's say you have an alert rule that triggers when the CPU usage exceeds a certain threshold. You want to create annotations that provide additional context when this alert is triggered, such as including the specific server that experienced the high CPU usage. + + The host {{ index $labels "instance" }} has exceeded 80% CPU usage for the last 5 minutes + + The outcome of this template would print: + + The host instance 1 has exceeded 80% CPU usage for the last 5 minutes + +- The `$value` variable, which is a string containing the labels and values of all instant queries; threshold, reduce and math expressions, and classic conditions in the alert rule. + + In the context of the previous example, $value variable would write something like this: + + CPU usage for {{ index $labels "instance" }} has exceeded 80% for the last 5 minutes: {{ $value }} + + The outcome of this template would print: + + CPU usage for instance1 has exceeded 80% for the last 5 minutes: [ var='A' labels={instance=instance1} value=81.234 ] + +- The `$values` variable is a table containing the labels and floating point values of all instant queries and expressions, indexed by their Ref IDs (i.e. the id that identifies the query or expression. By default the Red ID of the query is “A”). + + Given an alert with the labels instance=server1 and an instant query with the value 81.2345, would write like this: + + CPU usage for {{ index $labels "instance" }} has exceeded 80% for the last 5 minutes: {{ index $values "A" }} + + And it would print: + + CPU usage for instance1 has exceeded 80% for the last 5 minutes: 81.2345 + +### Template annotations + +Both labels and annotations have the same structure: a set of named values; however their intended uses are different. The purpose of annotations is to add additional information to existing alerts. + +There are a number of suggested annotations in Grafana such as `description`, `summary`, `runbook_url`, `dashboardUId` and `panelId`. Like labels, annotations must have a name, and their value can contain a combination of text and template code that is evaluated when an alert is fired. + +Here is an example of templating an annotation in the context of an alert rule. The text/template is added into the Add annotations section. + + CPU usage for {{ index $labels "instance" }} has exceeded 80% for the last 5 minutes + +The outcome of this template would print + + CPU usage for Instance 1 has exceeded 80% for the last 5 minutes + +### Template notifications + +Notification templates represent the alternative approach to templating designed for reusing templates. Notifications are messages to inform users about events or conditions triggered by alerts. You can create reusable notification templates to customize the content and format of alert notifications. Variables, labels, or other context-specific details can be added to the templates to dynamically insert information like metric values. + +Here is an example of a notification template: + +```go +{ define "alerts.message" -}} +{{ if .Alerts.Firing -}} +{{ len .Alerts.Firing }} firing alert(s) +{{ template "alerts.summarize" .Alerts.Firing }} +{{- end }} +{{- if .Alerts.Resolved -}} +{{ len .Alerts.Resolved }} resolved alert(s) +{{ template "alerts.summarize" .Alerts.Resolved }} +{{- end }} +{{- end }} + +{{ define "alerts.summarize" -}} +{{ range . -}} +- {{ index .Annotations "summary" }} +{{ end }} +{{ end }} +``` + +This is the message you would receive in your contact point: + + 1 firing alert(s) + - The database server db1 has exceeded 75% of available disk space. Disk space used is 76%, please resize the disk size within the next 24 hours + + 1 resolved alert(s) + - The web server web1 has been responding to 5% of HTTP requests with 5xx errors for the last 5 minutes + +Once the template is created, you need to make reference to it in your **Contact point** (in the Optional [contact point] settings) . + +{{}} +It's not recommended to include individual alert information within notification templates. Instead, it's more effective to incorporate such details within the rule using labels and annotations. +{{}} + +{{% docs/reference %}} +[variables-label-annotation]: "/docs/grafana/ -> /docs/grafana//alerting/alerting-rules/templating-labels-annotations" +[variables-label-annotation]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/alerting/alerting-rules/templating-labels-annotations" +{{% /docs/reference %}} diff --git a/docs/sources/alerting/set-up/configure-alert-state-history/index.md b/docs/sources/alerting/set-up/configure-alert-state-history/index.md index a9892dc716b..5ce753a6ad7 100644 --- a/docs/sources/alerting/set-up/configure-alert-state-history/index.md +++ b/docs/sources/alerting/set-up/configure-alert-state-history/index.md @@ -18,17 +18,17 @@ weight: 250 Starting with Grafana 10, Alerting can record all alert rule state changes for your Grafana managed alert rules in a Loki instance. -This allows you to explore the behavior of your alert rules in the Grafana explore view and levels up the existing state history modal with a powerful new visualisation. +This allows you to explore the behavior of your alert rules in the Grafana explore view and levels up the existing state history dialog box with a powerful new visualisation. ## Configuring Loki -To set up alert state history, make sure to have a Loki instance Grafana can write data to. The default settings might need some tweaking as the state history modal might query up to 30 days of data. +To set up alert state history, make sure to have a Loki instance Grafana can write data to. The default settings might need some tweaking as the state history dialog box might query up to 30 days of data. -The following change to the default configuration should work for most instances, but we recommend looking at the full Loki configuration settings and adjust according to your needs. +The following change to the default configuration should work for most instances, but look at the full Loki configuration settings and adjust according to your needs. -As this might impact the performances of an existing Loki instance, we recommend using a separate Loki instance for the alert state history. +As this might impact the performances of an existing Loki instance, use a separate Loki instance for the alert state history. ```yaml limits_config: @@ -38,7 +38,7 @@ limits_config: ## Configuring Grafana -We need some additional configuration in the Grafana configuration file to have it working with the alert state history. +Additional configuration is required in the Grafana configuration file to have it working with the alert state history. The example below instructs Grafana to write alert state history to a local Loki instance: @@ -56,7 +56,7 @@ enable = alertStateHistoryLokiSecondary, alertStateHistoryLokiPrimary, alertStat ## Adding the Loki data source -See our instructions on [adding a data source](/docs/grafana/latest/administration/data-source-management/). +Refer to the instructions on [adding a data source](/docs/grafana/latest/administration/data-source-management/). ## Querying the history 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 4992fbbe9eb..04594a87554 100644 --- a/docs/sources/alerting/set-up/configure-high-availability/_index.md +++ b/docs/sources/alerting/set-up/configure-high-availability/_index.md @@ -26,7 +26,7 @@ Grafana Alerting uses the Prometheus model of separating the evaluation of alert {{< figure src="/static/img/docs/alerting/unified/high-availability-ua.png" class="docs-image--no-shadow" max-width= "750px" caption="High availability" >}} -When running multiple instances of Grafana, all alert rules are evaluated on all instances. You can think of the evaluation of alert rules as being duplicated by the number of running Grafana instances. This is how Grafana Alerting makes sure that as long as at least one Grafana instance is working, alert rules will still be evaluated and notifications for alerts will still be sent. +When running multiple instances of Grafana, all alert rules are evaluated on all instances. You can think of the evaluation of alert rules as being duplicated by the number of running Grafana instances. This is how Grafana Alerting makes sure that as long as at least one Grafana instance is working, alert rules are still be evaluated and notifications for alerts are still sent. You can find this duplication in state history and it is a good way to confirm if you are using high availability. @@ -36,8 +36,8 @@ The Alertmanager uses a gossip protocol to share information about notifications {{% admonition type="note" %}} -If using a mix of `execute_alerts=false` and `execute_alerts=true` on the HA nodes, since the alert state is not shared amongst the Grafana instances, the instances with `execute_alerts=false` will not show any alert status. -This is because the HA settings (`ha_peers`, etc), only apply to the alert notification delivery (i.e. de-duplication of alert notifications, and silences, as mentioned above). +If using a mix of `execute_alerts=false` and `execute_alerts=true` on the HA nodes, since the alert state is not shared amongst the Grafana instances, the instances with `execute_alerts=false` do not show any alert status. +This is because the HA settings (`ha_peers`, etc) only apply to the alert notification delivery (i.e. de-duplication of alert notifications, and silences, as mentioned above). {{% /admonition %}} @@ -61,13 +61,13 @@ Since gossiping of notifications and silences uses both TCP and UDP port `9094`, As an alternative to Memberlist, you can use Redis for high availability. This is useful if you want to have a central database for HA and cannot support the meshing of all Grafana servers. -1. Make sure you have a redis server that supports pub/sub. If you use a proxy in front of your redis cluster, make sure the proxy supports pub/sub. +1. Make sure you have a redis server that supports pub/sub. If you use a proxy in front of your Redis cluster, make sure the proxy supports pub/sub. 1. In your custom configuration file ($WORKING_DIR/conf/custom.ini), go to the [unified_alerting] section. -1. Set `ha_redis_address` to the redis server address Grafana should connect to. +1. Set `ha_redis_address` to the Redis server address Grafana should connect to. 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. -The following metrics can be used for meta monitoring, exposed by Grafana's `/metrics` endpoint: +The following metrics can be used for meta monitoring, exposed by the `/metrics` endpoint in Grafana: | Metric | Description | | ---------------------------------------------------- | -------------------------------------------------------------------------------------------------------------- | @@ -83,7 +83,7 @@ The following metrics can be used for meta monitoring, exposed by Grafana's `/me ## Enable alerting high availability using Kubernetes -1. You can expose the pod IP [through an environment variable](https://kubernetes.io/docs/tasks/inject-data-application/environment-variable-expose-pod-information/) via the container definition. +1. You can expose the Pod IP [through an environment variable](https://kubernetes.io/docs/tasks/inject-data-application/environment-variable-expose-pod-information/) via the container definition. ```yaml env: @@ -115,7 +115,7 @@ The following metrics can be used for meta monitoring, exposed by Grafana's `/me fieldPath: status.podIP ``` -1. Create a headless service that returns the pod IP instead of the service IP, which is what the `ha_peers` need: +1. Create a headless service that returns the Pod IP instead of the service IP, which is what the `ha_peers` need: ```yaml apiVersion: v1 diff --git a/docs/sources/dashboards/share-dashboards-panels/index.md b/docs/sources/dashboards/share-dashboards-panels/index.md index b5a90ba97f5..7fd7a4a4e93 100644 --- a/docs/sources/dashboards/share-dashboards-panels/index.md +++ b/docs/sources/dashboards/share-dashboards-panels/index.md @@ -91,7 +91,17 @@ You can publish snapshots to your local instance or to [snapshots.raintank.io](h 1. Copy the snapshot link, and share it either within your organization or publicly on the web. -If you created a snapshot by mistake, click **Delete snapshot** to remove the snapshot from your Grafana instance. +If you created a snapshot by mistake, click **Delete snapshot** in the dialog box to remove the snapshot from your Grafana instance. + +#### Delete a snapshot + +To delete existing snapshots, follow these steps: + +1. In the primary menu, click **Dashboards**. +1. Click **Snapshots** to go to the snapshots management page. +1. Click the red **x** next to the snapshot URL that you want to delete. + +The snapshot is immediately deleted. You may need to clear your browser cache or use a private or incognito browser to confirm this. ### Export a dashboard as JSON @@ -183,7 +193,17 @@ You can optionally set an expiration time if you want the snapshot to be removed 1. Copy the snapshot link, and share it either within your organization or publicly on the web. -If you created a snapshot by mistake, click **Delete snapshot** to remove the snapshot from your Grafana instance. +If you created a snapshot by mistake, click **Delete snapshot** in the dialog box to remove the snapshot from your Grafana instance. + +#### Delete a snapshot + +To delete existing snapshots, follow these steps: + +1. In the primary menu, click **Dashboards**. +1. Click **Snapshots** to go to the snapshots management page. +1. Click the red **x** next to the snapshot URL that you want to delete. + +The snapshot is immediately deleted. You may need to clear your browser cache or use a private or incognito browser to confirm this. ### Embed panel diff --git a/docs/sources/datasources/jaeger/_index.md b/docs/sources/datasources/jaeger/_index.md index 36dc7d0131e..55e18194421 100644 --- a/docs/sources/datasources/jaeger/_index.md +++ b/docs/sources/datasources/jaeger/_index.md @@ -194,7 +194,7 @@ datasources: filterByTraceID: false filterBySpanID: false customQuery: true - query: 'method="${__span.tags.method}"' + query: 'method="$${__span.tags.method}"' tracesToMetrics: datasourceUid: 'prom' spanStartTimeShift: '1h' diff --git a/docs/sources/datasources/tempo/configure-tempo-data-source.md b/docs/sources/datasources/tempo/configure-tempo-data-source.md index ffde7d5ffe8..ab34f5875c2 100644 --- a/docs/sources/datasources/tempo/configure-tempo-data-source.md +++ b/docs/sources/datasources/tempo/configure-tempo-data-source.md @@ -238,7 +238,7 @@ datasources: filterByTraceID: false filterBySpanID: false customQuery: true - query: 'method="${__span.tags.method}"' + query: 'method="$${__span.tags.method}"' tracesToMetrics: datasourceUid: 'prom' spanStartTimeShift: '1h' @@ -252,7 +252,7 @@ datasources: tags: ['job', 'instance', 'pod', 'namespace'] profileTypeId: 'process_cpu:cpu:nanoseconds:cpu:nanoseconds' customQuery: true - query: 'method="${__span.tags.method}"' + query: 'method="$${__span.tags.method}"' serviceMap: datasourceUid: 'prometheus' nodeGraph: diff --git a/docs/sources/datasources/zipkin/_index.md b/docs/sources/datasources/zipkin/_index.md index b12037fbe2c..50c749c3d35 100644 --- a/docs/sources/datasources/zipkin/_index.md +++ b/docs/sources/datasources/zipkin/_index.md @@ -192,7 +192,7 @@ datasources: filterByTraceID: false filterBySpanID: false customQuery: true - query: 'method="${__span.tags.method}"' + query: 'method="$${__span.tags.method}"' tracesToMetrics: datasourceUid: 'prom' spanStartTimeShift: '1h' diff --git a/docs/sources/developers/http_api/user.md b/docs/sources/developers/http_api/user.md index 1443f72b1b4..37c64bffa24 100644 --- a/docs/sources/developers/http_api/user.md +++ b/docs/sources/developers/http_api/user.md @@ -66,7 +66,7 @@ Content-Type: application/json "isAdmin": true, "isDisabled": false, "lastSeenAt": "2020-04-10T20:29:27+03:00", - "lastSeenAtAge': "2m", + "lastSeenAtAge": "2m", "authLabels": ["OAuth"] }, { diff --git a/docs/sources/developers/plugins/plugin.schema.json b/docs/sources/developers/plugins/plugin.schema.json index 4947f96e0f2..09ab8106176 100644 --- a/docs/sources/developers/plugins/plugin.schema.json +++ b/docs/sources/developers/plugins/plugin.schema.json @@ -555,6 +555,35 @@ } } } + }, + "generated": { + "type": "object", + "description": "Auto-generated metadata for the plugin (usually automatically extracted from the source code during build time).", + "properties": { + "extensions": { + "type": "array", + "description": "List of the extensions that the plugin registers.", + "items": { + "type": "object", + "properties": { + "extensionPointId": { + "type": "string" + }, + "title": { + "type": "string" + }, + "description": { + "type": "string" + }, + "type": { + "type": "string", + "enum": ["link", "component"] + } + }, + "required": ["extensionPointId", "title", "description", "type"] + } + } + } } } } diff --git a/docs/sources/explore/_index.md b/docs/sources/explore/_index.md index 640c9785aa2..b7ec03e8f0a 100644 --- a/docs/sources/explore/_index.md +++ b/docs/sources/explore/_index.md @@ -25,6 +25,8 @@ If you just want to explore your data and do not want to create a dashboard, the - [Query management in Explore]({{< relref "query-management/" >}}) - [Logs integration in Explore]({{< relref "logs-integration/" >}}) - [Trace integration in Explore]({{< relref "trace-integration/" >}}) +- [Explore metrics]({{< relref "explore-metrics/" >}}) +- [Correlations Editor in Explore]({{< relref "correlations-editor-in-explore/" >}}) - [Inspector in Explore]({{< relref "explore-inspector/" >}}) ## Start exploring @@ -137,7 +139,7 @@ Available in Grafana 7.3 and later versions. The Share shortened link capability allows you to create smaller and simpler URLs of the format /goto/:uid instead of using longer URLs with query parameters. To create a shortened link to the executed query, click the **Share** option in the Explore toolbar. -A shortened link will automatically get deleted after seven (7) days from its creation if it's never used. If a link is used at least once, it won't ever get deleted. +A shortened link that is not accessed will automatically get deleted after a [configurable period](https://grafana.com/docs/grafana//setup-grafana/configure-grafana/#short_links) (defaulting to seven days). If a link is used at least once, it won't be deleted. ### Sharing shortened links with absolute time diff --git a/docs/sources/explore/explore-metrics.md b/docs/sources/explore/explore-metrics.md index 7433d18c455..6ad02a1d824 100644 --- a/docs/sources/explore/explore-metrics.md +++ b/docs/sources/explore/explore-metrics.md @@ -14,7 +14,9 @@ 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. -{{< docs/public-preview product="Explore 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: @@ -23,7 +25,7 @@ With Explore Metrics, you can: - surface other metrics relevant to the current metric - “explore in a drawer” - expand a drawer over a dashboard with more content so you don’t lose your place - view a history of user steps when navigating through metrics and their filters -- easily pivot to other related telemetry, including logs or traces + You can access Explore Metrics either as a standalone experience or as part of Grafana dashboards. diff --git a/docs/sources/panels-visualizations/configure-tooltips/index.md b/docs/sources/panels-visualizations/configure-tooltips/index.md index 0c486671e99..4b2956e7dc9 100644 --- a/docs/sources/panels-visualizations/configure-tooltips/index.md +++ b/docs/sources/panels-visualizations/configure-tooltips/index.md @@ -67,6 +67,10 @@ Set the hover proximity (in pixels) to control how close the cursor must be to a ![Adding a hover proximity limit for tooltips](/media/docs/grafana/gif-grafana-10-4-hover-proximity.gif) +### Max height + +Set the maximum height of the tooltip box. The default is 600 pixels. + ### Show histogram (Y axis) For the heatmap visualization only, when you set the **Tooltip mode** to **Single**, the **Show histogram (Y axis)** option is displayed. This option controls whether or not the tooltip includes a histogram representing the y-axis. diff --git a/docs/sources/panels-visualizations/visualizations/bar-chart/index.md b/docs/sources/panels-visualizations/visualizations/bar-chart/index.md index 918cae3a833..ed87d1427ea 100644 --- a/docs/sources/panels-visualizations/visualizations/bar-chart/index.md +++ b/docs/sources/panels-visualizations/visualizations/bar-chart/index.md @@ -129,7 +129,13 @@ Transparency of the gradient is calculated based on the values on the y-axis. Op Gradient color is generated based on the hue of the line color. -{{< docs/shared lookup="visualizations/tooltip-mode.md" source="grafana" version="" >}} +## Tooltip options + +{{< docs/shared lookup="visualizations/tooltip-options-1.md" source="grafana" version="" >}} + +## Legend options + +Legend options control the series names and statistics that appear under or to the right of the graph. {{< docs/shared lookup="visualizations/legend-mode.md" source="grafana" version="" >}} @@ -191,7 +197,11 @@ Set a **Soft min** or **soft max** option for better control of Y-axis limits. B You can set standard min/max options to define hard limits of the Y-axis. For more information, refer to [Standard options definitions][]. -{{< docs/shared lookup="visualizations/multiple-y-axes.md" source="grafana" version="" leveloffset="+2" >}} +{{< docs/shared lookup="visualizations/multiple-y-axes.md" source="grafana" version="" leveloffset="+2" >}} + +## Value mappings + +{{< docs/shared lookup="visualizations/value-mappings-options.md" source="grafana" version="" >}} {{% docs/reference %}} [Add a field override]: "/docs/grafana/ -> /docs/grafana//panels-visualizations/configure-overrides#add-a-field-override" diff --git a/docs/sources/panels-visualizations/visualizations/bar-gauge/index.md b/docs/sources/panels-visualizations/visualizations/bar-gauge/index.md index 263d56c3c11..41198f0bb72 100644 --- a/docs/sources/panels-visualizations/visualizations/bar-gauge/index.md +++ b/docs/sources/panels-visualizations/visualizations/bar-gauge/index.md @@ -130,6 +130,10 @@ Automatically show y-axis scrollbar when there's a large amount of data. This option only applies when bar size is set to manual. {{% /admonition %}} +## Value mappings + +{{< docs/shared lookup="visualizations/value-mappings-options.md" source="grafana" version="" >}} + {{% docs/reference %}} [Calculation types]: "/docs/grafana/ -> /docs/grafana//panels-visualizations/query-transform-data/calculation-types" [Calculation types]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/visualizations/panels-visualizations/query-transform-data/calculation-types" diff --git a/docs/sources/panels-visualizations/visualizations/candlestick/index.md b/docs/sources/panels-visualizations/visualizations/candlestick/index.md index 770ac1368d5..099222aa64b 100644 --- a/docs/sources/panels-visualizations/visualizations/candlestick/index.md +++ b/docs/sources/panels-visualizations/visualizations/candlestick/index.md @@ -21,12 +21,55 @@ weight: 100 # Candlestick -The candlestick visualization allows you to visualize data that includes a number of consistent dimensions focused on price movement. The candlestick visualization includes an Open-High-Low-Close (OHLC) mode, as well as support for additional dimensions based on time series data. - -{{< figure src="/static/img/docs/candlestick-panel/candlestick-panel-8-3.png" max-width="1200px" caption="Candlestick visualization" >}} +The candlestick visualization allows you to visualize data that includes a number of consistent dimensions focused on price movements, such as stock prices. The candlestick visualization includes an [Open-High-Low-Close (OHLC) mode](#open-high-low-close), as well as support for additional dimensions based on time series data. Candlestick visualizations build upon the foundation of the [time series visualization][] and include many common configuration settings. +You can use a candlestick if you want to visualize, at a glance, how a price moved over time, whether it went up, down, or stayed the same, and how much it fluctuated: + +{{< figure src="/static/img/docs/candlestick-panel/candlestick-panel-8-3.png" max-width="1065px" alt="A candlestick visualization" >}} + +Each candlestick is represented as a rectangle, referred to as the _candlestick body_. The candlestick body displays the opening and closing prices during a time period. Green candlesticks represent when the price appreciated while the red candlesticks represent when the price depreciated. The lines sticking out the candlestick body are referred to as _wicks_ or _shadows_, which represent the highest and lowest prices during the time period. + +Use a candlestick when you need to: + +- Monitor and identify trends in price movements of specific assets such as stocks, currencies, or commodities. +- Analyze any volatility in the stock market. +- Provide data analysis to help with trading decisions. + +## Configure a candlestick + +Once you’ve created a [dashboard](https://grafana.com/docs/grafana//dashboards/build-dashboards/create-dashboard/), the following video shows you how to configure a candlestick visualization: + +{{< youtube id="IOFKBgbf3aM" >}} + +{{< docs/play title="Candlestick" url="https://play.grafana.org/d/candlestick/candlestick" >}} + +## Supported data formats + +The candlestick visualization works best with price movement data for an asset. The data must include: + +- **Timestamps** - The time at which each price movement occurred. +- **Opening price** - The price of the asset at the beginning of the time period. +- **Closing price** - The price of the asset at the end of the time period. +- **Highest price** - The highest price the asset reached during the time period. +- **Lowest price** - The lowest price the asset reached during the time period. + +### Example + +| Timestamps | Open | High | Low | Close | +| ------------------- | ----- | ----- | ----- | ----- | +| 2024-03-13 10:05:00 | 0.200 | 0.205 | 0.201 | 0.203 | +| 2024-03-14 10:10:10 | 0.204 | 0.205 | 0.201 | 0.200 | +| 2024-03-15 10:15:10 | 0.204 | 0.205 | 0.201 | 0.200 | +| 2024-03-16 10:20:11 | 0.203 | 0.203 | 0.202 | 0.203 | +| 2024-03-17 10:25:11 | 0.203 | 0.203 | 0.202 | 0.203 | +| 2024-03-18 10:30:12 | 0.202 | 0.202 | 0.201 | 0.201 | + +The data is converted as follows: + +{{< figure src="/static/img/docs/candlestick-panel/candlestick.png" max-width="1065px" alt="A candlestick visualization showing the price movements of specific asset." >}} + ## Mode The mode options allow you to toggle which dimensions are used for the visualization. @@ -63,21 +106,21 @@ The candlestick visualization will attempt to map fields from your data to the a The candlestick visualization legend doesn't display these values. {{% /admonition %}} -To properly map these dimensions, the query results table from your data must include _at least_ the following columns: - -- timestamp -- open -- high -- low -- close - If your data can't be mapped to these dimensions for some reason (for example, because the column names aren't the same), you can map them manually using the **Open**, **High**, **Low**, and **Close** fields under the **Candlestick** options in the panel editor: ![Open, High, Low, and Close fields in the panel editor](/media/docs/grafana/panels-visualizations/screenshot-olhc-options-10.3.png) ## Additional fields -The candlestick visualization is based on the time series visualization. It can visualize additional data dimensions beyond open, high, low, close, and volume The **Include** and **Ignore** options allow it to visualize other included data such as simple moving averages, Bollinger bands and more, using the same styles and configurations available in the [time series][] visualization. +The candlestick visualization is based on the time series visualization. It can visualize additional data dimensions beyond open, high, low, close, and volume The **Include** and **Ignore** options allow it to visualize other included data such as simple moving averages, Bollinger bands and more, using the same styles and configurations available in the [time series][time series visualization] visualization. + +## Tooltip options + +{{< docs/shared lookup="visualizations/tooltip-options-2.md" source="grafana" version="" >}} + +## Value mappings + +{{< docs/shared lookup="visualizations/value-mappings-options.md" source="grafana" version="" >}} {{% docs/reference %}} [time series visualization]: "/docs/grafana/ -> /docs/grafana//panels-visualizations/visualizations/time-series" diff --git a/docs/sources/panels-visualizations/visualizations/canvas/index.md b/docs/sources/panels-visualizations/visualizations/canvas/index.md index d853ab32a55..a378485ec5d 100644 --- a/docs/sources/panels-visualizations/visualizations/canvas/index.md +++ b/docs/sources/panels-visualizations/visualizations/canvas/index.md @@ -172,3 +172,7 @@ If multiple elements use the same field name, and you want to control which elem 1. Reference the new unique field alias to create the element and field override. {{< video-embed src="/media/docs/grafana/canvas-data-links-9-4-0.mp4" max-width="750px" caption="Data links demo" >}} + +## Value mappings + +{{< docs/shared lookup="visualizations/value-mappings-options.md" source="grafana" version="" >}} diff --git a/docs/sources/panels-visualizations/visualizations/gauge/index.md b/docs/sources/panels-visualizations/visualizations/gauge/index.md index aa2b3224058..047f33eaccc 100644 --- a/docs/sources/panels-visualizations/visualizations/gauge/index.md +++ b/docs/sources/panels-visualizations/visualizations/gauge/index.md @@ -105,7 +105,11 @@ Adjust the sizes of the gauge text. - **Title -** Enter a numeric value for the gauge title size. - **Value -** Enter a numeric value for the gauge value size. +## Value mappings + +{{< docs/shared lookup="visualizations/value-mappings-options.md" source="grafana" version="" >}} + {{% docs/reference %}} -[Calculation types]: "/docs/grafana/ -> /docs/grafana//panels-visualizations/query-transform-data/calculation-types" +[Calculation types]: "/docs/grafana/ -> /docs/grafana//panels-visualizations/query-transform-data/calculation-types" [Calculation types]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/visualizations/panels-visualizations/query-transform-data/calculation-types" {{% /docs/reference %}} diff --git a/docs/sources/panels-visualizations/visualizations/geomap/index.md b/docs/sources/panels-visualizations/visualizations/geomap/index.md index d3d3cc12e62..ff3df90ac7f 100644 --- a/docs/sources/panels-visualizations/visualizations/geomap/index.md +++ b/docs/sources/panels-visualizations/visualizations/geomap/index.md @@ -603,6 +603,10 @@ Displays debug information in the upper right corner. This can be useful for deb - **None** displays tooltips only when a data point is clicked. - **Details** displays tooltips when a mouse pointer hovers over a data point. +## Value mappings + +{{< docs/shared lookup="visualizations/value-mappings-options.md" source="grafana" version="" >}} + {{% docs/reference %}} [provisioning docs page]: "/docs/grafana/ -> /docs/grafana//administration/provisioning" [provisioning docs page]: "/docs/grafana-cloud/ -> /docs/grafana//administration/provisioning" diff --git a/docs/sources/panels-visualizations/visualizations/heatmap/index.md b/docs/sources/panels-visualizations/visualizations/heatmap/index.md index 70efed729ca..18c757800e4 100644 --- a/docs/sources/panels-visualizations/visualizations/heatmap/index.md +++ b/docs/sources/panels-visualizations/visualizations/heatmap/index.md @@ -150,9 +150,25 @@ Use these settings to refine your visualization. ### Tooltip -- **Show tooltip -** Show heatmap tooltip. -- **Show Histogram -** Show a Y-axis histogram on the tooltip. A histogram represents the distribution of the bucket values for a specific timestamp. -- **Show color scale -** Show a color scale on the tooltip. The color scale represents the mapping between bucket value and color. +#### Tooltip mode + +When you hover your cursor over the visualization, Grafana can display tooltips. Choose how tooltips behave. + +- **Single -** The hover tooltip shows only a single series, the one that you are hovering over on the visualization. +- **All -** The hover tooltip shows all series in the visualization. Grafana highlights the series that you are hovering over in bold in the series list in the tooltip. +- **Hidden -** Do not display the tooltip when you interact with the visualization. + +Use an override to hide individual series from the tooltip. + +#### Show histogram (Y axis) + +When you set the **Tooltip mode** to **Single**, this option is displayed. This option controls whether or not the tooltip includes a histogram representing the y-axis. + +#### Show color scale + +When you set the **Tooltip mode** to **Single**, this option is displayed. This option controls whether or not the tooltip includes the color scale that's also represented in the legend. When the color scale is included in the tooltip, it shows the hovered value on the scale: + +![Heatmap with a tooltip displayed showing the hovered value reflected in the color scale](/media/docs/grafana/panels-visualizations/screenshot-heatmap-tooltip-color-scale-v11.0.png) ### Legend diff --git a/docs/sources/panels-visualizations/visualizations/histogram/index.md b/docs/sources/panels-visualizations/visualizations/histogram/index.md index 1c2583154d8..37dc3c95e28 100644 --- a/docs/sources/panels-visualizations/visualizations/histogram/index.md +++ b/docs/sources/panels-visualizations/visualizations/histogram/index.md @@ -127,6 +127,10 @@ Gradient color is generated based on the hue of the line color. Choose a [standard calculations][] to show in the legend. You can select more than one. +## Value mappings + +{{< docs/shared lookup="visualizations/value-mappings-options.md" source="grafana" version="" >}} + {{% docs/reference %}} [color scheme]: "/docs/grafana/ -> /docs/grafana//panels-visualizations/configure-standard-options#color-scheme" [color scheme]: "/docs/grafana-cloud/ -> /docs/grafana//panels-visualizations/configure-standard-options#color-scheme" diff --git a/docs/sources/panels-visualizations/visualizations/pie-chart/index.md b/docs/sources/panels-visualizations/visualizations/pie-chart/index.md index ffb8f272911..c10605930f3 100644 --- a/docs/sources/panels-visualizations/visualizations/pie-chart/index.md +++ b/docs/sources/panels-visualizations/visualizations/pie-chart/index.md @@ -80,7 +80,9 @@ The following example shows a pie chart with **Name** and **Percent** labels dis ![Pie chart labels](/static/img/docs/pie-chart-panel/pie-chart-labels-7-5.png) -{{< docs/shared lookup="visualizations/tooltip-mode.md" source="grafana" version="" >}} +## Tooltip options + +{{< docs/shared lookup="visualizations/tooltip-options-1.md" source="grafana" version="" >}} ## Legend options @@ -111,6 +113,10 @@ Select values to display in the legend. You can select more than one. - **Percent:** The percentage of the whole. - **Value:** The raw numerical value. +## Value mappings + +{{< docs/shared lookup="visualizations/value-mappings-options.md" source="grafana" version="" >}} + {{% docs/reference %}} [Calculation types]: "/docs/grafana/ -> /docs/grafana//panels-visualizations/query-transform-data/calculation-types" [Calculation types]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/visualizations/panels-visualizations/query-transform-data/calculation-types" diff --git a/docs/sources/panels-visualizations/visualizations/stat/index.md b/docs/sources/panels-visualizations/visualizations/stat/index.md index 87e74088d0b..8a7bc8632e4 100644 --- a/docs/sources/panels-visualizations/visualizations/stat/index.md +++ b/docs/sources/panels-visualizations/visualizations/stat/index.md @@ -189,6 +189,10 @@ Adjust the sizes of the gauge text. - **Title -** Enter a numeric value for the gauge title size. - **Value -** Enter a numeric value for the gauge value size. +## Value mappings + +{{< docs/shared lookup="visualizations/value-mappings-options.md" source="grafana" version="" >}} + {{% docs/reference %}} [Calculation types]: "/docs/grafana/ -> /docs/grafana//panels-visualizations/query-transform-data/calculation-types" [Calculation types]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/visualizations/panels-visualizations/query-transform-data/calculation-types" diff --git a/docs/sources/panels-visualizations/visualizations/state-timeline/index.md b/docs/sources/panels-visualizations/visualizations/state-timeline/index.md index ff8ede7aeb5..3bf01a2a768 100644 --- a/docs/sources/panels-visualizations/visualizations/state-timeline/index.md +++ b/docs/sources/panels-visualizations/visualizations/state-timeline/index.md @@ -136,6 +136,14 @@ When the legend option is enabled it can show either the value mappings or the t {{< docs/shared lookup="visualizations/legend-mode.md" source="grafana" version="" >}} +## Tooltip options + +{{< docs/shared lookup="visualizations/tooltip-options-1.md" source="grafana" version="" >}} + +## Value mappings + +{{< docs/shared lookup="visualizations/value-mappings-options.md" source="grafana" version="" >}} + {{% docs/reference %}} [Color scheme]: "/docs/grafana/ -> /docs/grafana//panels-visualizations/configure-standard-options#color-scheme" [Color scheme]: "/docs/grafana-cloud/ -> /docs/grafana//panels-visualizations/configure-standard-options#color-scheme" diff --git a/docs/sources/panels-visualizations/visualizations/status-history/index.md b/docs/sources/panels-visualizations/visualizations/status-history/index.md index e73cf0b3281..c478ee225f7 100644 --- a/docs/sources/panels-visualizations/visualizations/status-history/index.md +++ b/docs/sources/panels-visualizations/visualizations/status-history/index.md @@ -116,6 +116,14 @@ When the legend option is enabled it can show either the value mappings or the t {{< docs/shared lookup="visualizations/legend-mode.md" source="grafana" version="" >}} +## Tooltip options + +{{< docs/shared lookup="visualizations/tooltip-options-1.md" source="grafana" version="" >}} + +## Value mappings + +{{< docs/shared lookup="visualizations/value-mappings-options.md" source="grafana" version="" >}} + {{% docs/reference %}} [Value mappings]: "/docs/grafana/ -> /docs/grafana//panels-visualizations/configure-value-mappings" [Value mappings]: "/docs/grafana-cloud/ -> /docs/grafana//panels-visualizations/configure-value-mappings" diff --git a/docs/sources/panels-visualizations/visualizations/table/index.md b/docs/sources/panels-visualizations/visualizations/table/index.md index a23ce3c233d..2c3a93b2f76 100644 --- a/docs/sources/panels-visualizations/visualizations/table/index.md +++ b/docs/sources/panels-visualizations/visualizations/table/index.md @@ -234,6 +234,10 @@ The system applies the calculation to all numeric fields if you do not select a If you want to show the number of rows in the dataset instead of the number of values in the selected fields, select the **Count** calculation and enable **Count rows**. +## Value mappings + +{{< docs/shared lookup="visualizations/value-mappings-options.md" source="grafana" version="" >}} + {{% docs/reference %}} [calculations]: "/docs/grafana/ -> /docs/grafana//panels-visualizations/query-transform-data/calculation-types" [calculations]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/visualizations/panels-visualizations/query-transform-data/calculation-types" diff --git a/docs/sources/panels-visualizations/visualizations/text/index.md b/docs/sources/panels-visualizations/visualizations/text/index.md index 51276ae365a..f9ed03275af 100644 --- a/docs/sources/panels-visualizations/visualizations/text/index.md +++ b/docs/sources/panels-visualizations/visualizations/text/index.md @@ -37,6 +37,10 @@ Use a text visualization when you need to: **Mode** determines how embedded content appears. +{{< admonition type="note" >}} +To allow embedding of iframes and other websites, you need set `allow_embedding = true` in your Grafana `config.ini` or environment variables (depending on your employment). +{{< /admonition >}} + ### Markdown This option formats the content as [markdown](https://en.wikipedia.org/wiki/Markdown). diff --git a/docs/sources/panels-visualizations/visualizations/time-series/index.md b/docs/sources/panels-visualizations/visualizations/time-series/index.md index 1846d877077..91eb649aa1c 100644 --- a/docs/sources/panels-visualizations/visualizations/time-series/index.md +++ b/docs/sources/panels-visualizations/visualizations/time-series/index.md @@ -52,19 +52,13 @@ The following video guides you through the creation steps and common customizati ## Tooltip options -Tooltip options control the information overlay that appears when you hover over data points in the graph. - -{{< docs/shared lookup="visualizations/tooltip-mode.md" source="grafana" version="" >}} - -### Hover proximity - -This option controls how close your cursor must be to a data point before the tooltip appears. Values are in pixels. +{{< docs/shared lookup="visualizations/tooltip-options-2.md" source="grafana" version="" >}} ## Legend options -Legend options control the series names and statistics that appear under or to the right of the graph. +Legend options control the series names and statistics that appear under or to the right of the visualization. -{{< docs/shared lookup="visualizations/legend-mode.md" source="grafana" version="" >}} +{{< docs/shared lookup="visualizations/legend-mode.md" source="grafana" version="" >}} ## Graph styles @@ -309,6 +303,10 @@ The following image shows a bar chart with the **Green-Yellow-Red (by value)** c {{< figure src="/static/img/docs/time-series-panel/gradient_mode_scheme_bars.png" max-width="1200px" caption="Color scheme: Green-Yellow-Red" >}} +## Value mappings + +{{< docs/shared lookup="visualizations/value-mappings-options.md" source="grafana" version="" >}} + {{% docs/reference %}} [Color scheme]: "/docs/grafana/ -> /docs/grafana//panels-visualizations/configure-standard-options#color-scheme" [Color scheme]: "/docs/grafana-cloud/ -> /docs/grafana//panels-visualizations/configure-standard-options#color-scheme" diff --git a/docs/sources/panels-visualizations/visualizations/trend/index.md b/docs/sources/panels-visualizations/visualizations/trend/index.md index c7ae5cef5e4..1fa72c8d947 100644 --- a/docs/sources/panels-visualizations/visualizations/trend/index.md +++ b/docs/sources/panels-visualizations/visualizations/trend/index.md @@ -38,6 +38,14 @@ For example, you could represent engine power and torque versus speed where spee {{< figure src="/media/docs/grafana/screenshot-grafana-10-0-trend-panel-new-colors.png" max-width="750px" caption="Trend engine power and torque curves" >}} +## Tooltip options + +{{< docs/shared lookup="visualizations/tooltip-options-2.md" source="grafana" version="" >}} + +## Value mappings + +{{< docs/shared lookup="visualizations/value-mappings-options.md" source="grafana" version="" >}} + {{% docs/reference %}} [Time series visualization]: "/docs/grafana/ -> /docs/grafana//panels-visualizations/visualizations/time-series" [Time series visualization]: "/docs/grafana-cloud/ -> /docs/grafana//panels-visualizations/visualizations/time-series" diff --git a/docs/sources/panels-visualizations/visualizations/xy-chart/index.md b/docs/sources/panels-visualizations/visualizations/xy-chart/index.md index fcf479b34a2..141e83966f7 100644 --- a/docs/sources/panels-visualizations/visualizations/xy-chart/index.md +++ b/docs/sources/panels-visualizations/visualizations/xy-chart/index.md @@ -132,9 +132,20 @@ Set the width of the lines in pixels. ## Tooltip options -Tooltip options control the information overlay that appears when you hover over data points in the graph. +Tooltip options control the information overlay that appears when you hover over data points in the visualization. -{{< docs/shared lookup="visualizations/tooltip-options-2.md" source="grafana" version="" >}} +### Tooltip mode + +When you hover your cursor over the visualization, Grafana can display tooltips. Choose how tooltips behave. + +- **Single -** The hover tooltip shows only a single series, the one that you are hovering over on the visualization. +- **Hidden -** Do not display the tooltip when you interact with the visualization. + +Use an override to hide individual series from the tooltip. + +### Max height + +Set the maximum height of the tooltip box. The default is 600 pixels. ## Legend options diff --git a/docs/sources/setup-grafana/configure-grafana/_index.md b/docs/sources/setup-grafana/configure-grafana/_index.md index 114f1ae6bd7..0cbd5fa157a 100644 --- a/docs/sources/setup-grafana/configure-grafana/_index.md +++ b/docs/sources/setup-grafana/configure-grafana/_index.md @@ -1779,6 +1779,16 @@ Enable or disable the Query history. Default is `enabled`.
+## [short_links] + +Configures settings around the short link feature. + +### expire_time + +Short links which are never accessed are considered expired or stale, and will be deleted as cleanup. Set the expiration time in days. Default is `7` days. Maximum is `365` days, and setting above the maximum will have `365` set instead. Setting `0` means the short links will be cleaned up approximately every 10 minutes. + +
+ ## [metrics] For detailed instructions, refer to [Internal Grafana metrics]({{< relref "../set-up-grafana-monitoring" >}}). 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 9e04b40cf6a..9c71a50e4d3 100644 --- a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md +++ b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md @@ -26,6 +26,7 @@ Some features are enabled by default. You can disable these feature by setting t | `featureHighlights` | Highlight Grafana Enterprise features | | | `correlations` | Correlations page | | | `exploreContentOutline` | Content outline sidebar | Yes | +| `returnToPrevious` | Enables the return to previous context functionality | Yes | | `cloudWatchCrossAccountQuerying` | Enables cross-account querying in CloudWatch datasources | Yes | | `nestedFolders` | Enable folder nesting | Yes | | `nestedFolderPicker` | Enables the new folder picker to work with nested folders. Requires the nestedFolders feature toggle | Yes | @@ -74,7 +75,6 @@ Some features are enabled by default. You can disable these feature by setting t | `autoMigrateStatPanel` | Migrate old stat panel to supported stat panel - broken out from autoMigrateOldPanels to enable granular tracking | | `autoMigrateXYChartPanel` | Migrate old XYChart panel to new XYChart2 model | | `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. | -| `returnToPrevious` | Enables the return to previous context functionality | | `grpcServer` | Run the GRPC server | | `accessControlOnCall` | Access control primitives for OnCall | | `alertingNoNormalState` | Stop maintaining state of alerts that are not firing | @@ -176,6 +176,7 @@ Experimental features might be changed or removed without prior notice. | `expressionParser` | Enable new expression parser | | `accessActionSets` | Introduces action sets for resource permissions | | `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 | ## Development feature toggles diff --git a/docs/sources/setup-grafana/configure-security/configure-authentication/azuread/index.md b/docs/sources/setup-grafana/configure-security/configure-authentication/azuread/index.md index 8d0b79451e7..9dbda4334f9 100644 --- a/docs/sources/setup-grafana/configure-security/configure-authentication/azuread/index.md +++ b/docs/sources/setup-grafana/configure-security/configure-authentication/azuread/index.md @@ -49,7 +49,7 @@ To enable the Azure AD OAuth2, register your application with Azure AD. 1. Click **Certificates & secrets**, then add a new entry under **Client secrets** with the following configuration. - Description: Grafana OAuth - - Expires: Never + - Expires: Select an expiration period 1. Click **Add** then copy the key value. This is the OAuth client secret. diff --git a/docs/sources/setup-grafana/installation/kubernetes/index.md b/docs/sources/setup-grafana/installation/kubernetes/index.md index 5db13ecea67..d6ca75911bb 100644 --- a/docs/sources/setup-grafana/installation/kubernetes/index.md +++ b/docs/sources/setup-grafana/installation/kubernetes/index.md @@ -499,6 +499,98 @@ By default, Kubernetes deployment rollout history remains in the system so that If you need to go back to any other `REVISION`, just repeat the steps above and use the correct revision number in the `--to-revision` parameter. +## Provision Grafana resources using configuration files + +Provisioning can add, update, or delete resources specified in your configuration files when Grafana starts. For detailed information, refer to [Grafana Provisioning](/docs/grafana//administration/provisioning). + +This section outlines general instructions for provisioning Grafana resources within Kubernetes, using a persistent volume to supply the configuration files to the Grafana pod. + +1. Add a new `PersistentVolumeClaim` to the `grafana.yaml` file. + + ```yaml + --- + apiVersion: v1 + kind: PersistentVolumeClaim + metadata: + name: grafana-provisioning-pvc + spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 1Mi + ``` + +1. In the `grafana.yaml` file, mount the persistent volume into `/etc/grafana/provisioning` as follows. + + ```yaml + ... + volumeMounts: + - mountPath: /etc/grafana/provisioning + name: grafana-provisioning-pv + ... + volumes: + - name: grafana-provisioning-pv + persistentVolumeClaim: + claimName: grafana-provisioning-pvc + ... + ``` + +1. Find or create the provision resources you want to add. For instance, create a `alerting.yaml` file adding a mute timing (alerting resource). + + ```yaml + apiVersion: 1 + muteTimes: + - orgId: 1 + name: MuteWeekends + time_intervals: + - weekdays: [saturday, sunday] + ``` + +1. By default, configuration files for alerting resources need to be placed in the `provisioning/alerting` directory. + + Save the `alerting.yaml` file in a directory named `alerting`, as we will next supply this `alerting` directory to the `/etc/grafana/provisioning` folder of the Grafana pod. + +1. Verify first the content of the provisioning directory in the running Grafana pod. + + ```bash + kubectl exec -n my-grafana -- ls /etc/grafana/provisioning/ + ``` + + ```bash + kubectl exec -n my-grafana -- ls /etc/grafana/provisioning/alerting + ``` + + Because the `alerting` folder is not available yet, the last command should output a `No such file or directory` error. + +1. Copy the local `alerting` directory to `/etc/grafana/provisioning/` in the Grafana pod. + + ```bash + kubectl cp alerting my-grafana/:/etc/grafana/provisioning/ + ``` + + You can follow the same process to provision additional Grafana resources by supplying the following folders: + + - `provisioning/dashboards` + - `provisioning/datasources` + - `provisioning/plugins` + +1. Verify the `alerting` directory in the running Grafana pod includes the `alerting.yaml` file. + + ```bash + kubectl exec -n my-grafana -- ls /etc/grafana/provisioning/alerting + ``` + +1. Restart the Grafana pod to provision the resources. + + ```bash + kubectl rollout restart -n my-grafana deployment --selector=app=grafana + ``` + + Note that `rollout restart` kills the previous pod and scales a new pod. When the old pod terminates, you may have to enable port-forwarding in the new pod. For instructions, refer to the previous sections about port forwarding in this guide. + +1. Verify the Grafana resources are properly provisioned within the Grafana instance. + ## Troubleshooting This section includes troubleshooting tips you might find helpful when deploying Grafana on Kubernetes. diff --git a/docs/sources/shared/visualizations/tooltip-mode.md b/docs/sources/shared/visualizations/tooltip-mode.md deleted file mode 100644 index 75c2c4d2358..00000000000 --- a/docs/sources/shared/visualizations/tooltip-mode.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -title: Tooltip mode ---- - -### Tooltip mode - -When you hover your cursor over the visualization, Grafana can display tooltips. Choose how tooltips behave. - -- **Single -** The hover tooltip shows only a single series, the one that you are hovering over on the visualization. -- **All -** The hover tooltip shows all series in the visualization. Grafana highlights the series that you are hovering over in bold in the series list in the tooltip. -- **Hidden -** Do not display the tooltip when you interact with the visualization. - -Use an override to hide individual series from the tooltip. diff --git a/docs/sources/shared/visualizations/tooltip-options-1.md b/docs/sources/shared/visualizations/tooltip-options-1.md new file mode 100644 index 00000000000..1ff0a90c821 --- /dev/null +++ b/docs/sources/shared/visualizations/tooltip-options-1.md @@ -0,0 +1,31 @@ +--- +title: Tooltip mode +comments: | + There are two tooltip shared files, tooltip-options-1.md and tooltip-options-2.md, to cover the most common combinations of options. + Using two shared files ensures that content remains consistent across visualizations that share the same options and users don't have to figure out which options apply to a specific visualization when reading that content. + This file is used in the following visualizations: bar chart, pie chart, state timeline, status history +--- + +Tooltip options control the information overlay that appears when you hover over data points in the visualization. + +### Tooltip mode + +When you hover your cursor over the visualization, Grafana can display tooltips. Choose how tooltips behave. + +- **Single -** The hover tooltip shows only a single series, the one that you are hovering over on the visualization. +- **All -** The hover tooltip shows all series in the visualization. Grafana highlights the series that you are hovering over in bold in the series list in the tooltip. +- **Hidden -** Do not display the tooltip when you interact with the visualization. + +Use an override to hide individual series from the tooltip. + +### Values sort order + +When you set the **Tooltip mode** to **All**, the **Values sort order** option is displayed. This option controls the order in which values are listed in a tooltip. Choose from the following: + +- **None** - Grafana automatically sorts the values displayed in a tooltip. +- **Ascending** - Values in the tooltip are listed from smallest to largest. +- **Descending** - Values in the tooltip are listed from largest to smallest. + +### Max height + +Set the maximum height of the tooltip box. The default is 600 pixels. diff --git a/docs/sources/shared/visualizations/tooltip-options-2.md b/docs/sources/shared/visualizations/tooltip-options-2.md index 9bb487fb9c5..2a5d35e2bd0 100644 --- a/docs/sources/shared/visualizations/tooltip-options-2.md +++ b/docs/sources/shared/visualizations/tooltip-options-2.md @@ -1,19 +1,36 @@ --- title: Tooltip options +comments: | + There are two tooltip shared files, tooltip-options-1.md and tooltip-options-2.md, to cover the most common combinations of options. + Using two shared files ensures that content remains consistent across visualizations that share the same options and users don't have to figure out which options apply to a specific visualization when reading that content. + This file is used in the following visualizations: candlestick, time series, trend --- +Tooltip options control the information overlay that appears when you hover over data points in the visualization. + ### Tooltip mode When you hover your cursor over the visualization, Grafana can display tooltips. Choose how tooltips behave. - **Single -** The hover tooltip shows only a single series, the one that you are hovering over on the visualization. +- **All -** The hover tooltip shows all series in the visualization. Grafana highlights the series that you are hovering over in bold in the series list in the tooltip. - **Hidden -** Do not display the tooltip when you interact with the visualization. Use an override to hide individual series from the tooltip. -### Max width +### Values sort order -Set the maximum width of the tooltip box. The default is 300 pixels. +When you set the **Tooltip mode** to **All**, the **Values sort order** option is displayed. This option controls the order in which values are listed in a tooltip. Choose from the following: + +- **None** - Grafana automatically sorts the values displayed in a tooltip. +- **Ascending** - Values in the tooltip are listed from smallest to largest. +- **Descending** - Values in the tooltip are listed from largest to smallest. + +### Hover proximity + +Set the hover proximity (in pixels) to control how close the cursor must be to a data point to trigger the tooltip to display. + +![Adding a hover proximity limit for tooltips](/media/docs/grafana/gif-grafana-10-4-hover-proximity.gif) ### Max height diff --git a/docs/sources/shared/visualizations/value-mappings-options.md b/docs/sources/shared/visualizations/value-mappings-options.md new file mode 100644 index 00000000000..937801addec --- /dev/null +++ b/docs/sources/shared/visualizations/value-mappings-options.md @@ -0,0 +1,20 @@ +--- +title: Value mappings options +comments: | + This file is used in the following visualizations: bar chart, bar gauge, candlestick, canvas, gauge, geomap, histogram, pie chart, stat, state timeline, status history, table, time series, trend +--- + +Value mapping is a technique you can use to change how data appears in a visualization. + +For each value mapping, set the following options: + +- **Condition** - Choose what's mapped to the display text and (optionally) color: + - **Value** - Specific values + - **Range** - Numerical ranges + - **Regex** - Regular expressions + - **Special** - Special values like `Null`, `NaN` (not a number), or boolean values like `true` and `false` +- **Display text** +- **Color** (Optional) +- **Icon** (Canvas only) + +To learn more, refer to [Configure value mappings](../../configure-value-mappings/). diff --git a/e2e/various-suite/filter-annotations.spec.ts b/e2e/various-suite/filter-annotations.spec.ts index 23d64eb0826..26cf83f00e2 100644 --- a/e2e/various-suite/filter-annotations.spec.ts +++ b/e2e/various-suite/filter-annotations.spec.ts @@ -18,19 +18,21 @@ describe('Annotations filtering', () => { .should('be.visible') .within(() => { // All panels - e2e.components.Annotations.annotationsTypeInput().click({ force: true }).type('All panels{enter}'); + e2e.components.Annotations.annotationsTypeInput().find('input').type('All panels{enter}', { force: true }); e2e.components.Annotations.annotationsChoosePanelInput().should('not.exist'); // All panels except - e2e.components.Annotations.annotationsTypeInput().click({ force: true }).type('All panels except{enter}'); + e2e.components.Annotations.annotationsTypeInput() + .find('input') + .type('All panels except{enter}', { force: true }); e2e.components.Annotations.annotationsChoosePanelInput().should('be.visible'); // Selected panels - e2e.components.Annotations.annotationsTypeInput().click({ force: true }).type('Selected panels{enter}'); + e2e.components.Annotations.annotationsTypeInput().find('input').type('Selected panels{enter}', { force: true }); e2e.components.Annotations.annotationsChoosePanelInput() .should('be.visible') - .click({ force: true }) - .type('Panel two{enter}'); + .find('input') + .type('Panel two{enter}', { force: true }); }); e2e.pages.Dashboard.Settings.Annotations.NewAnnotation.previewInDashboard().click({ force: true }); diff --git a/e2e/various-suite/loki-query-builder.spec.ts b/e2e/various-suite/loki-query-builder.spec.ts index 1707a32dd85..45b527fca5b 100644 --- a/e2e/various-suite/loki-query-builder.spec.ts +++ b/e2e/various-suite/loki-query-builder.spec.ts @@ -72,9 +72,9 @@ describe('Loki query builder', () => { // Add labels to remove error e2e.components.QueryBuilder.labelSelect().should('be.visible').click(); // wait until labels are loaded and set on the component before starting to type - e2e.components.QueryBuilder.labelSelect().children('div').children('input').type('i'); + e2e.components.QueryBuilder.inputSelect().type('i'); cy.wait('@labelsRequest'); - e2e.components.QueryBuilder.labelSelect().children('div').children('input').type('nstance{enter}'); + e2e.components.QueryBuilder.inputSelect().type('nstance{enter}'); e2e.components.QueryBuilder.matchOperatorSelect() .should('be.visible') .click({ force: true }) diff --git a/go.mod b/go.mod index ef4b86c7da1..0b2dfc7df6d 100644 --- a/go.mod +++ b/go.mod @@ -15,355 +15,432 @@ replace cuelang.org/go => github.com/grafana/cue v0.0.0-20230926092038-971951014 // import that instead of v0.X even though v0.X is newer. replace github.com/prometheus/prometheus => github.com/prometheus/prometheus v0.49.0 +// Update when github.com/deepmap/oapi-codegen/v2 is updated to support later versions. +replace github.com/getkin/kin-openapi => github.com/getkin/kin-openapi v0.122.0 + require ( + buf.build/gen/go/parca-dev/parca/bufbuild/connect-go v1.4.1-20221222094228-8b1d3d0f62e6.1 // @grafana/observability-traces-and-profiling + buf.build/gen/go/parca-dev/parca/protocolbuffers/go v1.33.0-20240414232344-9ca06271cb73.1 // @grafana/observability-traces-and-profiling + cloud.google.com/go/kms v1.15.7 // @grafana/grafana-backend-group cloud.google.com/go/storage v1.37.0 // @grafana/grafana-backend-group cuelang.org/go v0.6.0-0.dev // @grafana/grafana-as-code + filippo.io/age v1.1.1 // @grafana/identity-access-team github.com/Azure/azure-sdk-for-go v68.0.0+incompatible // @grafana/partner-datasources + github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.5.1 // @grafana/grafana-backend-group + github.com/Azure/azure-sdk-for-go/sdk/keyvault/azkeys v0.9.0 // @grafana/grafana-backend-group + 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/Masterminds/semver v1.5.0 // @grafana/grafana-backend-group + github.com/Masterminds/semver/v3 v3.1.1 // @grafana/grafana-release-guild + github.com/Masterminds/sprig/v3 v3.2.2 // @grafana/grafana-backend-group + github.com/ProtonMail/go-crypto v0.0.0-20230828082145-3c4c8a2d2371 // @grafana/plugins-platform-backend github.com/VividCortex/mysqlerr v0.0.0-20170204212430-6c6b55f8796f // @grafana/grafana-backend-group - github.com/aws/aws-sdk-go v1.50.8 // @grafana/aws-datasources + github.com/alicebob/miniredis/v2 v2.30.1 // @grafana/alerting-squad-backend + github.com/andybalholm/brotli v1.0.5 // @grafana/partner-datasources + github.com/apache/arrow/go/v15 v15.0.2 // @grafana/observability-metrics + github.com/armon/go-radix v1.0.0 // @grafana/grafana-app-platform-squad + github.com/aws/aws-sdk-go v1.50.29 // @grafana/aws-datasources github.com/beevik/etree v1.2.0 // @grafana/grafana-backend-group github.com/benbjohnson/clock v1.3.5 // @grafana/alerting-squad-backend github.com/blang/semver/v4 v4.0.0 // @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/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.30.2 // @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/deepmap/oapi-codegen/v2 v2.1.0 // @grafana/grafana-as-code + github.com/dlmiddlecote/sqlstats v1.0.2 // @grafana/grafana-backend-group + github.com/docker/docker v24.0.7+incompatible // @grafana/grafana-release-guild + github.com/drone/drone-cli v1.6.1 // @grafana/grafana-release-guild github.com/fatih/color v1.15.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.124.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 + github.com/go-openapi/loads v0.21.5 // @grafana/alerting-squad-backend + github.com/go-openapi/runtime v0.27.1 // @grafana/alerting-squad-backend github.com/go-openapi/strfmt v0.22.0 // @grafana/alerting-squad-backend github.com/go-redis/redis/v8 v8.11.5 // @grafana/grafana-backend-group github.com/go-sourcemap/sourcemap v2.1.3+incompatible // @grafana/grafana-backend-group github.com/go-sql-driver/mysql v1.7.1 // @grafana/grafana-search-and-storage github.com/go-stack/stack v1.8.1 // @grafana/grafana-backend-group github.com/gobwas/glob v0.2.3 // @grafana/grafana-backend-group - github.com/gofrs/uuid v4.4.0+incompatible // indirect github.com/gogo/protobuf v1.3.2 // @grafana/alerting-squad-backend + github.com/golang-jwt/jwt/v4 v4.5.0 // @grafana/grafana-backend-group + github.com/golang-migrate/migrate/v4 v4.7.0 // @grafana/grafana-backend-group github.com/golang/mock v1.6.0 // @grafana/alerting-squad-backend + github.com/golang/protobuf v1.5.4 // @grafana/grafana-backend-group github.com/golang/snappy v0.0.4 // @grafana/alerting-squad-backend github.com/google/go-cmp v0.6.0 // @grafana/grafana-backend-group + github.com/google/go-github v17.0.0+incompatible // @grafana/grafana-release-guild + github.com/google/go-github/v45 v45.2.0 // @grafana/grafana-release-guild github.com/google/uuid v1.6.0 // @grafana/grafana-backend-group github.com/google/wire v0.5.0 // @grafana/grafana-backend-group + github.com/googleapis/gax-go/v2 v2.12.3 // @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-20240409171830-e039a7f57a92 // @grafana/alerting-squad-backend + github.com/grafana/alerting v0.0.0-20240424080142-bb4f4f429d36 // @grafana/alerting-squad-backend + github.com/grafana/authlib v0.0.0-20240328140636-a7388d0bac72 // @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 + github.com/grafana/dataplane/sdata v0.0.9 // @grafana/observability-metrics + github.com/grafana/dskit v0.0.0-20240104111617-ea101a3b86eb // @grafana/grafana-backend-group + github.com/grafana/gofpdf v0.0.0-20231002120153-857cc45be447 // @grafana/sharing-squad github.com/grafana/grafana-aws-sdk v0.25.0 // @grafana/aws-datasources - github.com/grafana/grafana-azure-sdk-go/v2 v2.0.1 // @grafana/partner-datasources - github.com/grafana/grafana-plugin-sdk-go v0.224.0 // @grafana/plugins-platform-backend - github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 // @grafana/grafana-backend-group + github.com/grafana/grafana-azure-sdk-go/v2 v2.0.2 // @grafana/partner-datasources + 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.228.0 // @grafana/plugins-platform-backend + github.com/grafana/grafana/pkg/apimachinery v0.0.0-20240226124929-648abdbd0ea4 // @grafana/grafana-app-platform-squad + github.com/grafana/grafana/pkg/apiserver v0.0.0-20240226124929-648abdbd0ea4 // @grafana/grafana-app-platform-squad + // This needs to be here for other projects that import grafana/grafana + // For local development grafana/grafana will always use the local files + // Check go.work file for details + github.com/grafana/grafana/pkg/promlib v0.0.5 // @grafana/observability-metrics + github.com/grafana/pyroscope-go/godeltaprof v0.1.6 // @grafana/observability-traces-and-profiling + github.com/grafana/pyroscope/api v0.3.0 // @grafana/observability-traces-and-profiling + github.com/grafana/tempo v1.5.1-0.20230524121406-1dc1bfe7085b // @grafana/observability-traces-and-profiling + github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.0.1 // @grafana/plugins-platform-backend + github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.1.0 // @grafana/grafana-backend-group github.com/hashicorp/go-hclog v1.6.3 // @grafana/plugins-platform-backend + github.com/hashicorp/go-multierror v1.1.1 // @grafana/alerting-squad github.com/hashicorp/go-plugin v1.6.0 // @grafana/plugins-platform-backend github.com/hashicorp/go-version v1.6.0 // @grafana/grafana-backend-group + github.com/hashicorp/golang-lru/v2 v2.0.7 // @grafana/alerting-squad-backend github.com/hashicorp/hcl/v2 v2.17.0 // @grafana/alerting-squad-backend - github.com/influxdata/influxdb-client-go/v2 v2.12.3 // @grafana/observability-metrics - github.com/influxdata/line-protocol v0.0.0-20210311194329-9aa0e372d097 // @grafana/grafana-app-platform-squad + github.com/huandu/xstrings v1.3.2 // @grafana/partner-datasources + github.com/influxdata/influxdb-client-go/v2 v2.13.0 // @grafana/observability-metrics + github.com/influxdata/line-protocol v0.0.0-20210922203350-b1ad95c89adf // @grafana/grafana-app-platform-squad github.com/jmespath/go-jmespath v0.4.0 // @grafana/grafana-backend-group + github.com/jmoiron/sqlx v1.3.5 // @grafana/grafana-backend-group github.com/json-iterator/go v1.1.12 // @grafana/grafana-backend-group + github.com/krasun/gosqlparser v1.0.5 // @grafana/grafana-app-platform-squad github.com/lib/pq v1.10.9 // @grafana/grafana-backend-group github.com/linkedin/goavro/v2 v2.10.0 // @grafana/grafana-backend-group github.com/m3db/prometheus_remote_client_golang v0.4.4 // @grafana/grafana-backend-group github.com/magefile/mage v1.15.0 // @grafana/grafana-release-guild - github.com/mattn/go-isatty v0.0.19 // @grafana/grafana-backend-group + github.com/matryer/is v1.4.0 // @grafana/grafana-as-code + github.com/mattn/go-isatty v0.0.20 // @grafana/grafana-backend-group github.com/mattn/go-sqlite3 v1.14.19 // @grafana/grafana-backend-group github.com/matttproud/golang_protobuf_extensions v1.0.4 // @grafana/alerting-squad-backend + github.com/microsoft/go-mssqldb v1.6.1-0.20240214161942-b65008136246 // @grafana/grafana-bi-squad + github.com/mitchellh/mapstructure v1.5.0 //@grafana/identity-access-team + github.com/modern-go/reflect2 v1.0.2 // @grafana/alerting-squad-backend + github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // @grafana/alerting-squad-backend github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f // @grafana/grafana-operator-experience-squad - github.com/opentracing/opentracing-go v1.2.0 // indirect + github.com/olekukonko/tablewriter v0.0.5 // @grafana/grafana-backend-group github.com/patrickmn/go-cache v2.1.0+incompatible // @grafana/alerting-squad-backend - github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect - github.com/pkg/errors v0.9.1 // indirect github.com/prometheus/alertmanager v0.26.0 // @grafana/alerting-squad-backend - github.com/prometheus/client_golang v1.18.0 // @grafana/alerting-squad-backend - github.com/prometheus/client_model v0.5.0 // @grafana/grafana-backend-group - github.com/prometheus/common v0.46.0 // @grafana/alerting-squad-backend + github.com/prometheus/client_golang v1.19.0 // @grafana/alerting-squad-backend + github.com/prometheus/client_model v0.6.1 // @grafana/grafana-backend-group + github.com/prometheus/common v0.53.0 // @grafana/alerting-squad-backend github.com/prometheus/prometheus v1.8.2-0.20221021121301-51a44e6657c3 // @grafana/alerting-squad-backend + github.com/redis/go-redis/v9 v9.1.0 // @grafana/alerting-squad-backend github.com/robfig/cron/v3 v3.0.1 // @grafana/grafana-backend-group github.com/russellhaering/goxmldsig v1.4.0 // @grafana/grafana-backend-group - github.com/scottlepp/go-duck v0.0.15 // @grafana/grafana-app-platform-squad + github.com/scottlepp/go-duck v0.0.19 // @grafana/grafana-app-platform-squad + github.com/spf13/cobra v1.8.0 // @grafana/grafana-app-platform-squad + github.com/spf13/pflag v1.0.5 // @grafana-app-platform-squad + github.com/spyzhov/ajson v0.9.0 // @grafana/grafana-app-platform-squad github.com/stretchr/testify v1.9.0 // @grafana/grafana-backend-group github.com/teris-io/shortid v0.0.0-20171029131806-771a37caa5cf // @grafana/grafana-backend-group github.com/ua-parser/uap-go v0.0.0-20211112212520-00c877edfe0f // @grafana/grafana-backend-group - github.com/uber/jaeger-client-go v2.30.0+incompatible // indirect + github.com/urfave/cli v1.22.15 // @grafana/grafana-backend-group github.com/urfave/cli/v2 v2.25.0 // @grafana/grafana-backend-group github.com/vectordotdev/go-datemath v0.1.1-0.20220323213446-f3954d0b18ae // @grafana/grafana-backend-group + github.com/wk8/go-ordered-map v1.0.0 // @grafana/grafana-backend-group + github.com/xlab/treeprint v1.2.0 // @grafana/observability-traces-and-profiling + github.com/xwb1989/sqlparser v0.0.0-20180606152119-120387863bf2 // @grafana/grafana-app-platform-squad github.com/yudai/gojsondiff v1.0.0 // @grafana/grafana-backend-group - go.opentelemetry.io/collector/pdata v1.0.1 // @grafana/grafana-backend-group - go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.49.0 // @grafana/grafana-operator-experience-squad - go.opentelemetry.io/otel/exporters/jaeger v1.10.0 // @grafana/grafana-backend-group - go.opentelemetry.io/otel/sdk v1.24.0 // @grafana/grafana-backend-group - go.opentelemetry.io/otel/trace v1.24.0 // @grafana/grafana-backend-group - golang.org/x/crypto v0.21.0 // @grafana/grafana-backend-group + go.opentelemetry.io/collector/pdata v1.6.0 // @grafana/grafana-backend-group + go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.51.0 // @grafana/plugins-platform-backend + go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.51.0 // @grafana/grafana-operator-experience-squad + go.opentelemetry.io/contrib/propagators/jaeger v1.26.0 // @grafana/grafana-backend-group + go.opentelemetry.io/contrib/samplers/jaegerremote v0.20.0 // @grafana/grafana-backend-group + go.opentelemetry.io/otel v1.26.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.26.0 // @grafana/grafana-backend-group + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.26.0 // @grafana/grafana-backend-group + go.opentelemetry.io/otel/sdk v1.26.0 // @grafana/grafana-backend-group + go.opentelemetry.io/otel/trace v1.26.0 // @grafana/grafana-backend-group + go.uber.org/atomic v1.11.0 // @grafana/alerting-squad-backend + go.uber.org/goleak v1.3.0 // @grafana/grafana-search-and-storage + gocloud.dev v0.25.0 // @grafana/grafana-app-platform-squad + golang.org/x/crypto v0.22.0 // @grafana/grafana-backend-group golang.org/x/exp v0.0.0-20231206192017-f3f8817b8deb // @grafana/alerting-squad-backend - golang.org/x/net v0.22.0 // @grafana/oss-big-tent @grafana/partner-datasources - golang.org/x/oauth2 v0.18.0 // @grafana/identity-access-team - golang.org/x/sync v0.6.0 // @grafana/alerting-squad-backend + golang.org/x/mod v0.15.0 // @grafana/grafana-backend-group + golang.org/x/net v0.24.0 // @grafana/oss-big-tent @grafana/partner-datasources + golang.org/x/oauth2 v0.19.0 // @grafana/identity-access-team + golang.org/x/sync v0.7.0 // @grafana/alerting-squad-backend + golang.org/x/text v0.14.0 // @grafana/grafana-backend-group golang.org/x/time v0.5.0 // @grafana/grafana-backend-group - golang.org/x/tools v0.17.0 // @grafana/grafana-as-code + golang.org/x/tools v0.18.0 // @grafana/grafana-as-code gonum.org/v1/gonum v0.12.0 // @grafana/observability-metrics - google.golang.org/api v0.162.0 // @grafana/grafana-backend-group + google.golang.org/api v0.176.0 // @grafana/grafana-backend-group google.golang.org/grpc v1.63.2 // @grafana/plugins-platform-backend google.golang.org/protobuf v1.33.0 // @grafana/plugins-platform-backend - gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect gopkg.in/ini.v1 v1.67.0 // @grafana/alerting-squad-backend gopkg.in/mail.v2 v2.3.1 // @grafana/grafana-backend-group - gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // @grafana/alerting-squad-backend - xorm.io/builder v0.3.6 // indirect; @grafana/grafana-backend-group + k8s.io/api v0.29.2 // @grafana/grafana-app-platform-squad + k8s.io/apimachinery v0.29.2 // @grafana/grafana-app-platform-squad + k8s.io/apiserver v0.29.2 // @grafana/grafana-app-platform-squad + k8s.io/client-go v0.29.2 // @grafana/grafana-app-platform-squad + k8s.io/code-generator v0.29.1 // @grafana/grafana-app-platform-squad + k8s.io/component-base v0.29.2 // @grafana/grafana-app-platform-squad + k8s.io/klog/v2 v2.120.1 // @grafana/grafana-app-platform-squad + k8s.io/kube-aggregator v0.29.0 // @grafana/grafana-app-platform-squad + k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 // @grafana/grafana-app-platform-squad + k8s.io/utils v0.0.0-20230726121419-3b25d923346b // @grafana/partner-datasources + sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // @grafana-app-platform-squad + xorm.io/builder v0.3.6 // @grafana/grafana-backend-group xorm.io/core v0.7.3 // @grafana/grafana-backend-group xorm.io/xorm v0.8.2 // @grafana/alerting-squad-backend ) require ( + cloud.google.com/go v0.112.0 // indirect + cloud.google.com/go/auth v0.2.2 // indirect + cloud.google.com/go/auth/oauth2adapt v0.2.1 // indirect + cloud.google.com/go/compute/metadata v0.3.0 // indirect + cloud.google.com/go/iam v1.1.6 // indirect + github.com/Azure/azure-pipeline-go v0.2.3 // indirect + github.com/Azure/azure-sdk-for-go/sdk/azcore v1.10.0 // indirect github.com/Azure/azure-sdk-for-go/sdk/internal v1.5.2 // indirect + github.com/Azure/azure-sdk-for-go/sdk/keyvault/internal v0.7.0 // indirect github.com/Azure/go-autorest v14.2.0+incompatible // indirect github.com/Azure/go-autorest/autorest/date v0.3.0 // indirect github.com/Azure/go-autorest/autorest/to v0.4.0 // indirect github.com/Azure/go-autorest/autorest/validation v0.3.1 // indirect github.com/Azure/go-autorest/logger v0.2.1 // indirect github.com/Azure/go-autorest/tracing v0.6.0 // indirect - github.com/FZambia/eagle v0.1.0 // indirect - github.com/alecthomas/units v0.0.0-20231202071711-9a357b53e9c9 // indirect - github.com/andybalholm/brotli v1.0.5 // @grafana/partner-datasources - github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect - github.com/beorn7/perks v1.0.1 // indirect - github.com/cenkalti/backoff/v4 v4.2.1 // indirect - github.com/cespare/xxhash/v2 v2.2.0 // indirect - github.com/cheekybits/genny v1.0.0 // indirect - github.com/cockroachdb/apd/v2 v2.0.2 // indirect - github.com/deepmap/oapi-codegen v1.13.0 // @grafana/grafana-as-code - github.com/dennwc/varint v1.0.0 // indirect - github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect - github.com/docker/go-units v0.5.0 // indirect - github.com/edsrzf/mmap-go v1.1.0 // indirect - github.com/emicklei/proto v1.10.0 // indirect - github.com/go-kit/log v0.2.1 // @grafana/grafana-backend-group - github.com/go-logfmt/logfmt v0.6.0 // indirect - github.com/go-openapi/analysis v0.22.2 // indirect - github.com/go-openapi/errors v0.21.0 // indirect - github.com/go-openapi/jsonpointer v0.20.2 // indirect - github.com/go-openapi/jsonreference v0.20.4 // indirect - github.com/go-openapi/loads v0.21.5 // @grafana/alerting-squad-backend - github.com/go-openapi/runtime v0.27.1 // @grafana/alerting-squad-backend - github.com/go-openapi/spec v0.20.14 // indirect - github.com/go-openapi/swag v0.22.9 // indirect - github.com/go-openapi/validate v0.23.0 // indirect - github.com/golang-jwt/jwt/v4 v4.5.0 // @grafana/grafana-backend-group - github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect - github.com/golang/glog v1.2.0 // indirect - github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect - github.com/golang/protobuf v1.5.4 // @grafana/grafana-backend-group - github.com/google/btree v1.1.2 // indirect - github.com/google/flatbuffers v23.5.26+incompatible // indirect - github.com/googleapis/gax-go/v2 v2.12.0 // @grafana/grafana-backend-group - github.com/gorilla/mux v1.8.1 // @grafana/grafana-backend-group - github.com/grafana/grafana-google-sdk-go v0.1.0 // @grafana/partner-datasources - github.com/grpc-ecosystem/go-grpc-prometheus v1.2.1-0.20191002090509-6af20e3a5340 // indirect; @grafana/plugins-platform-backend - github.com/hashicorp/errwrap v1.1.0 // indirect - github.com/hashicorp/go-msgpack v0.5.5 // indirect - github.com/hashicorp/go-multierror v1.1.1 // @grafana/alerting-squad - github.com/hashicorp/go-sockaddr v1.0.6 // indirect - github.com/hashicorp/yamux v0.1.1 // indirect - github.com/igm/sockjs-go/v3 v3.0.2 // indirect - github.com/jessevdk/go-flags v1.5.0 // indirect - github.com/jonboulle/clockwork v0.4.0 // indirect - github.com/josharian/intern v1.0.0 // indirect - github.com/jpillora/backoff v1.0.0 // indirect - github.com/mailru/easyjson v0.7.7 // indirect - github.com/mattermost/xml-roundtrip-validator v0.1.0 // indirect - github.com/mattetti/filebuffer v1.0.1 // indirect - github.com/mattn/go-runewidth v0.0.15 // indirect - github.com/miekg/dns v1.1.57 // indirect - github.com/mitchellh/go-testing-interface v1.14.1 // indirect - github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect - github.com/modern-go/reflect2 v1.0.2 // @grafana/alerting-squad-backend - github.com/mpvl/unique v0.0.0-20150818121801-cbe035fff7de // indirect - github.com/oklog/run v1.1.0 // indirect - github.com/oklog/ulid v1.3.1 // indirect - github.com/olekukonko/tablewriter v0.0.5 // @grafana/grafana-backend-group - github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect - github.com/prometheus/common/sigv4 v0.1.0 // indirect - github.com/prometheus/exporter-toolkit v0.11.0 // indirect - github.com/prometheus/procfs v0.12.0 // indirect - github.com/protocolbuffers/txtpbfmt v0.0.0-20220428173112-74888fd59c2b // indirect - github.com/rs/cors v1.10.1 // indirect - github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 // indirect - github.com/segmentio/encoding v0.3.6 // indirect - github.com/sergi/go-diff v1.3.1 // indirect - github.com/shurcooL/httpfs v0.0.0-20230704072500-f1e31cf0ba5c // indirect - github.com/shurcooL/vfsgen v0.0.0-20200824052919-0d455de96546 // indirect - github.com/stretchr/objx v0.5.2 // indirect - github.com/uber/jaeger-lib v2.4.1+incompatible // indirect - github.com/valyala/bytebufferpool v1.0.0 // indirect - github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 // indirect - go.mongodb.org/mongo-driver v1.13.1 // indirect - go.opencensus.io v0.24.0 // indirect - go.uber.org/atomic v1.11.0 // @grafana/alerting-squad-backend - go.uber.org/goleak v1.3.0 // @grafana/grafana-search-and-storage - golang.org/x/sys v0.18.0 // indirect - golang.org/x/text v0.14.0 // @grafana/grafana-backend-group - golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect - google.golang.org/appengine v1.6.8 // indirect - google.golang.org/genproto v0.0.0-20240227224415-6ceb2ff114de // indirect; @grafana/grafana-backend-group -) - -require ( - cloud.google.com/go/kms v1.15.7 // @grafana/grafana-backend-group - github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.5.1 // @grafana/grafana-backend-group - github.com/Azure/azure-sdk-for-go/sdk/keyvault/azkeys v0.9.0 // @grafana/grafana-backend-group - github.com/Azure/azure-storage-blob-go v0.15.0 // @grafana/grafana-backend-group - github.com/Azure/go-autorest/autorest/adal v0.9.23 // @grafana/grafana-backend-group - github.com/armon/go-radix v1.0.0 // @grafana/grafana-app-platform-squad - 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/bufbuild/connect-go v1.10.0 // @grafana/observability-traces-and-profiling - github.com/dlmiddlecote/sqlstats v1.0.2 // @grafana/grafana-backend-group - github.com/drone/drone-cli v1.6.1 // @grafana/grafana-release-guild - github.com/golang-migrate/migrate/v4 v4.7.0 // @grafana/grafana-backend-group - github.com/google/go-github v17.0.0+incompatible // @grafana/grafana-release-guild - github.com/google/go-github/v45 v45.2.0 // @grafana/grafana-release-guild - github.com/grafana/codejen v0.0.3 // @grafana/dataviz-squad - github.com/grafana/dskit v0.0.0-20240104111617-ea101a3b86eb // @grafana/grafana-backend-group - github.com/huandu/xstrings v1.3.2 // @grafana/partner-datasources - github.com/jmoiron/sqlx v1.3.5 // @grafana/grafana-backend-group - github.com/matryer/is v1.4.0 // @grafana/grafana-as-code - github.com/urfave/cli v1.22.14 // @grafana/grafana-backend-group - go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0 // @grafana/plugins-platform-backend - go.opentelemetry.io/contrib/propagators/jaeger v1.22.0 // @grafana/grafana-backend-group - go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.24.0 // @grafana/grafana-backend-group - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.24.0 // @grafana/grafana-backend-group - gocloud.dev v0.25.0 // @grafana/grafana-app-platform-squad -) - -require ( - buf.build/gen/go/parca-dev/parca/bufbuild/connect-go v1.4.1-20221222094228-8b1d3d0f62e6.1 // @grafana/observability-traces-and-profiling - buf.build/gen/go/parca-dev/parca/protocolbuffers/go v1.28.1-20221222094228-8b1d3d0f62e6.4 // @grafana/observability-traces-and-profiling - github.com/Masterminds/semver/v3 v3.1.1 // @grafana/grafana-release-guild - github.com/alicebob/miniredis/v2 v2.30.1 // @grafana/alerting-squad-backend - github.com/dave/dst v0.27.2 // @grafana/grafana-as-code - github.com/go-jose/go-jose/v3 v3.0.3 // @grafana/identity-access-team - github.com/grafana/dataplane/examples v0.0.1 // @grafana/observability-metrics - github.com/grafana/dataplane/sdata v0.0.9 // @grafana/observability-metrics - github.com/grafana/tempo v1.5.1-0.20230524121406-1dc1bfe7085b // @grafana/observability-traces-and-profiling - github.com/microsoft/go-mssqldb v1.6.1-0.20240214161942-b65008136246 // @grafana/grafana-bi-squad - github.com/redis/go-redis/v9 v9.1.0 // @grafana/alerting-squad-backend - go.opentelemetry.io/contrib/samplers/jaegerremote v0.18.0 // @grafana/grafana-backend-group - golang.org/x/mod v0.14.0 // @grafana/grafana-backend-group - k8s.io/utils v0.0.0-20230726121419-3b25d923346b // @grafana/partner-datasources -) - -require ( - github.com/spf13/cobra v1.8.0 // @grafana/grafana-app-platform-squad - go.opentelemetry.io/otel v1.24.0 // @grafana/grafana-backend-group - k8s.io/api v0.29.2 // @grafana/grafana-app-platform-squad - k8s.io/apimachinery v0.29.2 // @grafana/grafana-app-platform-squad - k8s.io/apiserver v0.29.2 // @grafana/grafana-app-platform-squad - k8s.io/client-go v0.29.2 // @grafana/grafana-app-platform-squad - k8s.io/component-base v0.29.2 // @grafana/grafana-app-platform-squad - k8s.io/klog/v2 v2.120.1 // @grafana/grafana-app-platform-squad - k8s.io/kube-aggregator v0.29.0 // @grafana/grafana-app-platform-squad - k8s.io/kube-openapi v0.0.0-20240220201932-37d671a357a5 // @grafana/grafana-app-platform-squad -) - -require github.com/grafana/gofpdf v0.0.0-20231002120153-857cc45be447 // @grafana/sharing-squad - -require github.com/grafana/pyroscope/api v0.3.0 // @grafana/observability-traces-and-profiling - -require github.com/grafana/pyroscope-go/godeltaprof v0.1.6 // @grafana/observability-traces-and-profiling - -require github.com/apache/arrow/go/v15 v15.0.2 // @grafana/observability-metrics - -require ( - cloud.google.com/go v0.112.0 // indirect - cloud.google.com/go/compute/metadata v0.2.3 // indirect - github.com/Azure/azure-pipeline-go v0.2.3 // indirect github.com/Azure/go-ntlmssp v0.0.0-20220621081337-cb9428e4ac1e // indirect + github.com/AzureAD/microsoft-authentication-library-for-go v1.2.1 // indirect + github.com/FZambia/eagle v0.1.0 // indirect + github.com/JohnCGriffin/overflow v0.0.0-20211019200055-46fa312c352c // indirect github.com/Masterminds/goutils v1.1.1 // indirect + github.com/Microsoft/go-winio v0.6.1 // indirect github.com/NYTimes/gziphandler v1.1.1 // indirect + github.com/RoaringBitmap/roaring v0.9.4 // indirect github.com/agext/levenshtein v1.2.1 // indirect + github.com/alecthomas/units v0.0.0-20231202071711-9a357b53e9c9 // indirect github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a // indirect + github.com/antlr/antlr4/runtime/Go/antlr/v4 v4.0.0-20230512164433-5d1fd1a340c9 // indirect + github.com/apache/arrow/go/arrow v0.0.0-20211112161151-bc219186db40 // indirect + github.com/apache/thrift v0.18.1 // indirect github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect github.com/apparentlymart/go-textseg/v13 v13.0.0 // indirect github.com/armon/go-metrics v0.4.1 // indirect + github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect + github.com/axiomhq/hyperloglog v0.0.0-20191112132149-a4c4c47bc57f // indirect + github.com/bahlo/generic-list-go v0.2.0 // indirect + github.com/beorn7/perks v1.0.1 // indirect + github.com/bits-and-blooms/bitset v1.2.0 // indirect + github.com/blevesearch/go-porterstemmer v1.0.3 // indirect + github.com/blevesearch/mmap-go v1.0.4 // indirect + github.com/blevesearch/segment v0.9.0 // indirect + github.com/blevesearch/snowballstem v0.9.0 // indirect + github.com/blevesearch/vellum v1.0.7 // indirect + github.com/blugelabs/ice v1.0.0 // indirect github.com/bmatcuk/doublestar v1.1.1 // indirect + github.com/bufbuild/protocompile v0.4.0 // indirect + github.com/buger/jsonparser v1.1.1 // indirect github.com/buildkite/yaml v2.1.0+incompatible // indirect - github.com/bwmarrin/snowflake v0.3.0 // @grafan/grafana-app-platform-squad + github.com/caio/go-tdigest v3.1.0+incompatible // indirect + github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/centrifugal/protocol v0.10.0 // 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-20230802225258-3cf4e6d46a89 // 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 github.com/coreos/go-systemd/v22 v22.5.0 // indirect - github.com/cpuguy83/go-md2man/v2 v2.0.3 // indirect + github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/dennwc/varint v1.0.0 // indirect + github.com/dgryski/go-metro v0.0.0-20211217172704-adc40b04c140 // indirect + github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/docker/distribution v2.8.2+incompatible // indirect github.com/docker/go-connections v0.4.0 // indirect + github.com/docker/go-units v0.5.0 // indirect github.com/drone-runners/drone-runner-docker v1.8.2 // indirect github.com/drone/drone-go v1.7.1 // indirect github.com/drone/envsubst v1.0.3 // indirect github.com/drone/runner-go v1.12.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-20230731152917-f99041a5c027 // indirect github.com/emicklei/go-restful/v3 v3.11.0 // indirect + github.com/emicklei/proto v1.10.0 // indirect github.com/evanphx/json-patch v5.6.0+incompatible // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect + github.com/ghodss/yaml v1.0.1-0.20190212211648-25d852aebe32 // indirect github.com/go-asn1-ber/asn1-ber v1.5.4 // indirect + github.com/go-logfmt/logfmt v0.6.0 // indirect + github.com/go-logr/logr v1.4.1 // indirect; @grafana/grafana-app-platform-squad + github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-logr/zapr v1.3.0 // indirect + github.com/go-openapi/analysis v0.22.2 // indirect + github.com/go-openapi/errors v0.21.0 // indirect + github.com/go-openapi/jsonpointer v0.20.2 // indirect + github.com/go-openapi/jsonreference v0.20.4 // indirect + github.com/go-openapi/spec v0.20.14 // indirect + github.com/go-openapi/swag v0.22.9 // indirect + github.com/go-openapi/validate v0.23.0 // indirect github.com/goccy/go-json v0.10.2 // 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.0 // 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.17.7 // indirect + github.com/google/flatbuffers v23.5.26+incompatible // indirect + github.com/google/gnostic-models v0.6.8 // indirect github.com/google/go-querystring v1.1.0 // indirect github.com/google/gofuzz v1.2.0 // indirect github.com/google/s2a-go v0.1.7 // indirect github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect 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-prometheus v1.2.1-0.20191002090509-6af20e3a5340 // indirect; @grafana/plugins-platform-backend + github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.1 // indirect + github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-immutable-radix v1.3.1 // indirect - github.com/hashicorp/golang-lru/v2 v2.0.7 // @grafana/alerting-squad-backend + github.com/hashicorp/go-msgpack v0.5.5 // indirect + github.com/hashicorp/go-sockaddr v1.0.6 // indirect + github.com/hashicorp/go-uuid v1.0.3 // indirect + github.com/hashicorp/golang-lru v0.6.0 // indirect github.com/hashicorp/memberlist v0.5.0 // indirect + github.com/hashicorp/yamux v0.1.1 // indirect + github.com/igm/sockjs-go/v3 v3.0.2 // indirect + github.com/imdario/mergo v0.3.16 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/invopop/jsonschema v0.12.0 // indirect github.com/invopop/yaml v0.2.0 // indirect + github.com/jcmturner/aescts/v2 v2.0.0 // indirect + github.com/jcmturner/dnsutils/v2 v2.0.0 // indirect + github.com/jcmturner/gofork v1.7.6 // indirect + github.com/jcmturner/goidentity/v6 v6.0.1 // indirect + github.com/jcmturner/gokrb5/v8 v8.4.4 // indirect + github.com/jcmturner/rpc/v2 v2.0.3 // indirect + github.com/jessevdk/go-flags v1.5.0 // indirect + github.com/jhump/protoreflect v1.15.1 // indirect + github.com/jonboulle/clockwork v0.4.0 // indirect + github.com/josharian/intern v1.0.0 // indirect + github.com/jpillora/backoff v1.0.0 // indirect github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect + github.com/klauspost/asmfmt v1.3.2 // indirect + github.com/klauspost/compress v1.17.4 // indirect github.com/klauspost/cpuid/v2 v2.2.5 // indirect github.com/kr/text v0.2.0 // indirect + github.com/kylelemons/godebug v1.1.0 // indirect + github.com/mailru/easyjson v0.7.7 // indirect + github.com/mattermost/xml-roundtrip-validator v0.1.0 // indirect + github.com/mattetti/filebuffer v1.0.1 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-ieproxy v0.0.3 // indirect + github.com/mattn/go-runewidth v0.0.15 // indirect + github.com/miekg/dns v1.1.57 // indirect + github.com/minio/asm2plan9s v0.0.0-20200509001527-cdd76441f9d8 // indirect + github.com/minio/c2goasm v0.0.0-20190812172519-36a3d3bbc4f3 // indirect github.com/mitchellh/copystructure v1.2.0 // indirect - github.com/mitchellh/mapstructure v1.5.0 //@grafana/identity-access-team + github.com/mitchellh/go-homedir v1.1.0 // indirect + github.com/mitchellh/go-testing-interface v1.14.1 // indirect + github.com/mitchellh/go-wordwrap v1.0.1 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect - github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // @grafana/alerting-squad-backend + github.com/mithrandie/csvq v1.17.10 // indirect + github.com/mithrandie/csvq-driver v1.6.8 // indirect + github.com/mithrandie/go-file/v2 v2.1.0 // indirect + github.com/mithrandie/go-text v1.5.4 // indirect + github.com/mithrandie/ternary v1.1.1 // indirect + github.com/moby/spdystream v0.2.0 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/mpvl/unique v0.0.0-20150818121801-cbe035fff7de // indirect + github.com/mschoch/smat v0.2.0 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect + github.com/oapi-codegen/runtime v1.1.1 // indirect + github.com/oklog/run v1.1.0 // indirect + github.com/oklog/ulid v1.3.1 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.0.3-0.20220512140940-7b36cea86235 // indirect github.com/opentracing-contrib/go-stdlib v1.0.0 // indirect + github.com/opentracing/opentracing-go v1.2.0 // indirect github.com/perimeterx/marshmallow v1.1.5 // indirect + github.com/pierrec/lz4/v4 v4.1.18 // indirect + github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/prometheus/common/sigv4 v0.1.0 // indirect + github.com/prometheus/exporter-toolkit v0.11.0 // indirect + github.com/prometheus/procfs v0.14.0 // indirect + github.com/protocolbuffers/txtpbfmt v0.0.0-20220428173112-74888fd59c2b // indirect github.com/redis/rueidis v1.0.16 // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/rivo/uniseg v0.3.4 // indirect + github.com/rogpeppe/go-internal v1.12.0 // indirect + github.com/rs/cors v1.10.1 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect + github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 // indirect github.com/segmentio/asm v1.2.0 // indirect + github.com/segmentio/encoding v0.3.6 // indirect + github.com/sergi/go-diff v1.3.1 // indirect github.com/shopspring/decimal v1.2.0 // indirect + github.com/shurcooL/httpfs v0.0.0-20230704072500-f1e31cf0ba5c // indirect + github.com/shurcooL/vfsgen v0.0.0-20200824052919-0d455de96546 // indirect github.com/spf13/cast v1.5.0 // indirect - github.com/spf13/pflag v1.0.5 // @grafana-app-platform-squad github.com/stoewer/go-strcase v1.3.0 // indirect + github.com/stretchr/objx v0.5.2 // indirect + github.com/uber/jaeger-client-go v2.30.0+incompatible // indirect + github.com/uber/jaeger-lib v2.4.1+incompatible // indirect github.com/unknwon/bra v0.0.0-20200517080246-1e3013ecaff8 // indirect github.com/unknwon/com v1.0.1 // indirect github.com/unknwon/log v0.0.0-20150304194804-e617c87089d3 // indirect + github.com/valyala/bytebufferpool v1.0.0 // indirect + github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect + github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 // indirect + github.com/yudai/pp v2.0.1+incompatible // indirect github.com/yuin/gopher-lua v1.1.0 // indirect github.com/zclconf/go-cty v1.13.0 // indirect github.com/zeebo/xxh3 v1.0.2 // indirect go.etcd.io/etcd/api/v3 v3.5.10 // indirect go.etcd.io/etcd/client/pkg/v3 v3.5.10 // indirect go.etcd.io/etcd/client/v3 v3.5.10 // indirect - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect - go.opentelemetry.io/otel/metric v1.24.0 // indirect + go.mongodb.org/mongo-driver v1.13.1 // indirect + go.opencensus.io v0.24.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.51.0 // indirect + go.opentelemetry.io/otel/metric v1.26.0 // indirect + go.opentelemetry.io/proto/otlp v1.2.0 // indirect go.starlark.net v0.0.0-20230525235612-a134d8f9ddca // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.26.0 // indirect - golang.org/x/term v0.18.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20240227224415-6ceb2ff114de // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20240227224415-6ceb2ff114de // indirect + golang.org/x/sys v0.19.0 // indirect + golang.org/x/term v0.19.0 // indirect + golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect + google.golang.org/genproto v0.0.0-20240227224415-6ceb2ff114de // indirect; @grafana/grafana-backend-group + google.golang.org/genproto/googleapis/api v0.0.0-20240415180920-8c6c420018be // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240415180920-8c6c420018be // 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 gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect + gotest.tools/v3 v3.5.1 // indirect k8s.io/kms v0.29.2 // indirect lukechampine.com/uint128 v1.3.0 // indirect modernc.org/cc/v3 v3.40.0 // indirect @@ -377,144 +454,17 @@ require ( modernc.org/token v1.1.0 // indirect sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.28.0 // indirect sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect - sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // @grafana-app-platform-squad sigs.k8s.io/yaml v1.3.0 // indirect; @grafana-app-platform-squad ) -require ( - cloud.google.com/go/compute v1.24.0 // indirect - cloud.google.com/go/iam v1.1.6 // indirect - filippo.io/age v1.1.1 // @grafana/identity-access-team - github.com/Azure/azure-sdk-for-go/sdk/azcore v1.10.0 // indirect - github.com/Azure/azure-sdk-for-go/sdk/keyvault/internal v0.7.0 // indirect - github.com/AzureAD/microsoft-authentication-library-for-go v1.2.1 // indirect - github.com/Masterminds/sprig/v3 v3.2.2 // @grafana/grafana-backend-group - github.com/Microsoft/go-winio v0.6.1 // indirect - github.com/ProtonMail/go-crypto v0.0.0-20230828082145-3c4c8a2d2371 // @grafana/plugins-platform-backend - github.com/RoaringBitmap/roaring v0.9.4 // indirect - github.com/axiomhq/hyperloglog v0.0.0-20191112132149-a4c4c47bc57f // indirect - github.com/bits-and-blooms/bitset v1.2.0 // indirect - github.com/blevesearch/go-porterstemmer v1.0.3 // indirect - github.com/blevesearch/mmap-go v1.0.4 // indirect - github.com/blevesearch/segment v0.9.0 // indirect - github.com/blevesearch/snowballstem v0.9.0 // indirect - github.com/blevesearch/vellum v1.0.7 // indirect - github.com/blugelabs/ice v1.0.0 // indirect - github.com/caio/go-tdigest v3.1.0+incompatible // indirect - github.com/chromedp/cdproto v0.0.0-20230802225258-3cf4e6d46a89 // indirect - github.com/coreos/go-semver v0.3.1 // indirect - github.com/dgryski/go-metro v0.0.0-20211217172704-adc40b04c140 // indirect - github.com/docker/docker v24.0.7+incompatible // @grafana/grafana-release-guild - github.com/elazarl/goproxy v0.0.0-20230731152917-f99041a5c027 // indirect - github.com/ghodss/yaml v1.0.1-0.20190212211648-25d852aebe32 // indirect - github.com/go-logr/logr v1.4.1 // indirect; @grafana/grafana-app-platform-squad - github.com/go-logr/stdr v1.2.2 // indirect - github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0 // indirect - github.com/imdario/mergo v0.3.16 // indirect - github.com/klauspost/compress v1.17.4 // indirect - github.com/kylelemons/godebug v1.1.0 // indirect - github.com/labstack/echo/v4 v4.11.1 // indirect - github.com/labstack/gommon v0.4.0 // indirect - github.com/mitchellh/go-wordwrap v1.0.1 // indirect - github.com/mschoch/smat v0.2.0 // indirect - github.com/pierrec/lz4/v4 v4.1.18 // indirect - github.com/valyala/fasttemplate v1.2.2 // indirect - github.com/wk8/go-ordered-map v1.0.0 // @grafana/grafana-backend-group - github.com/xlab/treeprint v1.2.0 // @grafana/observability-traces-and-profiling - go.opentelemetry.io/proto/otlp v1.1.0 // indirect -) - -require ( - github.com/antlr/antlr4/runtime/Go/antlr/v4 v4.0.0-20230305170008-8188dc5388df // indirect - github.com/golang-jwt/jwt/v5 v5.2.1 // indirect - github.com/google/gnostic-models v0.6.8 // indirect - github.com/grafana/grafana-openapi-client-go v0.0.0-20231213163343-bd475d63fb79 // @grafana/grafana-backend-group - github.com/moby/spdystream v0.2.0 // indirect - github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect -) - -require k8s.io/code-generator v0.29.1 // @grafana/grafana-app-platform-squad - -require github.com/spyzhov/ajson v0.9.0 // @grafana/grafana-app-platform-squad - -require github.com/fullstorydev/grpchan v1.1.1 // @grafana/grafana-backend-group - -// This needs to be here for other projects that import grafana/grafana -// For local development grafana/grafana will always use the local files -// Check go.work file for details -require github.com/grafana/grafana/pkg/promlib v0.0.5 // @grafana/observability-metrics - -require ( - github.com/JohnCGriffin/overflow v0.0.0-20211019200055-46fa312c352c // indirect - github.com/apache/arrow/go/arrow v0.0.0-20211112161151-bc219186db40 // indirect - github.com/apache/thrift v0.18.1 // indirect - github.com/grafana/grafana/pkg/apimachinery v0.0.0-20240226124929-648abdbd0ea4 // @grafana/grafana-app-platform-squad - github.com/grafana/grafana/pkg/apiserver v0.0.0-20240226124929-648abdbd0ea4 // @grafana/grafana-app-platform-squad -) - -require ( - github.com/bufbuild/protocompile v0.4.0 // indirect - github.com/grafana/sqlds/v3 v3.2.0 // indirect - github.com/jhump/protoreflect v1.15.1 // indirect - github.com/klauspost/asmfmt v1.3.2 // indirect - github.com/krasun/gosqlparser v1.0.5 // @grafana/grafana-app-platform-squad - github.com/minio/asm2plan9s v0.0.0-20200509001527-cdd76441f9d8 // indirect - github.com/minio/c2goasm v0.0.0-20190812172519-36a3d3bbc4f3 // indirect - github.com/mitchellh/go-homedir v1.1.0 // indirect - github.com/mithrandie/csvq v1.17.10 // indirect - github.com/mithrandie/csvq-driver v1.6.8 // indirect - github.com/mithrandie/go-file/v2 v2.1.0 // indirect - github.com/mithrandie/go-text v1.5.4 // indirect - github.com/mithrandie/ternary v1.1.1 // indirect - github.com/xwb1989/sqlparser v0.0.0-20180606152119-120387863bf2 // @grafana/grafana-app-platform-squad -) - -require github.com/getkin/kin-openapi v0.120.0 // @grafana/grafana-as-code - -require github.com/grafana/authlib v0.0.0-20240328140636-a7388d0bac72 // @grafana/identity-access-team - -require ( - github.com/bahlo/generic-list-go v0.2.0 // indirect - github.com/buger/jsonparser v1.1.1 // indirect - github.com/bytedance/sonic v1.9.1 // indirect - github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect - github.com/gabriel-vasile/mimetype v1.4.2 // indirect - github.com/gin-contrib/sse v0.1.0 // indirect - github.com/gin-gonic/gin v1.9.1 // indirect - github.com/go-playground/locales v0.14.1 // indirect - github.com/go-playground/universal-translator v0.18.1 // indirect - github.com/go-playground/validator/v10 v10.14.0 // indirect - github.com/hashicorp/go-uuid v1.0.3 // indirect - github.com/hashicorp/golang-lru v0.6.0 // indirect - github.com/invopop/jsonschema v0.12.0 // indirect - github.com/jcmturner/aescts/v2 v2.0.0 // indirect - github.com/jcmturner/dnsutils/v2 v2.0.0 // indirect - github.com/jcmturner/gofork v1.7.6 // indirect - github.com/jcmturner/goidentity/v6 v6.0.1 // indirect - github.com/jcmturner/gokrb5/v8 v8.4.4 // indirect - github.com/jcmturner/rpc/v2 v2.0.3 // indirect - github.com/leodido/go-urn v1.2.4 // indirect - github.com/pelletier/go-toml/v2 v2.0.8 // indirect - github.com/twitchyliquid64/golang-asm v0.15.1 // indirect - github.com/ugorji/go/codec v1.2.11 // indirect - github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect - github.com/yudai/pp v2.0.1+incompatible // indirect - golang.org/x/arch v0.3.0 // indirect -) - // Use fork of crewjam/saml with fixes for some issues until changes get merged into upstream replace github.com/crewjam/saml => github.com/grafana/saml v0.4.15-0.20231025143828-a6c0e9b86a4c // replace github.com/google/cel-go => github.com/google/cel-go v0.16.1 -// Thema's thema CLI requires cobra, which eventually works its way down to go-hclog@v1.0.0. -// Upgrading affects backend plugins: https://github.com/grafana/grafana/pull/47653#discussion_r850508593 -// No harm to Thema because it's only a dependency in its main package. -replace github.com/hashicorp/go-hclog => github.com/hashicorp/go-hclog v0.16.1 - // Use our fork of the upstream alertmanagers. // This is required in order to get notification delivery errors from the receivers API. -replace github.com/prometheus/alertmanager => github.com/grafana/prometheus-alertmanager v0.25.1-0.20240321101410-40158de684b2 +replace github.com/prometheus/alertmanager => github.com/grafana/prometheus-alertmanager v0.25.1-0.20240422145632-c33c6b5b6e6b exclude github.com/mattn/go-sqlite3 v2.0.3+incompatible diff --git a/go.sum b/go.sum index 75b558532fa..bb7d8040cc4 100644 --- a/go.sum +++ b/go.sum @@ -1,8 +1,7 @@ -buf.build/gen/go/grpc-ecosystem/grpc-gateway/protocolbuffers/go v1.28.1-20221127060915-a1ecdc58eccd.4/go.mod h1:92ejKVTiuvnKoAtRlpJpIxKfloI935DDqhs0NCRx+KM= buf.build/gen/go/parca-dev/parca/bufbuild/connect-go v1.4.1-20221222094228-8b1d3d0f62e6.1 h1:wQ75SnlaD0X30PnrmA+07A/5fnQWrAHy1mzv+CPB5Oo= buf.build/gen/go/parca-dev/parca/bufbuild/connect-go v1.4.1-20221222094228-8b1d3d0f62e6.1/go.mod h1:VYzBTKhjl92cl3sv+xznQcJHCezU7qnI0FhBAUb4n8c= -buf.build/gen/go/parca-dev/parca/protocolbuffers/go v1.28.1-20221222094228-8b1d3d0f62e6.4 h1:3ThI7dcndwVLimMCxuiaVqMJEx5FfMXydD7flXlnDkQ= -buf.build/gen/go/parca-dev/parca/protocolbuffers/go v1.28.1-20221222094228-8b1d3d0f62e6.4/go.mod h1:7dY08PsClUI7xt/6lEMJERgOcdf3d5Gnchm8qScIhRg= +buf.build/gen/go/parca-dev/parca/protocolbuffers/go v1.33.0-20240414232344-9ca06271cb73.1 h1:arEscdzM2EZPZT8x7tSzuBVgyZrnsKuvoftapClxUgw= +buf.build/gen/go/parca-dev/parca/protocolbuffers/go v1.33.0-20240414232344-9ca06271cb73.1/go.mod h1:/vvnaG5MGgbuJxYTucpo0QOom9xbSb6Y43za3/as9qk= cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.37.4/go.mod h1:NHPJ89PdicEuT9hdPXMROBD91xc5uRDxsMtSB16k7hw= @@ -169,6 +168,10 @@ cloud.google.com/go/assuredworkloads v1.11.1/go.mod h1:+F04I52Pgn5nmPG36CWFtxmav cloud.google.com/go/assuredworkloads v1.11.2/go.mod h1:O1dfr+oZJMlE6mw0Bp0P1KZSlj5SghMBvTpZqIcUAW4= cloud.google.com/go/assuredworkloads v1.11.3/go.mod h1:vEjfTKYyRUaIeA0bsGJceFV2JKpVRgyG2op3jfa59Zs= cloud.google.com/go/assuredworkloads v1.11.4/go.mod h1:4pwwGNwy1RP0m+y12ef3Q/8PaiWrIDQ6nD2E8kvWI9U= +cloud.google.com/go/auth v0.2.2 h1:gmxNJs4YZYcw6YvKRtVBaF2fyUE6UrWPyzU8jHvYfmI= +cloud.google.com/go/auth v0.2.2/go.mod h1:2bDNJWtWziDT3Pu1URxHHbkHE/BbOCuyUiKIGcNvafo= +cloud.google.com/go/auth/oauth2adapt v0.2.1 h1:VSPmMmUlT8CkIZ2PzD9AlLN+R3+D1clXMWHHa6vG/Ag= +cloud.google.com/go/auth/oauth2adapt v0.2.1/go.mod h1:tOdK/k+D2e4GEwfBRA48dKNQiDsqIXxLh7VU319eV0g= cloud.google.com/go/automl v1.5.0/go.mod h1:34EjfoFGMZ5sgJ9EoLsRtdPSNZLcfflJR39VbVNS2M0= cloud.google.com/go/automl v1.6.0/go.mod h1:ugf8a6Fx+zP0D59WLhqgTDsQI9w07o64uf/Is3Nh5p8= cloud.google.com/go/automl v1.7.0/go.mod h1:RL9MYCCsJEOmt0Wf3z9uzG0a7adTT1fe+aObgSpkCt8= @@ -314,13 +317,12 @@ cloud.google.com/go/compute v1.23.0/go.mod h1:4tCnrn48xsqlwSAiLf1HXMQk8CONslYbdi cloud.google.com/go/compute v1.23.1/go.mod h1:CqB3xpmPKKt3OJpW2ndFIXnA9A4xAy/F3Xp1ixncW78= cloud.google.com/go/compute v1.23.2/go.mod h1:JJ0atRC0J/oWYiiVBmsSsrRnh92DhZPG4hFDcR04Rns= cloud.google.com/go/compute v1.23.3/go.mod h1:VCgBUoMnIVIR0CscqQiPJLAG25E3ZRZMzcFZeQ+h8CI= -cloud.google.com/go/compute v1.24.0 h1:phWcR2eWzRJaL/kOiJwfFsPs4BaKq1j6vnpZrc1YlVg= -cloud.google.com/go/compute v1.24.0/go.mod h1:kw1/T+h/+tK2LJK0wiPPx1intgdAM3j/g3hFDlscY40= cloud.google.com/go/compute/metadata v0.1.0/go.mod h1:Z1VN+bulIf6bt4P/C37K4DyZYZEXYonfTBHHFPO/4UU= cloud.google.com/go/compute/metadata v0.2.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k= cloud.google.com/go/compute/metadata v0.2.1/go.mod h1:jgHgmJd2RKBGzXqF5LR2EZMGxBkeanZ9wwa75XHJgOM= -cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY= cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA= +cloud.google.com/go/compute/metadata v0.3.0 h1:Tz+eQXMEqDIKRsmY3cHTL6FVaynIjX2QxYC4trgAKZc= +cloud.google.com/go/compute/metadata v0.3.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k= cloud.google.com/go/contactcenterinsights v1.3.0/go.mod h1:Eu2oemoePuEFc/xKFPjbTuPSj0fYJcPls9TFlPNnHHY= cloud.google.com/go/contactcenterinsights v1.4.0/go.mod h1:L2YzkGbPsv+vMQMCADxJoT9YiTTnSEd6fEvCeHTYVck= cloud.google.com/go/contactcenterinsights v1.6.0/go.mod h1:IIDlT6CLcDoyv79kDv8iWxMSTZhLxSCofVV5W6YFM/w= @@ -1350,8 +1352,8 @@ github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHG github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs= github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= -github.com/antlr/antlr4/runtime/Go/antlr/v4 v4.0.0-20230305170008-8188dc5388df h1:7RFfzj4SSt6nnvCPbCqijJi1nWCd+TqAT3bYCStRC18= -github.com/antlr/antlr4/runtime/Go/antlr/v4 v4.0.0-20230305170008-8188dc5388df/go.mod h1:pSwJ0fSY5KhvocuWSx4fz3BA8OrA1bQn+K1Eli3BRwM= +github.com/antlr/antlr4/runtime/Go/antlr/v4 v4.0.0-20230512164433-5d1fd1a340c9 h1:goHVqTbFX3AIo0tzGr14pgfAW2ZfPChKO21Z9MGf/gk= +github.com/antlr/antlr4/runtime/Go/antlr/v4 v4.0.0-20230512164433-5d1fd1a340c9/go.mod h1:pSwJ0fSY5KhvocuWSx4fz3BA8OrA1bQn+K1Eli3BRwM= github.com/apache/arrow/go/arrow v0.0.0-20210223225224-5bea62493d91/go.mod h1:c9sxoIT3YgLxH4UhLOCKaBlEojuMhVYpk4Ntv3opUTQ= github.com/apache/arrow/go/arrow v0.0.0-20211112161151-bc219186db40 h1:q4dksr6ICHXqG5hm0ZW5IHyeEJXoIJSOZeBLmWPNeIQ= github.com/apache/arrow/go/arrow v0.0.0-20211112161151-bc219186db40/go.mod h1:Q7yQnSMnLvcXlZ8RV+jwz/6y1rQTqbX6C82SndT52Zs= @@ -1397,8 +1399,8 @@ github.com/aws/aws-sdk-go v1.38.35/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2z github.com/aws/aws-sdk-go v1.40.45/go.mod h1:585smgzpB/KqRA+K3y/NL/oYRqQvpNJYvLm+LY1U59Q= github.com/aws/aws-sdk-go v1.43.31/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4oIKwKHZo= github.com/aws/aws-sdk-go v1.48.14/go.mod h1:LF8svs817+Nz+DmiMQKTO3ubZ/6IaTpq3TjupRn3Eqk= -github.com/aws/aws-sdk-go v1.50.8 h1:gY0WoOW+/Wz6XmYSgDH9ge3wnAevYDSQWPxxJvqAkP4= -github.com/aws/aws-sdk-go v1.50.8/go.mod h1:LF8svs817+Nz+DmiMQKTO3ubZ/6IaTpq3TjupRn3Eqk= +github.com/aws/aws-sdk-go v1.50.29 h1:Ol2FYzesF2tsQrgVSnDWRFI60+FsSqKKdt7MLlZKubc= +github.com/aws/aws-sdk-go v1.50.29/go.mod h1:LF8svs817+Nz+DmiMQKTO3ubZ/6IaTpq3TjupRn3Eqk= github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g= github.com/aws/aws-sdk-go-v2 v1.9.1/go.mod h1:cK/D0BBs0b/oWPIcX/Z/obahJK1TT7IPVjy53i/mX/4= github.com/aws/aws-sdk-go-v2 v1.16.2 h1:fqlCk6Iy3bnCumtrLz9r3mJ/2gUT0pJ0wLFVIdWh+JA= @@ -1504,17 +1506,15 @@ github.com/buildkite/yaml v2.1.0+incompatible/go.mod h1:UoU8vbcwu1+vjZq01+KrpSeL github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= github.com/bwmarrin/snowflake v0.3.0 h1:xm67bEhkKh6ij1790JB83OujPR5CzNe8QuQqAgISZN0= github.com/bwmarrin/snowflake v0.3.0/go.mod h1:NdZxfVWX+oR6y2K0o6qAYv6gIOP9rjG0/E9WsDpxqwE= -github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM= -github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s= -github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U= github.com/caio/go-tdigest v3.1.0+incompatible h1:uoVMJ3Q5lXmVLCCqaMGHLBWnbGoN6Lpu7OAUPR60cds= github.com/caio/go-tdigest v3.1.0+incompatible/go.mod h1:sHQM/ubZStBUmF1WbB8FAm8q9GjDajLC5T7ydxE3JHI= github.com/casbin/casbin/v2 v2.1.2/go.mod h1:YcPU1XXisHhLzuxH9coDNf2FbKpjGlbCg3n9yuLkIJQ= github.com/casbin/casbin/v2 v2.37.0/go.mod h1:vByNa/Fchek0KZUgG5wEsl7iFsiviAYKRtgrQfcJqHg= github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= github.com/cenkalti/backoff/v4 v4.1.1/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw= -github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM= github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= +github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/census-instrumentation/opencensus-proto v0.3.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/census-instrumentation/opencensus-proto v0.4.1/go.mod h1:4T9NM4+4Vw91VeyqjLS6ao50K5bOcLKN6Q42XnYaRYw= @@ -1525,13 +1525,11 @@ github.com/centrifugal/protocol v0.10.0/go.mod h1:Tq5I1mBpLHkLxNM9gfb3Gth+sTE2kK github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cheekybits/genny v1.0.0 h1:uGGa4nei+j20rOSeDeP5Of12XVm7TGUd4dJA9RDitfE= github.com/cheekybits/genny v1.0.0/go.mod h1:+tQajlRqAUrPI7DOSpB0XAqZYtQakVtB7wXkRAgjxjQ= -github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY= -github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams= -github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk= github.com/chromedp/cdproto v0.0.0-20230802225258-3cf4e6d46a89 h1:aPflPkRFkVwbW6dmcVqfgwp1i+UWGFH6VgR1Jim5Ygc= github.com/chromedp/cdproto v0.0.0-20230802225258-3cf4e6d46a89/go.mod h1:GKljq0VrfU4D5yc+2qA6OVr8pmO/MBbPEWqWQ/oqGEs= github.com/chromedp/chromedp v0.9.2/go.mod h1:LkSXJKONWTCHAfQasKFUZI+mxqS4tZqhmtGzzhLsnLs= @@ -1590,9 +1588,9 @@ github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSV github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= -github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= -github.com/cpuguy83/go-md2man/v2 v2.0.3 h1:qMCsGGgs+MAzDFyp9LpAe1Lqy/fY/qCovCm0qnXZOBM= github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/cpuguy83/go-md2man/v2 v2.0.4 h1:wfIWP927BUkWJb2NmU/kNDYIBTh/ziUX91+lVfRxZq4= +github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= @@ -1615,8 +1613,8 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs 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/dchest/uniuri v0.0.0-20160212164326-8902c56451e9/go.mod h1:GgB8SF9nRG+GqaDtLcwJZsQFhcogVCJ79j4EdT0c2V4= -github.com/deepmap/oapi-codegen v1.13.0 h1:cnFHelhsRQbYvanCUAbRSn/ZpkUb1HPRlQcu8YqSORQ= -github.com/deepmap/oapi-codegen v1.13.0/go.mod h1:Amy7tbubKY9qkZOXqymI3Z6xSbndmu+atMJheLdyg44= +github.com/deepmap/oapi-codegen/v2 v2.1.0 h1:I/NMVhJCtuvL9x+S2QzZKpSjGi33oDZwPRdemvOZWyQ= +github.com/deepmap/oapi-codegen/v2 v2.1.0/go.mod h1:R1wL226vc5VmCNJUvMyYr3hJMm5reyv25j952zAVXZ8= github.com/denisenkom/go-mssqldb v0.0.0-20190515213511-eb9f6a1743f3/go.mod h1:zAg7JM8CkOJ43xKXIj7eRO9kmWm/TW578qo+oDO6tuM= github.com/denisenkom/go-mssqldb v0.12.0/go.mod h1:iiK0YP1ZeepvmBQk/QpLEhhTNJgfzrpArPY/aFvc9yU= github.com/dennwc/varint v1.0.0 h1:kGNFFSSw8ToIy3obO/kKr8U9GZYUAxQEVuix4zfDWzE= @@ -1742,21 +1740,16 @@ github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyT github.com/fsouza/fake-gcs-server v1.7.0/go.mod h1:5XIRs4YvwNbNoz+1JF8j6KLAyDh7RHGAyAK3EP2EsNk= github.com/fullstorydev/grpchan v1.1.1 h1:heQqIJlAv5Cnks9a70GRL2EJke6QQoUB25VGR6TZQas= github.com/fullstorydev/grpchan v1.1.1/go.mod h1:f4HpiV8V6htfY/K44GWV1ESQzHBTq7DinhzqQ95lpgc= -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/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.120.0 h1:MqJcNJFrMDFNc07iwE8iFC5eT2k/NPUFDIpNeiZv8Jg= -github.com/getkin/kin-openapi v0.120.0/go.mod h1:PCWw/lfBrJY4HcdqE3jj+QFkaFK8ABoqo7PvqVhXXqw= +github.com/getkin/kin-openapi v0.122.0 h1:WB9Jbl0Hp/T79/JF9xlSW5Kl9uYdk/AWD0yAd9HOM10= +github.com/getkin/kin-openapi v0.122.0/go.mod h1:PCWw/lfBrJY4HcdqE3jj+QFkaFK8ABoqo7PvqVhXXqw= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/ghodss/yaml v1.0.1-0.20190212211648-25d852aebe32 h1:Mn26/9ZMNWSw9C9ERFA1PUxfmGpolnw2v0bKOREu5ew= github.com/ghodss/yaml v1.0.1-0.20190212211648-25d852aebe32/go.mod h1:GIjDIg/heH5DOkXY3YJ/wNhfHsQHoXGjl8G8amsYQ1I= -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.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M= github.com/gin-gonic/gin v1.7.3/go.mod h1:jD2toBW3GZUr5UMcdrwQA10I7RuaFOl/SGeDjXkfUtY= -github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg= -github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU= 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= github.com/go-fonts/dejavu v0.1.0/go.mod h1:4Wt4I4OU2Nq9asgDCteaAaWZOV24E+0/Pwo0gppep4g= @@ -1799,8 +1792,8 @@ github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= -github.com/go-logr/zapr v1.2.3 h1:a9vnzlIBPQBBkeaR9IuMUfmVOrQlkoC4YfPoFkX3T7A= -github.com/go-logr/zapr v1.2.3/go.mod h1:eIauM6P8qSvTw5o2ez6UEAfGjQKrxQTl5EoK+Qa2oG4= +github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ= +github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg= github.com/go-openapi/analysis v0.21.2/go.mod h1:HZwRk4RRisyG8vx2Oe6aqeSQcoxRp47Xkp3+K6q+LdY= github.com/go-openapi/analysis v0.21.4/go.mod h1:4zQ35W4neeZTqh3ol0rv/O8JBbka9QyAgQRPp9y3pfo= github.com/go-openapi/analysis v0.21.5/go.mod h1:25YcZosX9Lwz2wBsrFrrsL8bmjjXdlyP6zsr2AMy29M= @@ -1864,18 +1857,10 @@ github.com/go-openapi/validate v0.23.0/go.mod h1:EeiAZ5bmpSIOJV1WLfyYF9qp/B1ZgSa github.com/go-pdf/fpdf v0.5.0/go.mod h1:HzcnA+A23uwogo0tp9yU+l3V+KXhiESpt1PMayhOh5M= github.com/go-pdf/fpdf v0.6.0/go.mod h1:HzcnA+A23uwogo0tp9yU+l3V+KXhiESpt1PMayhOh5M= github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= -github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= -github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= -github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= -github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= -github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= -github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI= github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4= -github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js= -github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI= github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo= github.com/go-resty/resty/v2 v2.9.1/go.mod h1:4/GYJVjh9nhkhGR6AUNW3XhpDYNUr+Uvy9gV/VGZIy4= @@ -2021,8 +2006,6 @@ github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEW github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= -github.com/golangci/lint-1 v0.0.0-20181222135242-d2cdd8c08219 h1:utua3L2IbQJmauC5IXdEA547bcoU5dozgQAfc8Onsg4= -github.com/golangci/lint-1 v0.0.0-20181222135242-d2cdd8c08219/go.mod h1:/X8TswGSh1pIozq4ZwCfxS0WA5JGXguxk94ar/4c87Y= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA= @@ -2138,8 +2121,9 @@ github.com/googleapis/gax-go/v2 v2.7.1/go.mod h1:4orTrqY6hXxxaUL4LHIPl6lGo8vAE38 github.com/googleapis/gax-go/v2 v2.8.0/go.mod h1:4orTrqY6hXxxaUL4LHIPl6lGo8vAE38/qKbhSAKP6QI= github.com/googleapis/gax-go/v2 v2.10.0/go.mod h1:4UOEnMCrxsSqQ940WnTiD6qJ63le2ev3xfyagutxiPw= github.com/googleapis/gax-go/v2 v2.11.0/go.mod h1:DxmR61SGKkGLa2xigwuZIQpkCI2S5iydzRfb3peWZJI= -github.com/googleapis/gax-go/v2 v2.12.0 h1:A+gCJKdRfqXkr+BIRGtZLibNXf0m1f9E4HG56etFpas= github.com/googleapis/gax-go/v2 v2.12.0/go.mod h1:y+aIqrI5eb1YGMVJfuV3185Ts/D7qKpsEkdD5+I6QGU= +github.com/googleapis/gax-go/v2 v2.12.3 h1:5/zPPDvw8Q1SuXjrqrZslrqT7dL/uJT2CQii/cLCKqA= +github.com/googleapis/gax-go/v2 v2.12.3/go.mod h1:AKloxT6GtNbaLm8QTNSidHUVsHYcBHwWRvkNFJUQcS4= github.com/googleapis/go-type-adapters v1.0.0/go.mod h1:zHW75FOG2aur7gAO2B+MLby+cLsWGBF62rFAi7WjWO4= github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= github.com/gophercloud/gophercloud v1.8.0 h1:TM3Jawprb2NrdOnvcHhWJalmKmAmOGgfZElM/3oBYCk= @@ -2163,8 +2147,8 @@ github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/ad 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-20240409171830-e039a7f57a92 h1:DfzT76QLAyvJZiQMEnHTRRkQsvOF0S2jH9bO1OIX5yw= -github.com/grafana/alerting v0.0.0-20240409171830-e039a7f57a92/go.mod h1:jriidrGFnZWvAn3xw/BOoCkJbGWnncuojMi9jq6xxwE= +github.com/grafana/alerting v0.0.0-20240424080142-bb4f4f429d36 h1:v4aQ0cde8SCzNRrD2RczzmFolEkXWriSY9tKakAD0ng= +github.com/grafana/alerting v0.0.0-20240424080142-bb4f4f429d36/go.mod h1:8nOsn7PWmttOmWiR7bvYIl3VLl+tIq72ZF+1y54w36M= github.com/grafana/authlib v0.0.0-20240328140636-a7388d0bac72 h1:lGEuhD/KhhN1OiPrvwQejl9Lg8MvaHdj3lHZNref4is= github.com/grafana/authlib v0.0.0-20240328140636-a7388d0bac72/go.mod h1:86rRD5P6u2JPWtNWTMOlqlU+YMv2fUvVz/DomA6L7w4= github.com/grafana/codejen v0.0.3 h1:tAWxoTUuhgmEqxJPOLtJoxlPBbMULFwKFOcRsPRPXDw= @@ -2183,15 +2167,14 @@ 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/grafana-aws-sdk v0.25.0 h1:XNi3iA/C/KPArmVbQfbwKQROaIotd38nCRjNE6P1UP0= github.com/grafana/grafana-aws-sdk v0.25.0/go.mod h1:3zghFF6edrxn0d6k6X9HpGZXDH+VfA+MwD2Pc/9X0ec= -github.com/grafana/grafana-azure-sdk-go/v2 v2.0.1 h1:a/zb8uX7EvmS2YAFbYPyGEnZP8jMp7WppAm05Qtunok= -github.com/grafana/grafana-azure-sdk-go/v2 v2.0.1/go.mod h1:nW7pr7POOGafhyOrq8V0ouXBcXTmpRCer3sDAfeSV+Y= +github.com/grafana/grafana-azure-sdk-go/v2 v2.0.2 h1:CWT7mOBPUht9n7F/NiBQnEM05pFmCP3Z8CZPGCVC1tM= github.com/grafana/grafana-google-sdk-go v0.1.0 h1:LKGY8z2DSxKjYfr2flZsWgTRTZ6HGQbTqewE3JvRaNA= github.com/grafana/grafana-google-sdk-go v0.1.0/go.mod h1:Vo2TKWfDVmNTELBUM+3lkrZvFtBws0qSZdXhQxRdJrE= 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.224.0 h1:WBpKJEhzEcGBAmmNB/OGGF/pHrt654O3m7idjnpSc40= -github.com/grafana/grafana-plugin-sdk-go v0.224.0/go.mod h1:dR9hYmI18hOMArvMrQ1O7oiNOUqCGbxDAe2t6z1xLH8= +github.com/grafana/grafana-plugin-sdk-go v0.228.0 h1:LlPqyB+RZTtDy8RVYD7iQVJW5A0gMoGSI/+Ykz8HebQ= +github.com/grafana/grafana-plugin-sdk-go v0.228.0/go.mod h1:u4K9vVN6eU86loO68977eTXGypC4brUCnk4sfDzutZU= github.com/grafana/grafana/pkg/apimachinery v0.0.0-20240226124929-648abdbd0ea4 h1:hpyusz8c3yRFoJPlA0o34rWnsLbaOOBZleqRhFBi5Lg= github.com/grafana/grafana/pkg/apimachinery v0.0.0-20240226124929-648abdbd0ea4/go.mod h1:vrRQJuNprTWqwm6JPxHf3BoTJhvO15QMEjQ7Q/YUOnI= github.com/grafana/grafana/pkg/apiserver v0.0.0-20240226124929-648abdbd0ea4 h1:tIbI5zgos92vwJ8lV3zwHwuxkV03GR3FGLkFW9V5LxY= @@ -2200,8 +2183,8 @@ github.com/grafana/grafana/pkg/promlib v0.0.5 h1:LiJP4ZPdk4qsgvrcw+0DoW0/+RjAND/ github.com/grafana/grafana/pkg/promlib v0.0.5/go.mod h1:iZNjkJBN8DU/5/DxrmwuHaSeiKODT72DYiQ0c9Da1JQ= github.com/grafana/grafana/pkg/util/xorm v0.0.1 h1:72QZjxWIWpSeOF8ob4aMV058kfgZyeetkAB8dmeti2o= github.com/grafana/grafana/pkg/util/xorm v0.0.1/go.mod h1:eNfbB9f2jM8o9RfwqwjY8SYm5tvowJ8Ly+iE4P9rXII= -github.com/grafana/prometheus-alertmanager v0.25.1-0.20240321101410-40158de684b2 h1:wqqaqZw+J8sLXzOjXMMde219jc6dfJPI8599UpaILZw= -github.com/grafana/prometheus-alertmanager v0.25.1-0.20240321101410-40158de684b2/go.mod h1:8Ia/R3urPmbzJ8OsdvmZvIprDwvwmYCmUbwBL+jlPOE= +github.com/grafana/prometheus-alertmanager v0.25.1-0.20240422145632-c33c6b5b6e6b h1:HCbWyVL6vi7gxyO76gQksSPH203oBJ1MJ3JcG1OQlsg= +github.com/grafana/prometheus-alertmanager v0.25.1-0.20240422145632-c33c6b5b6e6b/go.mod h1:01sXtHoRwI8W324IPAzuxDFOmALqYLCOhvSC2fUHWXc= github.com/grafana/pyroscope-go/godeltaprof v0.1.6 h1:nEdZ8louGAplSvIJi1HVp7kWvFvdiiYg3COLlTwJiFo= github.com/grafana/pyroscope-go/godeltaprof v0.1.6/go.mod h1:Tk376Nbldo4Cha9RgiU7ik8WKFkNpfds98aUzS8omLE= github.com/grafana/pyroscope/api v0.3.0 h1:WcVKNZ8JlriJnD28wTkZray0wGo8dGkizSJXnbG7Gd8= @@ -2220,6 +2203,10 @@ github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de github.com/grpc-ecosystem/go-grpc-middleware v1.3.0/go.mod h1:z0ButlSOZa5vEBq9m2m2hlwIgKw+rp3sdCBRoJY+30Y= github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 h1:UH//fgunKIs4JdUbpDl1VZCDaL56wXCB/5+wF6uHfaI= github.com/grpc-ecosystem/go-grpc-middleware v1.4.0/go.mod h1:g5qyo/la0ALbONm6Vbp88Yd8NsDy6rZz+RcrMPxvld8= +github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.0.1 h1:qnpSQwGEnkcRpTqNOIR6bJbR0gAorgP9CSALpRcKoAA= +github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.0.1/go.mod h1:lXGCsh6c22WGtjr+qGHj1otzZpV/1kwTMAqkwZsnWRU= +github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.1.0 h1:pRhl55Yx1eC7BZ1N+BBWwnKaMyD8uC+34TLdndZMAKk= +github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.1.0/go.mod h1:XKMd7iuf/RGPSMJ/U4HP0zS2Z9Fh8Ps9a+6X26m/tmI= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.1-0.20191002090509-6af20e3a5340 h1:uGoIog/wiQHI9GAxXO5TJbT0wWKH3O9HhOJW1F9c3fY= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.1-0.20191002090509-6af20e3a5340/go.mod h1:3bDW6wMZJB7tiONtC/1Xpicra6Wp5GgbTbQWCbI5fkc= @@ -2229,8 +2216,8 @@ github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFb github.com/grpc-ecosystem/grpc-gateway/v2 v2.7.0/go.mod h1:hgWBS7lorOAVIJEQMi4ZsPv9hVvWI6+ch50m39Pf2Ks= 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 h1:Wqo399gCIufwto+VfwCSvsnfGpF/w5E9CNxSwbpD6No= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0/go.mod h1:qmOFXW2epJhM0qSnUUYpldc7gVz2KMQwJ/QYCDIa7XU= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.1 h1:/c3QmbOGMGTOumP2iT/rCwB7b0QDGLKzqOmktBjT+Is= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.1/go.mod h1:5SN9VR2LTsRFsrEC6FHgRbTWrTHu6tqPeKxEQv15giM= github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed/go.mod h1:tMWxXQ9wFIaZeTI9F+hmhFiGpFmhOHzyShyFUhRm0H4= github.com/hanwen/go-fuse v1.0.0/go.mod h1:unqXarDXqzAk0rt98O2tVndEPIpUgLD9+rwFisZH3Ok= github.com/hanwen/go-fuse/v2 v2.1.0/go.mod h1:oRyA5eK+pvJyv5otpO/DgccS8y/RvYMaO00GgRLGryc= @@ -2251,8 +2238,14 @@ github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtng github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= -github.com/hashicorp/go-hclog v0.16.1 h1:IVQwpTGNRRIHafnTs2dQLIk4ENtneRIEEJWOVDqz99o= -github.com/hashicorp/go-hclog v0.16.1/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= +github.com/hashicorp/go-hclog v0.0.0-20180709165350-ff2cf002a8dd/go.mod h1:9bjs9uLqI8l75knNv3lV1kA55veR+WUPSiKIWcQHudI= +github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ= +github.com/hashicorp/go-hclog v0.12.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= +github.com/hashicorp/go-hclog v0.16.2/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= +github.com/hashicorp/go-hclog v1.2.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= +github.com/hashicorp/go-hclog v1.5.0/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= +github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k= +github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= github.com/hashicorp/go-immutable-radix v1.3.1 h1:DKHmCUm2hRBK510BaiZlwvpD40f8bJFeZnpfm2KLowc= github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= @@ -2341,12 +2334,12 @@ github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANyt github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/influxdata/influxdb v1.7.6/go.mod h1:qZna6X/4elxqT3yI9iZYdZrWWdeFOOprn86kgg4+IzY= -github.com/influxdata/influxdb-client-go/v2 v2.12.3 h1:28nRlNMRIV4QbtIUvxhWqaxn0IpXeMSkY/uJa/O/vC4= -github.com/influxdata/influxdb-client-go/v2 v2.12.3/go.mod h1:IrrLUbCjjfkmRuaCiGQg4m2GbkaeJDcuWoxiWdQEbA0= +github.com/influxdata/influxdb-client-go/v2 v2.13.0 h1:ioBbLmR5NMbAjP4UVA5r9b5xGjpABD7j65pI8kFphDM= +github.com/influxdata/influxdb-client-go/v2 v2.13.0/go.mod h1:k+spCbt9hcvqvUiz0sr5D8LolXHqAAOfPw9v/RIRHl4= github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo= github.com/influxdata/influxdb1-client v0.0.0-20200827194710-b269163b24ab/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo= -github.com/influxdata/line-protocol v0.0.0-20210311194329-9aa0e372d097 h1:vilfsDSy7TDxedi9gyBkMvAirat/oRcL0lFdJBf6tdM= -github.com/influxdata/line-protocol v0.0.0-20210311194329-9aa0e372d097/go.mod h1:xaLFMmpvUxqXtVkUJfg9QmT88cDaCJ3ZKgdZ78oO8Qo= +github.com/influxdata/line-protocol v0.0.0-20210922203350-b1ad95c89adf h1:7JTmneyiNEwVBOHSjoMxiWAqB992atOeepeFYegn5RU= +github.com/influxdata/line-protocol v0.0.0-20210922203350-b1ad95c89adf/go.mod h1:xaLFMmpvUxqXtVkUJfg9QmT88cDaCJ3ZKgdZ78oO8Qo= github.com/invopop/jsonschema v0.12.0 h1:6ovsNSuvn9wEQVOyc72aycBMVQFKz7cPdMJn10CvzRI= github.com/invopop/jsonschema v0.12.0/go.mod h1:ffZ5Km5SWWRAIN6wbDXItl95euhFz2uON45H2qjYt+0= github.com/invopop/yaml v0.2.0 h1:7zky/qH+O0DwAyoobXUqvVBwgBFRxKoQ/3FjcVpjTMY= @@ -2497,16 +2490,10 @@ github.com/kshvakov/clickhouse v1.3.5/go.mod h1:DMzX7FxRymoNkVgizH0DWAL8Cur7wHLg github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348/go.mod h1:B69LEHPfb2qLo0BaaOLcbitczOKLWTsrBG9LczfCD4k= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= -github.com/labstack/echo/v4 v4.11.1 h1:dEpLU2FLg4UVmvCGPuk/APjlH6GDpbEPti61srUUUs4= -github.com/labstack/echo/v4 v4.11.1/go.mod h1:YuYRTSM3CHs2ybfrL8Px48bO6BAnYIN4l8wSTMP6BDQ= -github.com/labstack/gommon v0.4.0 h1:y7cvthEAEbU0yHOf4axH8ZG2NH8knB9iNSoTO8dyIk8= -github.com/labstack/gommon v0.4.0/go.mod h1:uW6kP17uPlLJsD3ijUYn3/M5bAxtlZhMI6m3MFxTMTM= github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80/go.mod h1:imJHygn/1yfhB7XSJJKlFZKl/J+dCPAknuiaGOshXAs= github.com/leesper/go_rng v0.0.0-20190531154944-a612b043e353 h1:X/79QL0b4YJVO5+OsPH9rF2u428CIrGL/jLmPsoOQQ4= github.com/leesper/go_rng v0.0.0-20190531154944-a612b043e353/go.mod h1:N0SVk0uhy+E1PZ3C9ctsPRlvOPAFPkCNlcPBDkt0N3U= github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= -github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q= -github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4= github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= @@ -2551,7 +2538,6 @@ github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVc github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= -github.com/mattn/go-colorable v0.1.11/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= @@ -2569,8 +2555,9 @@ github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Ky github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= -github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +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= @@ -2693,6 +2680,8 @@ github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLA github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= +github.com/oapi-codegen/runtime v1.1.1 h1:EXLHh0DXIJnWhdRPN2w4MXAzFyE4CskzhNLUmtpMYro= +github.com/oapi-codegen/runtime v1.1.1/go.mod h1:SK9X900oXmPWilYR5/WKPzt3Kqxn/uS/+lbpREv+eCg= github.com/oklog/oklog v0.3.2/go.mod h1:FCV+B7mhrz4o+ueLpx+KqkyXRGMWOYEvfiXtdGtbWGs= github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA= github.com/oklog/run v1.1.0 h1:GEenZ1cK0+q0+wsJew9qUg/DyD8k3JzYsZAi5gYi2mA= @@ -2776,8 +2765,6 @@ github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/9 github.com/pelletier/go-toml v1.7.0/go.mod h1:vwGMzjaWMwyfHwgIBhI2YUM4fB6nL6lVAvS1LBMMhTE= github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= github.com/pelletier/go-toml/v2 v2.0.5/go.mod h1:OMHamSCAODeSsVrwwvcJOaoN0LIUIaFVNZzmWyNfXas= -github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ= -github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4= github.com/performancecopilot/speed v3.0.0+incompatible/go.mod h1:/CLtqpZ5gBg1M9iaPbIdPPGyKcA8hKdoy6hAWba7Yac= github.com/performancecopilot/speed/v4 v4.0.0/go.mod h1:qxrSyuDGrTOWfV+uKRFhfxw6h/4HXRGUiZiufxo49BM= github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX50IvK2s= @@ -2826,8 +2813,9 @@ github.com/prometheus/client_golang v1.12.1/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrb github.com/prometheus/client_golang v1.14.0/go.mod h1:8vpkKitgIVNcqrRBWh1C4TIUQgYNtG/XQE4E/Zae36Y= github.com/prometheus/client_golang v1.15.1/go.mod h1:e9yaBhRPU2pPNsZwE+JdQl0KEt1N9XgF6zxWmaC0xOk= github.com/prometheus/client_golang v1.17.0/go.mod h1:VeL+gMmOAxkS2IqfCq0ZmHSL+LjWfWDUmp1mBz9JgUY= -github.com/prometheus/client_golang v1.18.0 h1:HzFfmkOzH5Q8L8G+kSJKUx5dtG87sewO+FoDDqP5Tbk= github.com/prometheus/client_golang v1.18.0/go.mod h1:T+GXkCk5wSJyOqMIzVgvvjFDlkOQntgjkJWKrN5txjA= +github.com/prometheus/client_golang v1.19.0 h1:ygXvpU1AoN1MhdzckN+PyD9QJOSD4x7kmXYlnfbA6JU= +github.com/prometheus/client_golang v1.19.0/go.mod h1:ZRM9uEAypZakd+q/x7+gmsvXdURP+DABIEIjnmDdp+k= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= @@ -2837,8 +2825,9 @@ github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6T github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w= github.com/prometheus/client_model v0.4.0/go.mod h1:oMQmHW1/JoDwqLtg57MGgP/Fb1CJEYF2imWWhWtMkYU= github.com/prometheus/client_model v0.4.1-0.20230718164431-9a2bf3000d16/go.mod h1:oMQmHW1/JoDwqLtg57MGgP/Fb1CJEYF2imWWhWtMkYU= -github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw= github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI= +github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= +github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= github.com/prometheus/common v0.0.0-20181126121408-4724e9255275/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= github.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= @@ -2855,8 +2844,9 @@ github.com/prometheus/common v0.37.0/go.mod h1:phzohg0JFMnBEFGxTDbfu3QyL5GI8gTQJ github.com/prometheus/common v0.42.0/go.mod h1:xBwqVerjNdUDjgODMpudtOMwlOwf2SaTr1yjz4b7Zbc= github.com/prometheus/common v0.44.0/go.mod h1:ofAIvZbQ1e/nugmZGz4/qCb9Ap1VoSTIO7x0VV9VvuY= github.com/prometheus/common v0.45.0/go.mod h1:YJmSTw9BoKxJplESWWxlbyttQR4uaEcGyv9MZjVOJsY= -github.com/prometheus/common v0.46.0 h1:doXzt5ybi1HBKpsZOL0sSkaNHJJqkyfEWZGGqqScV0Y= -github.com/prometheus/common v0.46.0/go.mod h1:Tp0qkxpb9Jsg54QMe+EAmqXkSV7Evdy1BTn+g2pa/hQ= +github.com/prometheus/common v0.48.0/go.mod h1:0/KsvlIEfPQCQ5I2iNSAWKPZziNCvRs5EC6ILDTlAPc= +github.com/prometheus/common v0.53.0 h1:U2pL9w9nmJwJDa4qqLQ3ZaePJ6ZTwt7cMD3AG3+aLCE= +github.com/prometheus/common v0.53.0/go.mod h1:BrxBKv3FWBIGXw89Mg1AeBq7FSyRzXWI3l3e7W3RN5U= github.com/prometheus/common/assets v0.2.0/go.mod h1:D17UVUE12bHbim7HzwUvtqm6gwBEaDQ0F+hIGbFbccI= github.com/prometheus/common/sigv4 v0.1.0 h1:qoVebwtwwEhS85Czm2dSROY5fTo2PAPEVdDeppTwGX4= github.com/prometheus/common/sigv4 v0.1.0/go.mod h1:2Jkxxk9yYvCkE5G1sQT7GuEXm57JrvHu9k5YwTjsNtI= @@ -2874,8 +2864,9 @@ github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1 github.com/prometheus/procfs v0.8.0/go.mod h1:z7EfXMXOkbkqb9IINtpCn86r/to3BnA0uaxHdg830/4= github.com/prometheus/procfs v0.9.0/go.mod h1:+pB4zwohETzFnmlpe6yd2lSc+0/46IYZRB/chUwxUZY= github.com/prometheus/procfs v0.11.1/go.mod h1:eesXgaPo1q7lBpVMoMy0ZOFTth9hBn4W/y0/p/ScXhY= -github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo= github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= +github.com/prometheus/procfs v0.14.0 h1:Lw4VdGGoKEZilJsayHf0B+9YgLGREba2C6xr+Fdfq6s= +github.com/prometheus/procfs v0.14.0/go.mod h1:XL+Iwz8k8ZabyZfMFHPiilCniixqQarAy5Mu67pHlNQ= github.com/prometheus/prometheus v0.49.0 h1:i0CEhreJo3ZcZNeK7ulISinCac0MgL0krVOGgNmfFRY= github.com/prometheus/prometheus v0.49.0/go.mod h1:aDogiyqmv3aBIWDb5z5Sdcxuuf2BOfiJwOIm9JGpMnI= github.com/protocolbuffers/txtpbfmt v0.0.0-20220428173112-74888fd59c2b h1:zd/2RNzIRkoGGMjE+YIsZ85CnDIz672JK2F3Zl4vux4= @@ -2903,8 +2894,9 @@ github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTE github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= -github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= +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/rs/cors v1.10.1 h1:L0uuZVXIKlI1SShY2nhFfo44TYvDPQ1w4oFkUJNfhyo= github.com/rs/cors v1.10.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= @@ -2927,8 +2919,8 @@ github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da/go.mod h1:gi+0 github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= github.com/scaleway/scaleway-sdk-go v1.0.0-beta.21 h1:yWfiTPwYxB0l5fGMhl/G+liULugVIHD9AU77iNLrURQ= github.com/scaleway/scaleway-sdk-go v1.0.0-beta.21/go.mod h1:fCa7OJZ/9DRTnOKmxvT6pn+LPWUptQAmHF/SBJUGEcg= -github.com/scottlepp/go-duck v0.0.15 h1:qrSF3pXlXAA4a7uxAfLYajqXLkeBjv8iW1wPdSfkMj0= -github.com/scottlepp/go-duck v0.0.15/go.mod h1:GL+hHuKdueJRrFCduwBc7A7TQk+Tetc5BPXPVtduihY= +github.com/scottlepp/go-duck v0.0.19 h1:SjO0HF+xe6TN9agMram+CG8+NWKgGMSj8LfqRm0JvpA= +github.com/scottlepp/go-duck v0.0.19/go.mod h1:GL+hHuKdueJRrFCduwBc7A7TQk+Tetc5BPXPVtduihY= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 h1:nn5Wsu0esKSJiIVhscUtVbo7ada43DJhG55ua/hjS5I= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= github.com/segmentio/asm v1.1.3/go.mod h1:Ld3L4ZXGNcSLRg4JBsZ3//1+f/TjYl0Mzen/DQy1EJg= @@ -3033,14 +3025,13 @@ github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1 github.com/tmc/grpc-websocket-proxy v0.0.0-20220101234140-673ab2c3ae75 h1:6fotK7otjonDflCTK0BCfls4SPy3NcCVb5dqqmbRknE= github.com/tmc/grpc-websocket-proxy v0.0.0-20220101234140-673ab2c3ae75/go.mod h1:KO6IkyS8Y3j8OdNO85qEYBsRPuteD+YciPomcXdrMnk= github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= -github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= -github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/ua-parser/uap-go v0.0.0-20211112212520-00c877edfe0f h1:A+MmlgpvrHLeUP8dkBVn4Pnf5Bp5Yk2OALm7SEJLLE8= github.com/ua-parser/uap-go v0.0.0-20211112212520-00c877edfe0f/go.mod h1:OBcG9bn7sHtXgarhUEb3OfCnNsgtGnkVf41ilSZ3K3E= github.com/uber/jaeger-client-go v2.30.0+incompatible h1:D6wyKGCecFaSRUpo8lCVbaOOb6ThwMmTEbhRwtKR97o= github.com/uber/jaeger-client-go v2.30.0+incompatible/go.mod h1:WVhlPFC8FDjOFMMWRy2pZqQJSXxYSwNYOkTr/Z6d3Kk= github.com/uber/jaeger-lib v2.4.1+incompatible h1:td4jdvLcExb4cBISKIpHuGoVXh+dVKhn2Um6rjCsSsg= github.com/uber/jaeger-lib v2.4.1+incompatible/go.mod h1:ComeNDZlWwrWnDv8aPp0Ba6+uUTzImX/AauajbLI56U= +github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo= github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= @@ -3054,15 +3045,12 @@ github.com/unknwon/log v0.0.0-20150304194804-e617c87089d3 h1:4EYQaWAatQokdji3zqZ github.com/unknwon/log v0.0.0-20150304194804-e617c87089d3/go.mod h1:1xEUf2abjfP92w2GZTV+GgaRxXErwRXcClbUwrNJffU= github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= -github.com/urfave/cli v1.22.14 h1:ebbhrRiGK2i4naQJr+1Xj92HXZCrK7MsyTS/ob3HnAk= -github.com/urfave/cli v1.22.14/go.mod h1:X0eDS6pD6Exaclxm99NJ3FiCDRED7vIHpx2mDOHLvkA= +github.com/urfave/cli v1.22.15 h1:nuqt+pdC/KqswQKhETJjo7pvn/k4xMUxgW6liI7XpnM= +github.com/urfave/cli v1.22.15/go.mod h1:wSan1hmo5zeyLGBjRJbzRTNk8gwoYa2B9n4q9dmRIc0= github.com/urfave/cli/v2 v2.25.0 h1:ykdZKuQey2zq0yin/l7JOm9Mh+pg72ngYMeB0ABn6q8= github.com/urfave/cli/v2 v2.25.0/go.mod h1:GHupkWPMM0M/sj1a2b4wUrWBPzazNrIjouW6fmdJLxc= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= -github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= -github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo= -github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= github.com/vectordotdev/go-datemath v0.1.1-0.20220323213446-f3954d0b18ae h1:oyiy3uBj1F4O3AaFh7hUGBrJjAssJhKyAbwxtkslxqo= github.com/vectordotdev/go-datemath v0.1.1-0.20220323213446-f3954d0b18ae/go.mod h1:PnwzbSst7KD3vpBzzlntZU5gjVa455Uqa5QPiKSYJzQ= github.com/vultr/govultr/v2 v2.17.2 h1:gej/rwr91Puc/tgh+j33p/BLR16UrIPnSr+AIwYWZQs= @@ -3167,51 +3155,51 @@ go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= go.opentelemetry.io/collector/featuregate v1.0.0/go.mod h1:xGbRuw+GbutRtVVSEy3YR2yuOlEyiUMhN2M9DJljgqY= go.opentelemetry.io/collector/pdata v1.0.0/go.mod h1:TsDFgs4JLNG7t6x9D8kGswXUz4mme+MyNChHx8zSF6k= -go.opentelemetry.io/collector/pdata v1.0.1 h1:dGX2h7maA6zHbl5D3AsMnF1c3Nn+3EUftbVCLzeyNvA= -go.opentelemetry.io/collector/pdata v1.0.1/go.mod h1:jutXeu0QOXYY8wcZ/hege+YAnSBP3+jpTqYU1+JTI5Y= +go.opentelemetry.io/collector/pdata v1.6.0 h1:ZIByleLu7ZfHkfPuL8xIMb9M4Gv1R6568LAjhNOO9zY= +go.opentelemetry.io/collector/pdata v1.6.0/go.mod h1:pQv6AJO6wDUDxrPxhNaj3JdSzaOIo5glTGL1b4h4KTg= go.opentelemetry.io/collector/semconv v0.90.1/go.mod h1:j/8THcqVxFna1FpvA2zYIsUperEtOaRaqoLYIN4doWw= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0 h1:4Pp6oUg3+e/6M4C0A/3kJ2VYa++dsWVTtGgLVj5xtHg= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0/go.mod h1:Mjt1i1INqiaoZOMGR1RIUJN+i3ChKoFRqzrRQhlkbs0= -go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.49.0 h1:RtcvQ4iw3w9NBB5yRwgA4sSa82rfId7n4atVpvKx3bY= -go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.49.0/go.mod h1:f/PbKbRd4cdUICWell6DmzvVJ7QrmBgFrRHjXmAXbK4= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.51.0 h1:A3SayB3rNyt+1S6qpI9mHPkeHTZbD7XILEqWnYZb2l0= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.51.0/go.mod h1:27iA5uvhuRNmalO+iEUdVn5ZMj2qy10Mm+XRIpRmyuU= +go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.51.0 h1:974XTyIwHI4nHa1+uSLxHtUnlJ2DiVtAJjk7fd07p/8= +go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.51.0/go.mod h1:ZvX/taFlN6TGaOOM6D42wrNwPKUV1nGO2FuUXkityBU= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1/go.mod h1:sEGXWArGqc3tVa+ekntsN65DmVbVeW+7lTKTjZF3/Fo= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 h1:jq9TW8u3so/bN+JPT166wjOI6/vQPF6Xe7nMNIltagk= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0/go.mod h1:p8pYQP+m5XfbZm9fxtSKAbM6oIllS7s2AfxrChvc7iw= -go.opentelemetry.io/contrib/propagators/jaeger v1.22.0 h1:bAHX+zN/inu+Rbqk51REmC8oXLl+Dw6pp9ldQf/onaY= -go.opentelemetry.io/contrib/propagators/jaeger v1.22.0/go.mod h1:bH9GkgkN21mscXcQP6lQJYI8XnEPDxlTN/ZOBuHDjqE= -go.opentelemetry.io/contrib/samplers/jaegerremote v0.18.0 h1:Q9PrD94WoMolBx44ef5UWWvufpVSME0MiSymXZfedso= -go.opentelemetry.io/contrib/samplers/jaegerremote v0.18.0/go.mod h1:tjp49JHNvreAAoWjdCHIVD7NXMjuJ3Dp/9iNOuPPlC8= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.51.0 h1:Xs2Ncz0gNihqu9iosIZ5SkBbWo5T8JhhLJFMQL1qmLI= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.51.0/go.mod h1:vy+2G/6NvVMpwGX/NyLqcC41fxepnuKHk16E6IZUcJc= +go.opentelemetry.io/contrib/propagators/jaeger v1.26.0 h1:RH76Cl2pfOLLoCtxAPax9c7oYzuL1tiI7/ZPJEmEmOw= +go.opentelemetry.io/contrib/propagators/jaeger v1.26.0/go.mod h1:W/cylm0ZtJK1uxsuTqoYGYPnqpZ8CeVGgW7TwfXPsGw= +go.opentelemetry.io/contrib/samplers/jaegerremote v0.20.0 h1:ja+d7Aea/9PgGxB63+E0jtRFpma717wubS0KFkZpmYw= +go.opentelemetry.io/contrib/samplers/jaegerremote v0.20.0/go.mod h1:Yc1eg51SJy7xZdOTyg1xyFcwE+ghcWh3/0hKeLo6Wlo= go.opentelemetry.io/otel v1.17.0/go.mod h1:I2vmBGtFaODIVMBSTPVDlJSzBDNf93k60E6Ft0nyjo0= go.opentelemetry.io/otel v1.21.0/go.mod h1:QZzNPQPm1zLX4gZK4cMi+71eaorMSGT3A4znnUvNNEo= -go.opentelemetry.io/otel v1.24.0 h1:0LAOdjNmQeSTzGBzduGe/rU4tZhMwL5rWgtp9Ku5Jfo= -go.opentelemetry.io/otel v1.24.0/go.mod h1:W7b9Ozg4nkF5tWI5zsXkaKKDjdVjpD4oAt9Qi/MArHo= -go.opentelemetry.io/otel/exporters/jaeger v1.10.0 h1:7W3aVVjEYayu/GOqOVF4mbTvnCuxF1wWu3eRxFGQXvw= -go.opentelemetry.io/otel/exporters/jaeger v1.10.0/go.mod h1:n9IGyx0fgyXXZ/i0foLHNxtET9CzXHzZeKCucvRBFgA= +go.opentelemetry.io/otel v1.26.0 h1:LQwgL5s/1W7YiiRwxf03QGnWLb2HW4pLiAhaA5cZXBs= +go.opentelemetry.io/otel v1.26.0/go.mod h1:UmLkJHUAidDval2EICqBMbnAd0/m2vmpf/dAM+fvFs4= +go.opentelemetry.io/otel/exporters/jaeger v1.17.0 h1:D7UpUy2Xc2wsi1Ras6V40q806WM07rqoCWzXu7Sqy+4= +go.opentelemetry.io/otel/exporters/jaeger v1.17.0/go.mod h1:nPCqOnEH9rNLKqH/+rrUjiMzHJdV1BlpKcTwRTyKkKI= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.21.0/go.mod h1:zgBdWWAu7oEEMC06MMKc5NLbA/1YDXV1sMpSqEeLQLg= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.24.0 h1:t6wl9SPayj+c7lEIFgm4ooDBZVb01IhLB4InpomhRw8= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.24.0/go.mod h1:iSDOcsnSA5INXzZtwaBPrKp/lWu/V14Dd+llD0oI2EA= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.26.0 h1:1u/AyyOqAWzy+SkPxDpahCNZParHV8Vid1RnI2clyDE= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.26.0/go.mod h1:z46paqbJ9l7c9fIPCXTqTGwhQZ5XoTIsfeFYWboizjs= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.21.0/go.mod h1:nUeKExfxAQVbiVFn32YXpXZZHZ61Cc3s3Rn1pDBGAb0= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.24.0 h1:Mw5xcxMwlqoJd97vwPxA8isEaIoxsta9/Q51+TTJLGE= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.24.0/go.mod h1:CQNu9bj7o7mC6U7+CA/schKEYakYXWr79ucDHTMGhCM= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.26.0 h1:Waw9Wfpo/IXzOI8bCB7DIk+0JZcqqsyn1JFnAc+iam8= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.26.0/go.mod h1:wnJIG4fOqyynOnnQF/eQb4/16VlX2EJAHhHgqIqWfAo= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.21.0/go.mod h1:/OpE/y70qVkndM0TrxT4KBoN3RsFZP0QaofcfYrj76I= go.opentelemetry.io/otel/metric v1.17.0/go.mod h1:h4skoxdZI17AxwITdmdZjjYJQH5nzijUUjm+wtPph5o= go.opentelemetry.io/otel/metric v1.21.0/go.mod h1:o1p3CA8nNHW8j5yuQLdc1eeqEaPfzug24uvsyIEJRWM= -go.opentelemetry.io/otel/metric v1.24.0 h1:6EhoGWWK28x1fbpA4tYTOWBkPefTDQnb8WSGXlc88kI= -go.opentelemetry.io/otel/metric v1.24.0/go.mod h1:VYhLe1rFfxuTXLgj4CBiyz+9WYBA8pNGJgDcSFRKBco= +go.opentelemetry.io/otel/metric v1.26.0 h1:7S39CLuY5Jgg9CrnA9HHiEjGMF/X2VHvoXGgSllRz30= +go.opentelemetry.io/otel/metric v1.26.0/go.mod h1:SY+rHOI4cEawI9a7N1A4nIg/nTQXe1ccCNWYOJUrpX4= go.opentelemetry.io/otel/sdk v1.17.0/go.mod h1:U87sE0f5vQB7hwUoW98pW5Rz4ZDuCFBZFNUBlSgmDFQ= go.opentelemetry.io/otel/sdk v1.21.0/go.mod h1:Nna6Yv7PWTdgJHVRD9hIYywQBRx7pbox6nwBnZIxl/E= -go.opentelemetry.io/otel/sdk v1.24.0 h1:YMPPDNymmQN3ZgczicBY3B6sf9n62Dlj9pWD3ucgoDw= -go.opentelemetry.io/otel/sdk v1.24.0/go.mod h1:KVrIYw6tEubO9E96HQpcmpTKDVn9gdv35HoYiQWGDFg= +go.opentelemetry.io/otel/sdk v1.26.0 h1:Y7bumHf5tAiDlRYFmGqetNcLaVUZmh4iYfmGxtmz7F8= +go.opentelemetry.io/otel/sdk v1.26.0/go.mod h1:0p8MXpqLeJ0pzcszQQN4F0S5FVjBLgypeGSngLsmirs= go.opentelemetry.io/otel/trace v1.17.0/go.mod h1:I/4vKTgFclIsXRVucpH25X0mpFSczM7aHeaz0ZBLWjY= go.opentelemetry.io/otel/trace v1.21.0/go.mod h1:LGbsEB0f9LGjN+OZaQQ26sohbOmiMR+BaslueVtS/qQ= -go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y1YELI= -go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU= +go.opentelemetry.io/otel/trace v1.26.0 h1:1ieeAUb4y0TE26jUFrCIXKpTuVK7uJGN9/Z/2LP5sQA= +go.opentelemetry.io/otel/trace v1.26.0/go.mod h1:4iDxvGDQuUkHve82hJJ8UqrwswHYsZuWCBllGV2U2y0= go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= go.opentelemetry.io/proto/otlp v0.15.0/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI405h3+duxN4U= go.opentelemetry.io/proto/otlp v0.19.0/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI405h3+duxN4U= go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM= -go.opentelemetry.io/proto/otlp v1.1.0 h1:2Di21piLrCqJ3U3eXGCTPHE9R8Nh+0uglSnOyxikMeI= -go.opentelemetry.io/proto/otlp v1.1.0/go.mod h1:GpBHCBWiqvVLDqmHZsoMM3C5ySeKTC7ej/RNTae6MdY= +go.opentelemetry.io/proto/otlp v1.2.0 h1:pVeZGk7nXDC9O2hncA6nHldxEjm6LByfA2aN8IOkz94= +go.opentelemetry.io/proto/otlp v1.2.0/go.mod h1:gGpR8txAl5M03pDhMC79G6SdqNV26naRm/KDsgaHD8A= go.starlark.net v0.0.0-20230525235612-a134d8f9ddca h1:VdD38733bfYv5tUZwEIskMM93VanwNIi5bIKnDrJdEY= go.starlark.net v0.0.0-20230525235612-a134d8f9ddca/go.mod h1:jxU+3+j+71eXOW14274+SmmuW82qJzl6iZSeqEtTGds= go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= @@ -3223,7 +3211,6 @@ go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= go.uber.org/automaxprocs v1.5.3/go.mod h1:eRbA25aqJrxAbsLO0xy5jVwPt7FQnRgjW+efnwa1WM0= -go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A= go.uber.org/goleak v1.1.11-0.20210813005559-691160354723/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= @@ -3241,16 +3228,12 @@ go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo= -go.uber.org/zap v1.18.1/go.mod h1:xg/QME4nWcxGxrpdeYfq7UvYrLh66cuVKdrbD1XF/NI= go.uber.org/zap v1.19.1/go.mod h1:j3DNczoxDZroyBnOT1L/Q79cfUMGZxlv/9dzN7SM1rI= go.uber.org/zap v1.21.0/go.mod h1:wjWOCqI0f2ZZrJF/UufIOkiC8ii6tm1iqIsLo76RfJw= go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo= go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so= gocloud.dev v0.25.0 h1:Y7vDq8xj7SyM848KXf32Krda2e6jQ4CLh/mTeCSqXtk= gocloud.dev v0.25.0/go.mod h1:7HegHVCYZrMiU3IE1qtnzf/vRrDwLYnRNR3EhWX8x9Y= -golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= -golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k= -golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= @@ -3304,8 +3287,8 @@ golang.org/x/crypto v0.15.0/go.mod h1:4ChreQoLWfG3xLDer1WdlH5NdlQ3+mwnQq1YTKY+72 golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= -golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA= -golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= +golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30= +golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -3350,6 +3333,7 @@ golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRu golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20210508222113-6edffad5e616 h1:VLliZ0d+/avPrXXH+OakdXhpJuEoBZuwh1m2j7U6Iug= golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= @@ -3374,8 +3358,9 @@ golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.10.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.11.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0= golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.15.0 h1:SernR4v+D55NyBH2QiEQrlBAnj1ECL6AGrA5+dPaMY8= +golang.org/x/mod v0.15.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= @@ -3473,8 +3458,8 @@ golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= golang.org/x/net v0.18.0/go.mod h1:/czyP5RqHAH4odGYxBJ1qz0+CE5WZ+2j1YgoEo8F2jQ= golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U= golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= -golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc= -golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= +golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w= +golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20181106182150-f42d05182288/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -3516,8 +3501,8 @@ golang.org/x/oauth2 v0.13.0/go.mod h1:/JMhi4ZRXAf4HG9LiNmxvk+45+96RUlVThiH8FzNBn golang.org/x/oauth2 v0.14.0/go.mod h1:lAtNWgaWfL4cm7j2OV8TxGi9Qb7ECORx8DktCY74OwM= golang.org/x/oauth2 v0.15.0/go.mod h1:q48ptWNTY5XWf+JNten23lcvHpLJ0ZSxF5ttTHKVCAM= golang.org/x/oauth2 v0.16.0/go.mod h1:hqZ+0LWXsiVoZpeld6jVt06P3adbS2Uu911W1SsJv2o= -golang.org/x/oauth2 v0.18.0 h1:09qnuIAgzdx1XplqJvW6CQqMCtGZykZWcXzPMPUusvI= -golang.org/x/oauth2 v0.18.0/go.mod h1:Wf7knwG0MPoWIMMBgFlEaSUDaKskp0dCfrlJRJXbBi8= +golang.org/x/oauth2 v0.19.0 h1:9+E/EZBCbTLNrbN35fHv/a/d/mOBatymz1zbtQrXpIg= +golang.org/x/oauth2 v0.19.0/go.mod h1:vYi7skDa1x015PmRRYZ7+s1cWyPgrPiSYRe4rnsexc8= 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= @@ -3540,8 +3525,9 @@ golang.org/x/sync v0.2.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sync v0.4.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ= golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -3642,8 +3628,6 @@ golang.org/x/sys v0.0.0-20210917161153-d61c044b1678/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211110154304-99a53858aa08/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211116061358-0a5406a5449c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -3689,8 +3673,8 @@ golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= -golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= +golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= @@ -3712,8 +3696,8 @@ golang.org/x/term v0.14.0/go.mod h1:TySc+nGkYR6qt8km8wUhuFRTVSMIX3XPR58y2lC8vww= golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0= golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY= golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= -golang.org/x/term v0.18.0 h1:FcHjZXDMxI8mM3nwhX9HlKop4C0YQvCVCdwYl2wOtE8= -golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58= +golang.org/x/term v0.19.0 h1:+ThwsDv+tYfnJFhF4L8jITxu1tdTWRTZpdsWgEgjL6Q= +golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -3782,7 +3766,6 @@ golang.org/x/tools v0.0.0-20190927191325-030b2cf1153e/go.mod h1:b+2E5dAYhXwXZwtn golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191108193012-7d206e10da11/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= @@ -3841,8 +3824,9 @@ golang.org/x/tools v0.10.0/go.mod h1:UJwyiVBsOA2uwvK/e5OY3GTpDUJriEd+/YlqAwLPmyM golang.org/x/tools v0.12.1-0.20230815132531-74c255bcf846/go.mod h1:Sc0INKfu04TlqNoRA1hgpFZbhYXHPr4V5DzpSBTPqQM= golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= golang.org/x/tools v0.16.0/go.mod h1:kYVVN6I1mBNoB1OX+noeBjbRk4IUEPa7JJ+TJMEooJ0= -golang.org/x/tools v0.17.0 h1:FvmRgNOcs3kOa+T20R1uhfP9F6HgG2mfxDv1vrx1Htc= golang.org/x/tools v0.17.0/go.mod h1:xsh6VxdV005rRVaS6SSAf9oiAqljS7UZUacMZ8Bnsps= +golang.org/x/tools v0.18.0 h1:k8NLag8AGHnn+PHbl7g43CtqZAwG60vZkLqgyZgIHgQ= +golang.org/x/tools v0.18.0/go.mod h1:GL7B4CwcLLeo59yx/9UWWuNOW1n3VZ4f5axWfML7Lcg= golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -3942,8 +3926,8 @@ google.golang.org/api v0.128.0/go.mod h1:Y611qgqaE92On/7g65MQgxYul3c0rEB894kniWL google.golang.org/api v0.139.0/go.mod h1:CVagp6Eekz9CjGZ718Z+sloknzkDJE7Vc1Ckj9+viBk= google.golang.org/api v0.149.0/go.mod h1:Mwn1B7JTXrzXtnvmzQE2BD6bYZQ8DShKZDZbeN9I7qI= google.golang.org/api v0.153.0/go.mod h1:3qNJX5eOmhiWYc67jRA/3GsDw97UFb5ivv7Y2PrriAY= -google.golang.org/api v0.162.0 h1:Vhs54HkaEpkMBdgGdOT2P6F0csGG/vxDS0hWHJzmmps= -google.golang.org/api v0.162.0/go.mod h1:6SulDkfoBIg4NFmCuZ39XeeAgSHCPecfSUuDyYlAHs0= +google.golang.org/api v0.176.0 h1:dHj1/yv5Dm/eQTXiP9hNCRT3xzJHWXeNdRq29XbMxoE= +google.golang.org/api v0.176.0/go.mod h1:Rra+ltKu14pps/4xTycZfobMgLpbosoaaL7c+SEMrO8= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= @@ -3954,8 +3938,6 @@ google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM= -google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds= google.golang.org/genproto v0.0.0-20170818010345-ee236bd376b0/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20180518175338-11a468237815/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= @@ -4151,8 +4133,8 @@ google.golang.org/genproto/googleapis/api v0.0.0-20231016165738-49dd2c1f3d0b/go. google.golang.org/genproto/googleapis/api v0.0.0-20231030173426-d783a09b4405/go.mod h1:oT32Z4o8Zv2xPQTg0pbVaPr0MPOH6f14RgXt7zfIpwg= google.golang.org/genproto/googleapis/api v0.0.0-20231106174013-bbf56f31fb17/go.mod h1:0xJLfVdJqpAPl8tDg1ujOCGzx6LFLttXT5NhllGOXY4= google.golang.org/genproto/googleapis/api v0.0.0-20231127180814-3a041ad873d4/go.mod h1:k2dtGpRrbsSyKcNPKKI5sstZkrNCZwpU/ns96JoHbGg= -google.golang.org/genproto/googleapis/api v0.0.0-20240227224415-6ceb2ff114de h1:jFNzHPIeuzhdRwVhbZdiym9q0ory/xY3sA+v2wPg8I0= -google.golang.org/genproto/googleapis/api v0.0.0-20240227224415-6ceb2ff114de/go.mod h1:5iCWqnniDlqZHrd3neWVTOwvh/v6s3232omMecelax8= +google.golang.org/genproto/googleapis/api v0.0.0-20240415180920-8c6c420018be h1:Zz7rLWqp0ApfsR/l7+zSHhY3PMiH2xqgxlfYfAfNpoU= +google.golang.org/genproto/googleapis/api v0.0.0-20240415180920-8c6c420018be/go.mod h1:dvdCTIoAGbkWbcIKBniID56/7XHTt6WfxXNMxuziJ+w= google.golang.org/genproto/googleapis/bytestream v0.0.0-20230530153820-e85fd2cbaebc/go.mod h1:ylj+BE99M198VPbBh6A8d9n3w8fChvyLK3wwBOjXBFA= google.golang.org/genproto/googleapis/bytestream v0.0.0-20230807174057-1744710a1577/go.mod h1:NjCQG/D8JandXxM57PZbAJL1DCNL6EypA0vPPwfsc7c= google.golang.org/genproto/googleapis/bytestream v0.0.0-20231030173426-d783a09b4405/go.mod h1:GRUCuLdzVqZte8+Dl/D4N25yLzcGqqWaYkeVOwulFqw= @@ -4175,8 +4157,8 @@ google.golang.org/genproto/googleapis/rpc v0.0.0-20231016165738-49dd2c1f3d0b/go. google.golang.org/genproto/googleapis/rpc v0.0.0-20231030173426-d783a09b4405/go.mod h1:67X1fPuzjcrkymZzZV1vvkFeTn2Rvc6lYF9MYFGCcwE= google.golang.org/genproto/googleapis/rpc v0.0.0-20231106174013-bbf56f31fb17/go.mod h1:oQ5rr10WTTMvP4A36n8JpR1OrO1BEiV4f78CneXZxkA= google.golang.org/genproto/googleapis/rpc v0.0.0-20231120223509-83a465c0220f/go.mod h1:L9KNLi232K1/xB6f7AlSX692koaRnKaWSR0stBki0Yc= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240227224415-6ceb2ff114de h1:cZGRis4/ot9uVm639a+rHCUaG0JJHEsdyzSQTMX+suY= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240227224415-6ceb2ff114de/go.mod h1:H4O17MA/PE9BsGx3w+a+W2VOLLD1Qf7oJneAoU6WktY= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240415180920-8c6c420018be h1:LG9vZxsWGOmUKieR8wPAUR3u3MpnYFQZROPIMaXh7/A= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240415180920-8c6c420018be/go.mod h1:WtryC6hu0hhx87FDGxWCDptyssuo68sk10vYjF+T9fY= 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= @@ -4309,8 +4291,9 @@ gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo= gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk= -gotest.tools/v3 v3.0.3 h1:4AuOwCGf4lLR9u3YOe2awrHygurzhO/HeQ6laiA6Sx0= gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8= +gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU= +gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU= honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= @@ -4349,8 +4332,8 @@ k8s.io/kms v0.29.2/go.mod h1:s/9RC4sYRZ/6Tn6yhNjbfJuZdb8LzlXhdlBnKizeFDo= k8s.io/kube-aggregator v0.29.0 h1:N4fmtePxOZ+bwiK1RhVEztOU+gkoVkvterHgpwAuiTw= k8s.io/kube-aggregator v0.29.0/go.mod h1:bjatII63ORkFg5yUFP2qm2OC49R0wwxZhRVIyJ4Z4X0= k8s.io/kube-openapi v0.0.0-20230717233707-2695361300d9/go.mod h1:wZK2AVp1uHCp4VamDVgBP2COHZjqD1T68Rf0CM3YjSM= -k8s.io/kube-openapi v0.0.0-20240220201932-37d671a357a5 h1:QSpdNrZ9uRlV0VkqLvVO0Rqg8ioKi3oSw7O5P7pJV8M= -k8s.io/kube-openapi v0.0.0-20240220201932-37d671a357a5/go.mod h1:Pa1PvrP7ACSkuX6I7KYomY6cmMA0Tx86waBhDUgoKPw= +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-20210802155522-efc7438f0176/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= k8s.io/utils v0.0.0-20230406110748-d93618cff8a2/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= k8s.io/utils v0.0.0-20230711102312-30195339c3c7/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= diff --git a/go.work b/go.work index 7ccf184922b..e5ee50b552c 100644 --- a/go.work +++ b/go.work @@ -12,3 +12,7 @@ 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 + +// this is required until a new version of k8s.io/component-base is released +// with an update to prometheus/common v0.48.0 +replace k8s.io/component-base => k8s.io/component-base v0.0.0-20240417101527-62c04b35eff6 diff --git a/go.work.sum b/go.work.sum index 80ad0ed6b81..08b659f334b 100644 --- a/go.work.sum +++ b/go.work.sum @@ -1,6 +1,9 @@ +buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.31.0-20230721003620-2341cbb21958.1/go.mod h1:xafc+XIsTxTy76GJQ1TKgvJWsSugFBqMaN27WhUblew= +buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.31.0-20230802163732-1c33ebd9ecfa.1/go.mod h1:xafc+XIsTxTy76GJQ1TKgvJWsSugFBqMaN27WhUblew= buf.build/gen/go/grpc-ecosystem/grpc-gateway/bufbuild/connect-go v1.4.1-20221127060915-a1ecdc58eccd.1 h1:vp9EaPFSb75qe/793x58yE5fY1IJ/gdxb/kcDUzavtI= buf.build/gen/go/grpc-ecosystem/grpc-gateway/bufbuild/connect-go v1.4.1-20221127060915-a1ecdc58eccd.1/go.mod h1:YDq2B5X5BChU0lxAG5MxHpDb8mx1fv9OGtF2mwOe7hY= buf.build/gen/go/grpc-ecosystem/grpc-gateway/protocolbuffers/go v1.28.1-20221127060915-a1ecdc58eccd.4 h1:z3Xc9n8yZ5k/Xr4ZTuff76TAYP20dWy7ZBV4cGIpbkM= +cloud.google.com/go v0.111.0/go.mod h1:0mibmpKP1TyOOFYQY5izo0LnT+ecvOQ0Sg3OdmMiNRU= cloud.google.com/go/accessapproval v1.7.4 h1:ZvLvJ952zK8pFHINjpMBY5k7LTAp/6pBf50RDMRgBUI= cloud.google.com/go/accessapproval v1.7.5 h1:uzmAMSgYcnlHa9X9YSQZ4Q1wlfl4NNkZyQgho1Z6p04= cloud.google.com/go/accessapproval v1.7.5/go.mod h1:g88i1ok5dvQ9XJsxpUInWWvUBrIZhyPDPbk4T01OoJ0= @@ -8,11 +11,15 @@ cloud.google.com/go/accesscontextmanager v1.8.4 h1:Yo4g2XrBETBCqyWIibN3NHNPQKUfQ cloud.google.com/go/accesscontextmanager v1.8.5 h1:2GLNaNu9KRJhJBFTIVRoPwk6xE5mUDgD47abBq4Zp/I= cloud.google.com/go/accesscontextmanager v1.8.5/go.mod h1:TInEhcZ7V9jptGNqN3EzZ5XMhT6ijWxTGjzyETwmL0Q= cloud.google.com/go/aiplatform v1.57.0 h1:WcZ6wDf/1qBWatmGM9Z+2BTiNjQQX54k2BekHUj93DQ= +cloud.google.com/go/aiplatform v1.57.0/go.mod h1:pwZMGvqe0JRkI1GWSZCtnAfrR4K1bv65IHILGA//VEU= cloud.google.com/go/aiplatform v1.58.0 h1:xyCAfpI4yUMOQ4VtHN/bdmxPQ8xoEkTwFM1nbVmuQhs= +cloud.google.com/go/aiplatform v1.58.0/go.mod h1:pwZMGvqe0JRkI1GWSZCtnAfrR4K1bv65IHILGA//VEU= +cloud.google.com/go/aiplatform v1.58.2/go.mod h1:c3kCiVmb6UC1dHAjZjcpDj6ZS0bHQ2slL88ZjC2LtlA= cloud.google.com/go/aiplatform v1.60.0 h1:0cSrii1ZeLr16MbBoocyy5KVnrSdiQ3KN/vtrTe7RqE= cloud.google.com/go/aiplatform v1.60.0/go.mod h1:eTlGuHOahHprZw3Hio5VKmtThIOak5/qy6pzdsqcQnM= cloud.google.com/go/analytics v0.21.6 h1:fnV7B8lqyEYxCU0LKk+vUL7mTlqRAq4uFlIthIdr/iA= cloud.google.com/go/analytics v0.22.0 h1:w8KIgW8NRUHFVKjpkwCpLaHsr685tJ+ckPStOaSCZz0= +cloud.google.com/go/analytics v0.22.0/go.mod h1:eiROFQKosh4hMaNhF85Oc9WO97Cpa7RggD40e/RBy8w= cloud.google.com/go/analytics v0.23.0 h1:Q+y94XH84jM8SK8O7qiY/PJRexb6n7dRbQ6PiUa4YGM= cloud.google.com/go/analytics v0.23.0/go.mod h1:YPd7Bvik3WS95KBok2gPXDqQPHy08TsCQG6CdUCb+u0= cloud.google.com/go/apigateway v1.6.4 h1:VVIxCtVerchHienSlaGzV6XJGtEM9828Erzyr3miUGs= @@ -35,7 +42,10 @@ cloud.google.com/go/artifactregistry v1.14.6 h1:/hQaadYytMdA5zBh+RciIrXZQBWK4vN7 cloud.google.com/go/artifactregistry v1.14.7 h1:W9sVlyb1VRcUf83w7aM3yMsnp4HS4PoyGqYQNG0O5lI= cloud.google.com/go/artifactregistry v1.14.7/go.mod h1:0AUKhzWQzfmeTvT4SjfI4zjot72EMfrkvL9g9aRjnnM= cloud.google.com/go/asset v1.15.3 h1:uI8Bdm81s0esVWbWrTHcjFDFKNOa9aB7rI1vud1hO84= +cloud.google.com/go/asset v1.16.0/go.mod h1:yYLfUD4wL4X589A9tYrv4rFrba0QlDeag0CMcM5ggXU= cloud.google.com/go/asset v1.17.0 h1:dLWfTnbwyrq/Kt8Tr2JiAbre1MEvS2Bl5cAMiYAy5Pg= +cloud.google.com/go/asset v1.17.0/go.mod h1:yYLfUD4wL4X589A9tYrv4rFrba0QlDeag0CMcM5ggXU= +cloud.google.com/go/asset v1.17.1/go.mod h1:byvDw36UME5AzGNK7o4JnOnINkwOZ1yRrGrKIahHrng= cloud.google.com/go/asset v1.17.2 h1:xgFnBP3luSbUcC9RWJvb3Zkt+y/wW6PKwPHr3ssnIP8= cloud.google.com/go/asset v1.17.2/go.mod h1:SVbzde67ehddSoKf5uebOD1sYw8Ab/jD/9EIeWg99q4= cloud.google.com/go/assuredworkloads v1.11.4 h1:FsLSkmYYeNuzDm8L4YPfLWV+lQaUrJmH5OuD37t1k20= @@ -48,6 +58,7 @@ cloud.google.com/go/baremetalsolution v1.2.3 h1:oQiFYYCe0vwp7J8ZmF6siVKEumWtiPFJ cloud.google.com/go/baremetalsolution v1.2.4 h1:LFydisRmS7hQk9P/YhekwuZGqb45TW4QavcrMToWo5A= cloud.google.com/go/baremetalsolution v1.2.4/go.mod h1:BHCmxgpevw9IEryE99HbYEfxXkAEA3hkMJbYYsHtIuY= cloud.google.com/go/batch v1.7.0 h1:AxuSPoL2fWn/rUyvWeNCNd0V2WCr+iHRCU9QO1PUmpY= +cloud.google.com/go/batch v1.7.0/go.mod h1:J64gD4vsNSA2O5TtDB5AAux3nJ9iV8U3ilg3JDBYejU= cloud.google.com/go/batch v1.8.0 h1:2HK4JerwVaIcCh/lJiHwh6+uswPthiMMWhiSWLELayk= cloud.google.com/go/batch v1.8.0/go.mod h1:k8V7f6VE2Suc0zUM4WtoibNrA6D3dqBpB+++e3vSGYc= cloud.google.com/go/beyondcorp v1.0.3 h1:VXf9SnrnSmj2BF2cHkoTHvOUp8gjsz1KJFOMW7czdsY= @@ -55,12 +66,15 @@ cloud.google.com/go/beyondcorp v1.0.4 h1:qs0J0O9Ol2h1yA0AU+r7l3hOCPzs2MjE1d6d/ka cloud.google.com/go/beyondcorp v1.0.4/go.mod h1:Gx8/Rk2MxrvWfn4WIhHIG1NV7IBfg14pTKv1+EArVcc= cloud.google.com/go/bigquery v1.57.1 h1:FiULdbbzUxWD0Y4ZGPSVCDLvqRSyCIO6zKV7E2nf5uA= cloud.google.com/go/bigquery v1.58.0 h1:drSd9RcPVLJP2iFMimvOB9SCSIrcl+9HD4II03Oy7A0= +cloud.google.com/go/bigquery v1.58.0/go.mod h1:0eh4mWNY0KrBTjUzLjoYImapGORq9gEPT7MWjCy9lik= cloud.google.com/go/bigquery v1.59.1 h1:CpT+/njKuKT3CEmswm6IbhNu9u35zt5dO4yPDLW+nG4= cloud.google.com/go/bigquery v1.59.1/go.mod h1:VP1UJYgevyTwsV7desjzNzDND5p6hZB+Z8gZJN1GQUc= cloud.google.com/go/billing v1.18.0 h1:GvKy4xLy1zF1XPbwP5NJb2HjRxhnhxjjXxvyZ1S/IAo= +cloud.google.com/go/billing v1.18.0/go.mod h1:5DOYQStCxquGprqfuid/7haD7th74kyMBHkjO/OvDtk= cloud.google.com/go/billing v1.18.2 h1:oWUEQvuC4JvtnqLZ35zgzdbuHt4Itbftvzbe6aEyFdE= cloud.google.com/go/billing v1.18.2/go.mod h1:PPIwVsOOQ7xzbADCwNe8nvK776QpfrOAUkvKjCUcpSE= cloud.google.com/go/binaryauthorization v1.8.0 h1:PHS89lcFayWIEe0/s2jTBiEOtqghCxzc7y7bRNlifBs= +cloud.google.com/go/binaryauthorization v1.8.0/go.mod h1:VQ/nUGRKhrStlGr+8GMS8f6/vznYLkdK5vaKfdCIpvU= cloud.google.com/go/binaryauthorization v1.8.1 h1:1jcyh2uIUwSZkJ/JmL8kd5SUkL/Krbv8zmYLEbAz6kY= cloud.google.com/go/binaryauthorization v1.8.1/go.mod h1:1HVRyBerREA/nhI7yLang4Zn7vfNVA3okoAR9qYQJAQ= cloud.google.com/go/certificatemanager v1.7.4 h1:5YMQ3Q+dqGpwUZ9X5sipsOQ1fLPsxod9HNq0+nrqc6I= @@ -68,6 +82,7 @@ cloud.google.com/go/certificatemanager v1.7.5 h1:UMBr/twXvH3jcT5J5/YjRxf2tvwTYIf cloud.google.com/go/certificatemanager v1.7.5/go.mod h1:uX+v7kWqy0Y3NG/ZhNvffh0kuqkKZIXdvlZRO7z0VtM= cloud.google.com/go/channel v1.17.3 h1:Rd4+fBrjiN6tZ4TR8R/38elkyEkz6oogGDr7jDyjmMY= cloud.google.com/go/channel v1.17.4 h1:yYHOORIM+wkBy3EdwArg/WL7Lg+SoGzlKH9o3Bw2/jE= +cloud.google.com/go/channel v1.17.4/go.mod h1:QcEBuZLGGrUMm7kNj9IbU1ZfmJq2apotsV83hbxX7eE= cloud.google.com/go/channel v1.17.5 h1:/omiBnyFjm4S1ETHoOmJbL7LH7Ljcei4rYG6Sj3hc80= cloud.google.com/go/channel v1.17.5/go.mod h1:FlpaOSINDAXgEext0KMaBq/vwpLMkkPAw9b2mApQeHc= cloud.google.com/go/cloudbuild v1.15.0 h1:9IHfEMWdCklJ1cwouoiQrnxmP0q3pH7JUt8Hqx4Qbck= @@ -79,10 +94,16 @@ cloud.google.com/go/clouddms v1.7.4/go.mod h1:RdrVqoFG9RWI5AvZ81SxJ/xvxPdtcRhFot cloud.google.com/go/cloudtasks v1.12.4 h1:5xXuFfAjg0Z5Wb81j2GAbB3e0bwroCeSF+5jBn/L650= cloud.google.com/go/cloudtasks v1.12.6 h1:EUt1hIZ9bLv8Iz9yWaCrqgMnIU+Tdh0yXM1MMVGhjfE= cloud.google.com/go/cloudtasks v1.12.6/go.mod h1:b7c7fe4+TJsFZfDyzO51F7cjq7HLUlRi/KZQLQjDsaY= +cloud.google.com/go/compute v1.23.4/go.mod h1:/EJMj55asU6kAFnuZET8zqgwgJ9FvXWXOkkfQZa4ioI= +cloud.google.com/go/compute v1.24.0 h1:phWcR2eWzRJaL/kOiJwfFsPs4BaKq1j6vnpZrc1YlVg= +cloud.google.com/go/compute v1.24.0/go.mod h1:kw1/T+h/+tK2LJK0wiPPx1intgdAM3j/g3hFDlscY40= cloud.google.com/go/contactcenterinsights v1.12.1 h1:EiGBeejtDDtr3JXt9W7xlhXyZ+REB5k2tBgVPVtmNb0= +cloud.google.com/go/contactcenterinsights v1.12.1/go.mod h1:HHX5wrz5LHVAwfI2smIotQG9x8Qd6gYilaHcLLLmNis= cloud.google.com/go/contactcenterinsights v1.13.0 h1:6Vs/YnDG5STGjlWMEjN/xtmft7MrOTOnOZYUZtGTx0w= cloud.google.com/go/contactcenterinsights v1.13.0/go.mod h1:ieq5d5EtHsu8vhe2y3amtZ+BE+AQwX5qAy7cpo0POsI= cloud.google.com/go/container v1.29.0 h1:jIltU529R2zBFvP8rhiG1mgeTcnT27KhU0H/1d6SQRg= +cloud.google.com/go/container v1.29.0/go.mod h1:b1A1gJeTBXVLQ6GGw9/9M4FG94BEGsqJ5+t4d/3N7O4= +cloud.google.com/go/container v1.30.1/go.mod h1:vkbfX0EnAKL/vgVECs5BZn24e1cJROzgszJirRKQ4Bg= cloud.google.com/go/container v1.31.0 h1:MAaNH7VRNPWEhvqOypq2j+7ONJKrKzon4v9nS3nLZe0= cloud.google.com/go/container v1.31.0/go.mod h1:7yABn5s3Iv3lmw7oMmyGbeV6tQj86njcTijkkGuvdZA= cloud.google.com/go/containeranalysis v0.11.3 h1:5rhYLX+3a01drpREqBZVXR9YmWH45RnML++8NsCtuD8= @@ -90,6 +111,7 @@ cloud.google.com/go/containeranalysis v0.11.4 h1:doJ0M1ljS4hS0D2UbHywlHGwB7sQLNr cloud.google.com/go/containeranalysis v0.11.4/go.mod h1:cVZT7rXYBS9NG1rhQbWL9pWbXCKHWJPYraE8/FTSYPE= cloud.google.com/go/datacatalog v1.19.0 h1:rbYNmHwvAOOwnW2FPXYkaK3Mf1MmGqRzK0mMiIEyLdo= cloud.google.com/go/datacatalog v1.19.2 h1:BV5sB7fPc8ccv/obwtHwQtCdLMAgI4KyaQWfkh8/mWg= +cloud.google.com/go/datacatalog v1.19.2/go.mod h1:2YbODwmhpLM4lOFe3PuEhHK9EyTzQJ5AXgIy7EDKTEE= cloud.google.com/go/datacatalog v1.19.3 h1:A0vKYCQdxQuV4Pi0LL9p39Vwvg4jH5yYveMv50gU5Tw= cloud.google.com/go/datacatalog v1.19.3/go.mod h1:ra8V3UAsciBpJKQ+z9Whkxzxv7jmQg1hfODr3N3YPJ4= cloud.google.com/go/dataflow v0.9.4 h1:7VmCNWcPJBS/srN2QnStTB6nu4Eb5TMcpkmtaPVhRt4= @@ -105,7 +127,10 @@ cloud.google.com/go/datalabeling v0.8.4 h1:zrq4uMmunf2KFDl/7dS6iCDBBAxBnKVDyw6+a cloud.google.com/go/datalabeling v0.8.5 h1:GpIFRdm0qIZNsxqURFJwHt0ZBJZ0nF/mUVEigR7PH/8= cloud.google.com/go/datalabeling v0.8.5/go.mod h1:IABB2lxQnkdUbMnQaOl2prCOfms20mcPxDBm36lps+s= cloud.google.com/go/dataplex v1.13.0 h1:ACVOuxwe7gP0SqEso9SLyXbcZNk5l8hjcTX+XLntI5s= +cloud.google.com/go/dataplex v1.13.0/go.mod h1:mHJYQQ2VEJHsyoC0OdNyy988DvEbPhqFs5OOLffLX0c= cloud.google.com/go/dataplex v1.14.0 h1:/WhVTR4v/L6ACKjlz/9CqkxkrVh2z7C44CLMUf0f60A= +cloud.google.com/go/dataplex v1.14.0/go.mod h1:mHJYQQ2VEJHsyoC0OdNyy988DvEbPhqFs5OOLffLX0c= +cloud.google.com/go/dataplex v1.14.1/go.mod h1:bWxQAbg6Smg+sca2+Ex7s8D9a5qU6xfXtwmq4BVReps= cloud.google.com/go/dataplex v1.14.2 h1:fxIfdU8fxzR3clhOoNI7XFppvAmndxDu1AMH+qX9WKQ= cloud.google.com/go/dataplex v1.14.2/go.mod h1:0oGOSFlEKef1cQeAHXy4GZPB/Ife0fz/PxBf+ZymA2U= cloud.google.com/go/dataproc v1.12.0 h1:W47qHL3W4BPkAIbk4SWmIERwsWBaNnWm0P2sdx3YgGU= @@ -120,18 +145,27 @@ cloud.google.com/go/datastream v1.10.3 h1:Z2sKPIB7bT2kMW5Uhxy44ZgdJzxzE5uKjavoW+ cloud.google.com/go/datastream v1.10.4 h1:o1QDKMo/hk0FN7vhoUQURREuA0rgKmnYapB+1M+7Qz4= cloud.google.com/go/datastream v1.10.4/go.mod h1:7kRxPdxZxhPg3MFeCSulmAJnil8NJGGvSNdn4p1sRZo= cloud.google.com/go/deploy v1.16.0 h1:5OVjzm8MPC5kP+Ywbs0mdE0O7AXvAUXksSyHAyMFyMg= +cloud.google.com/go/deploy v1.16.0/go.mod h1:e5XOUI5D+YGldyLNZ21wbp9S8otJbBE4i88PtO9x/2g= cloud.google.com/go/deploy v1.17.0 h1:P3SgJ+4rAktC2XqaI10G0ip/vzWluNBrC5VG0abMbLw= +cloud.google.com/go/deploy v1.17.0/go.mod h1:XBr42U5jIr64t92gcpOXxNrqL2PStQCXHuKK5GRUuYo= cloud.google.com/go/deploy v1.17.1 h1:m27Ojwj03gvpJqCbodLYiVmE9x4/LrHGGMjzc0LBfM4= cloud.google.com/go/deploy v1.17.1/go.mod h1:SXQyfsXrk0fBmgBHRzBjQbZhMfKZ3hMQBw5ym7MN/50= cloud.google.com/go/dialogflow v1.47.0 h1:tLCWad8HZhlyUNfDzDP5m+oH6h/1Uvw/ei7B9AnsWMk= +cloud.google.com/go/dialogflow v1.47.0/go.mod h1:mHly4vU7cPXVweuB5R0zsYKPMzy240aQdAu06SqBbAQ= +cloud.google.com/go/dialogflow v1.48.0/go.mod h1:mHly4vU7cPXVweuB5R0zsYKPMzy240aQdAu06SqBbAQ= cloud.google.com/go/dialogflow v1.48.1 h1:1Uq2jDJzjJ3M4xYB608FCCFHfW3JmrTmHIxRSd7JGmY= +cloud.google.com/go/dialogflow v1.48.1/go.mod h1:C1sjs2/g9cEwjCltkKeYp3FFpz8BOzNondEaAlCpt+A= +cloud.google.com/go/dialogflow v1.48.2/go.mod h1:7A2oDf6JJ1/+hdpnFRfb/RjJUOh2X3rhIa5P8wQSEX4= cloud.google.com/go/dialogflow v1.49.0 h1:KqG0oxGE71qo0lRVyAoeBozefCvsMfcDzDjoLYSY0F4= cloud.google.com/go/dialogflow v1.49.0/go.mod h1:dhVrXKETtdPlpPhE7+2/k4Z8FRNUp6kMV3EW3oz/fe0= cloud.google.com/go/dlp v1.11.1 h1:OFlXedmPP/5//X1hBEeq3D9kUVm9fb6ywYANlpv/EsQ= cloud.google.com/go/dlp v1.11.2 h1:lTipOuJaSjlYnnotPMbEhKURLC6GzCMDDzVbJAEbmYM= cloud.google.com/go/dlp v1.11.2/go.mod h1:9Czi+8Y/FegpWzgSfkRlyz+jwW6Te9Rv26P3UfU/h/w= cloud.google.com/go/documentai v1.23.6 h1:0/S3AhS23+0qaFe3tkgMmS3STxgDgmE1jg4TvaDOZ9g= +cloud.google.com/go/documentai v1.23.6/go.mod h1:ghzBsyVTiVdkfKaUCum/9bGBEyBjDO4GfooEcYKhN+g= cloud.google.com/go/documentai v1.23.7 h1:hlYieOXUwiJ7HpBR/vEPfr8nfSxveLVzbqbUkSK0c/4= +cloud.google.com/go/documentai v1.23.7/go.mod h1:ghzBsyVTiVdkfKaUCum/9bGBEyBjDO4GfooEcYKhN+g= +cloud.google.com/go/documentai v1.23.8/go.mod h1:Vd/y5PosxCpUHmwC+v9arZyeMfTqBR9VIwOwIqQYYfA= cloud.google.com/go/documentai v1.25.0 h1:lI62GMEEPO6vXJI9hj+G9WjOvnR0hEjvjokrnex4cxA= cloud.google.com/go/documentai v1.25.0/go.mod h1:ftLnzw5VcXkLItp6pw1mFic91tMRyfv6hHEY5br4KzY= cloud.google.com/go/domains v0.9.4 h1:ua4GvsDztZ5F3xqjeLKVRDeOvJshf5QFgWGg1CKti3A= @@ -166,6 +200,7 @@ cloud.google.com/go/gkehub v0.14.5 h1:RboLNFzf9wEMSo7DrKVBlf+YhK/A/jrLN454L5Tz99 cloud.google.com/go/gkehub v0.14.5/go.mod h1:6bzqxM+a+vEH/h8W8ec4OJl4r36laxTs3A/fMNHJ0wA= cloud.google.com/go/gkemulticloud v1.0.3 h1:NmJsNX9uQ2CT78957xnjXZb26TDIMvv+d5W2vVUt0Pg= cloud.google.com/go/gkemulticloud v1.1.0 h1:C2Suwn3uPz+Yy0bxVjTlsMrUCaDovkgvfdyIa+EnUOU= +cloud.google.com/go/gkemulticloud v1.1.0/go.mod h1:7NpJBN94U6DY1xHIbsDqB2+TFZUfjLUKLjUX8NGLor0= cloud.google.com/go/gkemulticloud v1.1.1 h1:rsSZAGLhyjyE/bE2ToT5fqo1qSW7S+Ubsc9jFOcbhSI= cloud.google.com/go/gkemulticloud v1.1.1/go.mod h1:C+a4vcHlWeEIf45IB5FFR5XGjTeYhF83+AYIpTy4i2Q= cloud.google.com/go/grafeas v0.3.0 h1:oyTL/KjiUeBs9eYLw/40cpSZglUC+0F7X4iu/8t7NWs= @@ -183,6 +218,7 @@ cloud.google.com/go/ids v1.4.5/go.mod h1:p0ZnyzjMWxww6d2DvMGnFwCsSxDJM666Iir1bK1 cloud.google.com/go/iot v1.7.4 h1:m1WljtkZnvLTIRYW1YTOv5A6H1yKgLHR6nU7O8yf27w= cloud.google.com/go/iot v1.7.5 h1:munTeBlbqI33iuTYgXy7S8lW2TCgi5l1hA4roSIY+EE= cloud.google.com/go/iot v1.7.5/go.mod h1:nq3/sqTz3HGaWJi1xNiX7F41ThOzpud67vwk0YsSsqs= +cloud.google.com/go/kms v1.15.6/go.mod h1:yF75jttnIdHfGBoE51AKsD/Yqf+/jICzB9v1s1acsms= cloud.google.com/go/language v1.12.2 h1:zg9uq2yS9PGIOdc0Kz/l+zMtOlxKWonZjjo5w5YPG2A= cloud.google.com/go/language v1.12.3 h1:iaJZg6K4j/2PvZZVcjeO/btcWWIllVRBhuTFjGO4LXs= cloud.google.com/go/language v1.12.3/go.mod h1:evFX9wECX6mksEva8RbRnr/4wi/vKGYnAJrTRXU8+f8= @@ -199,7 +235,9 @@ cloud.google.com/go/managedidentities v1.6.4 h1:SF/u1IJduMqQQdJA4MDyivlIQ4SrV5qA cloud.google.com/go/managedidentities v1.6.5 h1:+bpih1piZVLxla/XBqeSUzJBp8gv9plGHIMAI7DLpDM= cloud.google.com/go/managedidentities v1.6.5/go.mod h1:fkFI2PwwyRQbjLxlm5bQ8SjtObFMW3ChBGNqaMcgZjI= cloud.google.com/go/maps v1.6.2 h1:WxxLo//b60nNFESefLgaBQevu8QGUmRV3+noOjCfIHs= +cloud.google.com/go/maps v1.6.2/go.mod h1:4+buOHhYXFBp58Zj/K+Lc1rCmJssxxF4pJ5CJnhdz18= cloud.google.com/go/maps v1.6.3 h1:Qqs6Dza+PRp5CZO5AfgPnLwU1k3pp0IMFRDtLpT+aCA= +cloud.google.com/go/maps v1.6.3/go.mod h1:VGAn809ADswi1ASofL5lveOHPnE6Rk/SFTTBx1yuOLw= cloud.google.com/go/maps v1.6.4 h1:EVCZAiDvog9So46460BGbCasPhi613exoaQbpilMVlk= cloud.google.com/go/maps v1.6.4/go.mod h1:rhjqRy8NWmDJ53saCfsXQ0LKwBHfi6OSh5wkq6BaMhI= cloud.google.com/go/mediatranslation v0.8.4 h1:VRCQfZB4s6jN0CSy7+cO3m4ewNwgVnaePanVCQh/9Z4= @@ -213,6 +251,8 @@ cloud.google.com/go/metastore v1.13.4 h1:dR7vqWXlK6IYR8Wbu9mdFfwlVjodIBhd1JRrpZf cloud.google.com/go/metastore v1.13.4/go.mod h1:FMv9bvPInEfX9Ac1cVcRXp8EBBQnBcqH6gz3KvJ9BAE= cloud.google.com/go/monitoring v1.16.3 h1:mf2SN9qSoBtIgiMA4R/y4VADPWZA7VCNJA079qLaZQ8= cloud.google.com/go/monitoring v1.17.0 h1:blrdvF0MkPPivSO041ihul7rFMhXdVp8Uq7F59DKXTU= +cloud.google.com/go/monitoring v1.17.0/go.mod h1:KwSsX5+8PnXv5NJnICZzW2R8pWTis8ypC4zmdRD63Tw= +cloud.google.com/go/monitoring v1.17.1/go.mod h1:SJzPMakCF0GHOuKEH/r4hxVKF04zl+cRPQyc3d/fqII= cloud.google.com/go/monitoring v1.18.0 h1:NfkDLQDG2UR3WYZVQE8kwSbUIEyIqJUPl+aOQdFH1T4= cloud.google.com/go/monitoring v1.18.0/go.mod h1:c92vVBCeq/OB4Ioyo+NbN2U7tlg5ZH41PZcdvfc+Lcg= cloud.google.com/go/networkconnectivity v1.14.3 h1:e9lUkCe2BexsqsUc2bjV8+gFBpQa54J+/F3qKVtW+wA= @@ -235,6 +275,7 @@ cloud.google.com/go/orchestration v1.8.5 h1:YHgWMlrPttIVGItgGfuvO2KM7x+y9ivN/Yk9 cloud.google.com/go/orchestration v1.8.5/go.mod h1:C1J7HesE96Ba8/hZ71ISTV2UAat0bwN+pi85ky38Yq8= cloud.google.com/go/orgpolicy v1.11.4 h1:RWuXQDr9GDYhjmrredQJC7aY7cbyqP9ZuLbq5GJGves= cloud.google.com/go/orgpolicy v1.12.0 h1:sab7cDiyfdthpAL0JkSpyw1C3mNqkXToVOhalm79PJQ= +cloud.google.com/go/orgpolicy v1.12.0/go.mod h1:0+aNV/nrfoTQ4Mytv+Aw+stBDBjNf4d8fYRA9herfJI= cloud.google.com/go/orgpolicy v1.12.1 h1:2JbXigqBJVp8Dx5dONUttFqewu4fP0p3pgOdIZAhpYU= cloud.google.com/go/orgpolicy v1.12.1/go.mod h1:aibX78RDl5pcK3jA8ysDQCFkVxLj3aOQqrbBaUL2V5I= cloud.google.com/go/osconfig v1.12.4 h1:OrRCIYEAbrbXdhm13/JINn9pQchvTTIzgmOCA7uJw8I= @@ -242,6 +283,7 @@ cloud.google.com/go/osconfig v1.12.5 h1:Mo5jGAxOMKH/PmDY7fgY19yFcVbvwREb5D5zMPQj cloud.google.com/go/osconfig v1.12.5/go.mod h1:D9QFdxzfjgw3h/+ZaAb5NypM8bhOMqBzgmbhzWViiW8= cloud.google.com/go/oslogin v1.12.2 h1:NP/KgsD9+0r9hmHC5wKye0vJXVwdciv219DtYKYjgqE= cloud.google.com/go/oslogin v1.13.0 h1:gbA/G4p+youIR4O/Rk6DU181QlBlpwPS16kvJwqEz8o= +cloud.google.com/go/oslogin v1.13.0/go.mod h1:xPJqLwpTZ90LSE5IL1/svko+6c5avZLluiyylMb/sRA= cloud.google.com/go/oslogin v1.13.1 h1:1K4nOT5VEZNt7XkhaTXupBYos5HjzvJMfhvyD2wWdFs= cloud.google.com/go/oslogin v1.13.1/go.mod h1:vS8Sr/jR7QvPWpCjNqy6LYZr5Zs1e8ZGW/KPn9gmhws= cloud.google.com/go/phishingprotection v0.8.4 h1:sPLUQkHq6b4AL0czSJZ0jd6vL55GSTHz2B3Md+TCZI0= @@ -255,11 +297,13 @@ cloud.google.com/go/privatecatalog v0.9.5 h1:UZ0assTnATXSggoxUIh61RjTQ4P9zCMk/kE cloud.google.com/go/privatecatalog v0.9.5/go.mod h1:fVWeBOVe7uj2n3kWRGlUQqR/pOd450J9yZoOECcQqJk= cloud.google.com/go/pubsub v1.33.0 h1:6SPCPvWav64tj0sVX/+npCBKhUi/UjJehy9op/V3p2g= cloud.google.com/go/pubsub v1.34.0 h1:ZtPbfwfi5rLaPeSvDC29fFoE20/tQvGrUS6kVJZJvkU= +cloud.google.com/go/pubsub v1.34.0/go.mod h1:alj4l4rBg+N3YTFDDC+/YyFTs6JAjam2QfYsddcAW4c= cloud.google.com/go/pubsub v1.36.1 h1:dfEPuGCHGbWUhaMCTHUFjfroILEkx55iUmKBZTP5f+Y= cloud.google.com/go/pubsub v1.36.1/go.mod h1:iYjCa9EzWOoBiTdd4ps7QoMtMln5NwaZQpK1hbRfBDE= cloud.google.com/go/pubsublite v1.8.1 h1:pX+idpWMIH30/K7c0epN6V703xpIcMXWRjKJsz0tYGY= cloud.google.com/go/recaptchaenterprise v1.3.1 h1:u6EznTGzIdsyOsvm+Xkw0aSuKFXQlyjGE9a4exk6iNQ= cloud.google.com/go/recaptchaenterprise/v2 v2.9.0 h1:Zrd4LvT9PaW91X/Z13H0i5RKEv9suCLuk8zp+bfOpN4= +cloud.google.com/go/recaptchaenterprise/v2 v2.9.0/go.mod h1:Dak54rw6lC2gBY8FBznpOCAR58wKf+R+ZSJRoeJok4w= cloud.google.com/go/recaptchaenterprise/v2 v2.9.2 h1:U3Wfq12X9cVMuTpsWDSURnXF0Z9hSPTHj+xsnXDRLsw= cloud.google.com/go/recaptchaenterprise/v2 v2.9.2/go.mod h1:trwwGkfhCmp05Ll5MSJPXY7yvnO0p4v3orGANAFHAuU= cloud.google.com/go/recommendationengine v0.8.4 h1:JRiwe4hvu3auuh2hujiTc2qNgPPfVp+Q8KOpsXlEzKQ= @@ -267,6 +311,7 @@ cloud.google.com/go/recommendationengine v0.8.5 h1:ineqLswaCSBY0csYv5/wuXJMBlxAT cloud.google.com/go/recommendationengine v0.8.5/go.mod h1:A38rIXHGFvoPvmy6pZLozr0g59NRNREz4cx7F58HAsQ= cloud.google.com/go/recommender v1.11.3 h1:VndmgyS/J3+izR8V8BHa7HV/uun8//ivQ3k5eVKKyyM= cloud.google.com/go/recommender v1.12.0 h1:tC+ljmCCbuZ/ybt43odTFlay91n/HLIhflvaOeb0Dh4= +cloud.google.com/go/recommender v1.12.0/go.mod h1:+FJosKKJSId1MBFeJ/TTyoGQZiEelQQIZMKYYD8ruK4= cloud.google.com/go/recommender v1.12.1 h1:LVLYS3r3u0MSCxQSDUtLSkporEGi9OAE6hGvayrZNPs= cloud.google.com/go/recommender v1.12.1/go.mod h1:gf95SInWNND5aPas3yjwl0I572dtudMhMIG4ni8nr+0= cloud.google.com/go/redis v1.14.1 h1:J9cEHxG9YLmA9o4jTSvWt/RuVEn6MTrPlYSCRHujxDQ= @@ -279,6 +324,7 @@ cloud.google.com/go/resourcesettings v1.6.4 h1:yTIL2CsZswmMfFyx2Ic77oLVzfBFoWBYg cloud.google.com/go/resourcesettings v1.6.5 h1:BTr5MVykJwClASci/7Og4Qfx70aQ4n3epsNLj94ZYgw= cloud.google.com/go/resourcesettings v1.6.5/go.mod h1:WBOIWZraXZOGAgoR4ukNj0o0HiSMO62H9RpFi9WjP9I= cloud.google.com/go/retail v1.14.4 h1:geqdX1FNqqL2p0ADXjPpw8lq986iv5GrVcieTYafuJQ= +cloud.google.com/go/retail v1.15.1/go.mod h1:In9nSBOYhLbDGa87QvWlnE1XA14xBN2FpQRiRsUs9wU= cloud.google.com/go/retail v1.16.0 h1:Fn1GuAua1c6crCGqfJ1qMxG1Xh10Tg/x5EUODEHMqkw= cloud.google.com/go/retail v1.16.0/go.mod h1:LW7tllVveZo4ReWt68VnldZFWJRzsh9np+01J9dYWzE= cloud.google.com/go/run v1.3.3 h1:qdfZteAm+vgzN1iXzILo3nJFQbzziudkJrvd9wCf3FQ= @@ -294,6 +340,7 @@ cloud.google.com/go/security v1.15.4 h1:sdnh4Islb1ljaNhpIXlIPgb3eYj70QWgPVDKOUYv cloud.google.com/go/security v1.15.5 h1:wTKJQ10j8EYgvE8Y+KhovxDRVDk2iv/OsxZ6GrLP3kE= cloud.google.com/go/security v1.15.5/go.mod h1:KS6X2eG3ynWjqcIX976fuToN5juVkF6Ra6c7MPnldtc= cloud.google.com/go/securitycenter v1.24.3 h1:crdn2Z2rFIy8WffmmhdlX3CwZJusqCiShtnrGFRwpeE= +cloud.google.com/go/securitycenter v1.24.3/go.mod h1:l1XejOngggzqwr4Fa2Cn+iWZGf+aBLTXtB/vXjy5vXM= cloud.google.com/go/securitycenter v1.24.4 h1:/5jjkZ+uGe8hZ7pvd7pO30VW/a+pT2MrrdgOqjyucKQ= cloud.google.com/go/securitycenter v1.24.4/go.mod h1:PSccin+o1EMYKcFQzz9HMMnZ2r9+7jbc+LvPjXhpwcU= cloud.google.com/go/servicecontrol v1.11.1 h1:d0uV7Qegtfaa7Z2ClDzr9HJmnbJW7jn0WhZ7wOX6hLE= @@ -306,12 +353,17 @@ cloud.google.com/go/shell v1.7.4 h1:nurhlJcSVFZneoRZgkBEHumTYf/kFJptCK2eBUq/88M= cloud.google.com/go/shell v1.7.5 h1:3Fq2hzO0ZSyaqBboJrFkwwf/qMufDtqwwA6ep8EZxEI= cloud.google.com/go/shell v1.7.5/go.mod h1:hL2++7F47/IfpfTO53KYf1EC+F56k3ThfNEXd4zcuiE= cloud.google.com/go/spanner v1.53.1 h1:xNmE0SXMSxNBuk7lRZ5G/S+A49X91zkSTt7Jn5Ptlvw= +cloud.google.com/go/spanner v1.53.1/go.mod h1:liG4iCeLqm5L3fFLU5whFITqP0e0orsAW1uUSrd4rws= +cloud.google.com/go/spanner v1.54.0/go.mod h1:wZvSQVBgngF0Gq86fKup6KIYmN2be7uOKjtK97X+bQU= cloud.google.com/go/spanner v1.55.0 h1:YF/A/k73EMYCjp8wcJTpkE+TcrWutHRlsCtlRSfWS64= +cloud.google.com/go/spanner v1.55.0/go.mod h1:HXEznMUVhC+PC+HDyo9YFG2Ajj5BQDkcbqB9Z2Ffxi0= +cloud.google.com/go/spanner v1.56.0/go.mod h1:DndqtUKQAt3VLuV2Le+9Y3WTnq5cNKrnLb/Piqcj+h0= cloud.google.com/go/spanner v1.57.0 h1:fJq+ZfQUDHE+cy1li0bJA8+sy2oiSGhuGqN5nqVaZdU= cloud.google.com/go/spanner v1.57.0/go.mod h1:aXQ5QDdhPRIqVhYmnkAdwPYvj/DRN0FguclhEWw+jOo= cloud.google.com/go/speech v1.21.0 h1:qkxNao58oF8ghAHE1Eghen7XepawYEN5zuZXYWaUTA4= cloud.google.com/go/speech v1.21.1 h1:nuFc+Kj5B8de75nN4FdPyUbI2SiBoHZG6BLurXL56Q0= cloud.google.com/go/speech v1.21.1/go.mod h1:E5GHZXYQlkqWQwY5xRSLHw2ci5NMQNG52FfMU1aZrIA= +cloud.google.com/go/storage v1.36.0/go.mod h1:M6M/3V/D3KpzMTJyPOR/HU6n2Si5QdaXYEsng2xgOs8= cloud.google.com/go/storagetransfer v1.10.3 h1:YM1dnj5gLjfL6aDldO2s4GeU8JoAvH1xyIwXre63KmI= cloud.google.com/go/storagetransfer v1.10.4 h1:dy4fL3wO0VABvzM05ycMUPFHxTPbJz9Em8ikAJVqSbI= cloud.google.com/go/storagetransfer v1.10.4/go.mod h1:vef30rZKu5HSEf/x1tK3WfWrL0XVoUQN/EPDRGPzjZs= @@ -329,6 +381,7 @@ cloud.google.com/go/trace v1.10.5 h1:0pr4lIKJ5XZFYD9GtxXEWr0KkVeigc3wlGpZco0X1oA cloud.google.com/go/trace v1.10.5/go.mod h1:9hjCV1nGBCtXbAE4YK7OqJ8pmPYSxPA0I67JwRd5s3M= cloud.google.com/go/translate v1.9.3 h1:t5WXTqlrk8VVJu/i3WrYQACjzYJiff5szARHiyqqPzI= cloud.google.com/go/translate v1.10.0 h1:tncNaKmlZnayMMRX/mMM2d5AJftecznnxVBD4w070NI= +cloud.google.com/go/translate v1.10.0/go.mod h1:Kbq9RggWsbqZ9W5YpM94Q1Xv4dshw/gr/SHfsl5yCZ0= cloud.google.com/go/translate v1.10.1 h1:upovZ0wRMdzZvXnu+RPam41B0mRJ+coRXFP2cYFJ7ew= cloud.google.com/go/translate v1.10.1/go.mod h1:adGZcQNom/3ogU65N9UXHOnnSvjPwA/jKQUMnsYXOyk= cloud.google.com/go/video v1.20.3 h1:Xrpbm2S9UFQ1pZEeJt9Vqm5t2T/z9y/M3rNXhFoo8Is= @@ -339,6 +392,7 @@ cloud.google.com/go/videointelligence v1.11.5 h1:mYaWH8uhUCXLJCN3gdXswKzRa2+lK0z cloud.google.com/go/videointelligence v1.11.5/go.mod h1:/PkeQjpRponmOerPeJxNPuxvi12HlW7Em0lJO14FC3I= cloud.google.com/go/vision v1.2.0 h1:/CsSTkbmO9HC8iQpxbK8ATms3OQaX3YQUeTMGCxlaK4= cloud.google.com/go/vision/v2 v2.7.5 h1:T/ujUghvEaTb+YnFY/jiYwVAkMbIC8EieK0CJo6B4vg= +cloud.google.com/go/vision/v2 v2.7.6/go.mod h1:ZkvWTVNPBU3YZYzgF9Y1jwEbD1NBOCyJn0KFdQfE6Bw= cloud.google.com/go/vision/v2 v2.8.0 h1:W52z1b6LdGI66MVhE70g/NFty9zCYYcjdKuycqmlhtg= cloud.google.com/go/vision/v2 v2.8.0/go.mod h1:ocqDiA2j97pvgogdyhoxiQp2ZkDCyr0HWpicywGGRhU= cloud.google.com/go/vmmigration v1.7.4 h1:qPNdab4aGgtaRX+51jCOtJxlJp6P26qua4o1xxUDjpc= @@ -385,10 +439,14 @@ github.com/Azure/go-autorest/autorest/azure/cli v0.4.5 h1:0W/yGmFdTIT77fvdlGZ0LM github.com/Azure/go-autorest/autorest/azure/cli v0.4.5/go.mod h1:ADQAXrkgm7acgWVUNamOgh8YNrv4p27l3Wc55oVfpzg= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802 h1:1BDTz0u9nC3//pOCMdNH+CiXJVYJh5UQNCOBG7jbELc= github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53 h1:sR+/8Yb4slttB4vD+b9btVEnWgL3Q00OBTzVT8B9C0c= +github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53/go.mod h1:+3IMCy2vIlbG1XG/0ggNQv0SvxCAIpPM5b1nCz56Xno= github.com/CloudyKit/jet/v3 v3.0.0 h1:1PwO5w5VCtlUUl+KTOBsTGZlhjWkcybsGaAau52tOy8= +github.com/CloudyKit/jet/v6 v6.2.0 h1:EpcZ6SR9n28BUGtNJSvlBqf90IpjeFr36Tizxhn/oME= +github.com/CloudyKit/jet/v6 v6.2.0/go.mod h1:d3ypHeIRNo2+XyqnGA8s+aphtcVpjP5hPwP/Lzo7Ro4= github.com/DataDog/datadog-go v3.2.0+incompatible h1:qSG2N4FghB1He/r2mFrWKCaL7dXCilEuNEeAn20fdD4= github.com/GoogleCloudPlatform/cloudsql-proxy v1.29.0 h1:YNu23BtH0PKF+fg3ykSorCp6jSTjcEtfnYLzbmcjVRA= -github.com/Joker/hpp v1.0.0 h1:65+iuJYdRXv/XyN62C1uEmmOx3432rNG/rKlX6V7Kkc= +github.com/Joker/jade v1.1.3 h1:Qbeh12Vq6BxURXT1qZBRHsDxeURB8ztcL6f3EXSGeHk= +github.com/Joker/jade v1.1.3/go.mod h1:T+2WLyt7VH6Lp0TRxQrUYEs64nRc83wkMQrfeIQKduM= github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible h1:1G1pk05UrOh0NlF1oeaaix1x8XzrfjIDK47TY0Zehcw= github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 h1:TngWCqHvy9oXAN6lEVMRuU21PR1EtLVZJmdB18Gu3Rw= github.com/OneOfOne/xxhash v1.2.6 h1:U68crOE3y3MPttCMQGywZOLrTeF5HHJ3/vDBCJn9/bA= @@ -399,34 +457,42 @@ github.com/RaveNoX/go-jsoncommentstrip v1.0.0 h1:t527LHHE3HmiHrq74QMpNPZpGCIJzTx github.com/RoaringBitmap/gocroaring v0.4.0 h1:5nufXUgWpBEUNEJXw7926YAA58ZAQRpWPrQV1xCoSjc= github.com/RoaringBitmap/real-roaring-datasets v0.0.0-20190726190000-eb7c87156f76 h1:ZYlhPbqQFU+AHfgtCdHGDTtRW1a8geZyiE8c6Q+Sl1s= github.com/Shopify/goreferrer v0.0.0-20181106222321-ec9c9a553398 h1:WDC6ySpJzbxGWFh4aMxFFC28wwGp5pEuoTtvA4q/qQ4= +github.com/Shopify/goreferrer v0.0.0-20220729165902-8cddb4f5de06 h1:KkH3I3sJuOLP3TjA/dfr4NAY8bghDwnXiU7cTKxQqo0= +github.com/Shopify/goreferrer v0.0.0-20220729165902-8cddb4f5de06/go.mod h1:7erjKLwalezA0k99cWs5L11HWOAPNjdUZ6RxH1BXbbM= github.com/Shopify/sarama v1.38.1 h1:lqqPUPQZ7zPqYlWpTh+LQ9bhYNu2xJL6k1SJN4WVe2A= github.com/Shopify/sarama v1.38.1/go.mod h1:iwv9a67Ha8VNa+TifujYoWGxWnu2kNVAQdSdZ4X2o5g= github.com/Shopify/toxiproxy v2.1.4+incompatible h1:TKdv8HiTLgE5wdJuEML90aBgNWsokNbMijUGhmcoBJc= github.com/VividCortex/gohistogram v1.0.0 h1:6+hBz+qvs0JOrrNhhmR7lFxo5sINxBCGXrdtl/UvroE= github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5 h1:rFw4nCn9iMW+Vajsk51NtYIcwSTkXr+JGrMd36kTDJw= -github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU= github.com/ajstarks/deck v0.0.0-20200831202436-30c9fc6549a9 h1:7kQgkwGRoLzC9K0oyXdJo7nve/bynv/KwUsxbiTlzAM= github.com/ajstarks/deck/generate v0.0.0-20210309230005-c3f852c02e19 h1:iXUgAaqDcIUGbRoy2TdeofRG/j1zpGRSEmNK05T+bi8= github.com/ajstarks/svgo v0.0.0-20211024235047-1546f124cd8b h1:slYM766cy2nI3BwyRiyQj/Ud48djTMtMebDqepE95rw= +github.com/alecthomas/assert/v2 v2.2.2/go.mod h1:pXcQ2Asjp247dahGEmsZ6ru0UVwnkhktn7S0bBDLxvQ= github.com/alecthomas/assert/v2 v2.3.0 h1:mAsH2wmvjsuvyBvAmCtm7zFsBlb8mIHx5ySLVdDZXL0= +github.com/alecthomas/assert/v2 v2.3.0/go.mod h1:pXcQ2Asjp247dahGEmsZ6ru0UVwnkhktn7S0bBDLxvQ= github.com/alecthomas/kingpin/v2 v2.4.0 h1:f48lwail6p8zpO1bC4TxtqACaGqHYA22qkHjHpqDjYY= github.com/alecthomas/kong v0.2.11 h1:RKeJXXWfg9N47RYfMm0+igkxBCTF4bzbneAxaqid0c4= github.com/alecthomas/kong v0.2.11/go.mod h1:kQOmtJgV+Lb4aj+I2LEn40cbtawdWJ9Y8QLq+lElKxE= +github.com/alecthomas/participle/v2 v2.0.0/go.mod h1:rAKZdJldHu8084ojcWevWAL8KmEU+AT+Olodb+WoN2Y= github.com/alecthomas/participle/v2 v2.1.0 h1:z7dElHRrOEEq45F2TG5cbQihMtNTv8vwldytDj7Wrz4= github.com/alecthomas/participle/v2 v2.1.0/go.mod h1:Y1+hAs8DHPmc3YUFzqllV+eSQ9ljPTk0ZkPMtEdAx2c= github.com/alecthomas/repr v0.2.0 h1:HAzS41CIzNW5syS8Mf9UwXhNH1J9aix/BvDRf1Ml2Yk= +github.com/alecthomas/repr v0.2.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 h1:JYp7IbQjafoB+tBA3gMyHYHrpOtNuDiK/uB5uXxq5wM= github.com/alicebob/miniredis v2.5.0+incompatible h1:yBHoLpsyjupjz3NL3MhKMVkR41j82Yjf3KFv7ApYzUI= github.com/alicebob/miniredis v2.5.0+incompatible/go.mod h1:8HZjEj4yU0dwhYHky+DxYx+6BMjkBbe5ONFIF1MXffk= github.com/antihax/optional v1.0.0 h1:xK2lYat7ZLaVVcIuj82J8kIro4V6kDe0AUDFboUCwcg= +github.com/antlr/antlr4/runtime/Go/antlr/v4 v4.0.0-20230305170008-8188dc5388df/go.mod h1:pSwJ0fSY5KhvocuWSx4fz3BA8OrA1bQn+K1Eli3BRwM= github.com/apache/arrow/go/v10 v10.0.1 h1:n9dERvixoC/1JjDmBcs9FPaEryoANa2sCgVFo6ez9cI= github.com/apache/arrow/go/v11 v11.0.0 h1:hqauxvFQxww+0mEU/2XHG6LT7eZternCZq+A5Yly2uM= github.com/apache/arrow/go/v12 v12.0.0 h1:xtZE63VWl7qLdB0JObIXvvhGjoVNrQ9ciIHG2OK5cmc= github.com/apache/arrow/go/v12 v12.0.1 h1:JsR2+hzYYjgSUkBSaahpqCetqZMr76djX80fF/DiJbg= +github.com/apache/arrow/go/v12 v12.0.1/go.mod h1:weuTY7JvTG/HDPtMQxEUp7pU73vkLWMLpY67QwZ/WWw= github.com/apache/arrow/go/v13 v13.0.0 h1:kELrvDQuKZo8csdWYqBQfyi431x6Zs/YJTEgUuSVcWk= github.com/apache/arrow/go/v13 v13.0.0/go.mod h1:W69eByFNO0ZR30q1/7Sr9d83zcVZmF2MiP3fFYAWJOc= github.com/apache/arrow/go/v14 v14.0.2 h1:N8OkaJEOfI3mEZt07BIkvo4sC6XDbL+48MBPWO5IONw= github.com/apache/arrow/go/v14 v14.0.2/go.mod h1:u3fgh3EdgN/YQ8cVQRguVW3R+seMybFg8QBQ5LU+eBY= +github.com/apache/thrift v0.17.0/go.mod h1:OLxhMRJxomX+1I/KUw03qoV3mMz16BwaKI+d4fPBx7Q= github.com/apparentlymart/go-dump v0.0.0-20180507223929-23540a00eaa3 h1:ZSTrOEhiM5J5RFxEaFvMZVEAM1KvT1YzbEOwB2EAGjA= github.com/apparentlymart/go-dump v0.0.0-20180507223929-23540a00eaa3/go.mod h1:oL81AME2rN47vu18xqj1S1jPIPuN7afo62yKTNn3XMM= github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e h1:QEF07wC0T1rKkctt1RINW/+RMTVmiwxETico2l3gxJA= @@ -439,21 +505,31 @@ github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.15.4 h1:EmIEXOjAdXtxa2OGM github.com/aws/aws-sdk-go-v2/service/sns v1.17.4 h1:7TdmoJJBwLFyakXjfrGztejwY5Ie1JEto7YFfznCmAw= github.com/aws/aws-sdk-go-v2/service/sqs v1.18.3 h1:uHjK81fESbGy2Y9lspub1+C6VN5W2UXTDo2A/Pm4G0U= github.com/aws/aws-sdk-go-v2/service/ssm v1.24.1 h1:zc1YLcknvxdW/i1MuJKmEnFB2TNkOfguuQaGRvJXPng= +github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= +github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= github.com/aymerick/raymond v2.0.3-0.20180322193309-b565731e1464+incompatible h1:Ppm0npCCsmuR9oQaBtRuZcmILVE74aXE+AmrJj8L2ns= github.com/bgentry/speakeasy v0.1.0 h1:ByYyxL9InA1OWqxJqqp2A5pYHUrCiAL6K3J+LKSsQkY= github.com/bitly/go-hostpool v0.0.0-20171023180738-a3a6125de932 h1:mXoPYz/Ul5HYEDvkta6I8/rnYM5gSdSV2tJ6XbZuEtY= github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 h1:DDGfHa7BWjL4YnC6+E63dPcxHo2sUxDIu8g3QgEJdRY= github.com/boombuler/barcode v1.0.1 h1:NDBbPmhS+EqABEs5Kg3n/5ZNjy73Pz7SIV+KCeqyXcs= +github.com/bufbuild/protovalidate-go v0.2.1/go.mod h1:e7XXDtlxj5vlEyAgsrxpzayp4cEMKCSSb8ZCkin+MVA= github.com/bwesterb/go-ristretto v1.2.3 h1:1w53tCkGhCQ5djbat3+MH0BAQ5Kfgbt56UZQ/JMzngw= +github.com/bytedance/sonic v1.10.0-rc3/go.mod h1:iZcSUejdk5aukTND/Eu/ivjQuEL0Cu9/rf50Hi0u/g4= github.com/casbin/casbin/v2 v2.37.0 h1:/poEwPSovi4bTOcP752/CsTQiRz2xycyVKFG7GUhbDw= github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4= github.com/census-instrumentation/opencensus-proto v0.4.1 h1:iKLQ0xPNFxR/2hzXZMrBo8f1j86j5WHzznCCQxV/b8g= github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= +github.com/cheekybits/is v0.0.0-20150225183255-68e9c0620927/go.mod h1:h/aW8ynjgkuj+NQRlZcDbAbM1ORAbXjXX77sX7T289U= +github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d/go.mod h1:8EPpVsBuRksnlj1mLy4AWzRNQYxauNi62uWcE3to6eA= +github.com/chenzhuoyu/iasm v0.9.0/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLIrkAmYog= github.com/chromedp/cdproto v0.0.0-20220208224320-6efb837e6bc2/go.mod h1:At5TxYYdxkbQL0TSefRjhLE3Q0lgvqKKMSFUglJ7i1U= github.com/chromedp/chromedp v0.9.2 h1:dKtNz4kApb06KuSXoTQIyUC2TrA0fhGDwNZf3bcgfKw= github.com/chromedp/sysutil v1.0.0 h1:+ZxhTpfpZlmchB58ih/LBHX52ky7w2VhQVKQMucy3Ic= +github.com/chzyer/logex v1.2.0/go.mod h1:9+9sk7u7pGNWYMkh0hdiL++6OeibzJccyQU4p4MedaY= github.com/chzyer/logex v1.2.1 h1:XHDu3E6q+gdHgsdTPH6ImJMIp436vR6MPtH8gP05QzM= +github.com/chzyer/readline v1.5.0/go.mod h1:x22KAscuvRqlLoK9CsoYsmxoXZMMFVyOl86cAH8qUic= github.com/chzyer/readline v1.5.1 h1:upd/6fQk4src78LMRzh5vItIt361/o4uq553V8B5sGI= +github.com/chzyer/test v0.0.0-20210722231415-061457976a23/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/chzyer/test v1.0.0 h1:p3BQDXSxOhOG0P9z6/hGnII4LGiEPOYBhs8asl/fC04= github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible h1:C29Ae4G5GtYyYMm1aztcyj/J5ckgJm2zwdDajFbx1NY= github.com/circonus-labs/circonusllhist v0.1.3 h1:TJH+oke8D16535+jHExHj4nQvzlZrj7ug5D7I/orNUA= @@ -461,6 +537,7 @@ github.com/clbanning/mxj v1.8.4 h1:HuhwZtbyvyOw+3Z1AowPkU87JkJUSv751ELWaiTpj8I= github.com/clbanning/x2j v0.0.0-20191024224557-825249438eec h1:EdRZT3IeKQmfCSrgo8SZ8V3MEnskuJP0wCYNpe+aiXo= github.com/client9/misspell v0.3.4 h1:ta993UF76GwbvJcIo3Y68y/M3WxlpEHPWIGDkJYwzJI= github.com/cncf/udpa/go v0.0.0-20220112060539-c52dc94e7fbe h1:QQ3GSy+MqSHxm/d8nCtnAiZdYFd45cYZPs8vOOIYKfk= +github.com/cncf/xds/go v0.0.0-20231109132714-523115ebc101/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= 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= @@ -495,13 +572,19 @@ github.com/dchest/uniuri v1.2.0 h1:koIcOUdrTIivZgSLhHQvKgqdWZq5d7KdMEWF1Ud6+5g= github.com/dchest/uniuri v1.2.0/go.mod h1:fSzm4SLHzNZvWLvWJew423PhAzkpNQYq+uNLq4kxhkY= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 h1:YLtO71vCjJRCBcrPMtQ9nqBsqpA1m5sE92cU+pd5Mcc= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeCxkaw7y45JueMRL4DIyJDKs= +github.com/deepmap/oapi-codegen v1.8.2 h1:SegyeYGcdi0jLLrpbCMoJxnUUn8GBXHsvr4rbzjuhfU= +github.com/deepmap/oapi-codegen v1.8.2/go.mod h1:YLgSKSDv/bZQB7N4ws6luhozi3cEdRktEqrX88CvjIw= github.com/denisenkom/go-mssqldb v0.12.0 h1:VtrkII767ttSPNRfFekePK3sctr+joXgO58stqQbtUA= github.com/devigned/tab v0.1.1 h1:3mD6Kb1mUOYeLpJvTVSDwSg5ZsfSxfvxGRTxRsJsITA= github.com/dgraph-io/badger v1.6.0 h1:DshxFxZWXUcO0xX476VJC07Xsr6ZCBVRHKZ93Oh7Evo= +github.com/dgraph-io/badger/v2 v2.2007.4/go.mod h1:vSw/ax2qojzbN6eXHIx6KPKtCSHJN/Uz0X0VPruTIhk= +github.com/dgraph-io/ristretto v0.0.3-0.20200630154024-f66de99634de/go.mod h1:KPxhHT9ZxKefz+PCeOGsrHpl1qZ7i70dGTu2u+Ahh6E= github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2 h1:tdlZCpZ/P9DhczCTSixgIKmwPv6+wP5DGjqLYw5SUiA= +github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= github.com/dhui/dktest v0.3.0 h1:kwX5a7EkLcjo7VpsPQSYJcKGbXBXdjI9FGjuUj1jn6I= github.com/dimchansky/utfbom v1.1.1 h1:vV6w1AhK4VMnhBno/TPVCoK9U/LP0PkLCS9tbxHdi/U= +github.com/djherbis/atime v1.1.0/go.mod h1:28OF6Y8s3NQWwacXc5eZTsEsiMzp7LF8MbXE+XJPdBE= github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815 h1:bWDMxwH3px2JBh6AyO7hdCn/PkvCZXii8TGj7sbtEbQ= github.com/drone/drone-runtime v1.1.0 h1:IsKbwiLY6+ViNBzX0F8PERJVZZcEJm9rgxEh3uZP5IE= github.com/drone/drone-runtime v1.1.0/go.mod h1:+osgwGADc/nyl40J0fdsf8Z09bgcBZXvXXnLOY48zYs= @@ -518,24 +601,35 @@ github.com/eapache/queue v1.1.0 h1:YOEu7KNc61ntiQlcEeUIoDTJ2o8mQznoNvUhiigpIqc= github.com/eknkc/amber v0.0.0-20171010120322-cdade1c07385 h1:clC1lXBpe2kTj2VHdaIu9ajZQe4kcEY9j0NsnDDBZ3o= github.com/etcd-io/bbolt v1.3.3 h1:gSJmxrs37LgTqR/oyJBWok6k6SvXEUerFTbltIhXkBM= github.com/ettle/strcase v0.1.1 h1:htFueZyVeE1XNnMEfbqp5r67qAN/4r6ya1ysq8Q+Zcw= +github.com/ettle/strcase v0.1.1/go.mod h1:hzDLsPC7/lwKyBOywSHEP89nt2pDgdy+No1NBA9o9VY= github.com/facette/natsort v0.0.0-20181210072756-2cd4dd1e2dcb h1:IT4JYU7k4ikYg1SCxNI1/Tieq/NFvh6dzLdgi7eu0tM= github.com/facette/natsort v0.0.0-20181210072756-2cd4dd1e2dcb/go.mod h1:bH6Xx7IW64qjjJq8M2u4dxNaBiDfKK+z/3eGDpXEQhc= github.com/fasthttp-contrib/websocket v0.0.0-20160511215533-1f3b11f56072 h1:DddqAaWDpywytcG8w/qoQ5sAN8X12d3Z3koB0C3Rxsc= github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= +github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= +github.com/felixge/fgprof v0.9.3/go.mod h1:RdbpDgzqYVh/T9fPELJyV7EYJuHB55UTEULNun8eiPw= +github.com/flosch/pongo2/v4 v4.0.2 h1:gv+5Pe3vaSVmiJvh/BZa82b7/00YUGm0PIyVVLop0Hw= +github.com/flosch/pongo2/v4 v4.0.2/go.mod h1:B5ObFANs/36VwxxlgKpdchIJHMvHB562PW+BWPhwZD8= github.com/fogleman/gg v1.3.0 h1:/7zJX8F6AaYQc57WQCyN9cAIz+4bCJGO9B+dyW29am8= github.com/form3tech-oss/jwt-go v3.2.2+incompatible h1:TcekIExNqud5crz4xD2pavyTgWiPvpYe4Xau31I0PRk= github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw= github.com/franela/goblin v0.0.0-20210519012713-85d372ac71e2 h1:cZqz+yOJ/R64LcKjNQOdARott/jP7BnUQ9Ah7KaZCvw= github.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8 h1:a9ENSRDFBUPkJ5lCgVZh26+ZbGyoVJG7yb5SSzF5H54= 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/gavv/httpexpect v2.0.0+incompatible h1:1X9kcRshkSKEjNJJxX9Y9mQ5BRfbxU5kORdjhlA1yX8= +github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= github.com/gin-gonic/gin v1.8.1 h1:4+fr/el88TOO3ewCmQr8cx/CtZ/umlIRIs5M4NTNjf8= github.com/gin-gonic/gin v1.8.1/go.mod h1:ji8BvRH1azfM+SYow9zQ6SZMvR8qOMZHmsCuWR9tTTk= +github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg= +github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU= github.com/go-check/check v0.0.0-20180628173108-788fd7840127 h1:0gkP6mzaMqkmpcJYCFOLkIBwI7xFExG03bbkOkCvUPI= github.com/go-chi/chi/v5 v5.0.7 h1:rDTPXLDHGATaeHvVlLcR4Qe0zftYethFucbjVQ1PxU8= github.com/go-chi/chi/v5 v5.0.7/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= github.com/go-chi/chi/v5 v5.0.8 h1:lD+NLqFcAi1ovnVZpsnObHGW4xb4J8lNmoYVfECH1Y0= github.com/go-chi/chi/v5 v5.0.8/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= +github.com/go-chi/chi/v5 v5.0.10/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= github.com/go-fonts/dejavu v0.1.0 h1:JSajPXURYqpr+Cu8U9bt8K+XcACIHWqWrvWCKyeFmVQ= github.com/go-fonts/latin-modern v0.2.0 h1:5/Tv1Ek/QCr20C6ZOz15vw3g7GELYL98KWr8Hgo+3vk= github.com/go-fonts/liberation v0.2.0 h1:jAkAWJP4S+OsrPLZM4/eC9iW7CtHy+HBXrEwZXWo5VM= @@ -549,14 +643,21 @@ github.com/go-latex/latex v0.0.0-20210823091927-c0d11ff05a81 h1:6zl3BbBhdnMkpSj2 github.com/go-martini/martini v0.0.0-20170121215854-22fa46961aab h1:xveKWz2iaueeTaUgdetzel+U7exyigDYBryyVfV/rZk= github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= +github.com/go-openapi/swag v0.22.8/go.mod h1:6QT22icPLEqAM/z/TChgb4WAveCHF92+2gF0CNjHpPI= github.com/go-pdf/fpdf v0.6.0 h1:MlgtGIfsdMEEQJr2le6b/HNr1ZlQwxyWr77r2aj2U/8= github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A= github.com/go-playground/locales v0.14.0 h1:u50s323jtVGugKlcYeyzC0etD1HifMjqmJqb8WugfUU= github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= github.com/go-playground/universal-translator v0.18.0 h1:82dyy6p4OuJq4/CByFNOn/jYrnRPArHwAcmLoJZxyho= github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= github.com/go-playground/validator/v10 v10.11.1 h1:prmOlTVv+YjZjmRmNSF3VmspqJIxJWXmqUsHwfTRRkQ= github.com/go-playground/validator/v10 v10.11.1/go.mod h1:i+3WkQ1FvaUjjxh1kSvIA4dMGDBiPU55YFDl0WbKdWU= +github.com/go-playground/validator/v10 v10.14.1 h1:9c50NUPC30zyuKprjL3vNZ0m5oG+jU0zvx4AqHGnv4k= +github.com/go-playground/validator/v10 v10.14.1/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= github.com/gobuffalo/attrs v0.0.0-20190224210810-a9411de4debd h1:hSkbZ9XSyjyBirMeqSqUrK+9HboWrweVlzRNqoBi2d4= github.com/gobuffalo/depgen v0.1.0 h1:31atYa/UW9V5q8vMJ+W6wd64OaaTHUrCUXER358zLM4= github.com/gobuffalo/envy v1.7.0 h1:GlXgaiBkmrYMHco6t4j7SacKO4XUjvh5pwXh0f4uxXU= @@ -572,22 +673,32 @@ github.com/gobuffalo/syncx v0.0.0-20190224160051-33c29581e754 h1:tpom+2CJmpzAWj5 github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU= github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og= github.com/gobwas/ws v1.2.1 h1:F2aeBZrm2NDsc7vbovKrWSogd4wvfAxg0FQ89/iqOTk= +github.com/gobwas/ws v1.3.0/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/KY= +github.com/goccy/go-yaml v1.9.8/go.mod h1:JubOolP3gh0HpiBc4BLRD4YmjEjHAmIIB2aaXKkTfoE= github.com/goccy/go-yaml v1.11.0 h1:n7Z+zx8S9f9KgzG6KtQKf+kwqXZlLNR2F6018Dgau54= github.com/goccy/go-yaml v1.11.0/go.mod h1:H+mJrWtjPTJAHvRbV09MCK9xYwODM+wRTVFFTWckfng= github.com/gocql/gocql v0.0.0-20190301043612-f6df8288f9b4 h1:vF83LI8tAakwEwvWZtrIEx7pOySacl2TOxx6eXk4ePo= github.com/godbus/dbus/v5 v5.0.4 h1:9349emZab16e7zQvpmsbtjc18ykshndd8y2PG3sgJbA= github.com/gofiber/fiber/v2 v2.46.0 h1:wkkWotblsGVlLjXj2dpgKQAYHtXumsK/HyFugQM68Ns= github.com/gofiber/fiber/v2 v2.46.0/go.mod h1:DNl0/c37WLe0g92U6lx1VMQuxGUQY5V7EIaVoEsUffc= +github.com/gofiber/fiber/v2 v2.49.1/go.mod h1:nPUeEBUeeYGgwbDm59Gp7vS8MDyScL6ezr/Np9A13WU= +github.com/golang-jwt/jwt v3.2.1+incompatible h1:73Z+4BJcrTC+KczS6WvTPvRGOp1WmfEP4Q1lOd9Z/+c= github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g= +github.com/golangci/lint-1 v0.0.0-20181222135242-d2cdd8c08219 h1:utua3L2IbQJmauC5IXdEA547bcoU5dozgQAfc8Onsg4= +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= github.com/gomodule/redigo v1.8.9/go.mod h1:7ArFNvsTjH8GMMzB4uy1snslv2BwmginuMs06a1uzZE= +github.com/google/cel-go v0.17.1/go.mod h1:HXZKzB0LXqer5lHHgfWAnlYwJaQBDKMjxjulNQzhwhY= github.com/google/go-jsonnet v0.18.0 h1:/6pTy6g+Jh1a1I2UMoAODkqELFiVIdOxbNwv0DDzoOg= github.com/google/go-jsonnet v0.18.0/go.mod h1:C3fTzyVJDslXdiTqw/bTFk7vSGyCtH3MGRbDfvEwGd0= github.com/google/go-pkcs11 v0.2.1-0.20230907215043-c6f79328ddf9 h1:OF1IPgv+F4NmqmJ98KTjdN97Vs1JxDPB3vbmYzV2dpk= github.com/google/go-replayers/grpcreplay v1.1.0 h1:S5+I3zYyZ+GQz68OfbURDdt/+cSMqCK1wrvNx7WBzTE= github.com/google/go-replayers/httpreplay v1.1.1 h1:H91sIMlt1NZzN7R+/ASswyouLJfW0WLW7fhyUFvDEkY= +github.com/google/pprof v0.0.0-20211214055906-6f57359322fd/go.mod h1:KgnwoLYCZ8IQu3XUZ8Nc/bM9CCZFOyjUNOSygVozoDg= +github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo= github.com/google/renameio v0.1.0 h1:GOZbcHa3HfsPKPlmyPyN2KEohoMXOhdMbHrvbpl2QaA= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= @@ -595,6 +706,8 @@ github.com/google/subcommands v1.0.1 h1:/eqq+otEXm5vhfBrbREPCSVQbvofip6kIz+mX5TU github.com/googleapis/go-type-adapters v1.0.0 h1:9XdMn+d/G57qq1s8dNc5IesGCXHf6V2HZ2JwRxfA2tA= github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8 h1:tlyzajkF3030q6M8SvmJSemC9DTHL/xaMa18b65+JM4= github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8= +github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY= +github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c= 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= @@ -605,35 +718,51 @@ github.com/grafana/e2e v0.1.1-0.20221018202458-cffd2bb71c7b h1:Ha+kSIoTutf4ytlVw github.com/grafana/e2e v0.1.1-0.20221018202458-cffd2bb71c7b/go.mod h1:3UsooRp7yW5/NJQBlXcTsAHOoykEhNUYXkQ3r6ehEEY= github.com/grafana/gomemcache v0.0.0-20231023152154-6947259a0586 h1:/of8Z8taCPftShATouOrBVy6GaTTjgQd/VfNiZp/VXQ= github.com/grafana/gomemcache v0.0.0-20231023152154-6947259a0586/go.mod h1:PGk3RjYHpxMM8HFPhKKo+vve3DdlPUELZLSDEFehPuU= +github.com/grafana/grafana-azure-sdk-go/v2 v2.0.2 h1:CWT7mOBPUht9n7F/NiBQnEM05pFmCP3Z8CZPGCVC1tM= +github.com/grafana/grafana-azure-sdk-go/v2 v2.0.2/go.mod h1:s8GLONgVh/svnSsO0Eo+OgXc/RZqozI5/0n+pNm3MEE= github.com/grafana/grafana-plugin-sdk-go v0.212.0/go.mod h1:qsI4ktDf0lig74u8SLPJf9zRdVxWV/W4Wi+Ox6gifgs= github.com/grafana/grafana-plugin-sdk-go v0.215.0/go.mod h1:nBsh3jRItKQUXDF2BQkiQCPxqrsSQeb+7hiFyJTO1RE= github.com/grafana/grafana-plugin-sdk-go v0.216.0/go.mod h1:FdvSvOliqpVLnytM7e89zCFyYPDE6VOn9SIjVQRvVxM= +github.com/grafana/grafana-plugin-sdk-go v0.227.1-0.20240426134450-5fe9f7b9dfd4 h1:GV9u4RplRyMlqDicJ0t+m1nVTL1SSfqHd38B/RGum+k= +github.com/grafana/grafana-plugin-sdk-go v0.227.1-0.20240426134450-5fe9f7b9dfd4/go.mod h1:UBDIuvdUGUI5fMDHDAl6yAVpFhfwl5ojMaw1N68775w= +github.com/grafana/grafana-plugin-sdk-go v0.227.1-0.20240430073540-ce4d126ae8b8 h1:pyWJN79uW8QHZiQRasHGLCEkXSr3k6HCjdr0J2jZ3rU= +github.com/grafana/grafana-plugin-sdk-go v0.227.1-0.20240430073540-ce4d126ae8b8/go.mod h1:u4K9vVN6eU86loO68977eTXGypC4brUCnk4sfDzutZU= +github.com/grafana/grafana-plugin-sdk-go v0.228.0 h1:LlPqyB+RZTtDy8RVYD7iQVJW5A0gMoGSI/+Ykz8HebQ= +github.com/grafana/grafana-plugin-sdk-go v0.228.0/go.mod h1:u4K9vVN6eU86loO68977eTXGypC4brUCnk4sfDzutZU= github.com/grafana/grafana/pkg/promlib v0.0.3/go.mod h1:3El4NlsfALz8QQCbEGHGFvJUG+538QLMuALRhZ3pcoo= -github.com/grafana/grafana/pkg/promlib v0.0.5/go.mod h1:iZNjkJBN8DU/5/DxrmwuHaSeiKODT72DYiQ0c9Da1JQ= github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7 h1:pdN6V1QBWetyv/0+wjACpqVH+eVULgEjkurDLq3goeM= github.com/grpc-ecosystem/grpc-gateway/v2 v2.18.1/go.mod h1:YvJ2f6MplWDhfxiUC3KpyTy76kYUZA4W3pTv/wdKQ9Y= 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/hamba/avro/v2 v2.17.2/go.mod h1:Q9YK+qxAhtVrNqOhwlZTATLgLA8qxG2vtvkhK8fJ7Jo= +github.com/hashicorp/go-hclog v0.14.1/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= +github.com/hashicorp/go-hclog v0.16.1 h1:IVQwpTGNRRIHafnTs2dQLIk4ENtneRIEEJWOVDqz99o= +github.com/hashicorp/go-hclog v0.16.1/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/logutils v1.0.0 h1:dLEQVugN8vlakKOUE3ihGLTZJRB4j+M2cdTm/ORI65Y= github.com/hashicorp/mdns v1.0.4 h1:sY0CMhFmjIPDMlTB+HfymFHCaYLhgifZ0QhjaYKD/UQ= github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= +github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= github.com/hudl/fargo v1.4.0 h1:ZDDILMbB37UlAVLlWcJ2Iz1XuahZZTDZfdCKeclfq2s= github.com/hydrogen18/memlistener v0.0.0-20200120041712-dcc25e7acd91 h1:KyZDvZ/GGn+r+Y3DKZ7UOQ/TP4xV6HNkrwiVMB1GnNY= github.com/iancoleman/strcase v0.2.0 h1:05I4QRnGpI0m37iZQRuskXh+w77mr6Z41lwQzuHLwW0= github.com/iancoleman/strcase v0.3.0 h1:nTXanmYxhfFAMjZL34Ov6gkzEsSJZ5DbhxWjvSASxEI= github.com/iancoleman/strcase v0.3.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= +github.com/ianlancetaylor/demangle v0.0.0-20210905161508-09a460cdf81d/go.mod h1:aYm2/VgdVmcIU8iMfdMvDMsRAQjcfZSKFby6HOFvi/w= +github.com/ianlancetaylor/demangle v0.0.0-20220319035150-800ac71e25c2/go.mod h1:aYm2/VgdVmcIU8iMfdMvDMsRAQjcfZSKFby6HOFvi/w= github.com/ianlancetaylor/demangle v0.0.0-20230524184225-eabc099b10ab h1:BA4a7pe6ZTd9F8kXETBoijjFJ/ntaa//1wiH9BZu4zU= -github.com/imkira/go-interpol v1.1.0 h1:KIiKr0VSG2CUW1hl1jpiyuzuJeKUUpC8iM1AIE7N1Vk= github.com/influxdata/influxdb v1.7.6 h1:8mQ7A/V+3noMGCt/P9pD09ISaiz9XvgCk303UYA3gcs= github.com/influxdata/influxdb1-client v0.0.0-20200827194710-b269163b24ab h1:HqW4xhhynfjrtEiiSGcQUd6vrK23iMam1FO8rI7mwig= +github.com/invopop/yaml v0.1.0/go.mod h1:2XuRLgs/ouIrW3XNzuNj7J3Nvu/Dig5MXvbCEdiBN3Q= github.com/iris-contrib/blackfriday v2.0.0+incompatible h1:o5sHQHHm0ToHUlAJSTjW9UWicjJSDDauOOQ2AHuIVp4= github.com/iris-contrib/go.uuid v2.0.0+incompatible h1:XZubAYg61/JwnJNbZilGjf3b3pB80+OQg2qf6c8BfWE= +github.com/iris-contrib/go.uuid v2.0.0+incompatible/go.mod h1:iz2lgM/1UnEf1kP0L/+fafWORmlnuysV2EMP8MW+qe0= github.com/iris-contrib/jade v1.1.3 h1:p7J/50I0cjo0wq/VWVCDFd8taPJbuFC+bq23SniRFX0= github.com/iris-contrib/pongo2 v0.0.1 h1:zGP7pW51oi5eQZMIlGA3I+FHY9/HOQWDB+572yin0to= github.com/iris-contrib/schema v0.0.1 h1:10g/WnoRR+U+XXHWKBHeNy/+tZmM2kcAVGLOsz+yaDA= +github.com/iris-contrib/schema v0.0.6 h1:CPSBLyx2e91H2yJzPuhGuifVRnZBBJ3pCOMbOvPZaTw= +github.com/iris-contrib/schema v0.0.6/go.mod h1:iYszG0IOsuIsfzjymw1kMzTL8YQcCWlm65f3wX8J5iA= github.com/jackc/chunkreader v1.0.0 h1:4s39bBR8ByfqH+DKm8rQA3E1LHZWB9XWcrz8fqaZbe0= github.com/jackc/chunkreader/v2 v2.0.1 h1:i+RDz65UE+mmpjTfyz0MoVTnzeYxroil2G82ki7MGG8= github.com/jackc/fake v0.0.0-20150926172116-812a484cc733 h1:vr3AYkKovP8uR8AvSGGUK1IDqRa5lAAvEkZG1LKaCRc= @@ -646,58 +775,215 @@ github.com/jackc/pgproto3/v2 v2.2.0 h1:r7JypeP2D3onoQTCxWdTpCtJ4D+qpKr0TxvoyMhZ5 github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b h1:C8S2+VttkHFdOOCXJe+YGfa4vHYwlt4Zx+IVXQ97jYg= github.com/jackc/pgtype v1.10.0 h1:ILnBWrRMSXGczYvmkYD6PsYyVFUNLTnIUJHHDLmqk38= github.com/jackc/pgx v3.2.0+incompatible h1:0Vihzu20St42/UDsvZGdNE6jak7oi/UOeMzwMPHkgFY= +github.com/jackc/pgx/v4 v4.15.0 h1:B7dTkXsdILD3MF987WGGCcg+tvLW6bZJdEcqVFeU//w= github.com/jackc/puddle v1.2.1 h1:gI8os0wpRXFd4FiAY2dWiqRK037tjj3t7rKFeO4X5iw= +github.com/jackspirou/syscerts v0.0.0-20160531025014-b68f5469dff1 h1:9Xm8CKtMZIXgcopfdWk/qZ1rt0HjMgfMR9nxxSeK6vk= github.com/jackspirou/syscerts v0.0.0-20160531025014-b68f5469dff1/go.mod h1:zuHl3Hh+e9P6gmBPvcqR1HjkaWHC/csgyskg6IaFKFo= +github.com/jaegertracing/jaeger v1.41.0 h1:vVNky8dP46M2RjGaZ7qRENqylW+tBFay3h57N16Ip7M= github.com/jaegertracing/jaeger v1.41.0/go.mod h1:SIkAT75iVmA9U+mESGYuMH6UQv6V9Qy4qxo0lwfCQAc= +github.com/jarcoal/httpmock v1.3.0 h1:2RJ8GP0IIaWwcC9Fp2BmVi8Kog3v2Hn7VXM3fTd+nuc= +github.com/jedib0t/go-pretty/v6 v6.2.4 h1:wdaj2KHD2W+mz8JgJ/Q6L/T5dB7kyqEFI16eLq7GEmk= github.com/jedib0t/go-pretty/v6 v6.2.4/go.mod h1:+nE9fyyHGil+PuISTCrp7avEdo6bqoMwqZnuiK2r2a0= +github.com/jhump/gopoet v0.1.0 h1:gYjOPnzHd2nzB37xYQZxj4EIQNpBrBskRqQQ3q4ZgSg= +github.com/jhump/goprotoc v0.5.0 h1:Y1UgUX+txUznfqcGdDef8ZOVlyQvnV0pKWZH08RmZuo= +github.com/joho/godotenv v1.4.0 h1:3l4+N6zfMWnkbPEXKng2o2/MR5mSwTrBih4ZEkkz1lg= github.com/joho/godotenv v1.4.0/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/jstemmer/go-junit-report v0.9.1 h1:6QPYqodiu3GuPL+7mfx+NwDdp2eTkp9IfEUpgAwUN0o= +github.com/jsternberg/zap-logfmt v1.2.0 h1:1v+PK4/B48cy8cfQbxL4FmmNZrjnIMr2BsnyEmXqv2o= github.com/jsternberg/zap-logfmt v1.2.0/go.mod h1:kz+1CUmCutPWABnNkOu9hOHKdT2q3TDYCcsFy9hpqb0= +github.com/kataras/blocks v0.0.7 h1:cF3RDY/vxnSRezc7vLFlQFTYXG/yAr1o7WImJuZbzC4= +github.com/kataras/blocks v0.0.7/go.mod h1:UJIU97CluDo0f+zEjbnbkeMRlvYORtmc1304EeyXf4I= +github.com/kataras/golog v0.1.9 h1:vLvSDpP7kihFGKFAvBSofYo7qZNULYSHOH2D7rPTKJk= +github.com/kataras/golog v0.1.9/go.mod h1:jlpk/bOaYCyqDqH18pgDHdaJab72yBE6i0O3s30hpWY= +github.com/kataras/iris/v12 v12.2.6-0.20230908161203-24ba4e8933b9 h1:Vx8kDVhO2qepK8w44lBtp+RzN3ld743i+LYPzODJSpQ= +github.com/kataras/iris/v12 v12.2.6-0.20230908161203-24ba4e8933b9/go.mod h1:ldkoR3iXABBeqlTibQ3MYaviA1oSlPvim6f55biwBh4= +github.com/kataras/jwt v0.1.10/go.mod h1:xkimAtDhU/aGlQqjwvgtg+VyuPwMiyZHaY8LJRh0mYo= +github.com/kataras/neffos v0.0.22/go.mod h1:IIJZcUDvwBxJGlDj942dqQgyznVKYDti91f8Ez+RRxE= +github.com/kataras/pio v0.0.12 h1:o52SfVYauS3J5X08fNjlGS5arXHjW/ItLkyLcKjoH6w= +github.com/kataras/pio v0.0.12/go.mod h1:ODK/8XBhhQ5WqrAhKy+9lTPS7sBf6O3KcLhc9klfRcY= +github.com/kataras/sitemap v0.0.6 h1:w71CRMMKYMJh6LR2wTgnk5hSgjVNB9KL60n5e2KHvLY= +github.com/kataras/sitemap v0.0.6/go.mod h1:dW4dOCNs896OR1HmG+dMLdT7JjDk7mYBzoIRwuj5jA4= +github.com/kataras/tunnel v0.0.4 h1:sCAqWuJV7nPzGrlb0os3j49lk2JhILT0rID38NHNLpA= +github.com/kataras/tunnel v0.0.4/go.mod h1:9FkU4LaeifdMWqZu7o20ojmW4B7hdhv2CMLwfnHGpYw= github.com/klauspost/compress v1.16.7/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= +github.com/klauspost/cpuid/v2 v2.2.3/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY= +github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY= github.com/knadh/koanf v1.5.0/go.mod h1:Hgyjp4y8v44hpZtPzs7JZfRAW5AhN7KfZcwv1RYggDs= +github.com/konsorten/go-windows-terminal-sequences v1.0.3 h1:CE8S1cTafDpPvMhIxNJKvHsGVBgn1xWYf1NbHQhywc8= +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.8 h1:AkaSdXYQOWeaO3neb8EM634ahkXXe3jYbVh/F9lq+GI= +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= +github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0= +github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU= +github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80 h1:6Yzfa6GP0rIo/kULo2bwGEkFvCePZ3qHDDTC3/J9Swo= +github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q= +github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4= +github.com/lestrrat-go/backoff/v2 v2.0.8 h1:oNb5E5isby2kiro9AgdHLv5N5tint1AnDVVf2E2un5A= github.com/lestrrat-go/backoff/v2 v2.0.8/go.mod h1:rHP/q/r9aT27n24JQLa7JhSQZCKBBOiM/uP402WwN8Y= +github.com/lestrrat-go/blackmagic v1.0.0 h1:XzdxDbuQTz0RZZEmdU7cnQxUtFUzgCSPq8RCz4BxIi4= github.com/lestrrat-go/blackmagic v1.0.0/go.mod h1:TNgH//0vYSs8VXDCfkZLgIrVTTXQELZffUV0tz3MtdQ= +github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE= github.com/lestrrat-go/httpcc v1.0.1/go.mod h1:qiltp3Mt56+55GPVCbTdM9MlqhvzyuL6W/NMDA8vA5E= +github.com/lestrrat-go/iter v1.0.1 h1:q8faalr2dY6o8bV45uwrxq12bRa1ezKrB6oM9FUgN4A= github.com/lestrrat-go/iter v1.0.1/go.mod h1:zIdgO1mRKhn8l9vrZJZz9TUMMFbQbLeTsbqPDrJ/OJc= +github.com/lestrrat-go/jwx v1.2.25 h1:tAx93jN2SdPvFn08fHNAhqFJazn5mBBOB8Zli0g0otA= github.com/lestrrat-go/jwx v1.2.25/go.mod h1:zoNuZymNl5lgdcu6P7K6ie2QRll5HVfF4xwxBBK1NxY= +github.com/lestrrat-go/option v1.0.0 h1:WqAWL8kh8VcSoD6xjSH34/1m8yxluXQbDeKNfvFeEO4= github.com/lestrrat-go/option v1.0.0/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= +github.com/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-bc2310a04743 h1:143Bb8f8DuGWck/xpNUOckBVYfFbBTnLevfRZ1aVVqo= +github.com/lightstep/lightstep-tracer-go v0.18.1 h1:vi1F1IQ8N7hNWytK9DpJsUfQhGuNSc19z330K6vl4zk= +github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= +github.com/lyft/protoc-gen-star v0.6.1 h1:erE0rdztuaDq3bpGifD95wfoPrSZc95nGA6tbiNYh6M= +github.com/lyft/protoc-gen-star/v2 v2.0.3 h1:/3+/2sWyXeMLzKd1bX+ixWKgEMsULrIivpDsuaF441o= +github.com/lyft/protoc-gen-validate v0.0.13 h1:KNt/RhmQTOLr7Aj8PsJ7mTronaFyx80mRTT9qF261dA= +github.com/magiconair/properties v1.8.6 h1:5ibWZ6iY0NctNGWo87LalDlEZ6R41TqbbDamhfG/Qzo= +github.com/mailgun/raymond/v2 v2.0.48 h1:5dmlB680ZkFG2RN/0lvTAghrSxIESeu9/2aeDqACtjw= +github.com/mailgun/raymond/v2 v2.0.48/go.mod h1:lsgvL50kgt1ylcFJYZiULi5fjPBkkhNfj4KA0W54Z18= +github.com/markbates/oncer v0.0.0-20181203154359-bf2de49a0be2 h1:JgVTCPf0uBVcUSWpyXmGpgOc62nK5HWUBKAGc3Qqa5k= +github.com/markbates/safe v1.0.1 h1:yjZkbvRM6IzKj9tlu/zMJLS0n/V351OZWRnF3QfaUxI= +github.com/matryer/moq v0.3.1 h1:kLDiBJoGcusWS2BixGyTkF224aSCD8nLY24tj/NcTCs= github.com/matryer/moq v0.3.1/go.mod h1:RJ75ZZZD71hejp39j4crZLsEDszGk6iH4v4YsWFKH4s= +github.com/matryer/try v0.0.0-20161228173917-9ac251b645a2/go.mod h1:0KeJpeMD6o+O4hW7qJOT7vyQPKrWmj26uf5wMc/IiIs= +github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= +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/mediocregopher/radix/v3 v3.8.1/go.mod h1:8FL3F6UQRXHXIBSPUs5h0RybMF8i4n7wVopoX3x7Bv8= +github.com/microcosm-cc/bluemonday v1.0.25 h1:4NEwSfiJ+Wva0VxN5B8OwMicaJvD8r9tlJWm9rtloEg= +github.com/microcosm-cc/bluemonday v1.0.25/go.mod h1:ZIOjCQp1OrzBBPIJmfX4qDYFuhU02nx4bn030ixfHLE= +github.com/minio/highwayhash v1.0.2 h1:Aak5U0nElisjDCfPSG79Tgzkn2gl66NxOMspRrKnA/g= +github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34= github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM= +github.com/minio/minio-go/v7 v7.0.52 h1:8XhG36F6oKQUDDSuz6dY3rioMzovKjW40W6ANuN0Dps= github.com/minio/minio-go/v7 v7.0.52/go.mod h1:IbbodHyjUAguneyucUaahv+VMNs/EOTV9du7A7/Z3HU= +github.com/minio/sha256-simd v1.0.0 h1:v1ta+49hkWZyvaKwrQB8elexRqm6Y0aMLjCNsrYxo6g= github.com/minio/sha256-simd v1.0.0/go.mod h1:OuYzVNI5vcoYIAmbIvHPl3N3jUzVedXbKy5RFepssQM= +github.com/mitchellh/cli v1.1.5 h1:OxRIeJXpAMztws/XHlN2vu6imG5Dpq+j61AzAX5fLng= +github.com/mitchellh/gox v0.4.0 h1:lfGJxY7ToLJQjHHwi0EX6uYBdK78egf954SQl13PQJc= +github.com/mitchellh/iochan v1.0.0 h1:C+X3KsSTLFVBr/tK1eYN/vs4rJcvsiLU338UhYPJWeY= +github.com/mithrandie/readline-csvq v1.2.1 h1:4cfeYeVSrqKEWi/1t7CjyhFD2yS6fm+l+oe+WyoSNlI= github.com/mithrandie/readline-csvq v1.2.1/go.mod h1:ydD9Eyp3/wn8KPSNbKmMZe4RQQauCuxi26yEo4N40dk= +github.com/modocache/gover v0.0.0-20171022184752-b58185e213c5 h1:8Q0qkMVC/MmWkpIdlvZgcv2o2jrlF6zqVOh7W5YHdMA= +github.com/montanaflynn/stats v0.7.0 h1:r3y12KyNxj/Sb/iOE46ws+3mS1+MZca1wlHQFPsY/JU= +github.com/mostynb/go-grpc-compression v1.1.17 h1:N9t6taOJN3mNTTi0wDf4e3lp/G/ON1TP67Pn0vTUA9I= github.com/mostynb/go-grpc-compression v1.1.17/go.mod h1:FUSBr0QjKqQgoDG/e0yiqlR6aqyXC39+g/hFLDfSsEY= +github.com/nakagami/firebirdsql v0.0.0-20190310045651-3c02a58cfed8 h1:P48LjvUQpTReR3TQRbxSeSBsMXzfK0uol7eRcr7VBYQ= +github.com/natessilva/dag v0.0.0-20180124060714-7194b8dcc5c4 h1:dnMxwus89s86tI8rcGVp2HwZzlz7c5o92VOy7dSckBQ= +github.com/nats-io/jwt v1.2.2 h1:w3GMTO969dFg+UOKTmmyuu7IGdusK+7Ytlt//OYH/uU= +github.com/nats-io/jwt/v2 v2.0.3 h1:i/O6cmIsjpcQyWDYNcq2JyZ3/VTF8SJ4JWluI5OhpvI= +github.com/nats-io/jwt/v2 v2.5.0/go.mod h1:24BeQtRwxRV8ruvC4CojXlx/WQ/VjuwlYiH+vu/+ibI= +github.com/nats-io/nats-server/v2 v2.5.0 h1:wsnVaaXH9VRSg+A2MVg5Q727/CqxnmPLGFQ3YZYKTQg= +github.com/nats-io/nats.go v1.12.1 h1:+0ndxwUPz3CmQ2vjbXdkC1fo3FdiOQDim4gl3Mge8Qo= +github.com/nats-io/nats.go v1.28.0/go.mod h1:XpbWUlOElGwTYbMR7imivs7jJj9GtK7ypv321Wp6pjc= +github.com/nats-io/nkeys v0.3.0 h1:cgM5tL53EvYRU+2YLXIK0G2mJtK12Ft9oeooSZMA2G8= +github.com/nats-io/nkeys v0.4.4/go.mod h1:XUkxdLPTufzlihbamfzQ7mw/VGx6ObUs+0bN5sNvt64= +github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= +github.com/oapi-codegen/testutil v1.0.0/go.mod h1:ttCaYbHvJtHuiyeBF0tPIX+4uhEPTeizXKx28okijLw= +github.com/oklog/oklog v0.3.2 h1:wVfs8F+in6nTBMkA7CbRw+zZMIB7nNM825cM1wuzoTk= +github.com/oklog/ulid/v2 v2.1.0 h1:+9lhoxAP56we25tyYETBBY1YLA2SaoLvUFgrP2miPJU= github.com/oklog/ulid/v2 v2.1.0/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ= +github.com/onsi/gomega v1.27.10/go.mod h1:RsS8tutOdbdgzbPtzzATp12yT7kM5I5aElG3evPbQ0M= +github.com/op/go-logging v0.0.0-20160315200505-970db520ece7 h1:lDH9UUVJtmYCjyT0CI4q8xvlXPxeZ0gYCVvWbmPlp88= +github.com/open-telemetry/opentelemetry-collector-contrib/exporter/jaegerexporter v0.74.0 h1:0dve/IbuHfQOnlIBQQwpCxIeMp7uig9DQVuvisWPDRs= github.com/open-telemetry/opentelemetry-collector-contrib/exporter/jaegerexporter v0.74.0/go.mod h1:bIeSj+SaZdP3CE9Xae+zurdQC6DXX0tPP6NAEVmgtt4= +github.com/open-telemetry/opentelemetry-collector-contrib/exporter/kafkaexporter v0.74.0 h1:MrVOfBTNBe4n/daZjV4yvHZRR0Jg/MOCl/mNwymHwDM= github.com/open-telemetry/opentelemetry-collector-contrib/exporter/kafkaexporter v0.74.0/go.mod h1:v4H2ATSrKfOTbQnmjCxpvuOjrO/GUURAgey9RzrPsuQ= +github.com/open-telemetry/opentelemetry-collector-contrib/exporter/zipkinexporter v0.74.0 h1:8Kk5g5PKQBUV3idjJy1NWVLLReEzjnB8C1lFgQxZ0TI= github.com/open-telemetry/opentelemetry-collector-contrib/exporter/zipkinexporter v0.74.0/go.mod h1:UtVfxZGhPU2OvDh7H8o67VKWG9qHAHRNkhmZUWqCvME= +github.com/open-telemetry/opentelemetry-collector-contrib/internal/coreinternal v0.74.0 h1:vU5ZebauzCuYNXFlQaWaYnOfjoOAnS+Sc8+oNWoHkbM= github.com/open-telemetry/opentelemetry-collector-contrib/internal/coreinternal v0.74.0/go.mod h1:TEu3TnUv1TuyHtjllrUDQ/ImpyD+GrkDejZv4hxl3G8= +github.com/open-telemetry/opentelemetry-collector-contrib/internal/sharedcomponent v0.74.0 h1:COFBWXiWnhRs9x1oYJbDg5cyiNAozp8sycriD9+1/7E= github.com/open-telemetry/opentelemetry-collector-contrib/internal/sharedcomponent v0.74.0/go.mod h1:cAKlYKU+/8mk6ETOnD+EAi5gpXZjDrGweAB9YTYrv/g= +github.com/open-telemetry/opentelemetry-collector-contrib/pkg/translator/jaeger v0.74.0 h1:ww1pPXfAM0WHsymQnsN+s4B9DgwQC+GyoBq0t27JV/k= github.com/open-telemetry/opentelemetry-collector-contrib/pkg/translator/jaeger v0.74.0/go.mod h1:OpEw7tyCg+iG1ywEgZ03qe5sP/8fhYdtWCMoqA8JCug= +github.com/open-telemetry/opentelemetry-collector-contrib/pkg/translator/opencensus v0.74.0 h1:0Fh6OjlUB9HlnX90/gGiyyFvnmNBv6inj7bSaVqQ7UQ= github.com/open-telemetry/opentelemetry-collector-contrib/pkg/translator/opencensus v0.74.0/go.mod h1:13ekplz1UmvK99Vz2VjSBWPYqoRBEax5LPmA1tFHnhA= +github.com/open-telemetry/opentelemetry-collector-contrib/pkg/translator/zipkin v0.74.0 h1:A5xoBaMHX1WzLfvlqK6NBXq4XIbuSVJIpec5r6PDE7U= github.com/open-telemetry/opentelemetry-collector-contrib/pkg/translator/zipkin v0.74.0/go.mod h1:TJT7HkhFPrJic30Vk4seF/eRk8sa0VQ442Xq/qd+DLY= +github.com/open-telemetry/opentelemetry-collector-contrib/receiver/jaegerreceiver v0.74.0 h1:pWNSPCKD+V4rC+MnZj8uErEbcsYUpEqU3InNYyafAPY= github.com/open-telemetry/opentelemetry-collector-contrib/receiver/jaegerreceiver v0.74.0/go.mod h1:0lXcDf6LUbtDxZZO3zDbRzMuL7gL1Q0FPOR8/3IBwaQ= +github.com/open-telemetry/opentelemetry-collector-contrib/receiver/kafkareceiver v0.74.0 h1:NWd9+rQTd6pELLf3copo7CEuNgKp90kgyhPozpwax2U= github.com/open-telemetry/opentelemetry-collector-contrib/receiver/kafkareceiver v0.74.0/go.mod h1:anSbwGOousKpnNAVMNP5YieA4KOFuEzHkvya0vvtsaI= +github.com/open-telemetry/opentelemetry-collector-contrib/receiver/opencensusreceiver v0.74.0 h1:Law7+BImq8DIBsdniSX8Iy2/GH5CRHpT1gsRaC9ZT8A= github.com/open-telemetry/opentelemetry-collector-contrib/receiver/opencensusreceiver v0.74.0/go.mod h1:uiW3V9EX8A5DOoxqDLuSh++ewHr+owtonCSiqMcpy3w= +github.com/open-telemetry/opentelemetry-collector-contrib/receiver/zipkinreceiver v0.74.0 h1:2uysjsaqkf9STFeJN/M6i/sSYEN5pZJ94Qd2/Hg1pKE= github.com/open-telemetry/opentelemetry-collector-contrib/receiver/zipkinreceiver v0.74.0/go.mod h1:qoGuayD7cAtshnKosIQHd6dobcn6/sqgUn0v/Cg2UB8= +github.com/opentracing-contrib/go-grpc v0.0.0-20210225150812-73cb765af46e h1:4cPxUYdgaGzZIT5/j0IfqOrrXmq6bG8AwvwisMXpdrg= github.com/opentracing-contrib/go-grpc v0.0.0-20210225150812-73cb765af46e/go.mod h1:DYR5Eij8rJl8h7gblRrOZ8g0kW1umSpKqYIBTgeDtLo= +github.com/opentracing-contrib/go-observer v0.0.0-20170622124052-a52f23424492 h1:lM6RxxfUMrYL/f8bWEUqdXrANWtrL7Nndbm9iFN0DlU= +github.com/opentracing/basictracer-go v1.0.0 h1:YyUAhaEfjoWXclZVJ9sGoNct7j4TVk7lZWlQw5UXuoo= +github.com/openzipkin-contrib/zipkin-go-opentracing v0.4.5 h1:ZCnq+JUrvXcDVhX/xRolRBZifmabN1HcS1wrPSvxhrU= +github.com/openzipkin/zipkin-go v0.4.1 h1:kNd/ST2yLLWhaWrkgchya40TJabe8Hioj9udfPcEO5A= github.com/openzipkin/zipkin-go v0.4.1/go.mod h1:qY0VqDSN1pOBN94dBc6w2GJlWLiovAyg7Qt6/I9HecM= +github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde h1:x0TT0RDC7UhAVbbWWBzr41ElhJx5tXPWkIHA2HWPRuw= +github.com/pact-foundation/pact-go v1.0.4 h1:OYkFijGHoZAYbOIb1LWXrwKQbMMRUv1oQ89blD2Mh2Q= +github.com/pborman/uuid v1.2.0 h1:J7Q5mO4ysT1dv8hyrUGHb9+ooztCXu1D8MY8DZYsu3g= github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= +github.com/pelletier/go-toml/v2 v2.0.5 h1:ipoSadvV8oGUjnUbMub59IDPPwfxF694nG/jwbMiyQg= +github.com/pelletier/go-toml/v2 v2.0.9 h1:uH2qQXheeefCCkuBBSLi7jCiSmj3VRh2+Goq2N7Xxu0= +github.com/pelletier/go-toml/v2 v2.0.9/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= +github.com/performancecopilot/speed v3.0.0+incompatible h1:2WnRzIquHa5QxaJKShDkLM+sc0JPuwhXzK8OYOyt3Vg= +github.com/performancecopilot/speed/v4 v4.0.0 h1:VxEDCmdkfbQYDlcr/GC9YoN9PQ6p8ulk9xVsepYy9ZY= +github.com/perimeterx/marshmallow v1.1.4/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw= +github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI= +github.com/philhofer/fwd v1.1.2 h1:bnDivRJ1EWPjUIRXV5KfORO897HTbpFAQddBdE8t7Gw= github.com/philhofer/fwd v1.1.2/go.mod h1:qkPdfjR2SIEbspLqpe1tO4n5yICnr2DY7mqEx2tUTP0= +github.com/phpdave11/gofpdf v1.4.2 h1:KPKiIbfwbvC/wOncwhrpRdXVj2CZTCFlw4wnoyjtHfQ= +github.com/phpdave11/gofpdi v1.0.13 h1:o61duiW8M9sMlkVXWlvP92sZJtGKENvW3VExs6dZukQ= github.com/pierrec/lz4 v2.0.5+incompatible h1:2xWsjqPFWcplujydGg4WmhC/6fZqK42wMM8aXeqhl0I= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e h1:aoZm08cpOy4WuID//EZDgcC4zIxODThtZNPirFr42+A= +github.com/pkg/profile v1.2.1 h1:F++O52m40owAmADcojzM+9gyjmMOY/T4oYJkgFDH8RE= +github.com/pkg/profile v1.7.0/go.mod h1:8Uer0jas47ZQMJ7VD+OHknK4YDY07LPUC6dEvqDjvNo= +github.com/pkg/sftp v1.13.1 h1:I2qBYMChEhIjOgazfJmV3/mZM256btk6wkCDRmW7JYs= +github.com/posener/complete v1.2.3 h1:NP0eAhjcjImqslEwo/1hq7gpajME0fTLTezBKDqfXqo= +github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw= github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= +github.com/pquerna/cachecontrol v0.1.0 h1:yJMy84ti9h/+OEWa752kBTKv4XC30OtVVHYv/8cTqKc= github.com/pquerna/cachecontrol v0.1.0/go.mod h1:NrUG3Z7Rdu85UNR3vm7SOsl1nFIeSiQnrHV5K9mBcUI= +github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g= +github.com/prometheus/client_model v0.6.0/go.mod h1:NTQHnmxFpouOD0DpvP4XujX3CdOAGQPoaGhyTchlyt8= +github.com/prometheus/common/assets v0.2.0 h1:0P5OrzoHrYBOSM1OigWL3mY8ZvV2N4zIE/5AahrSrfM= +github.com/prometheus/statsd_exporter v0.22.7 h1:7Pji/i2GuhK6Lu7DHrtTkFmNBCudCPT1pX2CziuyQR0= github.com/prometheus/statsd_exporter v0.22.7/go.mod h1:N/TevpjkIh9ccs6nuzY3jQn9dFqnUakOjnEuMPJJJnI= +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/rogpeppe/fastuuid v1.2.0 h1:Ppwyp6VYCF1nvBTXL3trRso7mXMlRrw9ooo375wvi2s= +github.com/rogpeppe/go-charset v0.0.0-20180617210344-2471d30d28b4 h1:BN/Nyn2nWMoqGRA7G7paDNDqTXE30mXGqzzybrfo05w= +github.com/rs/xid v1.5.0 h1:mKX4bl4iPYJtEIxp6CYiUuLQ/8DYMoz0PUdtGgMFRVc= github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= +github.com/rs/zerolog v1.15.0 h1:uPRuwkWF4J6fGsJ2R0Gn2jB1EQiav9k3S6CSdygQJXY= github.com/russross/blackfriday v1.6.0 h1:KqfZb0pUVN2lYqZUYRddxF4OR8ZMURnJIG5Y3VRLtww= +github.com/ruudk/golang-pdf417 v0.0.0-20201230142125-a7e3863a1245 h1:K1Xf3bKttbF+koVGaX5xngRIZ5bVjbmPnaxE/dR08uY= +github.com/ryanuber/columnize v2.1.2+incompatible h1:C89EOx/XBWwIXl8wm8OPJBd7kPF25UfsK2X7Ph/zCAk= +github.com/sagikazarmark/crypt v0.6.0 h1:REOEXCs/NFY/1jOCEouMuT4zEniE5YoXbvpC5X/TLF8= +github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da h1:p3Vo3i64TCLY7gIfzeQaUJ+kppEO5WQG3cL8iE8tGHU= +github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww= +github.com/savsgio/dictpool v0.0.0-20221023140959-7bf2e61cea94 h1:rmMl4fXJhKMNWl+K+r/fq4FbbKI+Ia2m9hYBLm2h4G4= github.com/savsgio/dictpool v0.0.0-20221023140959-7bf2e61cea94/go.mod h1:90zrgN3D/WJsDd1iXHT96alCoN2KJo6/4x1DZC3wZs8= +github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee h1:8Iv5m6xEo1NR1AvpV+7XmhI4r39LGNzwUL4YpMuL5vk= github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee/go.mod h1:qwtSXrKuJh/zsFQ12yEE89xfCrGKK63Rr7ctU/uCo4g= +github.com/schollz/closestmatch v2.1.0+incompatible h1:Uel2GXEpJqOWBrlyI+oY9LTiyyjYS17cCYRqP13/SHk= +github.com/schollz/closestmatch v2.1.0+incompatible/go.mod h1:RtP1ddjLong6gTkbtmuhtR2uUrrJOpYzYRvbcPAid+g= +github.com/segmentio/fasthash v0.0.0-20180216231524-a72b379d632e h1:uO75wNGioszjmIzcY/tvdDYKRLVvzggtAmmJkn9j4GQ= github.com/segmentio/fasthash v0.0.0-20180216231524-a72b379d632e/go.mod h1:tm/wZFQ8e24NYaBGIlnO2WGCAi67re4HHuOm0sftE/M= +github.com/segmentio/parquet-go v0.0.0-20230427215636-d483faba23a5 h1:7CWCjaHrXSUCHrRhIARMGDVKdB82tnPAQMmANeflKOw= github.com/segmentio/parquet-go v0.0.0-20230427215636-d483faba23a5/go.mod h1:+J0xQnJjm8DuQUHBO7t57EnmPbstT6+b45+p3DC9k1Q= +github.com/sercand/kuberesolver/v4 v4.0.0 h1:frL7laPDG/lFm5n98ODmWnn+cvPpzlkf3LhzuPhcHP4= github.com/sercand/kuberesolver/v4 v4.0.0/go.mod h1:F4RGyuRmMAjeXHKL+w4P7AwUnPceEAPAhxUgXZjKgvM= +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/shirou/gopsutil/v3 v3.23.2 h1:PAWSuiAszn7IhPMBtXsbSCafej7PqUOvY6YywlQUExU= github.com/shirou/gopsutil/v3 v3.23.2/go.mod h1:gv0aQw33GLo3pG8SiWKiQrbDzbRY1K80RyZJ7V4Th1M= +github.com/shirou/gopsutil/v3 v3.23.8/go.mod h1:7hmCaBn+2ZwaZOr6jmPBZDfawwMGuo1id3C6aM8EDqQ= +github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ= +github.com/shoenig/test v0.6.6 h1:Oe8TPH9wAbv++YPNDKJWUnI8Q4PPWCx3UbOfH+FxiMU= +github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= +github.com/sony/gobreaker v0.4.1 h1:oMnRNZXX5j85zso6xCPRNPtmAycat+WcoKbklScLDgQ= +github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spf13/afero v1.10.0/go.mod h1:UBogFpq8E9Hx+xc5CNTTEpTnuHVmXDwZcZcE1eb/UhQ= github.com/spf13/viper v1.14.0/go.mod h1:WT//axPky3FdvXHzGw33dNdXXXfFQqmEalje+egj8As= @@ -709,61 +995,125 @@ github.com/stretchr/objx v0.5.1/go.mod h1:/iHQpkQwBD6DLUmQ4pE+s1TXdob1mORJ4/UFdr github.com/subosito/gotenv v1.4.1 h1:jyEFiXpy21Wm81FBN71l9VoMMV8H8jG+qIK3GCpY6Qs= github.com/substrait-io/substrait-go v0.4.2 h1:buDnjsb3qAqTaNbOR7VKmNgXf4lYQxWEcnSGUWBtmN8= github.com/substrait-io/substrait-go v0.4.2/go.mod h1:qhpnLmrcvAnlZsUyPXZRqldiHapPTXC3t7xFgDi3aQg= +github.com/tdewolff/minify/v2 v2.12.9 h1:dvn5MtmuQ/DFMwqf5j8QhEVpPX6fi3WGImhv8RUB4zA= +github.com/tdewolff/minify/v2 v2.12.9/go.mod h1:qOqdlDfL+7v0/fyymB+OP497nIxJYSvX4MQWA8OoiXU= +github.com/tdewolff/parse/v2 v2.6.8 h1:mhNZXYCx//xG7Yq2e/kVLNZw4YfYmeHbhx+Zc0OvFMA= +github.com/tdewolff/parse/v2 v2.6.8/go.mod h1:XHDhaU6IBgsryfdnpzUXBlT6leW/l25yrFBTEb4eIyM= github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= github.com/tinylib/msgp v1.1.8/go.mod h1:qkpG+2ldGg4xRFmx+jfTvZPxfGFhi64BcnL9vkCm/Tw= +github.com/tklauser/go-sysconf v0.3.11 h1:89WgdJhk5SNwJfu+GKyYveZ4IaJ7xAkecBo+KdJV0CM= github.com/tklauser/go-sysconf v0.3.11/go.mod h1:GqXfhXY3kiPa0nAXPDIQIWzJbMCB7AmcWpGR8lSZfqI= +github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI= +github.com/tklauser/numcpus v0.6.0 h1:kebhY2Qt+3U6RNK7UqpYNA+tJ23IBEGKkB7JQBfDYms= github.com/tklauser/numcpus v0.6.0/go.mod h1:FEZLMke0lhOUG6w2JadTzp0a+Nl8PF/GFkQ5UVIcaL4= +github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY= +github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926 h1:G3dpKMzFDjgEh2q1Z7zUUtKa8ViPtH+ocF0bE0g00O8= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +github.com/uber-go/atomic v1.4.0 h1:yOuPqEq4ovnhEjpHmfFwsqBXDYbQeT6Nb0bwD6XnD5o= github.com/uber-go/atomic v1.4.0/go.mod h1:/Ct5t2lcmbJ4OSe/waGBoaVvVqtO0bmtfVNex1PFV8g= -github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo= -github.com/ugorji/go v1.2.7 h1:qYhyWUUd6WbiM+C6JZAUkIJt/1WrjzNHY9+KCIjVqTo= +github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M= +github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY= github.com/urfave/negroni v1.0.0 h1:kIimOitoypq34K7TG7DUaJ9kq/N4Ofuwi1sjz0KipXc= github.com/valyala/fasthttp v1.6.0 h1:uWF8lgKmeaIewWVPwi4GRq2P6+R46IgYZdxWtM+GtEY= github.com/valyala/fasthttp v1.47.0 h1:y7moDoxYzMooFpT5aHgNgVOQDrS3qlkfiP9mDtGGK9c= github.com/valyala/fasthttp v1.47.0/go.mod h1:k2zXd82h/7UZc3VOdJ2WaUqt1uZ/XpXAfE9i+HBC3lA= +github.com/valyala/fasthttp v1.49.0/go.mod h1:k2zXd82h/7UZc3VOdJ2WaUqt1uZ/XpXAfE9i+HBC3lA= +github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo= +github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= +github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8= github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= +github.com/vinzenz/yaml v0.0.0-20170920082545-91409cdd725d h1:3wDi6J5APMqaHBVPuVd7RmHD2gRTfqbdcVSpCNoUWtk= github.com/vinzenz/yaml v0.0.0-20170920082545-91409cdd725d/go.mod h1:mb5taDqMnJiZNRQ3+02W2IFG+oEz1+dTuCXkp4jpkfo= +github.com/vmihailenco/msgpack/v5 v5.3.5 h1:5gO0H1iULLWGhs2H5tbAHIZTV8/cYafcFOr9znI5mJU= github.com/vmihailenco/msgpack/v5 v5.3.5/go.mod h1:7xyJ9e+0+9SaZT0Wt1RGleJXzli6Q/V5KbhBonMG9jc= +github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= +github.com/weaveworks/common v0.0.0-20230511094633-334485600903 h1:ph7R2CS/0o1gBzpzK/CioUKJVsXNVXfDGR8FZ9rMZIw= github.com/weaveworks/common v0.0.0-20230511094633-334485600903/go.mod h1:rgbeLfJUtEr+G74cwFPR1k/4N0kDeaeSv/qhUNE4hm8= +github.com/weaveworks/promrus v1.2.0 h1:jOLf6pe6/vss4qGHjXmGz4oDJQA+AOCqEL3FvvZGz7M= github.com/weaveworks/promrus v1.2.0/go.mod h1:SaE82+OJ91yqjrE1rsvBWVzNZKcHYFtMUyS1+Ogs/KA= +github.com/willf/bitset v1.1.11 h1:N7Z7E9UvjW+sGsEl7k/SJrvY2reP1A07MrGuCjIOjRE= github.com/willf/bitset v1.1.11/go.mod h1:83CECat5yLh5zVOf4P1ErAgKA5UDvKtgyUABdr3+MjI= +github.com/willf/bloom v2.0.3+incompatible h1:QDacWdqcAUI1MPOwIQZRy9kOR7yxfyEmxX8Wdm2/JPA= github.com/willf/bloom v2.0.3+incompatible/go.mod h1:MmAltL9pDMNTrvUkxdg0k0q5I0suxmuwp3KbyrZLOZ8= +github.com/xanzy/go-gitlab v0.15.0 h1:rWtwKTgEnXyNUGrOArN7yyc3THRkpYcKXIXia9abywQ= +github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c= +github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY= +github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8= +github.com/xdg/scram v0.0.0-20180814205039-7eeb5667e42c h1:u40Z8hqBAAQyv+vATcGgV0YCnDjqSL7/q/JyPhhJSPk= +github.com/xdg/stringprep v1.0.0 h1:d9X0esnoa3dFsV0FG35rAT0RIhYFlPq7MiP+DW89La0= +github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= +github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= +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/yosssi/ace v0.0.5 h1:tUkIP/BLdKqrlrPwcmH0shwEEhTRHoGnc1wFIWmaBUA= +github.com/yosssi/ace v0.0.5/go.mod h1:ALfIzm2vT7t5ZE7uoIZqF3TQ7SAOyupFZnkrF5id+K0= +github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d h1:splanxYIlg+5LfHAM6xpdFEAYOk8iySO56hMFq6uLyA= +github.com/yuin/goldmark v1.4.13 h1:fVcFKWvrslecOb/tg+Cc05dkeYx540o0FuFt3nUVDoE= +github.com/yusufpapurcu/wmi v1.2.2 h1:KBNDSne4vP5mbSWnJbO+51IMOXJB67QiYCSBrubbPRg= github.com/yusufpapurcu/wmi v1.2.2/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= +github.com/yusufpapurcu/wmi v1.2.3/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= +github.com/zclconf/go-cty-debug v0.0.0-20191215020915-b22d67c1ba0b h1:FosyBZYxY34Wul7O/MSKey3txpPYyCqVO5ZyceuQJEI= github.com/zclconf/go-cty-debug v0.0.0-20191215020915-b22d67c1ba0b/go.mod h1:ZRKQfBXbGkpdV6QMzT3rU1kSTAnfu1dO8dPKjYprgj8= +github.com/zenazn/goji v1.0.1 h1:4lbD8Mx2h7IvloP7r2C0D6ltZP6Ufip8Hn0wmSK5LR8= github.com/zenazn/goji v1.0.1/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= gitlab.com/nyarla/go-crypt v0.0.0-20160106005555-d9a5dc2b789b h1:7gd+rd8P3bqcn/96gOZa3F5dpJr/vEiDQYlNb/y2uNs= go.einride.tech/aip v0.66.0 h1:XfV+NQX6L7EOYK11yoHHFtndeaWh3KbD9/cN/6iWEt8= go.einride.tech/aip v0.66.0/go.mod h1:qAhMsfT7plxBX+Oy7Huol6YUvZ0ZzdUz26yZsQwfl1M= go.opentelemetry.io/collector v0.74.0 h1:0s2DKWczGj/pLTsXGb1P+Je7dyuGx9Is4/Dri1+cS7g= go.opentelemetry.io/collector v0.74.0/go.mod h1:7NjZAvkhQ6E+NLN4EAH2hw3Nssi+F14t7mV7lMNXCto= +go.opentelemetry.io/collector/component v0.74.0 h1:W32ILPgbA5LO+m9Se61hbbtiLM6FYusNM36K5/CCOi0= go.opentelemetry.io/collector/component v0.74.0/go.mod h1:zHbWqbdmnHeIZAuO3s1Fo/kWPC2oKuolIhlPmL4bzyo= +go.opentelemetry.io/collector/confmap v0.74.0 h1:tl4fSHC/MXZiEvsZhDhd03TgzvArOe69Qn020sZsTfQ= go.opentelemetry.io/collector/confmap v0.74.0/go.mod h1:NvUhMS2v8rniLvDAnvGjYOt0qBohk6TIibb1NuyVB1Q= +go.opentelemetry.io/collector/consumer v0.74.0 h1:+kjT/ixG+4SVSHg7u9mQe0+LNDc6PuG8Wn2hoL/yGYk= go.opentelemetry.io/collector/consumer v0.74.0/go.mod h1:MuGqt8/OKVAOjrh5WHr1TR2qwHizy64ZP2uNSr+XpvI= +go.opentelemetry.io/collector/exporter v0.74.0 h1:VZxDuVz9kJM/Yten3xA/abJwLJNkxLThiao6E1ULW7c= go.opentelemetry.io/collector/exporter v0.74.0/go.mod h1:kw5YoorpKqEpZZ/a5ODSoYFK1mszzcKBNORd32S8Z7c= +go.opentelemetry.io/collector/exporter/otlpexporter v0.74.0 h1:YKvTeYcBrJwbcXNy65fJ/xytUSMurpYn/KkJD0x+DAY= go.opentelemetry.io/collector/exporter/otlpexporter v0.74.0/go.mod h1:cRbvsnpSxzySoTSnXbOGPQZu9KHlEyKkTeE21f9Q1p4= +go.opentelemetry.io/collector/featuregate v1.0.0 h1:5MGqe2v5zxaoo73BUOvUTunftX5J8RGrbFsC2Ha7N3g= +go.opentelemetry.io/collector/receiver v0.74.0 h1:jlgBFa0iByvn8VuX27UxtqiPiZE8ejmU5lb1nSptWD8= go.opentelemetry.io/collector/receiver v0.74.0/go.mod h1:SQkyATvoZCJefNkI2jnrR63SOdrmDLYCnQqXJ7ACqn0= +go.opentelemetry.io/collector/receiver/otlpreceiver v0.74.0 h1:e/X/W0z2Jtpy3Yd3CXkmEm9vSpKq/P3pKUrEVMUFBRw= go.opentelemetry.io/collector/receiver/otlpreceiver v0.74.0/go.mod h1:9X9/RYFxJIaK0JLlRZ0PpmQSSlYpY+r4KsTOj2jWj14= go.opentelemetry.io/collector/semconv v0.90.1 h1:2fkQZbefQBbIcNb9Rk1mRcWlFZgQOk7CpST1e1BK8eg= go.opentelemetry.io/contrib v0.18.0 h1:uqBh0brileIvG6luvBjdxzoFL8lxDGuhxJWsvK3BveI= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.42.0/go.mod h1:5z+/ZWJQKXa9YT34fQNx5K8Hd1EoIhvtUygUQPqEOgQ= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.46.1/go.mod h1:4UoMYEZOC0yN/sPGH76KPkkU7zgiEWYWL9vwmbnTJPE= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.47.0/go.mod h1:r9vWsPS/3AQItv3OSlEJ/E4mbrhUbbw18meOjArPtKQ= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0/go.mod h1:Mjt1i1INqiaoZOMGR1RIUJN+i3ChKoFRqzrRQhlkbs0= go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.46.1/go.mod h1:GnOaBaFQ2we3b9AGWJpsBa7v1S5RlQzlC3O7dRMxZhM= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.47.0/go.mod h1:SK2UL73Zy1quvRPonmOmRDiWk1KBV3LyIeeIxcEApWw= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0/go.mod h1:p8pYQP+m5XfbZm9fxtSKAbM6oIllS7s2AfxrChvc7iw= go.opentelemetry.io/contrib/propagators/b3 v1.15.0 h1:bMaonPyFcAvZ4EVzkUNkfnUHP5Zi63CIDlA3dRsEg8Q= go.opentelemetry.io/contrib/propagators/b3 v1.15.0/go.mod h1:VjU0g2v6HSQ+NwfifambSLAeBgevjIcqmceaKWEzl0c= go.opentelemetry.io/contrib/samplers/jaegerremote v0.16.0/go.mod h1:StxwPndBVNZD2sZez0RQ0SP/129XGCd4aEmVGaw1/QM= +go.opentelemetry.io/otel v1.19.0/go.mod h1:i0QyjOq3UPoTzff0PJB2N66fb4S0+rSbSB15/oyH9fY= go.opentelemetry.io/otel v1.22.0/go.mod h1:eoV4iAi3Ea8LkAEI9+GFT44O6T/D0GWAVFyZVCC6pMI= +go.opentelemetry.io/otel v1.24.0/go.mod h1:W7b9Ozg4nkF5tWI5zsXkaKKDjdVjpD4oAt9Qi/MArHo= go.opentelemetry.io/otel/bridge/opencensus v0.37.0 h1:ieH3gw7b1eg90ARsFAlAsX5LKVZgnCYfaDwRrK6xLHU= go.opentelemetry.io/otel/bridge/opencensus v0.37.0/go.mod h1:ddiK+1PE68l/Xk04BGTh9Y6WIcxcLrmcVxVlS0w5WZ0= +go.opentelemetry.io/otel/bridge/opentracing v1.10.0 h1:WzAVGovpC1s7KD5g4taU6BWYZP3QGSDVTlbRu9fIHw8= go.opentelemetry.io/otel/bridge/opentracing v1.10.0/go.mod h1:J7GLR/uxxqMAzZptsH0pjte3Ep4GacTCrbGBoDuHBqk= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.21.0 h1:digkEZCJWobwBqMwC0cwCq8/wkkRy/OowZg5OArWZrM= +go.opentelemetry.io/otel/exporters/prometheus v0.37.0 h1:NQc0epfL0xItsmGgSXgfbH2C1fq2VLXkZoDFsfRNHpc= go.opentelemetry.io/otel/exporters/prometheus v0.37.0/go.mod h1:hB8qWjsStK36t50/R0V2ULFb4u95X/Q6zupXLgvjTh8= +go.opentelemetry.io/otel/metric v1.19.0/go.mod h1:L5rUsV9kM1IxCj1MmSdS+JQAcVm319EUrDVLrt7jqt8= go.opentelemetry.io/otel/metric v1.22.0/go.mod h1:evJGjVpZv0mQ5QBRJoBF64yMuOf4xCWdXjK8pzFvliY= +go.opentelemetry.io/otel/metric v1.24.0/go.mod h1:VYhLe1rFfxuTXLgj4CBiyz+9WYBA8pNGJgDcSFRKBco= +go.opentelemetry.io/otel/sdk v1.19.0/go.mod h1:NedEbbS4w3C6zElbLdPJKOpJQOrGUJ+GfzpjUvI0v1A= go.opentelemetry.io/otel/sdk v1.22.0/go.mod h1:iu7luyVGYovrRpe2fmj3CVKouQNdTOkxtLzPvPz1DOc= go.opentelemetry.io/otel/sdk/metric v0.39.0 h1:Kun8i1eYf48kHH83RucG93ffz0zGV1sh46FAScOTuDI= go.opentelemetry.io/otel/sdk/metric v0.39.0/go.mod h1:piDIRgjcK7u0HCL5pCA4e74qpK/jk3NiUoAHATVAmiI= +go.opentelemetry.io/otel/trace v1.19.0/go.mod h1:mfaSyvGyEJEI0nyV2I4qhNQnbBOUUmYZpYojqMnX2vo= go.opentelemetry.io/otel/trace v1.22.0/go.mod h1:RbbHXVqKES9QhzZq/fE5UnOSILqRt40a21sPw2He1xo= +go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU= go.uber.org/atomic v1.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= go.uber.org/automaxprocs v1.5.3 h1:kWazyxZUrS3Gs4qUpbwo5kEIMGe/DAvi5Z4tl2NW4j8= go.uber.org/goleak v1.2.0/go.mod h1:XJYK+MuIchqpmGmUSAzotztawfKvYLUIgg7guXrwVUo= @@ -772,34 +1122,111 @@ go.uber.org/mock v0.2.0/go.mod h1:J0y0rp9L3xiff1+ZBfKxlC1fz2+aO16tw0tsDOixfuM= go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee h1:0mgffUl7nfd+FpvXMVz4IDEaUSmT1ysygQC7qYo7sG4= go.uber.org/zap v1.19.0/go.mod h1:xg/QME4nWcxGxrpdeYfq7UvYrLh66cuVKdrbD1XF/NI= +golang.org/x/arch v0.4.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= +golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e/go.mod h1:Kr81I6Kryrl9sr8s2FK3vxD90NdsKWRuOIl2O4CvYbA= +golang.org/x/exp v0.0.0-20230206171751-46f607a40771/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= +golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w= golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo= +golang.org/x/image v0.0.0-20220302094943-723b81ca9867 h1:TcHcE0vrmgzNH1v3ppjcMGbhG5+9fMuvOmUYwNEF4q4= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028 h1:4+4C/Iv2U4fMZBiMCc98MG1In4gJY5YRhtpDNeDeHWs= golang.org/x/mod v0.13.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= +golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= +golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= 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/sys v0.0.0-20220406163625-3f8b81556e12/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/telemetry v0.0.0-20240208230135-b75ee8823808 h1:+Kc94D8UVEVxJnLXp/+FMfqQARZtWHfVrcRtcG8aT3g= +golang.org/x/telemetry v0.0.0-20240208230135-b75ee8823808/go.mod h1:KG1lNk5ZFNssSZLrpVb4sMXKMpGwGXOxSG3rnu2gZQQ= +golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58= golang.org/x/tools v0.12.0/go.mod h1:Sc0INKfu04TlqNoRA1hgpFZbhYXHPr4V5DzpSBTPqQM= golang.org/x/tools v0.14.0/go.mod h1:uYBEerGOWcJyEORxN+Ek8+TT266gXkNlHdJBwexUsBg= golang.org/x/tools v0.16.1/go.mod h1:kYVVN6I1mBNoB1OX+noeBjbRk4IUEPa7JJ+TJMEooJ0= 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/api v0.150.0/go.mod h1:ccy+MJ6nrYFgE3WgRx/AMXOxOmU8Q4hSa+jjibzhxcg= +google.golang.org/api v0.155.0/go.mod h1:GI5qK5f40kCpHfPn6+YzGAByIKWv8ujFnmoWm7Igduk= +google.golang.org/api v0.157.0/go.mod h1:+z4v4ufbZ1WEpld6yMGHyggs+PmAHiaLNj5ytP3N01g= +google.golang.org/api v0.160.0/go.mod h1:0mu0TpK33qnydLvWqbImq2b1eQ5FHRSDCBzAxX9ZHyw= +google.golang.org/api v0.162.0/go.mod h1:6SulDkfoBIg4NFmCuZ39XeeAgSHCPecfSUuDyYlAHs0= +google.golang.org/api v0.169.0/go.mod h1:gpNOiMA2tZ4mf5R9Iwf4rK/Dcz0fbdIgWYWVoxmsyLg= +google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM= +google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds= +google.golang.org/genproto v0.0.0-20231211222908-989df2bf70f3/go.mod h1:5RBcpGRxr25RbDzY5w+dmaqpSEvl8Gwl1x2CICf60ic= +google.golang.org/genproto v0.0.0-20231212172506-995d672761c0/go.mod h1:l/k7rMz0vFTBPy+tFSGvXEd3z+BcoG1k7EHbqm+YBsY= +google.golang.org/genproto v0.0.0-20240102182953-50ed04b92917/go.mod h1:pZqR+glSb11aJ+JQcczCvgf47+duRuzNSKqE8YAQnV0= +google.golang.org/genproto v0.0.0-20240116215550-a9fa1716bcac/go.mod h1:+Rvu7ElI+aLzyDQhpHMFMMltsD6m7nqpuWDd2CwJw3k= google.golang.org/genproto v0.0.0-20240125205218-1f4bbc51befe/go.mod h1:cc8bqMqtv9gMOr0zHg2Vzff5ULhhL2IXP4sbcn32Dro= +google.golang.org/genproto v0.0.0-20240205150955-31a09d347014/go.mod h1:xEgQu1e4stdSSsxPDK8Azkrk/ECl5HvdPf6nbZrTS5M= google.golang.org/genproto v0.0.0-20240213162025-012b6fc9bca9/go.mod h1:mqHbVIp48Muh7Ywss/AD6I5kNVKZMmAa/QEW58Gxp2s= +google.golang.org/genproto/googleapis/api v0.0.0-20231120223509-83a465c0220f/go.mod h1:Uy9bTZJqmfrw2rIBxgGLnamc78euZULUBrLZ9XTITKI= +google.golang.org/genproto/googleapis/api v0.0.0-20231211222908-989df2bf70f3/go.mod h1:k2dtGpRrbsSyKcNPKKI5sstZkrNCZwpU/ns96JoHbGg= +google.golang.org/genproto/googleapis/api v0.0.0-20231212172506-995d672761c0/go.mod h1:CAny0tYF+0/9rmDB9fahA9YLzX3+AEVl1qXbv5hhj6c= +google.golang.org/genproto/googleapis/api v0.0.0-20240102182953-50ed04b92917/go.mod h1:CmlNWB9lSezaYELKS5Ym1r44VrrbPUa7JTvw+6MbpJ0= +google.golang.org/genproto/googleapis/api v0.0.0-20240116215550-a9fa1716bcac/go.mod h1:B5xPO//w8qmBDjGReYLpR6UJPnkldGkCSMoH/2vxJeg= +google.golang.org/genproto/googleapis/api v0.0.0-20240122161410-6c6643bf1457/go.mod h1:4jWUdICTdgc3Ibxmr8nAJiiLHwQBY0UI0XZcEMaFKaA= +google.golang.org/genproto/googleapis/api v0.0.0-20240125205218-1f4bbc51befe/go.mod h1:4jWUdICTdgc3Ibxmr8nAJiiLHwQBY0UI0XZcEMaFKaA= google.golang.org/genproto/googleapis/api v0.0.0-20240205150955-31a09d347014/go.mod h1:rbHMSEDyoYX62nRVLOCc4Qt1HbsdytAYoVwgjiOhF3I= google.golang.org/genproto/googleapis/api v0.0.0-20240213162025-012b6fc9bca9/go.mod h1:PVreiBMirk8ypES6aw9d4p6iiBNSIfZEBqr3UGoAi2E= +google.golang.org/genproto/googleapis/api v0.0.0-20240227224415-6ceb2ff114de/go.mod h1:5iCWqnniDlqZHrd3neWVTOwvh/v6s3232omMecelax8= +google.golang.org/genproto/googleapis/api v0.0.0-20240311132316-a219d84964c2/go.mod h1:O1cOfN1Cy6QEYr7VxtjOyP5AdAuR0aJ/MYZaaof623Y= google.golang.org/genproto/googleapis/bytestream v0.0.0-20231120223509-83a465c0220f h1:hL+1ptbhFoeL1HcROQ8OGXaqH0jYRRibgWQWco0/Ugc= google.golang.org/genproto/googleapis/bytestream v0.0.0-20231212172506-995d672761c0 h1:Y6QQt9D/syZt/Qgnz5a1y2O3WunQeeVDfS9+Xr82iFA= +google.golang.org/genproto/googleapis/bytestream v0.0.0-20231212172506-995d672761c0/go.mod h1:guYXGPwC6jwxgWKW5Y405fKWOFNwlvUlUnzyp9i0uqo= +google.golang.org/genproto/googleapis/bytestream v0.0.0-20240116215550-a9fa1716bcac/go.mod h1:ZSvZ8l+AWJwXw91DoTjWjaVLpWU6o0eZ4YLYpH8aLeQ= google.golang.org/genproto/googleapis/bytestream v0.0.0-20240125205218-1f4bbc51befe h1:weYsP+dNijSQVoLAb5bpUos3ciBpNU/NEVlHFKrk8pg= google.golang.org/genproto/googleapis/bytestream v0.0.0-20240125205218-1f4bbc51befe/go.mod h1:SCz6T5xjNXM4QFPRwxHcfChp7V+9DcXR3ay2TkHR8Tg= +google.golang.org/genproto/googleapis/bytestream v0.0.0-20240325203815-454cdb8f5daa h1:wBkzraZsSqhj1M4L/nMrljUU6XasJkgHvUsq8oRGwF0= +google.golang.org/genproto/googleapis/bytestream v0.0.0-20240325203815-454cdb8f5daa/go.mod h1:IN9OQUXZ0xT+26MDwZL8fJcYw+y99b0eYPA2U15Jt8o= +google.golang.org/genproto/googleapis/rpc v0.0.0-20231211222908-989df2bf70f3/go.mod h1:eJVxU6o+4G1PSczBr85xmyvSNYAKvAYgkub40YGomFM= +google.golang.org/genproto/googleapis/rpc v0.0.0-20231212172506-995d672761c0/go.mod h1:FUoWkonphQm3RhTS+kOEhF8h0iDpm4tdXolVCeZ9KKA= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240102182953-50ed04b92917/go.mod h1:xtjpI3tXFPP051KaWnhvxkiubL/6dJ18vLVf7q2pTOU= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240116215550-a9fa1716bcac/go.mod h1:daQN87bsDqDoe316QbbvX60nMoJQa4r6Ds0ZuoAe5yA= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240122161410-6c6643bf1457/go.mod h1:PAREbraiVEVGVdTZsVWjSbbTtSyGbAgIIvni8a8CD5s= google.golang.org/genproto/googleapis/rpc v0.0.0-20240125205218-1f4bbc51befe/go.mod h1:PAREbraiVEVGVdTZsVWjSbbTtSyGbAgIIvni8a8CD5s= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240205150955-31a09d347014/go.mod h1:SaPjaZGWb0lPqs6Ittu0spdfrOArqji4ZdeP5IC/9N4= google.golang.org/genproto/googleapis/rpc v0.0.0-20240213162025-012b6fc9bca9/go.mod h1:YUWgXUFRPfoYK1IHMuxH5K6nPEXSCzIMljnQ59lLRCk= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240227224415-6ceb2ff114de/go.mod h1:H4O17MA/PE9BsGx3w+a+W2VOLLD1Qf7oJneAoU6WktY= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240228224816-df926f6c8641/go.mod h1:UCOku4NytXMJuLQE5VuqA5lX3PcHCBo8pxNyvkf4xBs= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240311132316-a219d84964c2/go.mod h1:UCOku4NytXMJuLQE5VuqA5lX3PcHCBo8pxNyvkf4xBs= +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/grpc v1.60.0/go.mod h1:OlCHIeLYqSSsLi6i49B5QGdzaMZK9+M7LXN2FKz4eGM= +google.golang.org/grpc v1.60.1/go.mod h1:OlCHIeLYqSSsLi6i49B5QGdzaMZK9+M7LXN2FKz4eGM= google.golang.org/grpc v1.61.0/go.mod h1:VUbo7IFqmF1QtCAstipjG0GIoq49KvMe9+h1jFLBNJs= +google.golang.org/grpc v1.61.1/go.mod h1:VUbo7IFqmF1QtCAstipjG0GIoq49KvMe9+h1jFLBNJs= +google.golang.org/grpc v1.62.0/go.mod h1:IWTG0VlJLCh1SkC58F7np9ka9mx/WNkjl4PGJaiq+QE= +google.golang.org/grpc v1.63.0/go.mod h1:WAX/8DgncnokcFUldAxq7GeB5DXHDbMF+lLvDomNkRA= +google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0 h1:M1YKkFIboKNieVO5DLUEVzQfGwJD30Nv2jfUgzb5UcE= +google.golang.org/protobuf v1.28.2-0.20230222093303-bc1253ad3743/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +gopkg.in/alecthomas/kingpin.v2 v2.2.6 h1:jMFz6MfLP0/4fUyZle81rXUoxOBFi19VUFKVDOQfozc= +gopkg.in/cheggaaa/pb.v1 v1.0.25 h1:Ev7yu1/f6+d+b3pi5vPdRPc6nNtP1umSfcWiEfRqv6I= +gopkg.in/errgo.v2 v2.1.0 h1:0vLT13EuvQ0hNvakwLuFZ/jYrLp5F3kcWHXdRggjCE8= +gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= +gopkg.in/gcfg.v1 v1.2.3 h1:m8OOJ4ccYHnx2f4gQwpno8nAX5OGOh7RLaaz0pj3Ogs= +gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec h1:RlWgLqCMMIYYEVcAR5MDsuHlVkaIPDAF+5Dehzg8L5A= +gopkg.in/resty.v1 v1.12.0 h1:CuXP0Pjfw9rOuY6EP+UvtNvt5DSqHpIxILZKT/quCZI= +gopkg.in/square/go-jose.v2 v2.6.0 h1:NGk74WTnPKBNUhNzQX7PYcTLUjoq7mzKk2OKbvwk2iI= gopkg.in/square/go-jose.v2 v2.6.0/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= +gopkg.in/src-d/go-billy.v4 v4.3.2 h1:0SQA1pRztfTFx2miS8sA97XvooFeNOmvUenF4o0EcVg= gopkg.in/src-d/go-billy.v4 v4.3.2/go.mod h1:nDjArDMp+XMs1aFAESLRjfGSgfvoYN0hDfzEk0GjC98= +k8s.io/component-base v0.0.0-20240417101527-62c04b35eff6 h1:WN8Lymy+dCTDHgn4vhUSNIB6U+0sDiv/c9Zdr0UeAnI= +k8s.io/component-base v0.0.0-20240417101527-62c04b35eff6/go.mod h1:l0ukbPS0lwFxOzSq5ZqjutzF+5IL2TLp495PswRPSZk= k8s.io/gengo v0.0.0-20230829151522-9cce18d56c01/go.mod h1:FiNAH4ZV3gBg2Kwh89tzAEV2be7d5xI0vBa/VySYy3E= +k8s.io/gengo/v2 v2.0.0-20240228010128-51d4e06bde70/go.mod h1:VH3AT8AaQOqiGjMF9p0/IM1Dj+82ZwjfxUP1IxaHE+8= k8s.io/kms v0.29.0/go.mod h1:mB0f9HLxRXeXUfHfn1A7rpwOlzXI1gIWu86z6buNoYA= k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00/go.mod h1:AsvuZPBlUDVuCdzJ87iajxtXuR9oktsTctW/R9wwouA= k8s.io/kube-openapi v0.0.0-20231214164306-ab13479f8bf8/go.mod h1:AsvuZPBlUDVuCdzJ87iajxtXuR9oktsTctW/R9wwouA= +k8s.io/kube-openapi v0.0.0-20240220201932-37d671a357a5/go.mod h1:Pa1PvrP7ACSkuX6I7KYomY6cmMA0Tx86waBhDUgoKPw= +modernc.org/cc/v3 v3.38.1/go.mod h1:vtL+3mdHx/wcj3iEGz84rQa8vEqR6XM84v5Lcvfph20= +modernc.org/ccgo/v3 v3.0.0-20220910160915-348f15de615a/go.mod h1:8p47QxPkdugex9J4n9P2tLZ9bK01yngIVp00g4nomW0= +modernc.org/libc v1.19.0/go.mod h1:ZRfIaEkgrYgZDl6pa4W39HgN5G/yDW+NRmNKZBDFrk0= +modernc.org/libc v1.21.2/go.mod h1:przBsL5RDOZajTVslkugzLBj1evTue36jEomFQOoYuI= nhooyr.io/websocket v1.8.7 h1:usjR2uOr/zjjkVMy0lW+PPohFok7PCow5sDjLgX4P4g= rsc.io/binaryregexp v0.2.0 h1:HfqmD5MEmC0zvwBuF187nq9mdnXjXsSivRiXN7SmRkE= rsc.io/pdf v0.1.1 h1:k1MczvYDUvJBe93bYd7wrZLLUEcLZAuF824/I4e5Xr4= diff --git a/lefthook.yml b/lefthook.yml index 9c3bed5f538..26077d9fb44 100644 --- a/lefthook.yml +++ b/lefthook.yml @@ -23,11 +23,6 @@ pre-commit: yarn prettier --write {staged_files} stage_fixed: true - internationalization: - glob: 'public/locales/en-US/grafana.json' - run: yarn i18n:pseudo - stage_fixed: true - other-format: glob: '*.{json,scss,md,mdx}' run: yarn prettier --write {staged_files} diff --git a/package.json b/package.json index 5d68c993123..8e3a4fb9219 100644 --- a/package.json +++ b/package.json @@ -66,6 +66,8 @@ "releaseNotesUrl": "https://grafana.com/docs/grafana/next/release-notes/" }, "devDependencies": { + "@babel/core": "7.24.4", + "@babel/preset-env": "7.24.4", "@babel/runtime": "7.24.4", "@betterer/betterer": "5.4.0", "@betterer/cli": "5.4.0", @@ -74,7 +76,7 @@ "@emotion/eslint-plugin": "11.11.0", "@grafana/eslint-config": "7.0.0", "@grafana/eslint-plugin": "link:./packages/grafana-eslint-rules", - "@grafana/plugin-e2e": "1.1.1", + "@grafana/plugin-e2e": "1.2.0", "@grafana/tsconfig": "^1.3.0-rc1", "@manypkg/get-packages": "^2.2.0", "@playwright/test": "1.43.1", @@ -93,6 +95,8 @@ "@testing-library/user-event": "14.5.2", "@types/angular": "1.8.9", "@types/angular-route": "1.7.6", + "@types/babel__core": "^7", + "@types/babel__preset-env": "^7", "@types/chance": "^1.1.3", "@types/common-tags": "^1.8.0", "@types/d3": "7.4.3", @@ -107,7 +111,6 @@ "@types/google.analytics": "^0.0.46", "@types/gtag.js": "^0.0.19", "@types/history": "4.7.11", - "@types/hoist-non-react-statics": "3.3.5", "@types/jest": "29.5.12", "@types/jquery": "3.5.29", "@types/js-yaml": "^4.0.5", @@ -117,9 +120,7 @@ "@types/lucene": "^2", "@types/mousetrap": "1.6.15", "@types/node": "20.12.7", - "@types/node-forge": "^1", "@types/ol-ext": "npm:@siedlerchr/types-ol-ext@3.2.4", - "@types/papaparse": "5.3.14", "@types/pluralize": "^0.0.33", "@types/prismjs": "1.26.3", "@types/react": "18.2.79", @@ -127,6 +128,7 @@ "@types/react-dom": "18.2.25", "@types/react-grid-layout": "1.3.5", "@types/react-highlight-words": "0.16.7", + "@types/react-resizable": "3.0.7", "@types/react-router": "5.1.20", "@types/react-router-dom": "5.3.3", "@types/react-table": "7.7.20", @@ -145,28 +147,30 @@ "@types/tinycolor2": "1.4.6", "@types/uuid": "9.0.8", "@types/webpack-assets-manifest": "^5", + "@types/webpack-env": "^1.18.4", "@types/yargs": "17.0.32", "@typescript-eslint/eslint-plugin": "6.21.0", "@typescript-eslint/parser": "6.21.0", "autoprefixer": "10.4.19", + "babel-loader": "9.1.3", "blob-polyfill": "7.0.20220408", "browserslist": "^4.21.4", "chance": "^1.0.10", "chrome-remote-interface": "0.33.0", "codeowners": "^5.1.1", "copy-webpack-plugin": "12.0.2", - "core-js": "3.36.1", - "css-loader": "6.10.0", + "core-js": "3.37.0", + "css-loader": "7.1.1", "css-minimizer-webpack-plugin": "6.0.0", "cypress": "13.1.0", "cypress-file-upload": "5.0.8", - "esbuild": "0.20.1", + "esbuild": "0.20.2", "esbuild-loader": "4.1.0", "esbuild-plugin-browserslist": "^0.11.0", "eslint": "8.57.0", "eslint-config-prettier": "9.1.0", "eslint-plugin-import": "^2.26.0", - "eslint-plugin-jest": "27.9.0", + "eslint-plugin-jest": "28.2.0", "eslint-plugin-jsdoc": "48.2.3", "eslint-plugin-jsx-a11y": "6.8.0", "eslint-plugin-lodash": "7.4.0", @@ -190,8 +194,9 @@ "jest-junit": "16.0.0", "jest-matcher-utils": "29.7.0", "jest-watch-typeahead": "^2.2.2", + "knip": "^5.10.0", "lerna": "8.1.2", - "mini-css-extract-plugin": "2.8.1", + "mini-css-extract-plugin": "2.9.0", "msw": "2.2.14", "mutationobserver-shim": "0.3.7", "ngtemplate-loader": "2.1.0", @@ -204,14 +209,13 @@ "prettier": "3.2.5", "react-refresh": "0.14.0", "react-select-event": "5.5.1", - "react-simple-compat": "1.2.3", "react-test-renderer": "18.2.0", "redux-mock-store": "1.5.4", "rimraf": "5.0.5", "rudder-sdk-js": "2.48.6", "sass": "1.75.0", "sass-loader": "14.2.1", - "style-loader": "3.3.4", + "style-loader": "4.0.0", "stylelint": "16.3.1", "stylelint-config-sass-guidelines": "11.1.0", "terser-webpack-plugin": "5.3.10", @@ -232,7 +236,6 @@ "yargs": "^17.5.1" }, "dependencies": { - "@daybrush/utils": "1.13.0", "@emotion/css": "11.11.2", "@emotion/react": "11.11.4", "@fingerprintjs/fingerprintjs": "^3.4.2", @@ -252,7 +255,7 @@ "@grafana/prometheus": "workspace:*", "@grafana/runtime": "workspace:*", "@grafana/saga-icons": "workspace:*", - "@grafana/scenes": "^4.10.0", + "@grafana/scenes": "^4.13.0", "@grafana/schema": "workspace:*", "@grafana/sql": "workspace:*", "@grafana/ui": "workspace:*", @@ -275,18 +278,12 @@ "@react-aria/focus": "3.16.2", "@react-aria/overlays": "3.21.1", "@react-aria/utils": "3.23.2", - "@react-awesome-query-builder/core": "6.4.2", "@react-awesome-query-builder/ui": "6.4.2", "@reduxjs/toolkit": "1.9.5", - "@remix-run/router": "^1.5.0", "@testing-library/react-hooks": "^8.0.1", - "@types/react-resizable": "3.0.7", - "@types/trusted-types": "2.0.7", - "@types/webpack-env": "1.18.4", "@visx/event": "3.3.0", "@visx/gradient": "3.3.0", "@visx/group": "3.3.0", - "@visx/scale": "3.5.0", "@visx/shape": "3.5.0", "@visx/tooltip": "3.3.0", "@welldone-software/why-did-you-render": "8.0.1", @@ -297,7 +294,6 @@ "ansicolor": "1.1.100", "baron": "3.0.3", "brace": "0.11.1", - "calculate-size": "1.1.1", "centrifuge": "5.1.1", "classnames": "2.5.1", "combokeys": "^3.0.0", @@ -310,14 +306,10 @@ "date-fns": "3.6.0", "debounce-promise": "3.1.2", "diff": "^5.1.0", - "emotion": "11.0.0", - "eventemitter3": "5.0.1", "fast-deep-equal": "^3.1.3", "fast-json-patch": "3.1.1", "file-saver": "2.0.5", - "framework-utils": "^1.1.0", "history": "4.10.1", - "hoist-non-react-statics": "3.3.2", "i18next": "^23.0.0", "i18next-browser-languagedetector": "^7.0.2", "immer": "10.0.4", @@ -335,29 +327,21 @@ "lru-memoize": "^1.1.0", "lucene": "^2.1.1", "marked": "12.0.2", - "marked-mangle": "1.1.7", "memoize-one": "6.0.0", "ml-regression-polynomial": "^3.0.0", "ml-regression-simple-linear": "^3.0.0", "moment": "2.30.1", "moment-timezone": "0.5.45", "monaco-editor": "0.34.1", - "monaco-promql": "1.7.4", "mousetrap": "1.6.5", "mousetrap-global-bind": "1.1.0", "moveable": "0.53.0", "nanoid": "^5.0.4", - "node-forge": "^1.3.1", "ol": "7.4.0", "ol-ext": "4.0.18", - "papaparse": "5.4.1", "pluralize": "^8.0.0", "prismjs": "1.29.0", - "prop-types": "15.8.1", - "pseudoizer": "^0.1.0", - "rc-cascader": "3.24.1", - "rc-drawer": "7.1.0", - "rc-slider": "10.5.0", + "rc-slider": "10.6.2", "rc-time-picker": "3.7.3", "rc-tree": "5.8.5", "re-resizable": "6.9.14", @@ -370,7 +354,7 @@ "react-grid-layout": "1.4.4", "react-highlight-words": "0.20.0", "react-hook-form": "^7.49.2", - "react-i18next": "^12.0.0", + "react-i18next": "^14.0.0", "react-inlinesvg": "3.0.2", "react-loading-skeleton": "3.4.0", "react-moveable": "0.56.0", @@ -411,7 +395,7 @@ "uuid": "9.0.1", "visjs-network": "4.25.0", "whatwg-fetch": "3.6.20", - "xlsx": "https://cdn.sheetjs.com/xlsx-0.19.1/xlsx-0.19.1.tgz" + "xlsx": "https://cdn.sheetjs.com/xlsx-0.20.2/xlsx-0.20.2.tgz" }, "resolutions": { "underscore": "1.13.6", diff --git a/packages/grafana-data/.eslintrc b/packages/grafana-data/.eslintrc index 41e607e143b..a9902770dcf 100644 --- a/packages/grafana-data/.eslintrc +++ b/packages/grafana-data/.eslintrc @@ -1,13 +1,13 @@ { "rules": { - "no-restricted-imports": ["error", { "patterns": ["@grafana/runtime", "@grafana/ui", "@grafana/data", "@grafana/e2e/*"] }] + "no-restricted-imports": ["error", { "patterns": ["@grafana/runtime", "@grafana/ui", "@grafana/data"] }], }, "overrides": [ { "files": ["**/*.test.{ts,tsx}"], "rules": { - "no-restricted-imports": "off" - } - } - ] + "no-restricted-imports": "off", + }, + }, + ], } diff --git a/packages/grafana-data/package.json b/packages/grafana-data/package.json index 25a26d743b0..11f2c7e58c2 100644 --- a/packages/grafana-data/package.json +++ b/packages/grafana-data/package.json @@ -71,7 +71,7 @@ "@types/react": "18.2.79", "@types/react-dom": "18.2.25", "@types/tinycolor2": "1.4.6", - "esbuild": "0.18.12", + "esbuild": "0.20.2", "react": "18.2.0", "react-dom": "18.2.0", "rimraf": "5.0.5", diff --git a/packages/grafana-data/src/datetime/moment_wrapper.ts b/packages/grafana-data/src/datetime/moment_wrapper.ts index 0b97de1a3bf..6b60bb524bd 100644 --- a/packages/grafana-data/src/datetime/moment_wrapper.ts +++ b/packages/grafana-data/src/datetime/moment_wrapper.ts @@ -53,7 +53,7 @@ export interface DateTimeDuration { export interface DateTime extends Object { add: (amount?: DateTimeInput, unit?: DurationUnit) => DateTime; - set: (unit: DurationUnit, amount: DateTimeInput) => void; + set: (unit: DurationUnit | 'date', amount: DateTimeInput) => void; diff: (amount: DateTimeInput, unit?: DurationUnit, truncate?: boolean) => number; endOf: (unitOfTime: DurationUnit) => DateTime; format: (formatInput?: FormatInput) => string; diff --git a/packages/grafana-data/src/themes/createTransitions.ts b/packages/grafana-data/src/themes/createTransitions.ts index d15b821aba5..723978b1741 100644 --- a/packages/grafana-data/src/themes/createTransitions.ts +++ b/packages/grafana-data/src/themes/createTransitions.ts @@ -53,6 +53,12 @@ export function create(props: string | string[] = ['all'], options: CreateTransi .join(','); } +type ReducedMotionProps = 'no-preference' | 'reduce'; + +export function handleMotion(...props: ReducedMotionProps[]) { + return props.map((prop) => `@media (prefers-reduced-motion: ${prop})`).join(','); +} + export function getAutoHeightDuration(height: number) { if (!height) { return 0; @@ -74,6 +80,7 @@ export interface ThemeTransitions { duration: typeof duration; easing: typeof easing; getAutoHeightDuration: typeof getAutoHeightDuration; + handleMotion: typeof handleMotion; } /** @internal */ @@ -83,5 +90,6 @@ export function createTransitions(): ThemeTransitions { duration, easing, getAutoHeightDuration, + handleMotion, }; } diff --git a/packages/grafana-data/src/themes/zIndex.ts b/packages/grafana-data/src/themes/zIndex.ts index 093c0ef93d4..1423bbaf664 100644 --- a/packages/grafana-data/src/themes/zIndex.ts +++ b/packages/grafana-data/src/themes/zIndex.ts @@ -9,7 +9,6 @@ export const zIndex = { tooltip: 1040, modalBackdrop: 1050, modal: 1060, - tooltipWithinModal: 1060, portal: 1061, }; diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index 63ff19f663e..7fbc72bfdf3 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -179,4 +179,6 @@ export interface FeatureToggles { cloudWatchNewLabelParsing?: boolean; accessActionSets?: boolean; disableNumericMetricsSortingInExpressions?: boolean; + grafanaManagedRecordingRules?: boolean; + queryLibrary?: boolean; } diff --git a/packages/grafana-data/src/types/pluginExtensions.ts b/packages/grafana-data/src/types/pluginExtensions.ts index e7a26223c34..c9ed2b0506d 100644 --- a/packages/grafana-data/src/types/pluginExtensions.ts +++ b/packages/grafana-data/src/types/pluginExtensions.ts @@ -116,6 +116,9 @@ export type PluginExtensionEventHelpers = { // Extension Points available in core Grafana export enum PluginExtensionPoints { AlertInstanceAction = 'grafana/alerting/instance/action', + AlertingHomePage = 'grafana/alerting/home', + AlertingAlertingRuleAction = 'grafana/alerting/alertingrule/action', + AlertingRecordingRuleAction = 'grafana/alerting/recordingrule/action', CommandPalette = 'grafana/commandpalette/action', DashboardPanelMenu = 'grafana/dashboard/panel/menu', DataSourceConfig = 'grafana/datasources/config', diff --git a/packages/grafana-data/src/types/time.ts b/packages/grafana-data/src/types/time.ts index edc92429ca6..f3eeefac9ce 100644 --- a/packages/grafana-data/src/types/time.ts +++ b/packages/grafana-data/src/types/time.ts @@ -86,3 +86,20 @@ export function getDefaultRelativeTimeRange(): RelativeTimeRange { to: 0, }; } + +/** + * Simple helper to quickly create a TimeRange object either from string representations of a dateTime or directly + * DateTime objects. + */ +export function makeTimeRange(from: DateTime | string, to: DateTime | string): TimeRange { + const fromDateTime = typeof from === 'string' ? dateTime(from) : from; + const toDateTime = typeof to === 'string' ? dateTime(to) : to; + return { + from: fromDateTime, + to: toDateTime, + raw: { + from: fromDateTime, + to: toDateTime, + }, + }; +} diff --git a/packages/grafana-data/src/valueFormats/symbolFormatters.test.ts b/packages/grafana-data/src/valueFormats/symbolFormatters.test.ts index 05273473e84..e5a926333fb 100644 --- a/packages/grafana-data/src/valueFormats/symbolFormatters.test.ts +++ b/packages/grafana-data/src/valueFormats/symbolFormatters.test.ts @@ -63,6 +63,12 @@ describe('SIPrefix', () => { it.each` value | expectedSuffix | expectedText ${0} | ${' V'} | ${'0'} + ${0.000000000001} | ${' pV'} | ${'1'} + ${0.000000001} | ${' nV'} | ${'1'} + ${0.000001} | ${' µV'} | ${'1'} + ${0.001} | ${' mV'} | ${'1'} + ${0.999} | ${' mV'} | ${'999'} + ${1} | ${' V'} | ${'1'} ${999} | ${' V'} | ${'999'} ${1000} | ${' kV'} | ${'1'} ${1000000} | ${' MV'} | ${'1'} diff --git a/packages/grafana-e2e-selectors/package.json b/packages/grafana-e2e-selectors/package.json index ae0a68fd669..035b22c0a85 100644 --- a/packages/grafana-e2e-selectors/package.json +++ b/packages/grafana-e2e-selectors/package.json @@ -41,7 +41,7 @@ "devDependencies": { "@rollup/plugin-node-resolve": "15.2.3", "@types/node": "20.12.7", - "esbuild": "0.18.12", + "esbuild": "0.20.2", "rimraf": "5.0.5", "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 ccfef3f8388..9d73f68f3ff 100644 --- a/packages/grafana-e2e-selectors/src/selectors/components.ts +++ b/packages/grafana-e2e-selectors/src/selectors/components.ts @@ -218,22 +218,22 @@ export const Components = { }, PanelEditor: { General: { - content: 'Panel editor content', + content: 'data-testid Panel editor content', }, OptionsPane: { - content: 'Panel editor option pane content', + content: 'data-testid Panel editor option pane content', select: 'Panel editor option pane select', fieldLabel: (type: string) => `${type} field property editor`, fieldInput: (title: string) => `data-testid Panel editor option pane field input ${title}`, }, // not sure about the naming *DataPane* DataPane: { - content: 'Panel editor data pane content', + content: 'data-testid Panel editor data pane content', }, applyButton: 'data-testid Apply changes and go back to dashboard', toggleVizPicker: 'data-testid toggle-viz-picker', toggleVizOptions: 'data-testid toggle-viz-options', - toggleTableView: 'toggle-table-view', + toggleTableView: 'data-testid toggle-table-view', // [Geomap] Map controls showZoomField: 'Map controls Show zoom control field property editor', @@ -252,7 +252,7 @@ export const Components = { content: 'Panel inspector Stats content', }, Json: { - content: 'Panel inspector Json content', + content: 'data-testid Panel inspector Json content', }, Query: { content: 'Panel inspector Query content', @@ -300,6 +300,15 @@ export const Components = { groupToggle: 'data-testid group-collapse-toggle', toggle: 'data-testid collapse-toggle', expandedContent: 'data-testid expanded-content', + previewButton: 'data-testid alert-rule preview-button', + ruleNameField: 'data-testid alert-rule name-field', + newFolderButton: 'data-testid alert-rule new-folder-button', + newFolderNameField: 'data-testid alert-rule name-folder-name-field', + newFolderNameCreateButton: 'data-testid alert-rule name-folder-name-create-button', + newEvaluationGroupButton: 'data-testid alert-rule new-evaluation-group-button', + newEvaluationGroupName: 'data-testid alert-rule new-evaluation-group-name', + newEvaluationGroupInterval: 'data-testid alert-rule new-evaluation-group-interval', + newEvaluationGroupCreate: 'data-testid alert-rule new-evaluation-group-create-button', }, Alert: { /** @@ -377,8 +386,9 @@ export const Components = { backArrow: 'data-testid Go Back', }, OptionsGroup: { - group: (title?: string) => (title ? `Options group ${title}` : 'Options group'), - toggle: (title?: string) => (title ? `Options group ${title} toggle` : 'Options group toggle'), + group: (title?: string) => (title ? `data-testid Options group ${title}` : 'data-testid Options group'), + toggle: (title?: string) => + title ? `data-testid Options group ${title} toggle` : 'data-testid Options group toggle', }, PluginVisualization: { item: (title: string) => `Plugin visualization item ${title}`, @@ -442,6 +452,7 @@ export const Components = { QueryBuilder: { queryPatterns: 'data-testid Query patterns', labelSelect: 'data-testid Select label', + inputSelect: 'data-testid Select label-input', valueSelect: 'data-testid Select value', matchOperatorSelect: 'data-testid Select match operator', }, @@ -535,8 +546,8 @@ export const Components = { variableOption: 'data-testid variable-option', }, Annotations: { - annotationsTypeInput: 'annotations-type-input', - annotationsChoosePanelInput: 'choose-panels-input', + annotationsTypeInput: 'data-testid annotations-type-input', + annotationsChoosePanelInput: 'data-testid choose-panels-input', editor: { testButton: 'data-testid annotations-test-button', resultContainer: 'data-testid annotations-query-result-container', diff --git a/packages/grafana-e2e-selectors/src/selectors/pages.ts b/packages/grafana-e2e-selectors/src/selectors/pages.ts index 3ae6d8dc99c..fabdda9e0b8 100644 --- a/packages/grafana-e2e-selectors/src/selectors/pages.ts +++ b/packages/grafana-e2e-selectors/src/selectors/pages.ts @@ -43,9 +43,9 @@ export const Pages = { AddDashboard: { url: '/dashboard/new', itemButton: (title: string) => `data-testid ${title}`, - addNewPanel: 'Add new panel', - addNewRow: 'Add new row', - addNewPanelLibrary: 'Add new panel from panel library', + addNewPanel: 'data-testid Add new panel', + addNewRow: 'data-testid Add new row', + addNewPanelLibrary: 'data-testid Add new panel from panel library', }, Dashboard: { url: (uid: string) => `/d/${uid}`, @@ -83,7 +83,7 @@ export const Pages = { close: 'data-testid dashboard-settings-close', }, General: { - deleteDashBoard: 'Dashboard settings page delete dashboard button', + deleteDashBoard: 'data-testid Dashboard settings page delete dashboard button', sectionItems: (item: string) => `Dashboard settings section item ${item}`, saveDashBoard: 'Dashboard settings aside actions Save button', saveAsDashBoard: 'Dashboard settings aside actions Save As button', @@ -103,11 +103,11 @@ export const Pages = { annotations: 'data-testid list-annotations', }, Settings: { - name: 'Annotations settings name input', + name: 'data-testid Annotations settings name input', }, NewAnnotation: { panelFilterSelect: 'data-testid annotations-panel-filter', - showInLabel: 'show-in-label', + showInLabel: 'data-testid show-in-label', previewInDashboard: 'data-testid annotations-preview', delete: 'data-testid annotations-delete', apply: 'data-testid annotations-apply', diff --git a/packages/grafana-e2e/.gitignore b/packages/grafana-e2e/.gitignore deleted file mode 100644 index 9770e5ad330..00000000000 --- a/packages/grafana-e2e/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -test/cypress/report.json -test/cypress/screenshots/actual -test/cypress/videos/ diff --git a/packages/grafana-e2e/CHANGELOG.md b/packages/grafana-e2e/CHANGELOG.md deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/packages/grafana-e2e/LICENSE_APACHE2 b/packages/grafana-e2e/LICENSE_APACHE2 deleted file mode 100644 index 373dde574a0..00000000000 --- a/packages/grafana-e2e/LICENSE_APACHE2 +++ /dev/null @@ -1,202 +0,0 @@ - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright 2015 Grafana Labs - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. diff --git a/packages/grafana-e2e/README.md b/packages/grafana-e2e/README.md deleted file mode 100644 index a488db87725..00000000000 --- a/packages/grafana-e2e/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# Grafana End-to-End Test library - -> [!CAUTION] -> This package is deprecated. -> If you'd like to write end-to-end tests for a Grafana plugin (core or external), use the [`@grafana/plugin-e2e`](https://grafana.com/developers/plugin-tools/e2e-test-a-plugin/introduction) package. diff --git a/packages/grafana-e2e/bin/grafana-e2e.js b/packages/grafana-e2e/bin/grafana-e2e.js deleted file mode 100755 index e2aefb16e00..00000000000 --- a/packages/grafana-e2e/bin/grafana-e2e.js +++ /dev/null @@ -1,3 +0,0 @@ -#!/usr/bin/env node - -require('../cli')(); diff --git a/packages/grafana-e2e/cli.js b/packages/grafana-e2e/cli.js deleted file mode 100644 index b35338a9195..00000000000 --- a/packages/grafana-e2e/cli.js +++ /dev/null @@ -1,57 +0,0 @@ -const { program } = require('commander'); -const execa = require('execa'); -const { resolve, sep } = require('path'); -const resolveBin = require('resolve-bin'); - -const cypress = (commandName, { updateScreenshots, browser }) => { - // Support running an unpublished dev build - const dirname = __dirname.split(sep).pop(); - const projectPath = resolve(`${__dirname}${dirname === 'dist' ? '/..' : ''}`); - - // For plugins/extendConfig - const CWD = `CWD=${process.cwd()}`; - - // For plugins/compareSnapshots - const UPDATE_SCREENSHOTS = `UPDATE_SCREENSHOTS=${updateScreenshots ? 1 : 0}`; - - const cypressOptions = [commandName, '--env', `${CWD},${UPDATE_SCREENSHOTS}`, `--project=${projectPath}`]; - - if (browser) { - cypressOptions.push('--browser', browser); - } - - const execaOptions = { - cwd: __dirname, - stdio: 'inherit', - }; - - return execa(resolveBin.sync('cypress'), cypressOptions, execaOptions) - .then(() => {}) // no return value - .catch((error) => { - console.error(error.message); - process.exitCode = 1; - }); -}; - -module.exports = () => { - const updateOption = '-u, --update-screenshots'; - const updateDescription = 'update expected screenshots'; - const browserOption = '-b, --browser '; - const browserDescription = 'specify which browser to use'; - - program - .command('open') - .description('runs tests within the interactive GUI') - .option(updateOption, updateDescription) - .option(browserOption, browserDescription) - .action((options) => cypress('open', options)); - - program - .command('run') - .description('runs tests from the CLI without the GUI') - .option(updateOption, updateDescription) - .option(browserOption, browserDescription) - .action((options) => cypress('run', options)); - - program.parse(process.argv); -}; diff --git a/packages/grafana-e2e/cypress.json b/packages/grafana-e2e/cypress.json deleted file mode 100644 index 7eafaf4bf2b..00000000000 --- a/packages/grafana-e2e/cypress.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "projectId": "zb7k1c", - "supportFile": "cypress/support/index.ts", - "videoCompression": 20, - "viewportWidth": 1920, - "viewportHeight": 1080 -} diff --git a/packages/grafana-e2e/cypress/fixtures/example.json b/packages/grafana-e2e/cypress/fixtures/example.json deleted file mode 100644 index 02e4254378e..00000000000 --- a/packages/grafana-e2e/cypress/fixtures/example.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "name": "Using fixtures to represent data", - "email": "hello@cypress.io", - "body": "Fixtures are a great way to mock data for responses to routes" -} diff --git a/packages/grafana-e2e/cypress/fixtures/exemplars-query-response.json b/packages/grafana-e2e/cypress/fixtures/exemplars-query-response.json deleted file mode 100644 index 6c26a9fbf7d..00000000000 --- a/packages/grafana-e2e/cypress/fixtures/exemplars-query-response.json +++ /dev/null @@ -1,323 +0,0 @@ -{ - "results": { - "A": { - "frames": [ - { - "schema": { - "name": "histogram_quantile(0.95, sum(rate(tns_request_duration_seconds_bucket[5m])) by (le))", - "refId": "A", - "meta": { "custom": { "resultType": "matrix" } }, - "fields": [ - { "name": "Time", "type": "time", "typeInfo": { "frame": "time.Time" } }, - { - "name": "Value", - "type": "number", - "typeInfo": { "frame": "float64" }, - "labels": {}, - "config": { - "displayNameFromDS": "histogram_quantile(0.95, sum(rate(tns_request_duration_seconds_bucket[5m])) by (le))" - } - } - ] - }, - "data": { - "values": [ - [ - 1633619595000, 1633619610000, 1633619625000, 1633619640000, 1633619655000, 1633619670000, 1633619685000, - 1633619700000, 1633619715000, 1633619730000, 1633619745000, 1633619760000, 1633619775000, 1633619790000, - 1633619805000, 1633619820000, 1633619835000, 1633619850000, 1633619865000, 1633619880000, 1633619895000 - ], - [ - 0.07245212135073513, 0.07253198890830721, 0.07247862573797707, 0.07238248338231042, 0.07221687487740913, - 0.07223291298743946, 0.07225427016727755, 0.024531677091864545, 0.02317081920915543, - 0.07548902139580993, 0.0777721702857508, 0.07768649905047344, 0.07782257603228229, 0.07788810213200052, - 0.07791835055437593, 0.07798387201529966, 0.07790826751849372, 0.07794858648610933, 0.07778729925797964, - 0.07769657495236215, 0.077550401329267 - ] - ] - } - }, - { - "schema": { - "name": "histogram_quantile(0.95, sum(rate(tns_request_duration_seconds_bucket[5m])) by (le))", - "refId": "A", - "meta": { "custom": { "resultType": "vector" } }, - "fields": [ - { "name": "Time", "type": "time", "typeInfo": { "frame": "time.Time" } }, - { - "name": "Value", - "type": "number", - "typeInfo": { "frame": "float64" }, - "labels": {}, - "config": { - "displayNameFromDS": "histogram_quantile(0.95, sum(rate(tns_request_duration_seconds_bucket[5m])) by (le))" - } - } - ] - }, - "data": { "values": [[1633619900000], [0.0775504013292671]] } - }, - { - "schema": { - "name": "exemplar", - "refId": "A", - "meta": { "custom": { "resultType": "exemplar" } }, - "fields": [ - { "name": "Time", "type": "time", "typeInfo": { "frame": "time.Time" } }, - { "name": "Value", "type": "number", "typeInfo": { "frame": "float64" } }, - { "name": "instance", "type": "string", "typeInfo": { "frame": "string" } }, - { "name": "__name__", "type": "string", "typeInfo": { "frame": "string" } }, - { "name": "job", "type": "string", "typeInfo": { "frame": "string" } }, - { "name": "status_code", "type": "string", "typeInfo": { "frame": "string" } }, - { "name": "method", "type": "string", "typeInfo": { "frame": "string" } }, - { "name": "traceID", "type": "string", "typeInfo": { "frame": "string" } }, - { "name": "route", "type": "string", "typeInfo": { "frame": "string" } }, - { "name": "ws", "type": "string", "typeInfo": { "frame": "string" } }, - { "name": "le", "type": "string", "typeInfo": { "frame": "string" } } - ] - }, - "data": { - "values": [ - [ - 1633619598000, 1633619622000, 1633619625000, 1633619646000, 1633619658000, 1633619682000, 1633619695000, - 1633619712000, 1633619712000, 1633619724000, 1633619717000, 1633619742000, 1633619757000, 1633619771000, - 1633619784000, 1633619801000, 1633619806000, 1633619833000, 1633619833000, 1633619845000, 1633619862000, - 1633619877000, 1633619889000 - ], - [ - 0.0146153, 0.0118506, 0.0473847, 0.026997, 0.0164318, 0.0113532, 0.0105197, 0.162789, 0.0556026, - 0.148856, 0.0433809, 0.0117758, 0.0114496, 0.0114099, 0.0421927, 0.0134148, 0.0152827, 0.6975967, - 0.0394788, 0.0137441, 0.0110939, 0.0104496, 0.0101284 - ], - [ - "app:80", - "app:80", - "app:80", - "app:80", - "app:80", - "app:80", - "app:80", - "app:80", - "app:80", - "app:80", - "db:80", - "app:80", - "app:80", - "app:80", - "app:80", - "app:80", - "app:80", - "app:80", - "app:80", - "app:80", - "app:80", - "app:80", - "app:80" - ], - [ - "tns_request_duration_seconds_bucket", - "tns_request_duration_seconds_bucket", - "tns_request_duration_seconds_bucket", - "tns_request_duration_seconds_bucket", - "tns_request_duration_seconds_bucket", - "tns_request_duration_seconds_bucket", - "tns_request_duration_seconds_bucket", - "tns_request_duration_seconds_bucket", - "tns_request_duration_seconds_bucket", - "tns_request_duration_seconds_bucket", - "tns_request_duration_seconds_bucket", - "tns_request_duration_seconds_bucket", - "tns_request_duration_seconds_bucket", - "tns_request_duration_seconds_bucket", - "tns_request_duration_seconds_bucket", - "tns_request_duration_seconds_bucket", - "tns_request_duration_seconds_bucket", - "tns_request_duration_seconds_bucket", - "tns_request_duration_seconds_bucket", - "tns_request_duration_seconds_bucket", - "tns_request_duration_seconds_bucket", - "tns_request_duration_seconds_bucket", - "tns_request_duration_seconds_bucket" - ], - [ - "tns/app", - "tns/app", - "tns/app", - "tns/app", - "tns/app", - "tns/app", - "tns/app", - "tns/app", - "tns/app", - "tns/app", - "tns/db", - "tns/app", - "tns/app", - "tns/app", - "tns/app", - "tns/app", - "tns/app", - "tns/app", - "tns/app", - "tns/app", - "tns/app", - "tns/app", - "tns/app" - ], - [ - "302", - "200", - "200", - "200", - "200", - "200", - "200", - "500", - "200", - "302", - "208", - "200", - "200", - "200", - "200", - "200", - "302", - "200", - "200", - "200", - "200", - "200", - "200" - ], - [ - "POST", - "GET", - "GET", - "GET", - "GET", - "GET", - "GET", - "GET", - "GET", - "POST", - "POST", - "GET", - "GET", - "GET", - "GET", - "GET", - "POST", - "GET", - "GET", - "GET", - "GET", - "GET", - "GET" - ], - [ - "6a3cf561ef6c32a0", - "396bcdf29601a149", - "57c04ef608f11158", - "77c757dab83c665f", - "3d1069567e873f5e", - "b337949f6213efd", - "21b20cbe533cf099", - "2c10b3aa30fabd66", - "42ac6088a757636b", - "2f81158008cd4dcc", - "320b803ad7323b37", - "7f15fd82aeb8b361", - "11c79266da8a74cd", - "5a8571bdcc04c990", - "3de3f4f42ccb93ae", - "23343ac91cc0638", - "5cea3aad17ab11c8", - "5d334e2843d3405a", - "3cf6834596d4b6b6", - "1ab6cff012959723", - "2f78bc2c398b8b20", - "6d5862a70c3abd42", - "f5421be4054f501" - ], - [ - "post", - "root", - "root", - "root", - "root", - "root", - "root", - "root", - "root", - "post", - "post", - "root", - "root", - "root", - "root", - "root", - "post", - "metrics", - "root", - "root", - "root", - "root", - "root" - ], - [ - "false", - "false", - "false", - "false", - "false", - "false", - "false", - "false", - "false", - "false", - "false", - "false", - "false", - "false", - "false", - "false", - "false", - "false", - "false", - "false", - "false", - "false", - "false" - ], - [ - "0.025", - "0.025", - "0.05", - "0.05", - "0.025", - "0.025", - "0.025", - "0.25", - "0.1", - "0.25", - "0.05", - "0.025", - "0.025", - "0.025", - "0.05", - "0.025", - "0.025", - "1.0", - "0.05", - "0.025", - "0.025", - "0.025", - "0.025" - ] - ] - } - } - ] - } - } -} diff --git a/packages/grafana-e2e/cypress/fixtures/long-trace-response.json b/packages/grafana-e2e/cypress/fixtures/long-trace-response.json deleted file mode 100644 index 80955535638..00000000000 --- a/packages/grafana-e2e/cypress/fixtures/long-trace-response.json +++ /dev/null @@ -1,7592 +0,0 @@ -{ - "data": [ - { - "traceID": "3fa414edcef6ad90", - "spans": [ - { - "traceID": "3fa414edcef6ad90", - "spanID": "1b26effbab24e95a", - "operationName": "FindTraceByID", - "references": [], - "startTime": 1605873894680581, - "duration": 1820, - "tags": [ - { "key": "component", "type": "string", "value": "gRPC" }, - { "key": "span.kind", "type": "string", "value": "client" } - ], - "logs": [], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "0f5c1808567e4403", - "operationName": "FindTraceByID", - "references": [], - "startTime": 1605873894680587, - "duration": 1847, - "tags": [ - { "key": "component", "type": "string", "value": "gRPC" }, - { "key": "span.kind", "type": "string", "value": "client" } - ], - "logs": [], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "59f093577238d61e", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894683862, - "duration": 10204, - "tags": [], - "logs": [ - { "timestamp": 1605873894683872, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, - { "timestamp": 1605873894694063, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "1cc731490b1da4c5", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894683858, - "duration": 10257, - "tags": [ - { "key": "organization", "type": "string", "value": "1" }, - { "key": "span.kind", "type": "string", "value": "client" } - ], - "logs": [], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "602204dc8b8fbc6d", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894683201, - "duration": 11185, - "tags": [], - "logs": [ - { "timestamp": 1605873894683207, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, - { "timestamp": 1605873894694385, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "586e5e4c0400de11", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894683196, - "duration": 11200, - "tags": [ - { "key": "organization", "type": "string", "value": "1" }, - { "key": "span.kind", "type": "string", "value": "client" } - ], - "logs": [], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "779ac3811ce65e40", - "operationName": "block.Find", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894683844, - "duration": 10983, - "tags": [ - { "key": "blockID", "type": "string", "value": "20a16df1-a312-4b1a-a2e2-33b55e9f3c8b" }, - { "key": "shardKey", "type": "int64", "value": 5 } - ], - "logs": [ - { - "timestamp": 1605873894694822, - "fields": [ - { "key": "bytes", "type": "int64", "value": 315664 }, - { "key": "msg", "type": "string", "value": "bloom" } - ] - } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "24203526fe09b1e2", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894682997, - "duration": 12453, - "tags": [], - "logs": [ - { "timestamp": 1605873894683002, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, - { "timestamp": 1605873894695448, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "0afe9ad5f5b01be7", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894682993, - "duration": 12466, - "tags": [ - { "key": "organization", "type": "string", "value": "1" }, - { "key": "span.kind", "type": "string", "value": "client" } - ], - "logs": [], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "51413d67348a4624", - "operationName": "block.Find", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894682986, - "duration": 13059, - "tags": [ - { "key": "blockID", "type": "string", "value": "08b90b09-c56e-4b4a-b95f-3f0409dc9ce9" }, - { "key": "shardKey", "type": "int64", "value": 5 } - ], - "logs": [ - { - "timestamp": 1605873894695963, - "fields": [ - { "key": "bytes", "type": "int64", "value": 239824 }, - { "key": "msg", "type": "string", "value": "bloom" } - ] - } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "60007a76ffde4644", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894682866, - "duration": 13279, - "tags": [], - "logs": [ - { "timestamp": 1605873894682872, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, - { "timestamp": 1605873894696144, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "09d7a8c1faef5a84", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894682861, - "duration": 13291, - "tags": [ - { "key": "organization", "type": "string", "value": "1" }, - { "key": "span.kind", "type": "string", "value": "client" } - ], - "logs": [], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "2755efbbfb1b537b", - "operationName": "block.Find", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894682846, - "duration": 14054, - "tags": [ - { "key": "blockID", "type": "string", "value": "f78b0397-d3ad-4514-9bf4-87b6ea7e920e" }, - { "key": "shardKey", "type": "int64", "value": 5 } - ], - "logs": [ - { - "timestamp": 1605873894696898, - "fields": [ - { "key": "bytes", "type": "int64", "value": 218440 }, - { "key": "msg", "type": "string", "value": "bloom" } - ] - } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "25223420e121413a", - "operationName": "block.Find", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894683188, - "duration": 14278, - "tags": [ - { "key": "blockID", "type": "string", "value": "3ae22086-9266-481a-9725-c921471e4a94" }, - { "key": "shardKey", "type": "int64", "value": 5 } - ], - "logs": [ - { - "timestamp": 1605873894697462, - "fields": [ - { "key": "bytes", "type": "int64", "value": 397880 }, - { "key": "msg", "type": "string", "value": "bloom" } - ] - } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "17a3baf85848a727", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894683030, - "duration": 14724, - "tags": [], - "logs": [ - { "timestamp": 1605873894683033, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, - { "timestamp": 1605873894697752, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "46ebfa6c443776c4", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894683027, - "duration": 14734, - "tags": [ - { "key": "organization", "type": "string", "value": "1" }, - { "key": "span.kind", "type": "string", "value": "client" } - ], - "logs": [], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "19b1afe02cf639cf", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894683883, - "duration": 14279, - "tags": [], - "logs": [ - { "timestamp": 1605873894683889, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, - { "timestamp": 1605873894698160, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "6e5a7dd55283f907", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894683879, - "duration": 14289, - "tags": [ - { "key": "organization", "type": "string", "value": "1" }, - { "key": "span.kind", "type": "string", "value": "client" } - ], - "logs": [], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "5085badf0c1dc842", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894683657, - "duration": 14886, - "tags": [], - "logs": [ - { "timestamp": 1605873894683663, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, - { "timestamp": 1605873894698542, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "71a0e94722b662ed", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894683653, - "duration": 14897, - "tags": [ - { "key": "organization", "type": "string", "value": "1" }, - { "key": "span.kind", "type": "string", "value": "client" } - ], - "logs": [], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "57e69d8f17b39563", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894683388, - "duration": 15548, - "tags": [], - "logs": [ - { "timestamp": 1605873894683394, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, - { "timestamp": 1605873894698936, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "6fe636103f47e1fc", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894683384, - "duration": 15558, - "tags": [ - { "key": "organization", "type": "string", "value": "1" }, - { "key": "span.kind", "type": "string", "value": "client" } - ], - "logs": [], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "52146a5c1b2c0030", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894683284, - "duration": 15701, - "tags": [], - "logs": [ - { "timestamp": 1605873894683290, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, - { "timestamp": 1605873894698984, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "160fb4c8329a2ea0", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894683280, - "duration": 15712, - "tags": [ - { "key": "organization", "type": "string", "value": "1" }, - { "key": "span.kind", "type": "string", "value": "client" } - ], - "logs": [], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "1e283fe0dd8cc773", - "operationName": "block.Find", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894683024, - "duration": 16029, - "tags": [ - { "key": "blockID", "type": "string", "value": "9e102b4e-115a-4bda-abd6-aa6221f9e4b7" }, - { "key": "shardKey", "type": "int64", "value": 5 } - ], - "logs": [ - { - "timestamp": 1605873894699050, - "fields": [ - { "key": "bytes", "type": "int64", "value": 395808 }, - { "key": "msg", "type": "string", "value": "bloom" } - ] - } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "1bae5c35dd7187ba", - "operationName": "block.Find", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894683644, - "duration": 15612, - "tags": [ - { "key": "blockID", "type": "string", "value": "b2f5a951-19a0-473d-8830-e1120ab7bf25" }, - { "key": "shardKey", "type": "int64", "value": 5 } - ], - "logs": [ - { - "timestamp": 1605873894699255, - "fields": [ - { "key": "bytes", "type": "int64", "value": 345992 }, - { "key": "msg", "type": "string", "value": "bloom" } - ] - } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "5af2c497b60703d9", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894683842, - "duration": 15628, - "tags": [], - "logs": [ - { "timestamp": 1605873894683848, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, - { "timestamp": 1605873894699469, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "6a64d382dd0239a7", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894683837, - "duration": 15639, - "tags": [ - { "key": "organization", "type": "string", "value": "1" }, - { "key": "span.kind", "type": "string", "value": "client" } - ], - "logs": [], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "04652166eaec115c", - "operationName": "block.Find", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894683378, - "duration": 16179, - "tags": [ - { "key": "blockID", "type": "string", "value": "30903640-5e8c-4cf6-9dc8-f84e0e2541c8" }, - { "key": "shardKey", "type": "int64", "value": 5 } - ], - "logs": [ - { - "timestamp": 1605873894699555, - "fields": [ - { "key": "bytes", "type": "int64", "value": 291056 }, - { "key": "msg", "type": "string", "value": "bloom" } - ] - } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "650c7f5ec8cc53a5", - "operationName": "block.Find", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894683871, - "duration": 15807, - "tags": [ - { "key": "blockID", "type": "string", "value": "19b49abb-e17a-4632-a4b9-3ce95208e3cf" }, - { "key": "shardKey", "type": "int64", "value": 5 } - ], - "logs": [ - { - "timestamp": 1605873894699675, - "fields": [ - { "key": "bytes", "type": "int64", "value": 424248 }, - { "key": "msg", "type": "string", "value": "bloom" } - ] - } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "1b30323ce39314b9", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894684553, - "duration": 15144, - "tags": [], - "logs": [ - { "timestamp": 1605873894684559, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, - { "timestamp": 1605873894699696, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "288816ad36c9020c", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894684549, - "duration": 15154, - "tags": [ - { "key": "organization", "type": "string", "value": "1" }, - { "key": "span.kind", "type": "string", "value": "client" } - ], - "logs": [], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "26e83a54365218ad", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894683881, - "duration": 16602, - "tags": [], - "logs": [ - { "timestamp": 1605873894683888, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, - { "timestamp": 1605873894700482, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "5e6a2e62081720fd", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894683878, - "duration": 16613, - "tags": [ - { "key": "organization", "type": "string", "value": "1" }, - { "key": "span.kind", "type": "string", "value": "client" } - ], - "logs": [], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "63332243ceed106c", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894683893, - "duration": 16666, - "tags": [], - "logs": [ - { "timestamp": 1605873894683900, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, - { "timestamp": 1605873894700557, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "7dbbbda52a6d32ce", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894683888, - "duration": 16678, - "tags": [ - { "key": "organization", "type": "string", "value": "1" }, - { "key": "span.kind", "type": "string", "value": "client" } - ], - "logs": [], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "195ed27075e44238", - "operationName": "block.Find", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894683828, - "duration": 16766, - "tags": [ - { "key": "blockID", "type": "string", "value": "6c5d1290-2b4b-4f33-9798-63b6654e16b4" }, - { "key": "shardKey", "type": "int64", "value": 5 } - ], - "logs": [ - { - "timestamp": 1605873894700591, - "fields": [ - { "key": "bytes", "type": "int64", "value": 367848 }, - { "key": "msg", "type": "string", "value": "bloom" } - ] - } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "35e5a12a53c6088a", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894682748, - "duration": 17901, - "tags": [], - "logs": [ - { "timestamp": 1605873894682751, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, - { "timestamp": 1605873894700647, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "690fcd8c8dc87ae8", - "operationName": "block.Find", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894683273, - "duration": 17376, - "tags": [ - { "key": "blockID", "type": "string", "value": "f1db0c64-befe-4790-af19-7b48e57a9558" }, - { "key": "shardKey", "type": "int64", "value": 5 } - ], - "logs": [ - { - "timestamp": 1605873894700646, - "fields": [ - { "key": "bytes", "type": "int64", "value": 386448 }, - { "key": "msg", "type": "string", "value": "bloom" } - ] - } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "113befce4abfecb2", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894682745, - "duration": 17911, - "tags": [ - { "key": "organization", "type": "string", "value": "1" }, - { "key": "span.kind", "type": "string", "value": "client" } - ], - "logs": [], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "277870fa55872b13", - "operationName": "block.Find", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894683865, - "duration": 17440, - "tags": [ - { "key": "blockID", "type": "string", "value": "f05f1d13-0250-492a-abc8-bca24ccf3a15" }, - { "key": "shardKey", "type": "int64", "value": 5 } - ], - "logs": [ - { - "timestamp": 1605873894701291, - "fields": [ - { "key": "bytes", "type": "int64", "value": 211672 }, - { "key": "msg", "type": "string", "value": "bloom" } - ] - } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "022b6c95374f166d", - "operationName": "block.Find", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894683879, - "duration": 17471, - "tags": [ - { "key": "blockID", "type": "string", "value": "0cba7eaf-2546-41ac-99d7-673ef23d6e98" }, - { "key": "shardKey", "type": "int64", "value": 5 } - ], - "logs": [ - { - "timestamp": 1605873894701347, - "fields": [ - { "key": "bytes", "type": "int64", "value": 406456 }, - { "key": "msg", "type": "string", "value": "bloom" } - ] - } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "6cee3530fc730d34", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894684631, - "duration": 16733, - "tags": [], - "logs": [ - { "timestamp": 1605873894684639, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, - { "timestamp": 1605873894701363, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "674b435291a256c4", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894684627, - "duration": 16745, - "tags": [ - { "key": "organization", "type": "string", "value": "1" }, - { "key": "span.kind", "type": "string", "value": "client" } - ], - "logs": [], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "1de85b574e5d906c", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894683373, - "duration": 18328, - "tags": [], - "logs": [ - { "timestamp": 1605873894683380, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, - { "timestamp": 1605873894701701, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "4c5ac8757f9888b7", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894683369, - "duration": 18338, - "tags": [ - { "key": "organization", "type": "string", "value": "1" }, - { "key": "span.kind", "type": "string", "value": "client" } - ], - "logs": [], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "3e5ab83b57207c74", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894683042, - "duration": 18823, - "tags": [], - "logs": [ - { "timestamp": 1605873894683045, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, - { "timestamp": 1605873894701863, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "7d9927e5c258d511", - "operationName": "block.Find", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894682730, - "duration": 19136, - "tags": [ - { "key": "blockID", "type": "string", "value": "794e2adc-701e-4c2d-907a-66221b4455d3" }, - { "key": "shardKey", "type": "int64", "value": 5 } - ], - "logs": [ - { - "timestamp": 1605873894701864, - "fields": [ - { "key": "bytes", "type": "int64", "value": 289928 }, - { "key": "msg", "type": "string", "value": "bloom" } - ] - } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "6c9178ed1e68f858", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894683039, - "duration": 18834, - "tags": [ - { "key": "organization", "type": "string", "value": "1" }, - { "key": "span.kind", "type": "string", "value": "client" } - ], - "logs": [], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "445d4f3f2dc4d0ad", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894684214, - "duration": 17919, - "tags": [], - "logs": [ - { "timestamp": 1605873894684221, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, - { "timestamp": 1605873894702132, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "30dd998b2082f2b9", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894684210, - "duration": 17958, - "tags": [ - { "key": "organization", "type": "string", "value": "1" }, - { "key": "span.kind", "type": "string", "value": "client" } - ], - "logs": [], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "2ff9bbb6c991a0ea", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894683877, - "duration": 18301, - "tags": [], - "logs": [ - { "timestamp": 1605873894683883, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, - { "timestamp": 1605873894702177, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "310a2399bb07e8bd", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894683873, - "duration": 18311, - "tags": [ - { "key": "organization", "type": "string", "value": "1" }, - { "key": "span.kind", "type": "string", "value": "client" } - ], - "logs": [], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "19021bbbe6310785", - "operationName": "block.Find", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894683360, - "duration": 18978, - "tags": [ - { "key": "blockID", "type": "string", "value": "d2212e62-5b1a-41e2-ae43-c0a596125f1b" }, - { "key": "shardKey", "type": "int64", "value": 5 } - ], - "logs": [ - { - "timestamp": 1605873894702335, - "fields": [ - { "key": "bytes", "type": "int64", "value": 199208 }, - { "key": "msg", "type": "string", "value": "bloom" } - ] - } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "021f72c9979124b5", - "operationName": "block.Find", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894684621, - "duration": 17816, - "tags": [ - { "key": "blockID", "type": "string", "value": "06ebaf3b-4501-4cda-91fb-c48a9d33a99c" }, - { "key": "shardKey", "type": "int64", "value": 5 } - ], - "logs": [ - { - "timestamp": 1605873894702434, - "fields": [ - { "key": "bytes", "type": "int64", "value": 384696 }, - { "key": "msg", "type": "string", "value": "bloom" } - ] - } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "68a1e78424019eb9", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894683657, - "duration": 18900, - "tags": [], - "logs": [ - { "timestamp": 1605873894683663, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, - { "timestamp": 1605873894702556, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "244e73561d0c691d", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894683653, - "duration": 18910, - "tags": [ - { "key": "organization", "type": "string", "value": "1" }, - { "key": "span.kind", "type": "string", "value": "client" } - ], - "logs": [], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "5c1d1b2d38dddcfb", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894683518, - "duration": 19222, - "tags": [], - "logs": [ - { "timestamp": 1605873894683526, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, - { "timestamp": 1605873894702739, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "364583eecf36b543", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894683513, - "duration": 19232, - "tags": [ - { "key": "organization", "type": "string", "value": "1" }, - { "key": "span.kind", "type": "string", "value": "client" } - ], - "logs": [], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "22e42286de359dc4", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894683843, - "duration": 18969, - "tags": [], - "logs": [ - { "timestamp": 1605873894683849, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, - { "timestamp": 1605873894702812, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "7b936283fac4d0ac", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894683838, - "duration": 18981, - "tags": [ - { "key": "organization", "type": "string", "value": "1" }, - { "key": "span.kind", "type": "string", "value": "client" } - ], - "logs": [], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "660886869edd36cf", - "operationName": "block.Find", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894684202, - "duration": 18627, - "tags": [ - { "key": "blockID", "type": "string", "value": "f07137b8-7a0b-4199-b1a7-6b7d5b230723" }, - { "key": "shardKey", "type": "int64", "value": 5 } - ], - "logs": [ - { - "timestamp": 1605873894702824, - "fields": [ - { "key": "bytes", "type": "int64", "value": 293936 }, - { "key": "msg", "type": "string", "value": "bloom" } - ] - } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "57ed8902af3a60b5", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894683549, - "duration": 19426, - "tags": [], - "logs": [ - { "timestamp": 1605873894683554, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, - { "timestamp": 1605873894702972, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "64cadcdb4f18b2f7", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894683544, - "duration": 19437, - "tags": [ - { "key": "organization", "type": "string", "value": "1" }, - { "key": "span.kind", "type": "string", "value": "client" } - ], - "logs": [], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "25f434fb5960aaef", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894683933, - "duration": 19303, - "tags": [], - "logs": [ - { "timestamp": 1605873894683939, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, - { "timestamp": 1605873894703235, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "62afac560d435620", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894683929, - "duration": 19314, - "tags": [ - { "key": "organization", "type": "string", "value": "1" }, - { "key": "span.kind", "type": "string", "value": "client" } - ], - "logs": [], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "58435ec74d79cc93", - "operationName": "block.Find", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894683865, - "duration": 19469, - "tags": [ - { "key": "blockID", "type": "string", "value": "f2a53e6e-e261-4ec2-92bd-97c5a4c4b760" }, - { "key": "shardKey", "type": "int64", "value": 5 } - ], - "logs": [ - { - "timestamp": 1605873894703331, - "fields": [ - { "key": "bytes", "type": "int64", "value": 390648 }, - { "key": "msg", "type": "string", "value": "bloom" } - ] - } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "578849d0d44400b5", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894684004, - "duration": 19335, - "tags": [], - "logs": [ - { "timestamp": 1605873894684012, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, - { "timestamp": 1605873894703337, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "1f5faebfb90378ad", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894683999, - "duration": 19346, - "tags": [ - { "key": "organization", "type": "string", "value": "1" }, - { "key": "span.kind", "type": "string", "value": "client" } - ], - "logs": [], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "41f1eb48b61ef185", - "operationName": "block.Find", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894683035, - "duration": 20463, - "tags": [ - { "key": "blockID", "type": "string", "value": "941a63d4-2739-4ba2-9a15-08256b5c9eae" }, - { "key": "shardKey", "type": "int64", "value": 5 } - ], - "logs": [ - { - "timestamp": 1605873894703490, - "fields": [ - { "key": "bytes", "type": "int64", "value": 438128 }, - { "key": "msg", "type": "string", "value": "bloom" } - ] - } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "71ee8c7b83046da0", - "operationName": "block.Find", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894683645, - "duration": 19895, - "tags": [ - { "key": "blockID", "type": "string", "value": "151c489c-a86a-49b7-9fa9-31d1714d59ee" }, - { "key": "shardKey", "type": "int64", "value": 5 } - ], - "logs": [ - { - "timestamp": 1605873894703538, - "fields": [ - { "key": "bytes", "type": "int64", "value": 325344 }, - { "key": "msg", "type": "string", "value": "bloom" } - ] - } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "1bf030a07aaceb80", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894683218, - "duration": 20692, - "tags": [], - "logs": [ - { "timestamp": 1605873894683225, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, - { "timestamp": 1605873894703909, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "54b34afd73af12d1", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894683215, - "duration": 21011, - "tags": [ - { "key": "organization", "type": "string", "value": "1" }, - { "key": "span.kind", "type": "string", "value": "client" } - ], - "logs": [], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "6a7ba0261825c53c", - "operationName": "block.Find", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894683505, - "duration": 20615, - "tags": [ - { "key": "blockID", "type": "string", "value": "db0fa030-4607-40e5-998b-47029aa3430e" }, - { "key": "shardKey", "type": "int64", "value": 5 } - ], - "logs": [ - { - "timestamp": 1605873894704118, - "fields": [ - { "key": "bytes", "type": "int64", "value": 411272 }, - { "key": "msg", "type": "string", "value": "bloom" } - ] - } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "2a597269b23b1bcb", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894683426, - "duration": 20720, - "tags": [], - "logs": [ - { "timestamp": 1605873894683431, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, - { "timestamp": 1605873894704146, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "66d886579510b6fd", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894683422, - "duration": 20841, - "tags": [ - { "key": "organization", "type": "string", "value": "1" }, - { "key": "span.kind", "type": "string", "value": "client" } - ], - "logs": [], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "1ef8e63340342174", - "operationName": "block.Find", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894683990, - "duration": 20334, - "tags": [ - { "key": "blockID", "type": "string", "value": "a10ec85d-9fd2-403e-abcd-6f4ec49b0396" }, - { "key": "shardKey", "type": "int64", "value": 5 } - ], - "logs": [ - { - "timestamp": 1605873894704322, - "fields": [ - { "key": "bytes", "type": "int64", "value": 442360 }, - { "key": "msg", "type": "string", "value": "bloom" } - ] - } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "46c6de90778460b1", - "operationName": "block.Find", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894683830, - "duration": 20675, - "tags": [ - { "key": "blockID", "type": "string", "value": "7e9e0142-15ff-461e-8e05-6c62d920603a" }, - { "key": "shardKey", "type": "int64", "value": 5 } - ], - "logs": [ - { - "timestamp": 1605873894704502, - "fields": [ - { "key": "bytes", "type": "int64", "value": 465744 }, - { "key": "msg", "type": "string", "value": "bloom" } - ] - } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "686f3e58fe28940f", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894696113, - "duration": 8480, - "tags": [], - "logs": [ - { "timestamp": 1605873894696126, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, - { "timestamp": 1605873894704591, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "5960c1f5750b1cde", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894696104, - "duration": 8495, - "tags": [ - { "key": "organization", "type": "string", "value": "1" }, - { "key": "span.kind", "type": "string", "value": "client" } - ], - "logs": [], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "6aa5ddd42d96f825", - "operationName": "block.Find", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894683921, - "duration": 20886, - "tags": [ - { "key": "blockID", "type": "string", "value": "43e5ad4f-11d6-4f25-9925-652cb801fd58" }, - { "key": "shardKey", "type": "int64", "value": 5 } - ], - "logs": [ - { - "timestamp": 1605873894704804, - "fields": [ - { "key": "bytes", "type": "int64", "value": 405344 }, - { "key": "msg", "type": "string", "value": "bloom" } - ] - } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "166377800e8e82a7", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894683214, - "duration": 21686, - "tags": [], - "logs": [ - { "timestamp": 1605873894683221, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, - { "timestamp": 1605873894704899, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "5e84f8676ef1efad", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894683209, - "duration": 21696, - "tags": [ - { "key": "organization", "type": "string", "value": "1" }, - { "key": "span.kind", "type": "string", "value": "client" } - ], - "logs": [], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "713c834576a0d9b0", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894683037, - "duration": 22197, - "tags": [], - "logs": [ - { "timestamp": 1605873894683045, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, - { "timestamp": 1605873894705234, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "209c0e336c71e932", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894683033, - "duration": 22209, - "tags": [ - { "key": "organization", "type": "string", "value": "1" }, - { "key": "span.kind", "type": "string", "value": "client" } - ], - "logs": [], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "1bd34d50efadb568", - "operationName": "block.Find", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894683414, - "duration": 21894, - "tags": [ - { "key": "blockID", "type": "string", "value": "b432160f-347c-41ad-882e-f1786e4b42b1" }, - { "key": "shardKey", "type": "int64", "value": 5 } - ], - "logs": [ - { - "timestamp": 1605873894705305, - "fields": [ - { "key": "bytes", "type": "int64", "value": 409104 }, - { "key": "msg", "type": "string", "value": "bloom" } - ] - } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "58cee6c544e69e4f", - "operationName": "block.Find", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894683206, - "duration": 22173, - "tags": [ - { "key": "blockID", "type": "string", "value": "b12afd19-298a-443f-97ce-b5b2e5bc9d79" }, - { "key": "shardKey", "type": "int64", "value": 5 } - ], - "logs": [ - { - "timestamp": 1605873894705375, - "fields": [ - { "key": "bytes", "type": "int64", "value": 376872 }, - { "key": "msg", "type": "string", "value": "bloom" } - ] - } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "4e48e93f70e06522", - "operationName": "block.Find", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894696061, - "duration": 9466, - "tags": [ - { "key": "blockID", "type": "string", "value": "45701f45-c93a-4c35-9fed-9cce2c316a19" }, - { "key": "shardKey", "type": "int64", "value": 5 } - ], - "logs": [ - { - "timestamp": 1605873894705524, - "fields": [ - { "key": "bytes", "type": "int64", "value": 453296 }, - { "key": "msg", "type": "string", "value": "bloom" } - ] - } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "2422bf6c2ed108c2", - "operationName": "block.Find", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894683536, - "duration": 20571, - "tags": [ - { "key": "blockID", "type": "string", "value": "51945006-c165-40af-baea-769b3199bf46" }, - { "key": "shardKey", "type": "int64", "value": 5 } - ], - "logs": [ - { - "timestamp": 1605873894704104, - "fields": [ - { "key": "bytes", "type": "int64", "value": 342200 }, - { "key": "msg", "type": "string", "value": "bloom" } - ] - } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "42fac7c66e0ca970", - "operationName": "block.Find", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894683200, - "duration": 22685, - "tags": [ - { "key": "blockID", "type": "string", "value": "8772aa40-3489-4b12-b685-9f708ae4de75" }, - { "key": "shardKey", "type": "int64", "value": 5 } - ], - "logs": [ - { - "timestamp": 1605873894705882, - "fields": [ - { "key": "bytes", "type": "int64", "value": 407152 }, - { "key": "msg", "type": "string", "value": "bloom" } - ] - } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "2a86d93e70a1720c", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894684627, - "duration": 21313, - "tags": [], - "logs": [ - { "timestamp": 1605873894684633, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, - { "timestamp": 1605873894705939, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "72991150a8c3cf08", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894684622, - "duration": 21322, - "tags": [ - { "key": "organization", "type": "string", "value": "1" }, - { "key": "span.kind", "type": "string", "value": "client" } - ], - "logs": [], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "3ceac51ce73f994e", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894683627, - "duration": 22375, - "tags": [], - "logs": [ - { "timestamp": 1605873894683633, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, - { "timestamp": 1605873894706001, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "704707012227a4f1", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894683623, - "duration": 22386, - "tags": [ - { "key": "organization", "type": "string", "value": "1" }, - { "key": "span.kind", "type": "string", "value": "client" } - ], - "logs": [], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "26cf501f6dcbb968", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894683959, - "duration": 22090, - "tags": [], - "logs": [ - { "timestamp": 1605873894683965, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, - { "timestamp": 1605873894706048, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "7ebb1c9d8a55ac56", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894683952, - "duration": 22104, - "tags": [ - { "key": "organization", "type": "string", "value": "1" }, - { "key": "span.kind", "type": "string", "value": "client" } - ], - "logs": [], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "1bd01ea1e13ac6fd", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894683733, - "duration": 22571, - "tags": [], - "logs": [ - { "timestamp": 1605873894683739, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, - { "timestamp": 1605873894706303, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "4f94f7e28081e1af", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894683728, - "duration": 22582, - "tags": [ - { "key": "organization", "type": "string", "value": "1" }, - { "key": "span.kind", "type": "string", "value": "client" } - ], - "logs": [], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "60fd2b3931676856", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894684310, - "duration": 22119, - "tags": [], - "logs": [ - { "timestamp": 1605873894684317, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, - { "timestamp": 1605873894706428, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "432bc11447588912", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894684305, - "duration": 22131, - "tags": [ - { "key": "organization", "type": "string", "value": "1" }, - { "key": "span.kind", "type": "string", "value": "client" } - ], - "logs": [], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "1e1aa88072a7cefc", - "operationName": "block.Find", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894683026, - "duration": 23483, - "tags": [ - { "key": "blockID", "type": "string", "value": "9064347a-7c49-48d8-b348-8d734f7fd542" }, - { "key": "shardKey", "type": "int64", "value": 5 } - ], - "logs": [ - { - "timestamp": 1605873894706506, - "fields": [ - { "key": "bytes", "type": "int64", "value": 365672 }, - { "key": "msg", "type": "string", "value": "bloom" } - ] - } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "1fb49823a6f803bf", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894682930, - "duration": 23695, - "tags": [], - "logs": [ - { "timestamp": 1605873894682935, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, - { "timestamp": 1605873894706622, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "7db786f0da6d756d", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894682926, - "duration": 23705, - "tags": [ - { "key": "organization", "type": "string", "value": "1" }, - { "key": "span.kind", "type": "string", "value": "client" } - ], - "logs": [], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "01cb21bacc3933da", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894697497, - "duration": 9150, - "tags": [], - "logs": [ - { "timestamp": 1605873894697507, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, - { "timestamp": 1605873894706646, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "260399c49430577a", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894697488, - "duration": 9166, - "tags": [ - { "key": "organization", "type": "string", "value": "1" }, - { "key": "span.kind", "type": "string", "value": "client" } - ], - "logs": [], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "5ba9d86263fc6da1", - "operationName": "block.Find", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894684614, - "duration": 22250, - "tags": [ - { "key": "blockID", "type": "string", "value": "ca346cf4-8162-49e5-a0d0-0619d3813794" }, - { "key": "shardKey", "type": "int64", "value": 5 } - ], - "logs": [ - { - "timestamp": 1605873894706862, - "fields": [ - { "key": "bytes", "type": "int64", "value": 416472 }, - { "key": "msg", "type": "string", "value": "bloom" } - ] - } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "77f27a840cd8b75b", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894685061, - "duration": 21838, - "tags": [], - "logs": [ - { "timestamp": 1605873894685068, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, - { "timestamp": 1605873894706897, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "4a48a86f95e117f9", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894685057, - "duration": 21850, - "tags": [ - { "key": "organization", "type": "string", "value": "1" }, - { "key": "span.kind", "type": "string", "value": "client" } - ], - "logs": [], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "7d1f782957acfe32", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894682777, - "duration": 24168, - "tags": [], - "logs": [ - { "timestamp": 1605873894682783, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, - { "timestamp": 1605873894706944, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "77f8165c15176536", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894682773, - "duration": 24206, - "tags": [ - { "key": "organization", "type": "string", "value": "1" }, - { "key": "span.kind", "type": "string", "value": "client" } - ], - "logs": [], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "7f20dbc684de78c8", - "operationName": "block.Find", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894683615, - "duration": 23703, - "tags": [ - { "key": "blockID", "type": "string", "value": "f518974f-2e1e-41c8-b70c-cd2088f5a081" }, - { "key": "shardKey", "type": "int64", "value": 5 } - ], - "logs": [ - { - "timestamp": 1605873894707316, - "fields": [ - { "key": "bytes", "type": "int64", "value": 278728 }, - { "key": "msg", "type": "string", "value": "bloom" } - ] - } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "70a453eeff8ec687", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894684828, - "duration": 22510, - "tags": [], - "logs": [ - { "timestamp": 1605873894684835, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, - { "timestamp": 1605873894707337, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "0c7d975a67c6d7bc", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894684823, - "duration": 22520, - "tags": [ - { "key": "organization", "type": "string", "value": "1" }, - { "key": "span.kind", "type": "string", "value": "client" } - ], - "logs": [], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "7ccb153793c6afd9", - "operationName": "block.Find", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894683720, - "duration": 23911, - "tags": [ - { "key": "blockID", "type": "string", "value": "6f72b73b-c5fe-4761-b91f-b92f447441fa" }, - { "key": "shardKey", "type": "int64", "value": 5 } - ], - "logs": [ - { - "timestamp": 1605873894707629, - "fields": [ - { "key": "bytes", "type": "int64", "value": 451984 }, - { "key": "msg", "type": "string", "value": "bloom" } - ] - } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "5bf10b9afef405a9", - "operationName": "block.Find", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894697476, - "duration": 10175, - "tags": [ - { "key": "blockID", "type": "string", "value": "61e0a11e-5e88-49c4-ad1d-81636670e642" }, - { "key": "shardKey", "type": "int64", "value": 5 } - ], - "logs": [ - { - "timestamp": 1605873894707648, - "fields": [ - { "key": "bytes", "type": "int64", "value": 296328 }, - { "key": "msg", "type": "string", "value": "bloom" } - ] - } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "1aae38562e2b6a1f", - "operationName": "block.Find", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894683945, - "duration": 23883, - "tags": [ - { "key": "blockID", "type": "string", "value": "7dda9580-666b-42f9-b8a1-1680a0de352f" }, - { "key": "shardKey", "type": "int64", "value": 5 } - ], - "logs": [ - { - "timestamp": 1605873894707823, - "fields": [ - { "key": "bytes", "type": "int64", "value": 402936 }, - { "key": "msg", "type": "string", "value": "bloom" } - ] - } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "1dc5a0697b5d6161", - "operationName": "block.Find", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894682764, - "duration": 25091, - "tags": [ - { "key": "blockID", "type": "string", "value": "bf101e70-4a86-4d88-890c-e976330ba857" }, - { "key": "shardKey", "type": "int64", "value": 5 } - ], - "logs": [ - { - "timestamp": 1605873894707853, - "fields": [ - { "key": "bytes", "type": "int64", "value": 385288 }, - { "key": "msg", "type": "string", "value": "bloom" } - ] - } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "3df0c4e2de834172", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894684425, - "duration": 23557, - "tags": [], - "logs": [ - { "timestamp": 1605873894684432, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, - { "timestamp": 1605873894707980, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "50f5d53109a047da", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894684421, - "duration": 23568, - "tags": [ - { "key": "organization", "type": "string", "value": "1" }, - { "key": "span.kind", "type": "string", "value": "client" } - ], - "logs": [], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "64e62db2206bdda3", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894682595, - "duration": 25553, - "tags": [], - "logs": [ - { "timestamp": 1605873894682603, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, - { "timestamp": 1605873894708147, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "40f0742ab8be92ab", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894682589, - "duration": 25564, - "tags": [ - { "key": "organization", "type": "string", "value": "1" }, - { "key": "span.kind", "type": "string", "value": "client" } - ], - "logs": [], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "6b89efb6b9fb16fc", - "operationName": "block.Find", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894684815, - "duration": 23341, - "tags": [ - { "key": "blockID", "type": "string", "value": "84b0a7ea-895d-49f9-892c-11f689f0c13f" }, - { "key": "shardKey", "type": "int64", "value": 5 } - ], - "logs": [ - { - "timestamp": 1605873894708154, - "fields": [ - { "key": "bytes", "type": "int64", "value": 424816 }, - { "key": "msg", "type": "string", "value": "bloom" } - ] - } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "33c05fda4c7d3921", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894684414, - "duration": 23815, - "tags": [], - "logs": [ - { "timestamp": 1605873894684420, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, - { "timestamp": 1605873894708228, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "742995638b3636e6", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894684409, - "duration": 23825, - "tags": [ - { "key": "organization", "type": "string", "value": "1" }, - { "key": "span.kind", "type": "string", "value": "client" } - ], - "logs": [], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "1cf9294062a5780b", - "operationName": "block.Find", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894682920, - "duration": 25427, - "tags": [ - { "key": "blockID", "type": "string", "value": "e43ee3db-63c9-4d2b-a791-99a5a9203e4e" }, - { "key": "shardKey", "type": "int64", "value": 5 } - ], - "logs": [ - { - "timestamp": 1605873894708341, - "fields": [ - { "key": "bytes", "type": "int64", "value": 396312 }, - { "key": "msg", "type": "string", "value": "bloom" } - ] - } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "6852631d2c6d1586", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894683843, - "duration": 24695, - "tags": [], - "logs": [ - { "timestamp": 1605873894683850, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, - { "timestamp": 1605873894708538, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "1691ee4e1f907b39", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894683839, - "duration": 24706, - "tags": [ - { "key": "organization", "type": "string", "value": "1" }, - { "key": "span.kind", "type": "string", "value": "client" } - ], - "logs": [], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "1bcd55e85df0601a", - "operationName": "block.Find", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894684400, - "duration": 24493, - "tags": [ - { "key": "blockID", "type": "string", "value": "211313f2-7284-43eb-b9dc-134b5b344524" }, - { "key": "shardKey", "type": "int64", "value": 5 } - ], - "logs": [ - { - "timestamp": 1605873894708891, - "fields": [ - { "key": "bytes", "type": "int64", "value": 249032 }, - { "key": "msg", "type": "string", "value": "bloom" } - ] - } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "7757c670662153b5", - "operationName": "block.Find", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894682578, - "duration": 26605, - "tags": [ - { "key": "blockID", "type": "string", "value": "99a8b127-bef6-4718-997b-18e5cb6bee81" }, - { "key": "shardKey", "type": "int64", "value": 5 } - ], - "logs": [ - { - "timestamp": 1605873894709180, - "fields": [ - { "key": "bytes", "type": "int64", "value": 443904 }, - { "key": "msg", "type": "string", "value": "bloom" } - ] - } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "033e809d9deb02fb", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894683129, - "duration": 26149, - "tags": [], - "logs": [ - { "timestamp": 1605873894683139, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, - { "timestamp": 1605873894709277, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "0701e7633d141024", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894683124, - "duration": 26160, - "tags": [ - { "key": "organization", "type": "string", "value": "1" }, - { "key": "span.kind", "type": "string", "value": "client" } - ], - "logs": [], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "5a1fcbfa2c2e077e", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894682990, - "duration": 26296, - "tags": [], - "logs": [ - { "timestamp": 1605873894682993, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, - { "timestamp": 1605873894709285, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "2639318a16168a94", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894682987, - "duration": 26304, - "tags": [ - { "key": "organization", "type": "string", "value": "1" }, - { "key": "span.kind", "type": "string", "value": "client" } - ], - "logs": [], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "573267e2aab9eb37", - "operationName": "block.Find", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894683831, - "duration": 25627, - "tags": [ - { "key": "blockID", "type": "string", "value": "9a5df823-d980-4671-b33f-ef92e485232f" }, - { "key": "shardKey", "type": "int64", "value": 5 } - ], - "logs": [ - { - "timestamp": 1605873894709455, - "fields": [ - { "key": "bytes", "type": "int64", "value": 357872 }, - { "key": "msg", "type": "string", "value": "bloom" } - ] - } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "3705123c90491605", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894683886, - "duration": 25575, - "tags": [], - "logs": [ - { "timestamp": 1605873894683890, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, - { "timestamp": 1605873894709460, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "46138581a74be710", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894683883, - "duration": 25585, - "tags": [ - { "key": "organization", "type": "string", "value": "1" }, - { "key": "span.kind", "type": "string", "value": "client" } - ], - "logs": [], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "369cd4694f877602", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894684325, - "duration": 25173, - "tags": [], - "logs": [ - { "timestamp": 1605873894684385, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, - { "timestamp": 1605873894709498, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "7aab906468c79c5b", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894703359, - "duration": 6145, - "tags": [], - "logs": [ - { "timestamp": 1605873894703375, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, - { "timestamp": 1605873894709503, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "57f0ffddbcc40049", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894684321, - "duration": 25185, - "tags": [ - { "key": "organization", "type": "string", "value": "1" }, - { "key": "span.kind", "type": "string", "value": "client" } - ], - "logs": [], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "0c27a77ad2f6bbb3", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894703354, - "duration": 6155, - "tags": [ - { "key": "organization", "type": "string", "value": "1" }, - { "key": "span.kind", "type": "string", "value": "client" } - ], - "logs": [], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "27f360a42e423410", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894696933, - "duration": 12666, - "tags": [], - "logs": [ - { "timestamp": 1605873894696942, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, - { "timestamp": 1605873894709598, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "7e5086a8bb3eb3b3", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894696926, - "duration": 12678, - "tags": [ - { "key": "organization", "type": "string", "value": "1" }, - { "key": "span.kind", "type": "string", "value": "client" } - ], - "logs": [], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "42f4a2e45bc6b552", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894683602, - "duration": 26169, - "tags": [], - "logs": [ - { "timestamp": 1605873894683608, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, - { "timestamp": 1605873894709771, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "693c3e7a4e085ce6", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894683598, - "duration": 26180, - "tags": [ - { "key": "organization", "type": "string", "value": "1" }, - { "key": "span.kind", "type": "string", "value": "client" } - ], - "logs": [], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "7645427b1d8ca012", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894694874, - "duration": 15023, - "tags": [], - "logs": [ - { "timestamp": 1605873894694896, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, - { "timestamp": 1605873894709897, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "2e6e130f1e7bf5ca", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894694866, - "duration": 15038, - "tags": [ - { "key": "organization", "type": "string", "value": "1" }, - { "key": "span.kind", "type": "string", "value": "client" } - ], - "logs": [], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "2155087a44565c8a", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894700685, - "duration": 9446, - "tags": [], - "logs": [ - { "timestamp": 1605873894700698, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, - { "timestamp": 1605873894710131, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "66ed873b2793ee77", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894700678, - "duration": 9459, - "tags": [ - { "key": "organization", "type": "string", "value": "1" }, - { "key": "span.kind", "type": "string", "value": "client" } - ], - "logs": [], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "69654d80ac69ec92", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894702020, - "duration": 8202, - "tags": [], - "logs": [ - { "timestamp": 1605873894702031, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, - { "timestamp": 1605873894710221, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "3a0447242878ba00", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894701338, - "duration": 8890, - "tags": [ - { "key": "organization", "type": "string", "value": "1" }, - { "key": "span.kind", "type": "string", "value": "client" } - ], - "logs": [], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "2e73b563bfa4df76", - "operationName": "block.Find", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894683106, - "duration": 27358, - "tags": [ - { "key": "blockID", "type": "string", "value": "b89a056f-d8cd-41e9-84ad-445e68d0a0d5" }, - { "key": "shardKey", "type": "int64", "value": 5 } - ], - "logs": [ - { - "timestamp": 1605873894710462, - "fields": [ - { "key": "bytes", "type": "int64", "value": 337664 }, - { "key": "msg", "type": "string", "value": "bloom" } - ] - } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "2e958ff5d95860cf", - "operationName": "block.Find", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894683591, - "duration": 27357, - "tags": [ - { "key": "blockID", "type": "string", "value": "4767ecb2-01d3-450b-b005-6b9219fdfd71" }, - { "key": "shardKey", "type": "int64", "value": 5 } - ], - "logs": [ - { - "timestamp": 1605873894710945, - "fields": [ - { "key": "bytes", "type": "int64", "value": 421576 }, - { "key": "msg", "type": "string", "value": "bloom" } - ] - } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "4854f2803a2439d0", - "operationName": "block.Find", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894684313, - "duration": 26669, - "tags": [ - { "key": "blockID", "type": "string", "value": "ec9c982f-485f-47b2-be74-a7f203368ede" }, - { "key": "shardKey", "type": "int64", "value": 5 } - ], - "logs": [ - { - "timestamp": 1605873894710972, - "fields": [ - { "key": "bytes", "type": "int64", "value": 409016 }, - { "key": "msg", "type": "string", "value": "bloom" } - ] - } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "62aa1124fbaafe29", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894684107, - "duration": 26934, - "tags": [], - "logs": [ - { "timestamp": 1605873894684113, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, - { "timestamp": 1605873894711040, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "67ee705c301e7e2a", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894684102, - "duration": 26945, - "tags": [ - { "key": "organization", "type": "string", "value": "1" }, - { "key": "span.kind", "type": "string", "value": "client" } - ], - "logs": [], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "417798c3fbab4244", - "operationName": "block.Find", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894696911, - "duration": 14252, - "tags": [ - { "key": "blockID", "type": "string", "value": "6a346739-04b1-4e86-8f87-e182b01cf5cd" }, - { "key": "shardKey", "type": "int64", "value": 5 } - ], - "logs": [ - { - "timestamp": 1605873894711160, - "fields": [ - { "key": "bytes", "type": "int64", "value": 407144 }, - { "key": "msg", "type": "string", "value": "bloom" } - ] - } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "17faaf92fbea2ed9", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894683730, - "duration": 27707, - "tags": [], - "logs": [ - { "timestamp": 1605873894683736, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, - { "timestamp": 1605873894711435, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "29d4e2aa59eae59e", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894683726, - "duration": 27719, - "tags": [ - { "key": "organization", "type": "string", "value": "1" }, - { "key": "span.kind", "type": "string", "value": "client" } - ], - "logs": [], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "3b9a85f6cd6075b8", - "operationName": "block.Find", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894701327, - "duration": 10230, - "tags": [ - { "key": "blockID", "type": "string", "value": "ece056d3-aa27-464b-81a8-643b3ae208e4" }, - { "key": "shardKey", "type": "int64", "value": 5 } - ], - "logs": [ - { - "timestamp": 1605873894711554, - "fields": [ - { "key": "bytes", "type": "int64", "value": 359960 }, - { "key": "msg", "type": "string", "value": "bloom" } - ] - } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "0da2897c85659567", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894683377, - "duration": 28594, - "tags": [], - "logs": [ - { "timestamp": 1605873894683384, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, - { "timestamp": 1605873894711971, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "4407d391acba81fc", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894683373, - "duration": 28605, - "tags": [ - { "key": "organization", "type": "string", "value": "1" }, - { "key": "span.kind", "type": "string", "value": "client" } - ], - "logs": [], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "1987773829521f8f", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894699105, - "duration": 12885, - "tags": [], - "logs": [ - { "timestamp": 1605873894699119, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, - { "timestamp": 1605873894711989, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "1c9553a6471269c6", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894699099, - "duration": 12896, - "tags": [ - { "key": "organization", "type": "string", "value": "1" }, - { "key": "span.kind", "type": "string", "value": "client" } - ], - "logs": [], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "3dedf220c1f51d38", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894704853, - "duration": 7356, - "tags": [], - "logs": [ - { "timestamp": 1605873894704879, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, - { "timestamp": 1605873894712209, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "25820f0eebf05ab3", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894704846, - "duration": 7370, - "tags": [ - { "key": "organization", "type": "string", "value": "1" }, - { "key": "span.kind", "type": "string", "value": "client" } - ], - "logs": [], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "7027388faf7e1bf1", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894684937, - "duration": 27418, - "tags": [], - "logs": [ - { "timestamp": 1605873894684943, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, - { "timestamp": 1605873894712354, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "44c6d6c7e1afb67d", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894684933, - "duration": 27429, - "tags": [ - { "key": "organization", "type": "string", "value": "1" }, - { "key": "span.kind", "type": "string", "value": "client" } - ], - "logs": [], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "3f654e75b41629f5", - "operationName": "block.Find", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894683718, - "duration": 28677, - "tags": [ - { "key": "blockID", "type": "string", "value": "0e5b1fbb-ab10-44b7-89a0-f8932ee26dcf" }, - { "key": "shardKey", "type": "int64", "value": 5 } - ], - "logs": [ - { - "timestamp": 1605873894712392, - "fields": [ - { "key": "bytes", "type": "int64", "value": 369000 }, - { "key": "msg", "type": "string", "value": "bloom" } - ] - } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "4fa1d1a031112ab0", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894682996, - "duration": 29565, - "tags": [], - "logs": [ - { "timestamp": 1605873894683006, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, - { "timestamp": 1605873894712560, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "2e0985a0b4168ff2", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894682988, - "duration": 29577, - "tags": [ - { "key": "organization", "type": "string", "value": "1" }, - { "key": "span.kind", "type": "string", "value": "client" } - ], - "logs": [], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "7a7bf32e81f4317e", - "operationName": "block.Find", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894682984, - "duration": 29753, - "tags": [ - { "key": "blockID", "type": "string", "value": "c56f4809-bc48-4f81-9656-a3bbb96ba87e" }, - { "key": "shardKey", "type": "int64", "value": 5 } - ], - "logs": [ - { - "timestamp": 1605873894712734, - "fields": [ - { "key": "bytes", "type": "int64", "value": 406296 }, - { "key": "msg", "type": "string", "value": "bloom" } - ] - } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "66d4f363dfa46bdb", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894684041, - "duration": 28730, - "tags": [], - "logs": [ - { "timestamp": 1605873894684111, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, - { "timestamp": 1605873894712771, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "616b800031f78e5f", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894684037, - "duration": 28741, - "tags": [ - { "key": "organization", "type": "string", "value": "1" }, - { "key": "span.kind", "type": "string", "value": "client" } - ], - "logs": [], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "5b0d3da4dac0a4ab", - "operationName": "block.Find", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894683364, - "duration": 29595, - "tags": [ - { "key": "blockID", "type": "string", "value": "10e42379-1c35-419e-a26c-2630b9d2cdd2" }, - { "key": "shardKey", "type": "int64", "value": 5 } - ], - "logs": [ - { - "timestamp": 1605873894712956, - "fields": [ - { "key": "bytes", "type": "int64", "value": 354744 }, - { "key": "msg", "type": "string", "value": "bloom" } - ] - } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "1571e420dca57b9f", - "operationName": "block.Find", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894683878, - "duration": 29122, - "tags": [ - { "key": "blockID", "type": "string", "value": "adb287c7-69e4-4ed8-8604-c3302c766db2" }, - { "key": "shardKey", "type": "int64", "value": 5 } - ], - "logs": [ - { - "timestamp": 1605873894712996, - "fields": [ - { "key": "bytes", "type": "int64", "value": 300104 }, - { "key": "msg", "type": "string", "value": "bloom" } - ] - } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "3120fb610c52c9a6", - "operationName": "block.Find", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894684415, - "duration": 28590, - "tags": [ - { "key": "blockID", "type": "string", "value": "faebcb3d-444a-4675-8e55-2f46dbcaa1d7" }, - { "key": "shardKey", "type": "int64", "value": 5 } - ], - "logs": [ - { - "timestamp": 1605873894713002, - "fields": [ - { "key": "bytes", "type": "int64", "value": 410672 }, - { "key": "msg", "type": "string", "value": "bloom" } - ] - } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "46feff0edeabb674", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894683135, - "duration": 29700, - "tags": [], - "logs": [ - { "timestamp": 1605873894683141, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, - { "timestamp": 1605873894712834, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "32df737f09cd2bf9", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894683131, - "duration": 29904, - "tags": [ - { "key": "organization", "type": "string", "value": "1" }, - { "key": "span.kind", "type": "string", "value": "client" } - ], - "logs": [], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "604de25c9811a395", - "operationName": "block.Find", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894699064, - "duration": 14141, - "tags": [ - { "key": "blockID", "type": "string", "value": "7df8ef23-0902-4b4b-92aa-b6c1aeb3c9c2" }, - { "key": "shardKey", "type": "int64", "value": 5 } - ], - "logs": [ - { - "timestamp": 1605873894713203, - "fields": [ - { "key": "bytes", "type": "int64", "value": 436328 }, - { "key": "msg", "type": "string", "value": "bloom" } - ] - } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "7867c14538ff0c61", - "operationName": "block.Find", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894684923, - "duration": 28333, - "tags": [ - { "key": "blockID", "type": "string", "value": "a94e5162-7e01-4bd6-b5c8-1bc3b50c67c6" }, - { "key": "shardKey", "type": "int64", "value": 5 } - ], - "logs": [ - { - "timestamp": 1605873894713253, - "fields": [ - { "key": "bytes", "type": "int64", "value": 433064 }, - { "key": "msg", "type": "string", "value": "bloom" } - ] - } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "04e793f4b075b20f", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894684557, - "duration": 28822, - "tags": [], - "logs": [ - { "timestamp": 1605873894684565, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, - { "timestamp": 1605873894713378, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "3084a10a11a62355", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894684553, - "duration": 28832, - "tags": [ - { "key": "organization", "type": "string", "value": "1" }, - { "key": "span.kind", "type": "string", "value": "client" } - ], - "logs": [], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "0a0b86e5738d630b", - "operationName": "block.Find", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894685048, - "duration": 28355, - "tags": [ - { "key": "blockID", "type": "string", "value": "36ce4c95-0cb6-4803-bdfd-b316b3c0cc4c" }, - { "key": "shardKey", "type": "int64", "value": 5 } - ], - "logs": [ - { - "timestamp": 1605873894713400, - "fields": [ - { "key": "bytes", "type": "int64", "value": 293136 }, - { "key": "msg", "type": "string", "value": "bloom" } - ] - } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "62ea00c2c871a91e", - "operationName": "block.Find", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894704821, - "duration": 8702, - "tags": [ - { "key": "blockID", "type": "string", "value": "b9c0dc2b-ee12-4876-a517-2902c6fe655e" }, - { "key": "shardKey", "type": "int64", "value": 5 } - ], - "logs": [ - { - "timestamp": 1605873894713521, - "fields": [ - { "key": "bytes", "type": "int64", "value": 415912 }, - { "key": "msg", "type": "string", "value": "bloom" } - ] - } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "0f54c2d4ac7df141", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894683769, - "duration": 29774, - "tags": [], - "logs": [ - { "timestamp": 1605873894683775, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, - { "timestamp": 1605873894713542, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "5e650633f1c4cb45", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894683764, - "duration": 29784, - "tags": [ - { "key": "organization", "type": "string", "value": "1" }, - { "key": "span.kind", "type": "string", "value": "client" } - ], - "logs": [], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "4cff4ebd296d36f0", - "operationName": "block.Find", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894684028, - "duration": 29524, - "tags": [ - { "key": "blockID", "type": "string", "value": "e6200492-f24a-40ef-946a-e89170d1ac54" }, - { "key": "shardKey", "type": "int64", "value": 5 } - ], - "logs": [ - { - "timestamp": 1605873894713549, - "fields": [ - { "key": "bytes", "type": "int64", "value": 447592 }, - { "key": "msg", "type": "string", "value": "bloom" } - ] - } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "0da82a874696fec5", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894684230, - "duration": 29351, - "tags": [], - "logs": [ - { "timestamp": 1605873894684236, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, - { "timestamp": 1605873894713581, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "20334815e0eb1b97", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894684226, - "duration": 29362, - "tags": [ - { "key": "organization", "type": "string", "value": "1" }, - { "key": "span.kind", "type": "string", "value": "client" } - ], - "logs": [], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "20de4a897b30c066", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894683315, - "duration": 30412, - "tags": [], - "logs": [ - { "timestamp": 1605873894683323, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, - { "timestamp": 1605873894713726, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "6edcc31aa4c96617", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894683309, - "duration": 30424, - "tags": [ - { "key": "organization", "type": "string", "value": "1" }, - { "key": "span.kind", "type": "string", "value": "client" } - ], - "logs": [], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "3594272577366bc9", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894684491, - "duration": 29257, - "tags": [], - "logs": [ - { "timestamp": 1605873894684498, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, - { "timestamp": 1605873894713747, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "01e9f897c4145c38", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894684485, - "duration": 29270, - "tags": [ - { "key": "organization", "type": "string", "value": "1" }, - { "key": "span.kind", "type": "string", "value": "client" } - ], - "logs": [], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "53e0bef2bbb77bea", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894684901, - "duration": 28911, - "tags": [], - "logs": [ - { "timestamp": 1605873894684908, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, - { "timestamp": 1605873894713812, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "3df7804d8e682193", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894684897, - "duration": 28919, - "tags": [ - { "key": "organization", "type": "string", "value": "1" }, - { "key": "span.kind", "type": "string", "value": "client" } - ], - "logs": [], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "5664530667612f1f", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894682860, - "duration": 31015, - "tags": [], - "logs": [ - { "timestamp": 1605873894682871, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, - { "timestamp": 1605873894713875, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "4b6340b15001f8c8", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894682855, - "duration": 31026, - "tags": [ - { "key": "organization", "type": "string", "value": "1" }, - { "key": "span.kind", "type": "string", "value": "client" } - ], - "logs": [], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "6b3d3f0643735e5f", - "operationName": "block.Find", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894694842, - "duration": 19190, - "tags": [ - { "key": "blockID", "type": "string", "value": "f0b87e56-00a6-4270-8ce0-b47affb9113e" }, - { "key": "shardKey", "type": "int64", "value": 5 } - ], - "logs": [ - { - "timestamp": 1605873894714027, - "fields": [ - { "key": "bytes", "type": "int64", "value": 310320 }, - { "key": "msg", "type": "string", "value": "bloom" } - ] - } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "4a4b3e0d2f115bcf", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894683987, - "duration": 30363, - "tags": [], - "logs": [ - { "timestamp": 1605873894683994, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, - { "timestamp": 1605873894714350, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "1e0da3179b38449d", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894683983, - "duration": 30374, - "tags": [ - { "key": "organization", "type": "string", "value": "1" }, - { "key": "span.kind", "type": "string", "value": "client" } - ], - "logs": [], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "318fcd8e3bfc42c7", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894684209, - "duration": 30152, - "tags": [], - "logs": [ - { "timestamp": 1605873894684215, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, - { "timestamp": 1605873894714360, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "37e82ddc44e6bf60", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894684205, - "duration": 30162, - "tags": [ - { "key": "organization", "type": "string", "value": "1" }, - { "key": "span.kind", "type": "string", "value": "client" } - ], - "logs": [], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "0271272ae09aac5f", - "operationName": "block.Find", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894684542, - "duration": 29977, - "tags": [ - { "key": "blockID", "type": "string", "value": "bfbb9652-84f8-4145-8091-8197ea922ad3" }, - { "key": "shardKey", "type": "int64", "value": 5 } - ], - "logs": [ - { - "timestamp": 1605873894714514, - "fields": [ - { "key": "bytes", "type": "int64", "value": 432848 }, - { "key": "msg", "type": "string", "value": "bloom" } - ] - } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "2d80feb23cbbb7cd", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894684508, - "duration": 30161, - "tags": [], - "logs": [ - { "timestamp": 1605873894684515, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, - { "timestamp": 1605873894714668, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "01ad9e5d3837c5b6", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894684503, - "duration": 30172, - "tags": [ - { "key": "organization", "type": "string", "value": "1" }, - { "key": "span.kind", "type": "string", "value": "client" } - ], - "logs": [], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "42698e68a26de8cf", - "operationName": "block.Find", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894683756, - "duration": 30942, - "tags": [ - { "key": "blockID", "type": "string", "value": "55f71d63-05b0-4c3a-b79f-a2563307bf40" }, - { "key": "shardKey", "type": "int64", "value": 5 } - ], - "logs": [ - { - "timestamp": 1605873894714695, - "fields": [ - { "key": "bytes", "type": "int64", "value": 402624 }, - { "key": "msg", "type": "string", "value": "bloom" } - ] - } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "6ea302c343fec88f", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894683645, - "duration": 31247, - "tags": [], - "logs": [ - { "timestamp": 1605873894683652, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, - { "timestamp": 1605873894714891, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "279e17d93d4978da", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894683641, - "duration": 31258, - "tags": [ - { "key": "organization", "type": "string", "value": "1" }, - { "key": "span.kind", "type": "string", "value": "client" } - ], - "logs": [], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "59b29ac1ab225873", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894683001, - "duration": 31930, - "tags": [], - "logs": [ - { "timestamp": 1605873894683006, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, - { "timestamp": 1605873894714930, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "4196b1f250632b3e", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894682998, - "duration": 31938, - "tags": [ - { "key": "organization", "type": "string", "value": "1" }, - { "key": "span.kind", "type": "string", "value": "client" } - ], - "logs": [], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "224b550ee6ad2bf2", - "operationName": "block.Find", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894684889, - "duration": 30088, - "tags": [ - { "key": "blockID", "type": "string", "value": "07baec6a-187a-493b-b160-772936b5a3f0" }, - { "key": "shardKey", "type": "int64", "value": 5 } - ], - "logs": [ - { - "timestamp": 1605873894714974, - "fields": [ - { "key": "bytes", "type": "int64", "value": 429360 }, - { "key": "msg", "type": "string", "value": "bloom" } - ] - } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "368bcd97b5e9dde0", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894684678, - "duration": 30934, - "tags": [], - "logs": [ - { "timestamp": 1605873894684685, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, - { "timestamp": 1605873894715611, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "3d88bddf112b8ae2", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894684672, - "duration": 30946, - "tags": [ - { "key": "organization", "type": "string", "value": "1" }, - { "key": "span.kind", "type": "string", "value": "client" } - ], - "logs": [], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "718a103bd19501b2", - "operationName": "block.Find", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894684196, - "duration": 31424, - "tags": [ - { "key": "blockID", "type": "string", "value": "a85669d8-148b-4d61-a359-8f97c036b880" }, - { "key": "shardKey", "type": "int64", "value": 5 } - ], - "logs": [ - { - "timestamp": 1605873894715617, - "fields": [ - { "key": "bytes", "type": "int64", "value": 401280 }, - { "key": "msg", "type": "string", "value": "bloom" } - ] - } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "2b56997697dd91c0", - "operationName": "block.Find", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894684472, - "duration": 31151, - "tags": [ - { "key": "blockID", "type": "string", "value": "75911f2c-fc5e-4ef1-bcff-9abad2120f23" }, - { "key": "shardKey", "type": "int64", "value": 5 } - ], - "logs": [ - { - "timestamp": 1605873894715621, - "fields": [ - { "key": "bytes", "type": "int64", "value": 397808 }, - { "key": "msg", "type": "string", "value": "bloom" } - ] - } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "1c049cad7edf280e", - "operationName": "block.Find", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894683630, - "duration": 32119, - "tags": [ - { "key": "blockID", "type": "string", "value": "fdcc5380-c15f-41c2-9a34-623d6cdd2d5a" }, - { "key": "shardKey", "type": "int64", "value": 5 } - ], - "logs": [ - { - "timestamp": 1605873894715733, - "fields": [ - { "key": "bytes", "type": "int64", "value": 397464 }, - { "key": "msg", "type": "string", "value": "bloom" } - ] - } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "6e3d16e8ed14d90c", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894702877, - "duration": 12975, - "tags": [], - "logs": [ - { "timestamp": 1605873894702894, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, - { "timestamp": 1605873894715852, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "7bd595782cdb70c3", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894699700, - "duration": 16154, - "tags": [], - "logs": [ - { "timestamp": 1605873894699708, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, - { "timestamp": 1605873894715853, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "08a9d074d520a512", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894702864, - "duration": 12993, - "tags": [ - { "key": "organization", "type": "string", "value": "1" }, - { "key": "span.kind", "type": "string", "value": "client" } - ], - "logs": [], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "083316368540b811", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894699695, - "duration": 16164, - "tags": [ - { "key": "organization", "type": "string", "value": "1" }, - { "key": "span.kind", "type": "string", "value": "client" } - ], - "logs": [], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "7f067cadc2b4569d", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894684535, - "duration": 31520, - "tags": [], - "logs": [ - { "timestamp": 1605873894684540, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, - { "timestamp": 1605873894716053, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "5061bd596bc8a7e7", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894684531, - "duration": 31530, - "tags": [ - { "key": "organization", "type": "string", "value": "1" }, - { "key": "span.kind", "type": "string", "value": "client" } - ], - "logs": [], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "4b9772650994e725", - "operationName": "block.Find", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894682990, - "duration": 33209, - "tags": [ - { "key": "blockID", "type": "string", "value": "61022db6-4401-40b6-a3a2-1f4cd5ccb430" }, - { "key": "shardKey", "type": "int64", "value": 5 } - ], - "logs": [ - { - "timestamp": 1605873894716196, - "fields": [ - { "key": "bytes", "type": "int64", "value": 380504 }, - { "key": "msg", "type": "string", "value": "bloom" } - ] - } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "0e9c6b89215308ba", - "operationName": "block.Find", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894683975, - "duration": 32340, - "tags": [ - { "key": "blockID", "type": "string", "value": "f17c848f-2f99-4215-a5e9-1f55d8e15c1e" }, - { "key": "shardKey", "type": "int64", "value": 5 } - ], - "logs": [ - { - "timestamp": 1605873894716311, - "fields": [ - { "key": "bytes", "type": "int64", "value": 447736 }, - { "key": "msg", "type": "string", "value": "bloom" } - ] - } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "030573bc0520e3c2", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894683061, - "duration": 33348, - "tags": [], - "logs": [ - { "timestamp": 1605873894683067, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, - { "timestamp": 1605873894716409, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "0d2e16a8cf201e5a", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894683057, - "duration": 33357, - "tags": [ - { "key": "organization", "type": "string", "value": "1" }, - { "key": "span.kind", "type": "string", "value": "client" } - ], - "logs": [], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "0361f359be22f9c8", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894702474, - "duration": 14135, - "tags": [], - "logs": [ - { "timestamp": 1605873894702483, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, - { "timestamp": 1605873894716607, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "5310c5c355550cad", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894702463, - "duration": 14156, - "tags": [ - { "key": "organization", "type": "string", "value": "1" }, - { "key": "span.kind", "type": "string", "value": "client" } - ], - "logs": [], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "16870d24920c25b8", - "operationName": "block.Find", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894699686, - "duration": 17072, - "tags": [ - { "key": "blockID", "type": "string", "value": "320a9ecd-a9fd-4c88-8aeb-e8a312dcce0c" }, - { "key": "shardKey", "type": "int64", "value": 5 } - ], - "logs": [ - { - "timestamp": 1605873894716753, - "fields": [ - { "key": "bytes", "type": "int64", "value": 424544 }, - { "key": "msg", "type": "string", "value": "bloom" } - ] - } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "62090e9e1c22bb56", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894707676, - "duration": 9095, - "tags": [], - "logs": [ - { "timestamp": 1605873894707691, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, - { "timestamp": 1605873894716771, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "02d91deb1ff0ea76", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894707670, - "duration": 9107, - "tags": [ - { "key": "organization", "type": "string", "value": "1" }, - { "key": "span.kind", "type": "string", "value": "client" } - ], - "logs": [], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "0cc47cc1eb5deb29", - "operationName": "block.Find", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894702844, - "duration": 13991, - "tags": [ - { "key": "blockID", "type": "string", "value": "04e21143-53ef-4083-948e-3bbe502c2d44" }, - { "key": "shardKey", "type": "int64", "value": 5 } - ], - "logs": [ - { - "timestamp": 1605873894716832, - "fields": [ - { "key": "bytes", "type": "int64", "value": 401728 }, - { "key": "msg", "type": "string", "value": "bloom" } - ] - } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "1b27a749f4d1b557", - "operationName": "block.Find", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894684297, - "duration": 32550, - "tags": [ - { "key": "blockID", "type": "string", "value": "d1ffbf86-0e11-4b6e-b9ae-8466e7c42a90" }, - { "key": "shardKey", "type": "int64", "value": 5 } - ], - "logs": [ - { - "timestamp": 1605873894716844, - "fields": [ - { "key": "bytes", "type": "int64", "value": 193992 }, - { "key": "msg", "type": "string", "value": "bloom" } - ] - } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "3c5f2282a3e7c658", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894683345, - "duration": 33584, - "tags": [], - "logs": [ - { "timestamp": 1605873894683352, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, - { "timestamp": 1605873894716927, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "6fc62f7a1ae1a6e9", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894683340, - "duration": 33596, - "tags": [ - { "key": "organization", "type": "string", "value": "1" }, - { "key": "span.kind", "type": "string", "value": "client" } - ], - "logs": [], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "524a9c941765266a", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894684584, - "duration": 32449, - "tags": [], - "logs": [ - { "timestamp": 1605873894684592, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, - { "timestamp": 1605873894717030, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "5bcaa6a4a1c06160", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894684579, - "duration": 32460, - "tags": [ - { "key": "organization", "type": "string", "value": "1" }, - { "key": "span.kind", "type": "string", "value": "client" } - ], - "logs": [], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "2d4e045a72c17ff2", - "operationName": "block.Find", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894684465, - "duration": 32666, - "tags": [ - { "key": "blockID", "type": "string", "value": "4c9f58b7-b944-4692-9d5b-14270bd1b8d6" }, - { "key": "shardKey", "type": "int64", "value": 5 } - ], - "logs": [ - { - "timestamp": 1605873894717127, - "fields": [ - { "key": "bytes", "type": "int64", "value": 436224 }, - { "key": "msg", "type": "string", "value": "bloom" } - ] - } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "25548a46750dfecb", - "operationName": "block.Find", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894684652, - "duration": 32684, - "tags": [ - { "key": "blockID", "type": "string", "value": "ffd8fb66-db97-4451-9a97-bfb6631b82a5" }, - { "key": "shardKey", "type": "int64", "value": 5 } - ], - "logs": [ - { - "timestamp": 1605873894717332, - "fields": [ - { "key": "bytes", "type": "int64", "value": 434536 }, - { "key": "msg", "type": "string", "value": "bloom" } - ] - } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "5f4913a50dcd37c8", - "operationName": "block.Find", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894683050, - "duration": 34487, - "tags": [ - { "key": "blockID", "type": "string", "value": "0255db6b-061e-4ceb-9ae9-598588995be8" }, - { "key": "shardKey", "type": "int64", "value": 5 } - ], - "logs": [ - { - "timestamp": 1605873894717533, - "fields": [ - { "key": "bytes", "type": "int64", "value": 392520 }, - { "key": "msg", "type": "string", "value": "bloom" } - ] - } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "6750f7ac4a5b50e8", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894684648, - "duration": 32974, - "tags": [], - "logs": [ - { "timestamp": 1605873894684654, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, - { "timestamp": 1605873894717621, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "41f4b72bd0a14291", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894684644, - "duration": 32984, - "tags": [ - { "key": "organization", "type": "string", "value": "1" }, - { "key": "span.kind", "type": "string", "value": "client" } - ], - "logs": [], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "03fac2f4c91b31b6", - "operationName": "block.Find", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894702448, - "duration": 15285, - "tags": [ - { "key": "blockID", "type": "string", "value": "f7da9248-f02e-44df-b243-c1f5f69e0f67" }, - { "key": "shardKey", "type": "int64", "value": 5 } - ], - "logs": [ - { - "timestamp": 1605873894717730, - "fields": [ - { "key": "bytes", "type": "int64", "value": 366872 }, - { "key": "msg", "type": "string", "value": "bloom" } - ] - } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "4934a16eeec96b0d", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894684005, - "duration": 33827, - "tags": [], - "logs": [ - { "timestamp": 1605873894684010, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, - { "timestamp": 1605873894717831, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "0ebf8034f9944320", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894684001, - "duration": 33836, - "tags": [ - { "key": "organization", "type": "string", "value": "1" }, - { "key": "span.kind", "type": "string", "value": "client" } - ], - "logs": [], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "0cc1f6dfcc153616", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894683672, - "duration": 34388, - "tags": [], - "logs": [ - { "timestamp": 1605873894683679, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, - { "timestamp": 1605873894718059, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "501e4211325ef503", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894683667, - "duration": 34399, - "tags": [ - { "key": "organization", "type": "string", "value": "1" }, - { "key": "span.kind", "type": "string", "value": "client" } - ], - "logs": [], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "7bcf0390730028ef", - "operationName": "block.Find", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894683332, - "duration": 34943, - "tags": [ - { "key": "blockID", "type": "string", "value": "cbeb7cd1-c8b1-4290-be74-4caf7b3f2d69" }, - { "key": "shardKey", "type": "int64", "value": 5 } - ], - "logs": [ - { - "timestamp": 1605873894718272, - "fields": [ - { "key": "bytes", "type": "int64", "value": 432424 }, - { "key": "msg", "type": "string", "value": "bloom" } - ] - } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "1b30f12cd1728ebf", - "operationName": "block.Find", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894683995, - "duration": 34418, - "tags": [ - { "key": "blockID", "type": "string", "value": "2dd90b29-ffb5-4a27-bcd7-0950ca151c14" }, - { "key": "shardKey", "type": "int64", "value": 5 } - ], - "logs": [ - { - "timestamp": 1605873894718411, - "fields": [ - { "key": "bytes", "type": "int64", "value": 210280 }, - { "key": "msg", "type": "string", "value": "bloom" } - ] - } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "22a3f914c23d3456", - "operationName": "block.Find", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894684570, - "duration": 34106, - "tags": [ - { "key": "blockID", "type": "string", "value": "4fca87b1-ceb1-4290-a3cb-c0a970a7c5a6" }, - { "key": "shardKey", "type": "int64", "value": 5 } - ], - "logs": [ - { - "timestamp": 1605873894718671, - "fields": [ - { "key": "bytes", "type": "int64", "value": 422528 }, - { "key": "msg", "type": "string", "value": "bloom" } - ] - } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "169359b95c501fae", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894683035, - "duration": 35830, - "tags": [], - "logs": [ - { "timestamp": 1605873894683041, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, - { "timestamp": 1605873894718864, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "71541fab4a38308f", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894683031, - "duration": 35841, - "tags": [ - { "key": "organization", "type": "string", "value": "1" }, - { "key": "span.kind", "type": "string", "value": "client" } - ], - "logs": [], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "3a7fc15a2fb60753", - "operationName": "block.Find", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894707659, - "duration": 11315, - "tags": [ - { "key": "blockID", "type": "string", "value": "57b3be9d-2234-4b8b-a380-424f30717e5b" }, - { "key": "shardKey", "type": "int64", "value": 5 } - ], - "logs": [ - { - "timestamp": 1605873894718970, - "fields": [ - { "key": "bytes", "type": "int64", "value": 437088 }, - { "key": "msg", "type": "string", "value": "bloom" } - ] - } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "10e57e001b6c6127", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894683210, - "duration": 35772, - "tags": [], - "logs": [ - { "timestamp": 1605873894683220, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, - { "timestamp": 1605873894718980, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "2c3fb8ad983d67fc", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894683206, - "duration": 35784, - "tags": [ - { "key": "organization", "type": "string", "value": "1" }, - { "key": "span.kind", "type": "string", "value": "client" } - ], - "logs": [], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "3fca8d21c0827061", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894684366, - "duration": 34674, - "tags": [], - "logs": [ - { "timestamp": 1605873894684372, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, - { "timestamp": 1605873894719039, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "25a226515c2f9150", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894684362, - "duration": 34687, - "tags": [ - { "key": "organization", "type": "string", "value": "1" }, - { "key": "span.kind", "type": "string", "value": "client" } - ], - "logs": [], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "5fb9111ac6a5d18d", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894683274, - "duration": 35965, - "tags": [], - "logs": [ - { "timestamp": 1605873894683286, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, - { "timestamp": 1605873894719238, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "6945097dfeae216a", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894683268, - "duration": 35979, - "tags": [ - { "key": "organization", "type": "string", "value": "1" }, - { "key": "span.kind", "type": "string", "value": "client" } - ], - "logs": [], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "6dd256468dea419f", - "operationName": "block.Find", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894684628, - "duration": 34986, - "tags": [ - { "key": "blockID", "type": "string", "value": "0a5b8e26-05d5-4df2-97c1-a57ccb631b5e" }, - { "key": "shardKey", "type": "int64", "value": 5 } - ], - "logs": [ - { - "timestamp": 1605873894719610, - "fields": [ - { "key": "bytes", "type": "int64", "value": 368392 }, - { "key": "msg", "type": "string", "value": "bloom" } - ] - } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "22c3bb99916b1cf9", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894684954, - "duration": 34724, - "tags": [], - "logs": [ - { "timestamp": 1605873894684961, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, - { "timestamp": 1605873894719678, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "7786d37aacb34302", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894684949, - "duration": 34735, - "tags": [ - { "key": "organization", "type": "string", "value": "1" }, - { "key": "span.kind", "type": "string", "value": "client" } - ], - "logs": [], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "0b4489a19011e658", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894702395, - "duration": 17621, - "tags": [], - "logs": [ - { "timestamp": 1605873894702404, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, - { "timestamp": 1605873894720015, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "5fe309dbb10a8aa0", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894702385, - "duration": 17639, - "tags": [ - { "key": "organization", "type": "string", "value": "1" }, - { "key": "span.kind", "type": "string", "value": "client" } - ], - "logs": [], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "28a88c33b44009c0", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894684502, - "duration": 35545, - "tags": [], - "logs": [ - { "timestamp": 1605873894684508, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, - { "timestamp": 1605873894720047, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "6d20c9fb0d7d023a", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894683045, - "duration": 37003, - "tags": [], - "logs": [ - { "timestamp": 1605873894683057, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, - { "timestamp": 1605873894720047, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "7aed634e79451eff", - "operationName": "block.Find", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894684353, - "duration": 35695, - "tags": [ - { "key": "blockID", "type": "string", "value": "c4b4a484-2704-49e8-926b-e7bb7a13c520" }, - { "key": "shardKey", "type": "int64", "value": 5 } - ], - "logs": [ - { - "timestamp": 1605873894720046, - "fields": [ - { "key": "bytes", "type": "int64", "value": 394640 }, - { "key": "msg", "type": "string", "value": "bloom" } - ] - } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "2361a627270177b2", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894684498, - "duration": 35556, - "tags": [ - { "key": "organization", "type": "string", "value": "1" }, - { "key": "span.kind", "type": "string", "value": "client" } - ], - "logs": [], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "6365c636dea9cf69", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894683040, - "duration": 37014, - "tags": [ - { "key": "organization", "type": "string", "value": "1" }, - { "key": "span.kind", "type": "string", "value": "client" } - ], - "logs": [], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "7fd1af0b8e4b13e5", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894704467, - "duration": 15587, - "tags": [], - "logs": [ - { "timestamp": 1605873894704477, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, - { "timestamp": 1605873894720054, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "4d0b05c2fe988374", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894704458, - "duration": 15600, - "tags": [ - { "key": "organization", "type": "string", "value": "1" }, - { "key": "span.kind", "type": "string", "value": "client" } - ], - "logs": [], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "45d91fa92cb81841", - "operationName": "block.Find", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894683191, - "duration": 36908, - "tags": [ - { "key": "blockID", "type": "string", "value": "abec5c1e-02b5-4165-8b3d-2940d3adb991" }, - { "key": "shardKey", "type": "int64", "value": 5 } - ], - "logs": [ - { - "timestamp": 1605873894720066, - "fields": [ - { "key": "bytes", "type": "int64", "value": 352392 }, - { "key": "msg", "type": "string", "value": "bloom" } - ] - } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "4287af315802d4cd", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894704355, - "duration": 15863, - "tags": [], - "logs": [ - { "timestamp": 1605873894704369, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, - { "timestamp": 1605873894720218, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "62b352c305041dcc", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894704348, - "duration": 15876, - "tags": [ - { "key": "organization", "type": "string", "value": "1" }, - { "key": "span.kind", "type": "string", "value": "client" } - ], - "logs": [], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "6ec165f264482f57", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894683346, - "duration": 37139, - "tags": [], - "logs": [ - { "timestamp": 1605873894683351, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, - { "timestamp": 1605873894720484, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "7400527184eeef19", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894683342, - "duration": 37152, - "tags": [ - { "key": "organization", "type": "string", "value": "1" }, - { "key": "span.kind", "type": "string", "value": "client" } - ], - "logs": [], - "processID": "p1", - "warnings": ["invalid parent span IDs=4ff7c150586c7e6f; skipping clock skew adjustment"] - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "1561e391ecd756d5", - "operationName": "block.Find", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894684939, - "duration": 35696, - "tags": [ - { "key": "blockID", "type": "string", "value": "7dbea947-c624-454c-a99a-b2aa0c96c19f" }, - { "key": "shardKey", "type": "int64", "value": 5 } - ], - "logs": [ - { - "timestamp": 1605873894720631, - "fields": [ - { "key": "bytes", "type": "int64", "value": 328592 }, - { "key": "msg", "type": "string", "value": "bloom" } - ] - } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "57f916fcf19f117f", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894682643, - "duration": 38011, - "tags": [], - "logs": [ - { "timestamp": 1605873894682653, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, - { "timestamp": 1605873894720650, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "1d4458304925bc0f", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894682638, - "duration": 38028, - "tags": [ - { "key": "organization", "type": "string", "value": "1" }, - { "key": "span.kind", "type": "string", "value": "client" } - ], - "logs": [], - "processID": "p1", - "warnings": ["invalid parent span IDs=3ff0fd3a1cdb9b5e; skipping clock skew adjustment"] - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "6f251bfe2c45ae12", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894709481, - "duration": 11241, - "tags": [], - "logs": [ - { "timestamp": 1605873894709489, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, - { "timestamp": 1605873894720722, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "5365877f5f1070a3", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894709476, - "duration": 11253, - "tags": [ - { "key": "organization", "type": "string", "value": "1" }, - { "key": "span.kind", "type": "string", "value": "client" } - ], - "logs": [], - "processID": "p1", - "warnings": ["invalid parent span IDs=5d1a0e533881c649; skipping clock skew adjustment"] - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "5472390246aac5c4", - "operationName": "block.Find", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894683265, - "duration": 37540, - "tags": [ - { "key": "blockID", "type": "string", "value": "31cd1597-b435-467c-8726-9fd43cb8f75a" }, - { "key": "shardKey", "type": "int64", "value": 5 } - ], - "logs": [ - { - "timestamp": 1605873894720802, - "fields": [ - { "key": "bytes", "type": "int64", "value": 263520 }, - { "key": "msg", "type": "string", "value": "bloom" } - ] - } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "38b14977915ca22c", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894683471, - "duration": 37385, - "tags": [], - "logs": [ - { "timestamp": 1605873894683477, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, - { "timestamp": 1605873894720855, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "08ecb88049158355", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894683466, - "duration": 37394, - "tags": [ - { "key": "organization", "type": "string", "value": "1" }, - { "key": "span.kind", "type": "string", "value": "client" } - ], - "logs": [], - "processID": "p1", - "warnings": ["invalid parent span IDs=241c721a4337e64b; skipping clock skew adjustment"] - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "4cb042697154defa", - "operationName": "block.Find", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894684490, - "duration": 36497, - "tags": [ - { "key": "blockID", "type": "string", "value": "37430ec1-eb84-4ad4-9bea-64b05bc05f0b" }, - { "key": "shardKey", "type": "int64", "value": 5 } - ], - "logs": [ - { - "timestamp": 1605873894720984, - "fields": [ - { "key": "bytes", "type": "int64", "value": 329440 }, - { "key": "msg", "type": "string", "value": "bloom" } - ] - } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "01afdbfe975f8d6d", - "operationName": "block.Find", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894704258, - "duration": 16738, - "tags": [ - { "key": "blockID", "type": "string", "value": "52136585-3c3c-418c-85bb-079c46f30ee8" }, - { "key": "shardKey", "type": "int64", "value": 5 } - ], - "logs": [ - { - "timestamp": 1605873894720994, - "fields": [ - { "key": "bytes", "type": "int64", "value": 271408 }, - { "key": "msg", "type": "string", "value": "bloom" } - ] - } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "1c881037e38b18ad", - "operationName": "block.Find", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894704334, - "duration": 16710, - "tags": [ - { "key": "blockID", "type": "string", "value": "293f4ca9-60cf-4dce-84f9-90d7a8903467" }, - { "key": "shardKey", "type": "int64", "value": 5 } - ], - "logs": [ - { - "timestamp": 1605873894721042, - "fields": [ - { "key": "bytes", "type": "int64", "value": 448208 }, - { "key": "msg", "type": "string", "value": "bloom" } - ] - } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "727cf2a7b14f8891", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894683209, - "duration": 37913, - "tags": [], - "logs": [ - { "timestamp": 1605873894683217, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, - { "timestamp": 1605873894721121, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "72a3d0dd535ed714", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894683196, - "duration": 37928, - "tags": [], - "logs": [ - { "timestamp": 1605873894683202, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, - { "timestamp": 1605873894721124, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "6e28aae41ed950a0", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894683204, - "duration": 37923, - "tags": [ - { "key": "organization", "type": "string", "value": "1" }, - { "key": "span.kind", "type": "string", "value": "client" } - ], - "logs": [], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "1034cca4b87566b9", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894683192, - "duration": 37937, - "tags": [ - { "key": "organization", "type": "string", "value": "1" }, - { "key": "span.kind", "type": "string", "value": "client" } - ], - "logs": [], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "5477c4334a555c1a", - "operationName": "block.Find", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894702369, - "duration": 18817, - "tags": [ - { "key": "blockID", "type": "string", "value": "65c35f00-7bf4-4d7c-884e-57e1f4f386f1" }, - { "key": "shardKey", "type": "int64", "value": 5 } - ], - "logs": [ - { - "timestamp": 1605873894721184, - "fields": [ - { "key": "bytes", "type": "int64", "value": 343160 }, - { "key": "msg", "type": "string", "value": "bloom" } - ] - } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "156254fce90fef6d", - "operationName": "block.Find", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894683261, - "duration": 38071, - "tags": [ - { "key": "blockID", "type": "string", "value": "5f6da848-1f43-4327-b791-c8607c834469" }, - { "key": "shardKey", "type": "int64", "value": 5 } - ], - "logs": [ - { - "timestamp": 1605873894721329, - "fields": [ - { "key": "bytes", "type": "int64", "value": 425008 }, - { "key": "msg", "type": "string", "value": "bloom" } - ] - } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "18174ee576735b69", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894683380, - "duration": 38087, - "tags": [], - "logs": [ - { "timestamp": 1605873894683386, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, - { "timestamp": 1605873894721466, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "3c984a418432da06", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894683376, - "duration": 38097, - "tags": [ - { "key": "organization", "type": "string", "value": "1" }, - { "key": "span.kind", "type": "string", "value": "client" } - ], - "logs": [], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "2cb4a90ec9e7ed56", - "operationName": "block.Find", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894684207, - "duration": 37304, - "tags": [ - { "key": "blockID", "type": "string", "value": "da15aeab-47f3-4150-a3a0-0b899f5728e0" }, - { "key": "shardKey", "type": "int64", "value": 5 } - ], - "logs": [ - { - "timestamp": 1605873894721505, - "fields": [ - { "key": "bytes", "type": "int64", "value": 435448 }, - { "key": "msg", "type": "string", "value": "bloom" } - ] - } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "31a678641a0daa14", - "operationName": "block.Find", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894683185, - "duration": 38722, - "tags": [ - { "key": "blockID", "type": "string", "value": "7f859498-9292-4be9-9902-9cc0cc94db7f" }, - { "key": "shardKey", "type": "int64", "value": 5 } - ], - "logs": [ - { - "timestamp": 1605873894721902, - "fields": [ - { "key": "bytes", "type": "int64", "value": 382184 }, - { "key": "msg", "type": "string", "value": "bloom" } - ] - } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "27ca437dde2b9612", - "operationName": "block.Find", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894683192, - "duration": 38914, - "tags": [ - { "key": "blockID", "type": "string", "value": "723cdf42-e4bc-48dc-bda5-6173eb15dee4" }, - { "key": "shardKey", "type": "int64", "value": 5 } - ], - "logs": [ - { - "timestamp": 1605873894722104, - "fields": [ - { "key": "bytes", "type": "int64", "value": 374272 }, - { "key": "msg", "type": "string", "value": "bloom" } - ] - } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "777ba94dbf7e2679", - "operationName": "block.Find", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894703342, - "duration": 18950, - "tags": [ - { "key": "blockID", "type": "string", "value": "9ffb7568-b253-46bf-ae30-275a5370abdd" }, - { "key": "shardKey", "type": "int64", "value": 5 } - ], - "logs": [ - { - "timestamp": 1605873894722288, - "fields": [ - { "key": "bytes", "type": "int64", "value": 296800 }, - { "key": "msg", "type": "string", "value": "bloom" } - ] - } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "447a1de4607678e0", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894716862, - "duration": 5567, - "tags": [], - "logs": [ - { "timestamp": 1605873894716881, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, - { "timestamp": 1605873894722428, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "4a1cb35fb165238f", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894716854, - "duration": 5582, - "tags": [ - { "key": "organization", "type": "string", "value": "1" }, - { "key": "span.kind", "type": "string", "value": "client" } - ], - "logs": [], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "263d88ba7760646a", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894707662, - "duration": 14938, - "tags": [], - "logs": [ - { "timestamp": 1605873894707677, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, - { "timestamp": 1605873894722599, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "7cb0a9332c646221", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894707654, - "duration": 14951, - "tags": [ - { "key": "organization", "type": "string", "value": "1" }, - { "key": "span.kind", "type": "string", "value": "client" } - ], - "logs": [], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "178dbf7349f30deb", - "operationName": "block.Find", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894682835, - "duration": 39846, - "tags": [ - { "key": "blockID", "type": "string", "value": "ae4b5b30-d87d-459f-8bfe-d05f4f169ced" }, - { "key": "shardKey", "type": "int64", "value": 5 } - ], - "logs": [ - { - "timestamp": 1605873894722679, - "fields": [ - { "key": "bytes", "type": "int64", "value": 384344 }, - { "key": "msg", "type": "string", "value": "bloom" } - ] - } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "150994409f1cb25a", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894700618, - "duration": 22118, - "tags": [], - "logs": [ - { "timestamp": 1605873894700633, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, - { "timestamp": 1605873894722736, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "585e5d65d550b215", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894700613, - "duration": 22129, - "tags": [ - { "key": "organization", "type": "string", "value": "1" }, - { "key": "span.kind", "type": "string", "value": "client" } - ], - "logs": [], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "27511615066e34db", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894684102, - "duration": 38879, - "tags": [], - "logs": [ - { "timestamp": 1605873894684108, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, - { "timestamp": 1605873894722980, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "79b947d9ed7866a6", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894684097, - "duration": 38891, - "tags": [ - { "key": "organization", "type": "string", "value": "1" }, - { "key": "span.kind", "type": "string", "value": "client" } - ], - "logs": [], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "6f2e6507fe12975c", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894717161, - "duration": 5887, - "tags": [], - "logs": [ - { "timestamp": 1605873894717174, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, - { "timestamp": 1605873894723047, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "00cdeb6a7479c53f", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894717155, - "duration": 5899, - "tags": [ - { "key": "organization", "type": "string", "value": "1" }, - { "key": "span.kind", "type": "string", "value": "client" } - ], - "logs": [], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "24bddd4ea3487e35", - "operationName": "block.Find", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894683371, - "duration": 39804, - "tags": [ - { "key": "blockID", "type": "string", "value": "d7601ebc-c33c-492c-a1da-4aa053533084" }, - { "key": "shardKey", "type": "int64", "value": 5 } - ], - "logs": [ - { - "timestamp": 1605873894723171, - "fields": [ - { "key": "bytes", "type": "int64", "value": 365144 }, - { "key": "msg", "type": "string", "value": "bloom" } - ] - } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "53d7bccf2fc103c5", - "operationName": "block.Find", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894716842, - "duration": 6390, - "tags": [ - { "key": "blockID", "type": "string", "value": "ca64f28a-77dc-4745-abdc-44054c1e5e40" }, - { "key": "shardKey", "type": "int64", "value": 5 } - ], - "logs": [ - { - "timestamp": 1605873894723228, - "fields": [ - { "key": "bytes", "type": "int64", "value": 289352 }, - { "key": "msg", "type": "string", "value": "bloom" } - ] - } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "552db884462abcc8", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894683762, - "duration": 39518, - "tags": [], - "logs": [ - { "timestamp": 1605873894683768, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, - { "timestamp": 1605873894723279, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "66bfd77009ceabee", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894683756, - "duration": 39531, - "tags": [ - { "key": "organization", "type": "string", "value": "1" }, - { "key": "span.kind", "type": "string", "value": "client" } - ], - "logs": [], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "39ecc86ead7ef908", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894684607, - "duration": 38759, - "tags": [], - "logs": [ - { "timestamp": 1605873894684613, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, - { "timestamp": 1605873894723365, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "23e63f9ee6638cc5", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894684602, - "duration": 38769, - "tags": [ - { "key": "organization", "type": "string", "value": "1" }, - { "key": "span.kind", "type": "string", "value": "client" } - ], - "logs": [], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "3995f8a937161d18", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894685603, - "duration": 37861, - "tags": [], - "logs": [ - { "timestamp": 1605873894685611, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, - { "timestamp": 1605873894723463, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "7d8cbf547f13bab8", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894685598, - "duration": 37871, - "tags": [ - { "key": "organization", "type": "string", "value": "1" }, - { "key": "span.kind", "type": "string", "value": "client" } - ], - "logs": [], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "50ea9bcb501f096f", - "operationName": "block.Find", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894707639, - "duration": 15983, - "tags": [ - { "key": "blockID", "type": "string", "value": "7ba16a23-7c29-4c4e-a0a6-f5a35697f61e" }, - { "key": "shardKey", "type": "int64", "value": 5 } - ], - "logs": [ - { - "timestamp": 1605873894723607, - "fields": [ - { "key": "bytes", "type": "int64", "value": 283976 }, - { "key": "msg", "type": "string", "value": "bloom" } - ] - } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "36174ad72177e7e0", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894683065, - "duration": 40713, - "tags": [], - "logs": [ - { "timestamp": 1605873894683103, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, - { "timestamp": 1605873894723777, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "6eb33f7265438984", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894683062, - "duration": 40722, - "tags": [ - { "key": "organization", "type": "string", "value": "1" }, - { "key": "span.kind", "type": "string", "value": "client" } - ], - "logs": [], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "0d173415b42b54df", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894684351, - "duration": 39618, - "tags": [], - "logs": [ - { "timestamp": 1605873894684357, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, - { "timestamp": 1605873894723968, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "0353b10977450cf7", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894684346, - "duration": 39628, - "tags": [ - { "key": "organization", "type": "string", "value": "1" }, - { "key": "span.kind", "type": "string", "value": "client" } - ], - "logs": [], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "7b094f4ebdce31c3", - "operationName": "block.Find", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894717141, - "duration": 6985, - "tags": [ - { "key": "blockID", "type": "string", "value": "f1a4a13f-5e5d-484e-8b53-1f4f8e1bad37" }, - { "key": "shardKey", "type": "int64", "value": 5 } - ], - "logs": [ - { - "timestamp": 1605873894724124, - "fields": [ - { "key": "bytes", "type": "int64", "value": 448472 }, - { "key": "msg", "type": "string", "value": "bloom" } - ] - } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "4bf98f62683b0e8e", - "operationName": "block.Find", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894700602, - "duration": 23557, - "tags": [ - { "key": "blockID", "type": "string", "value": "2c8d9e08-28c9-43e0-a38e-48e205e70c0a" }, - { "key": "shardKey", "type": "int64", "value": 5 } - ], - "logs": [ - { - "timestamp": 1605873894724156, - "fields": [ - { "key": "bytes", "type": "int64", "value": 454976 }, - { "key": "msg", "type": "string", "value": "bloom" } - ] - } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "3d32ea43a7ace6a3", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894685122, - "duration": 39094, - "tags": [], - "logs": [ - { "timestamp": 1605873894685153, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, - { "timestamp": 1605873894724214, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "42780d9e0c2beb80", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894685114, - "duration": 39108, - "tags": [ - { "key": "organization", "type": "string", "value": "1" }, - { "key": "span.kind", "type": "string", "value": "client" } - ], - "logs": [], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "6d4dfd6622f9d4e5", - "operationName": "block.Find", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894684594, - "duration": 39780, - "tags": [ - { "key": "blockID", "type": "string", "value": "6926ecb7-efff-41c5-ae95-85e0b8e75bed" }, - { "key": "shardKey", "type": "int64", "value": 5 } - ], - "logs": [ - { - "timestamp": 1605873894724372, - "fields": [ - { "key": "bytes", "type": "int64", "value": 364000 }, - { "key": "msg", "type": "string", "value": "bloom" } - ] - } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "462c7cc77e9bde26", - "operationName": "block.Find", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894684089, - "duration": 40384, - "tags": [ - { "key": "blockID", "type": "string", "value": "eacc5319-5c66-4ef0-bdc7-61ebcd665770" }, - { "key": "shardKey", "type": "int64", "value": 5 } - ], - "logs": [ - { - "timestamp": 1605873894724469, - "fields": [ - { "key": "bytes", "type": "int64", "value": 375312 }, - { "key": "msg", "type": "string", "value": "bloom" } - ] - } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "64c5e8cb8a8c9c87", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894684553, - "duration": 40087, - "tags": [], - "logs": [ - { "timestamp": 1605873894684559, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, - { "timestamp": 1605873894724639, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "6a2abf3fa1e44a02", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894684548, - "duration": 40098, - "tags": [ - { "key": "organization", "type": "string", "value": "1" }, - { "key": "span.kind", "type": "string", "value": "client" } - ], - "logs": [], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "5185d47ca37c94b2", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894684289, - "duration": 40372, - "tags": [], - "logs": [ - { "timestamp": 1605873894684296, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, - { "timestamp": 1605873894724661, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "63f3f66dc1b96cb5", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894684283, - "duration": 40383, - "tags": [ - { "key": "organization", "type": "string", "value": "1" }, - { "key": "span.kind", "type": "string", "value": "client" } - ], - "logs": [], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "09dfedf04619fd00", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894683527, - "duration": 41169, - "tags": [], - "logs": [ - { "timestamp": 1605873894683532, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, - { "timestamp": 1605873894724695, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "4fcf6b895ba07eb1", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894683524, - "duration": 41178, - "tags": [ - { "key": "organization", "type": "string", "value": "1" }, - { "key": "span.kind", "type": "string", "value": "client" } - ], - "logs": [], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "1d3e7eff78cb43af", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894684135, - "duration": 40664, - "tags": [], - "logs": [ - { "timestamp": 1605873894684142, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, - { "timestamp": 1605873894724800, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "43860f9f193430f0", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894684131, - "duration": 40675, - "tags": [ - { "key": "organization", "type": "string", "value": "1" }, - { "key": "span.kind", "type": "string", "value": "client" } - ], - "logs": [], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "7ea1f5a8b4dab8a7", - "operationName": "block.Find", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894684338, - "duration": 40775, - "tags": [ - { "key": "blockID", "type": "string", "value": "2906f33b-d748-4827-8eb7-de90927a65dd" }, - { "key": "shardKey", "type": "int64", "value": 5 } - ], - "logs": [ - { - "timestamp": 1605873894725108, - "fields": [ - { "key": "bytes", "type": "int64", "value": 431856 }, - { "key": "msg", "type": "string", "value": "bloom" } - ] - } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "512c60978bd2eac4", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894721019, - "duration": 4095, - "tags": [], - "logs": [ - { "timestamp": 1605873894721028, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, - { "timestamp": 1605873894725112, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "7e897df0e96d32b5", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894721010, - "duration": 4112, - "tags": [ - { "key": "organization", "type": "string", "value": "1" }, - { "key": "span.kind", "type": "string", "value": "client" } - ], - "logs": [], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "787b23c8fa301dd7", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894706900, - "duration": 18234, - "tags": [], - "logs": [ - { "timestamp": 1605873894706927, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, - { "timestamp": 1605873894725133, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "3ffd9ecc1161334a", - "operationName": "block.Find", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894683055, - "duration": 42085, - "tags": [ - { "key": "blockID", "type": "string", "value": "26f1bad8-cd58-4801-8785-d52b8d833a90" }, - { "key": "shardKey", "type": "int64", "value": 5 } - ], - "logs": [ - { - "timestamp": 1605873894725137, - "fields": [ - { "key": "bytes", "type": "int64", "value": 414056 }, - { "key": "msg", "type": "string", "value": "bloom" } - ] - } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "2f86e3ff470976d3", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894706887, - "duration": 18254, - "tags": [ - { "key": "organization", "type": "string", "value": "1" }, - { "key": "span.kind", "type": "string", "value": "client" } - ], - "logs": [], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "1a42c28bcd21acc8", - "operationName": "block.Find", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894685104, - "duration": 40144, - "tags": [ - { "key": "blockID", "type": "string", "value": "777d1eb8-cd33-44d5-8e34-2d253bd948a6" }, - { "key": "shardKey", "type": "int64", "value": 5 } - ], - "logs": [ - { - "timestamp": 1605873894725246, - "fields": [ - { "key": "bytes", "type": "int64", "value": 407792 }, - { "key": "msg", "type": "string", "value": "bloom" } - ] - } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "445f67ce7c86e918", - "operationName": "block.Find", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894684540, - "duration": 40905, - "tags": [ - { "key": "blockID", "type": "string", "value": "d4b28adc-eb54-4e54-8639-40acbe82196a" }, - { "key": "shardKey", "type": "int64", "value": 5 } - ], - "logs": [ - { - "timestamp": 1605873894725443, - "fields": [ - { "key": "bytes", "type": "int64", "value": 312232 }, - { "key": "msg", "type": "string", "value": "bloom" } - ] - } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "4dc9df94476d41ef", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894713432, - "duration": 12062, - "tags": [], - "logs": [ - { "timestamp": 1605873894713448, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, - { "timestamp": 1605873894725366, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "1f47248f05173a34", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894713423, - "duration": 12078, - "tags": [ - { "key": "organization", "type": "string", "value": "1" }, - { "key": "span.kind", "type": "string", "value": "client" } - ], - "logs": [], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "6ab47177b7f5e532", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894684151, - "duration": 41357, - "tags": [], - "logs": [ - { "timestamp": 1605873894684157, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, - { "timestamp": 1605873894725507, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "62088478a235ead5", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894684146, - "duration": 41368, - "tags": [ - { "key": "organization", "type": "string", "value": "1" }, - { "key": "span.kind", "type": "string", "value": "client" } - ], - "logs": [], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "021658e91c35b26e", - "operationName": "block.Find", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894683518, - "duration": 42121, - "tags": [ - { "key": "blockID", "type": "string", "value": "6deff928-b65a-437a-8d56-64d2397d9d1f" }, - { "key": "shardKey", "type": "int64", "value": 5 } - ], - "logs": [ - { - "timestamp": 1605873894725636, - "fields": [ - { "key": "bytes", "type": "int64", "value": 238936 }, - { "key": "msg", "type": "string", "value": "bloom" } - ] - } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "48401abd95ffa153", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894712421, - "duration": 13359, - "tags": [], - "logs": [ - { "timestamp": 1605873894712431, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, - { "timestamp": 1605873894725779, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "6a6bcedc4fc18a61", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894712416, - "duration": 13371, - "tags": [ - { "key": "organization", "type": "string", "value": "1" }, - { "key": "span.kind", "type": "string", "value": "client" } - ], - "logs": [], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "551f266c080ab0c6", - "operationName": "block.Find", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894684122, - "duration": 41711, - "tags": [ - { "key": "blockID", "type": "string", "value": "5180765b-50b6-4de3-abab-ceea6086afb8" }, - { "key": "shardKey", "type": "int64", "value": 5 } - ], - "logs": [ - { - "timestamp": 1605873894725830, - "fields": [ - { "key": "bytes", "type": "int64", "value": 393392 }, - { "key": "msg", "type": "string", "value": "bloom" } - ] - } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "72970316c65770af", - "operationName": "block.Find", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894706872, - "duration": 18990, - "tags": [ - { "key": "blockID", "type": "string", "value": "541a6a31-d25a-4275-9688-228355c81085" }, - { "key": "shardKey", "type": "int64", "value": 5 } - ], - "logs": [ - { - "timestamp": 1605873894725860, - "fields": [ - { "key": "bytes", "type": "int64", "value": 280296 }, - { "key": "msg", "type": "string", "value": "bloom" } - ] - } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "635a7471f4256c0b", - "operationName": "block.Find", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894684275, - "duration": 41695, - "tags": [ - { "key": "blockID", "type": "string", "value": "6bf57585-a03d-44e1-bd18-081b679d3e4a" }, - { "key": "shardKey", "type": "int64", "value": 5 } - ], - "logs": [ - { - "timestamp": 1605873894725967, - "fields": [ - { "key": "bytes", "type": "int64", "value": 434432 }, - { "key": "msg", "type": "string", "value": "bloom" } - ] - } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "7607fc837fc26251", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894683802, - "duration": 42223, - "tags": [], - "logs": [ - { "timestamp": 1605873894683808, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, - { "timestamp": 1605873894726025, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "01269c3f9d50434a", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894683798, - "duration": 42233, - "tags": [ - { "key": "organization", "type": "string", "value": "1" }, - { "key": "span.kind", "type": "string", "value": "client" } - ], - "logs": [], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "70114fe92b16120e", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894721062, - "duration": 5033, - "tags": [], - "logs": [ - { "timestamp": 1605873894721068, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, - { "timestamp": 1605873894726093, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "37a69429cff69860", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894721058, - "duration": 5043, - "tags": [ - { "key": "organization", "type": "string", "value": "1" }, - { "key": "span.kind", "type": "string", "value": "client" } - ], - "logs": [], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "5262951e45efa67d", - "operationName": "block.Find", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894720997, - "duration": 5110, - "tags": [ - { "key": "blockID", "type": "string", "value": "5c09e7bd-2f9c-42d3-8d2f-863e93eaa939" }, - { "key": "shardKey", "type": "int64", "value": 5 } - ], - "logs": [ - { - "timestamp": 1605873894726103, - "fields": [ - { "key": "bytes", "type": "int64", "value": 246840 }, - { "key": "msg", "type": "string", "value": "bloom" } - ] - } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "1d6a76dd2ca6c3e6", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894711590, - "duration": 14746, - "tags": [], - "logs": [ - { "timestamp": 1605873894711600, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, - { "timestamp": 1605873894726335, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "7c75cd286737359f", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894711582, - "duration": 14759, - "tags": [ - { "key": "organization", "type": "string", "value": "1" }, - { "key": "span.kind", "type": "string", "value": "client" } - ], - "logs": [], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "78415d3812916d77", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894707879, - "duration": 18497, - "tags": [], - "logs": [ - { "timestamp": 1605873894707887, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, - { "timestamp": 1605873894726375, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "5f7ac8c4fd5c680a", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894707874, - "duration": 18513, - "tags": [ - { "key": "organization", "type": "string", "value": "1" }, - { "key": "span.kind", "type": "string", "value": "client" } - ], - "logs": [], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "6b4bc2ee6a63726e", - "operationName": "block.Find", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894713413, - "duration": 13261, - "tags": [ - { "key": "blockID", "type": "string", "value": "b654e510-386a-470a-98c4-fb9833d21728" }, - { "key": "shardKey", "type": "int64", "value": 5 } - ], - "logs": [ - { - "timestamp": 1605873894726671, - "fields": [ - { "key": "bytes", "type": "int64", "value": 384056 }, - { "key": "msg", "type": "string", "value": "bloom" } - ] - } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "5f55f469fc2d6d29", - "operationName": "block.Find", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894712403, - "duration": 14367, - "tags": [ - { "key": "blockID", "type": "string", "value": "3fca3f89-a174-460d-9a87-92ed03555497" }, - { "key": "shardKey", "type": "int64", "value": 5 } - ], - "logs": [ - { - "timestamp": 1605873894726767, - "fields": [ - { "key": "bytes", "type": "int64", "value": 334896 }, - { "key": "msg", "type": "string", "value": "bloom" } - ] - } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "19235cb1f2dccb32", - "operationName": "block.Find", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894711567, - "duration": 15276, - "tags": [ - { "key": "blockID", "type": "string", "value": "de228107-4ff6-449e-9f1f-6ab34765ab68" }, - { "key": "shardKey", "type": "int64", "value": 5 } - ], - "logs": [ - { - "timestamp": 1605873894726841, - "fields": [ - { "key": "bytes", "type": "int64", "value": 225832 }, - { "key": "msg", "type": "string", "value": "bloom" } - ] - } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "29b186beff361524", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894699284, - "duration": 27562, - "tags": [], - "logs": [ - { "timestamp": 1605873894699298, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, - { "timestamp": 1605873894726845, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "0acce3e2af327bae", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894699278, - "duration": 27578, - "tags": [ - { "key": "organization", "type": "string", "value": "1" }, - { "key": "span.kind", "type": "string", "value": "client" } - ], - "logs": [], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "6b343c544d82bb3b", - "operationName": "block.Find", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894721050, - "duration": 5994, - "tags": [ - { "key": "blockID", "type": "string", "value": "459be32f-b91d-491b-aa61-5389b239eed8" }, - { "key": "shardKey", "type": "int64", "value": 5 } - ], - "logs": [ - { - "timestamp": 1605873894727041, - "fields": [ - { "key": "bytes", "type": "int64", "value": 429792 }, - { "key": "msg", "type": "string", "value": "bloom" } - ] - } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "62e3ccbe325de3d1", - "operationName": "block.Find", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894683746, - "duration": 43412, - "tags": [ - { "key": "blockID", "type": "string", "value": "a1b8740a-6430-4113-93f4-ad9c52e42d62" }, - { "key": "shardKey", "type": "int64", "value": 5 } - ], - "logs": [ - { - "timestamp": 1605873894727155, - "fields": [ - { "key": "bytes", "type": "int64", "value": 366096 }, - { "key": "msg", "type": "string", "value": "bloom" } - ] - } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "310178852fbf88e0", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894682731, - "duration": 44481, - "tags": [], - "logs": [ - { "timestamp": 1605873894682741, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, - { "timestamp": 1605873894727211, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "42bb5e9919ea40f4", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894682726, - "duration": 44493, - "tags": [ - { "key": "organization", "type": "string", "value": "1" }, - { "key": "span.kind", "type": "string", "value": "client" } - ], - "logs": [], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "150c8cfeedab6a38", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894708915, - "duration": 18374, - "tags": [], - "logs": [ - { "timestamp": 1605873894708922, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, - { "timestamp": 1605873894727288, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "1dda89d441503741", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894708910, - "duration": 18384, - "tags": [ - { "key": "organization", "type": "string", "value": "1" }, - { "key": "span.kind", "type": "string", "value": "client" } - ], - "logs": [], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "6c1c11a742626433", - "operationName": "block.Find", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894707863, - "duration": 19558, - "tags": [ - { "key": "blockID", "type": "string", "value": "ef68962f-224d-4b6c-9dcd-ef9be8607c72" }, - { "key": "shardKey", "type": "int64", "value": 5 } - ], - "logs": [ - { - "timestamp": 1605873894727419, - "fields": [ - { "key": "bytes", "type": "int64", "value": 423912 }, - { "key": "msg", "type": "string", "value": "bloom" } - ] - } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "4393a9fa65c8ceae", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894707853, - "duration": 19639, - "tags": [], - "logs": [ - { "timestamp": 1605873894707866, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, - { "timestamp": 1605873894727491, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "1392dcdbb07bc781", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894707848, - "duration": 19650, - "tags": [ - { "key": "organization", "type": "string", "value": "1" }, - { "key": "span.kind", "type": "string", "value": "client" } - ], - "logs": [], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "0804f1e82c8828e2", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894713028, - "duration": 14650, - "tags": [], - "logs": [ - { "timestamp": 1605873894713040, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, - { "timestamp": 1605873894727677, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "4e9e2ce15a6e596c", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894713023, - "duration": 14661, - "tags": [ - { "key": "organization", "type": "string", "value": "1" }, - { "key": "span.kind", "type": "string", "value": "client" } - ], - "logs": [], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "0c763a5ef614a2f4", - "operationName": "block.Find", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894685588, - "duration": 42409, - "tags": [ - { "key": "blockID", "type": "string", "value": "65fbe578-d535-4032-a12f-f249e7405363" }, - { "key": "shardKey", "type": "int64", "value": 5 } - ], - "logs": [ - { - "timestamp": 1605873894727993, - "fields": [ - { "key": "bytes", "type": "int64", "value": 441088 }, - { "key": "msg", "type": "string", "value": "bloom" } - ] - } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "31c1f2fd1bde2d49", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894682863, - "duration": 45441, - "tags": [], - "logs": [ - { "timestamp": 1605873894682870, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, - { "timestamp": 1605873894728303, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "2a694924a7a45244", - "operationName": "block.Find", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894682710, - "duration": 45639, - "tags": [ - { "key": "blockID", "type": "string", "value": "06a1301e-5b48-425e-a506-a4350afe3d0d" }, - { "key": "shardKey", "type": "int64", "value": 5 } - ], - "logs": [ - { - "timestamp": 1605873894728346, - "fields": [ - { "key": "bytes", "type": "int64", "value": 418168 }, - { "key": "msg", "type": "string", "value": "bloom" } - ] - } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "73f7696fdccac589", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894682859, - "duration": 45516, - "tags": [ - { "key": "organization", "type": "string", "value": "1" }, - { "key": "span.kind", "type": "string", "value": "client" } - ], - "logs": [], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "4b52acf382a86008", - "operationName": "block.Find", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894684137, - "duration": 44241, - "tags": [ - { "key": "blockID", "type": "string", "value": "57df43b4-8552-49e3-a1cd-f442609deaf2" }, - { "key": "shardKey", "type": "int64", "value": 5 } - ], - "logs": [ - { - "timestamp": 1605873894728373, - "fields": [ - { "key": "bytes", "type": "int64", "value": 425104 }, - { "key": "msg", "type": "string", "value": "bloom" } - ] - } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "18688fac379c4a9e", - "operationName": "block.Find", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894707836, - "duration": 20550, - "tags": [ - { "key": "blockID", "type": "string", "value": "c53e7db4-34cd-43d0-9383-de5e40936182" }, - { "key": "shardKey", "type": "int64", "value": 5 } - ], - "logs": [ - { - "timestamp": 1605873894728383, - "fields": [ - { "key": "bytes", "type": "int64", "value": 389928 }, - { "key": "msg", "type": "string", "value": "bloom" } - ] - } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "5fccf30d8fc010b8", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894726139, - "duration": 2327, - "tags": [], - "logs": [ - { "timestamp": 1605873894726153, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, - { "timestamp": 1605873894728465, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "1c2bf57fc386ac18", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894726130, - "duration": 2343, - "tags": [ - { "key": "organization", "type": "string", "value": "1" }, - { "key": "span.kind", "type": "string", "value": "client" } - ], - "logs": [], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "7cfea87d3c8d06ca", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894685457, - "duration": 43096, - "tags": [], - "logs": [ - { "timestamp": 1605873894685465, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, - { "timestamp": 1605873894728552, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "12a80c97c2a42f05", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894685452, - "duration": 43107, - "tags": [ - { "key": "organization", "type": "string", "value": "1" }, - { "key": "span.kind", "type": "string", "value": "client" } - ], - "logs": [], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "08122694d5e37cb7", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894723200, - "duration": 5455, - "tags": [], - "logs": [ - { "timestamp": 1605873894723209, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, - { "timestamp": 1605873894728654, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "313e6147867246d6", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894723194, - "duration": 5466, - "tags": [ - { "key": "organization", "type": "string", "value": "1" }, - { "key": "span.kind", "type": "string", "value": "client" } - ], - "logs": [], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "2e4f73b3315eb612", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894715649, - "duration": 13049, - "tags": [], - "logs": [ - { "timestamp": 1605873894715658, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, - { "timestamp": 1605873894728698, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "7e0457958022516b", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894715644, - "duration": 13060, - "tags": [ - { "key": "organization", "type": "string", "value": "1" }, - { "key": "span.kind", "type": "string", "value": "client" } - ], - "logs": [], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "04db9d1320bc1720", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894683309, - "duration": 45454, - "tags": [], - "logs": [ - { "timestamp": 1605873894683316, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, - { "timestamp": 1605873894728762, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "4702ca2057b120a0", - "operationName": "block.Find", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894726116, - "duration": 2652, - "tags": [ - { "key": "blockID", "type": "string", "value": "ec5b73d8-61f8-4210-8838-e6cfd253294b" }, - { "key": "shardKey", "type": "int64", "value": 5 } - ], - "logs": [ - { - "timestamp": 1605873894728766, - "fields": [ - { "key": "bytes", "type": "int64", "value": 173264 }, - { "key": "msg", "type": "string", "value": "bloom" } - ] - } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "5e2c536cf48b42a8", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894683305, - "duration": 45464, - "tags": [ - { "key": "organization", "type": "string", "value": "1" }, - { "key": "span.kind", "type": "string", "value": "client" } - ], - "logs": [], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "69bd1c5d5184626b", - "operationName": "block.Find", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894708900, - "duration": 19981, - "tags": [ - { "key": "blockID", "type": "string", "value": "de324468-b889-4f4c-af77-907353a719cd" }, - { "key": "shardKey", "type": "int64", "value": 5 } - ], - "logs": [ - { - "timestamp": 1605873894728878, - "fields": [ - { "key": "bytes", "type": "int64", "value": 307032 }, - { "key": "msg", "type": "string", "value": "bloom" } - ] - } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "6849c669006c2759", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894714727, - "duration": 14480, - "tags": [], - "logs": [ - { "timestamp": 1605873894714736, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, - { "timestamp": 1605873894729206, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "6836249c56ca87ae", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894714722, - "duration": 14490, - "tags": [ - { "key": "organization", "type": "string", "value": "1" }, - { "key": "span.kind", "type": "string", "value": "client" } - ], - "logs": [], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "4614a3c3374430a7", - "operationName": "block.Find", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894682850, - "duration": 46413, - "tags": [ - { "key": "blockID", "type": "string", "value": "ea416fc3-eedc-413e-9cc1-d8fd3548cfe6" }, - { "key": "shardKey", "type": "int64", "value": 5 } - ], - "logs": [ - { - "timestamp": 1605873894729260, - "fields": [ - { "key": "bytes", "type": "int64", "value": 392160 }, - { "key": "msg", "type": "string", "value": "bloom" } - ] - } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "33a5b0766e98269c", - "operationName": "block.Find", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894685441, - "duration": 44269, - "tags": [ - { "key": "blockID", "type": "string", "value": "b50c3305-8d80-42fc-88a4-97a8604c7066" }, - { "key": "shardKey", "type": "int64", "value": 5 } - ], - "logs": [ - { - "timestamp": 1605873894729705, - "fields": [ - { "key": "bytes", "type": "int64", "value": 346072 }, - { "key": "msg", "type": "string", "value": "bloom" } - ] - } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "38da2ce44e352b40", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894722577, - "duration": 7233, - "tags": [], - "logs": [ - { "timestamp": 1605873894722588, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, - { "timestamp": 1605873894729809, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "2a0fc54146d07c21", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894722569, - "duration": 7247, - "tags": [ - { "key": "organization", "type": "string", "value": "1" }, - { "key": "span.kind", "type": "string", "value": "client" } - ], - "logs": [], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "398ed8e3573cf781", - "operationName": "block.Find", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894715632, - "duration": 14249, - "tags": [ - { "key": "blockID", "type": "string", "value": "2d9f964a-aac5-42e1-b410-866ad1706d7a" }, - { "key": "shardKey", "type": "int64", "value": 5 } - ], - "logs": [ - { - "timestamp": 1605873894729879, - "fields": [ - { "key": "bytes", "type": "int64", "value": 441600 }, - { "key": "msg", "type": "string", "value": "bloom" } - ] - } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "47c93569ac9ecd04", - "operationName": "block.Find", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894713013, - "duration": 16911, - "tags": [ - { "key": "blockID", "type": "string", "value": "96c1b791-af15-4554-a1bb-b0f200625856" }, - { "key": "shardKey", "type": "int64", "value": 5 } - ], - "logs": [ - { - "timestamp": 1605873894729919, - "fields": [ - { "key": "bytes", "type": "int64", "value": 446504 }, - { "key": "msg", "type": "string", "value": "bloom" } - ] - } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "3d61171be03f3db4", - "operationName": "block.Find", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894714709, - "duration": 15261, - "tags": [ - { "key": "blockID", "type": "string", "value": "ae957436-40cc-4d15-a3c3-26d10613aaf6" }, - { "key": "shardKey", "type": "int64", "value": 5 } - ], - "logs": [ - { - "timestamp": 1605873894729967, - "fields": [ - { "key": "bytes", "type": "int64", "value": 287976 }, - { "key": "msg", "type": "string", "value": "bloom" } - ] - } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "3de17f2475734d79", - "operationName": "block.Find", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894723182, - "duration": 6796, - "tags": [ - { "key": "blockID", "type": "string", "value": "eb218dd8-99fa-4de7-87cd-8998b89e0778" }, - { "key": "shardKey", "type": "int64", "value": 5 } - ], - "logs": [ - { - "timestamp": 1605873894729976, - "fields": [ - { "key": "bytes", "type": "int64", "value": 403400 }, - { "key": "msg", "type": "string", "value": "bloom" } - ] - } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "712c995480f17232", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894713402, - "duration": 16771, - "tags": [], - "logs": [ - { "timestamp": 1605873894713412, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, - { "timestamp": 1605873894730172, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "26950bc4bf84c34b", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894713392, - "duration": 16787, - "tags": [ - { "key": "organization", "type": "string", "value": "1" }, - { "key": "span.kind", "type": "string", "value": "client" } - ], - "logs": [], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "1bd3c2e5acbea9c4", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894701374, - "duration": 29019, - "tags": [], - "logs": [ - { "timestamp": 1605873894701383, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, - { "timestamp": 1605873894730393, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "2106e0853647ac08", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894701369, - "duration": 29030, - "tags": [ - { "key": "organization", "type": "string", "value": "1" }, - { "key": "span.kind", "type": "string", "value": "client" } - ], - "logs": [], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "6e59a327558a7329", - "operationName": "block.Find", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894699265, - "duration": 31183, - "tags": [ - { "key": "blockID", "type": "string", "value": "d8b5fee2-e2b3-445c-8ea9-243519a5c104" }, - { "key": "shardKey", "type": "int64", "value": 5 } - ], - "logs": [ - { - "timestamp": 1605873894730443, - "fields": [ - { "key": "bytes", "type": "int64", "value": 400224 }, - { "key": "msg", "type": "string", "value": "bloom" } - ] - } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "037acce8385b1858", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894721017, - "duration": 9605, - "tags": [], - "logs": [ - { "timestamp": 1605873894721028, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, - { "timestamp": 1605873894730621, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "3547a2504168d8c7", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894721012, - "duration": 9617, - "tags": [ - { "key": "organization", "type": "string", "value": "1" }, - { "key": "span.kind", "type": "string", "value": "client" } - ], - "logs": [], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "0dc60016513590c8", - "operationName": "block.Find", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894722299, - "duration": 8583, - "tags": [ - { "key": "blockID", "type": "string", "value": "69120461-fbd8-4ca0-a5fb-957d745a22a8" }, - { "key": "shardKey", "type": "int64", "value": 5 } - ], - "logs": [ - { - "timestamp": 1605873894730879, - "fields": [ - { "key": "bytes", "type": "int64", "value": 370472 }, - { "key": "msg", "type": "string", "value": "bloom" } - ] - } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "5d18bf1b78bd4e75", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894684396, - "duration": 46590, - "tags": [], - "logs": [ - { "timestamp": 1605873894684402, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, - { "timestamp": 1605873894730985, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "3bdc5547104db28c", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894684392, - "duration": 46600, - "tags": [ - { "key": "organization", "type": "string", "value": "1" }, - { "key": "span.kind", "type": "string", "value": "client" } - ], - "logs": [], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "682e6d017ce71dad", - "operationName": "block.Find", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894713268, - "duration": 17796, - "tags": [ - { "key": "blockID", "type": "string", "value": "84644a06-da08-4c3a-936f-70a634d8052d" }, - { "key": "shardKey", "type": "int64", "value": 5 } - ], - "logs": [ - { - "timestamp": 1605873894731057, - "fields": [ - { "key": "bytes", "type": "int64", "value": 353808 }, - { "key": "msg", "type": "string", "value": "bloom" } - ] - } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "3bbbdf937ccb2ccb", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894721935, - "duration": 9258, - "tags": [], - "logs": [ - { "timestamp": 1605873894721944, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, - { "timestamp": 1605873894731193, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "7c40bff5b749a532", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894721928, - "duration": 9272, - "tags": [ - { "key": "organization", "type": "string", "value": "1" }, - { "key": "span.kind", "type": "string", "value": "client" } - ], - "logs": [], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "31f86900e7598e55", - "operationName": "block.Find", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894701358, - "duration": 29885, - "tags": [ - { "key": "blockID", "type": "string", "value": "186f31e1-409b-4c9c-95b5-abc662389d3b" }, - { "key": "shardKey", "type": "int64", "value": 5 } - ], - "logs": [ - { - "timestamp": 1605873894731240, - "fields": [ - { "key": "bytes", "type": "int64", "value": 434496 }, - { "key": "msg", "type": "string", "value": "bloom" } - ] - } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "7471b3c5a188e8da", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894729995, - "duration": 1327, - "tags": [], - "logs": [ - { "timestamp": 1605873894730003, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, - { "timestamp": 1605873894731321, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "33cb5e55876f584c", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894729989, - "duration": 1338, - "tags": [ - { "key": "organization", "type": "string", "value": "1" }, - { "key": "span.kind", "type": "string", "value": "client" } - ], - "logs": [], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "58b7d0b41550e88f", - "operationName": "block.Find", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894729978, - "duration": 1396, - "tags": [ - { "key": "blockID", "type": "string", "value": "e0d8f1ac-a48f-4dea-868f-183e1a124fb5" }, - { "key": "shardKey", "type": "int64", "value": 5 } - ], - "logs": [ - { - "timestamp": 1605873894731373, - "fields": [ - { "key": "bytes", "type": "int64", "value": 16488 }, - { "key": "msg", "type": "string", "value": "bloom" } - ] - } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "70405f4198f01d16", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894721544, - "duration": 9893, - "tags": [], - "logs": [ - { "timestamp": 1605873894721555, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, - { "timestamp": 1605873894731436, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "56f0f0d7120ea5a5", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894721540, - "duration": 9903, - "tags": [ - { "key": "organization", "type": "string", "value": "1" }, - { "key": "span.kind", "type": "string", "value": "client" } - ], - "logs": [], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "69bb9bf8c37d9faf", - "operationName": "block.Find", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894683022, - "duration": 48646, - "tags": [ - { "key": "blockID", "type": "string", "value": "cf7747f5-68f9-490b-840d-9975c057c7e6" }, - { "key": "shardKey", "type": "int64", "value": 5 } - ], - "logs": [ - { - "timestamp": 1605873894731663, - "fields": [ - { "key": "bytes", "type": "int64", "value": 425640 }, - { "key": "msg", "type": "string", "value": "bloom" } - ] - } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "4ef61721dfd7f61b", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894720840, - "duration": 10841, - "tags": [], - "logs": [ - { "timestamp": 1605873894720869, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, - { "timestamp": 1605873894731680, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "70e8aa6d13e56007", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894720827, - "duration": 10860, - "tags": [ - { "key": "organization", "type": "string", "value": "1" }, - { "key": "span.kind", "type": "string", "value": "client" } - ], - "logs": [], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "297ff96c736c18f4", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894705333, - "duration": 26573, - "tags": [], - "logs": [ - { "timestamp": 1605873894705342, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, - { "timestamp": 1605873894731904, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "6a80209f067be4f5", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894705328, - "duration": 26585, - "tags": [ - { "key": "organization", "type": "string", "value": "1" }, - { "key": "span.kind", "type": "string", "value": "client" } - ], - "logs": [], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "5b3db530e83db855", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894731272, - "duration": 796, - "tags": [], - "logs": [ - { "timestamp": 1605873894731284, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, - { "timestamp": 1605873894732067, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } - ], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "6b2458a9486a4298", - "operationName": "Memcache.GetMulti", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894731265, - "duration": 886, - "tags": [ - { "key": "organization", "type": "string", "value": "1" }, - { "key": "span.kind", "type": "string", "value": "client" } - ], - "logs": [], - "processID": "p1", - "warnings": null - }, - { - "traceID": "3fa414edcef6ad90", - "spanID": "3139145bb422702e", - "operationName": "block.Find", - "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], - "startTime": 1605873894731251, - "duration": 941, - "tags": [ - { "key": "blockID", "type": "string", "value": "f9fd03ba-91a8-476b-b809-d2b11bfa790d" }, - { "key": "shardKey", "type": "int64", "value": 5 } - ], - "logs": [ - { - "timestamp": 1605873894732190, - "fields": [ - { "key": "bytes", "type": "int64", "value": 8680 }, - { "key": "msg", "type": "string", "value": "bloom" } - ] - } - ], - "processID": "p1", - "warnings": null - } - ], - "processes": { - "p1": { - "serviceName": "s1" - } - }, - "warnings": null - } - ], - "total": 0, - "limit": 0, - "offset": 0, - "errors": null -} diff --git a/packages/grafana-e2e/cypress/fixtures/prometheus-query-range-response.json b/packages/grafana-e2e/cypress/fixtures/prometheus-query-range-response.json deleted file mode 100644 index 6a2501a45df..00000000000 --- a/packages/grafana-e2e/cypress/fixtures/prometheus-query-range-response.json +++ /dev/null @@ -1,81 +0,0 @@ -{ - "status": "success", - "data": { - "resultType": "matrix", - "result": [ - { - "metric": {}, - "values": [ - [1620758235, "0.07554431352019486"], - [1620758250, "0.0756695553961457"], - [1620758265, "0.0757369945411682"], - [1620758280, "0.07560212035898113"], - [1620758295, "0.07556358506832812"], - [1620758310, "0.07558766859344893"], - [1620758325, "0.07552022996976834"], - [1620758340, "0.07553949807996531"], - [1620758355, "0.07554913414209416"], - [1620758370, "0.07539017545449077"], - [1620758385, "0.07524566527721041"], - [1620758400, "0.06631294924007665"], - [1620758415, "0.020769530989205368"], - [1620758430, "0.05720168751283235"], - [1620758445, "0.07271760187022697"], - [1620758460, "0.07282398348834057"], - [1620758475, "0.07272243619599422"], - [1620758490, "0.0727659581600079"], - [1620758505, "0.07290135207155769"], - [1620758520, "0.07293036876672591"], - [1620758535, "0.0727901374111541"], - [1620758550, "0.07272727333735175"], - [1620758565, "0.07264506733699574"], - [1620758580, "0.07272243607717656"], - [1620758595, "0.0728288184987238"], - [1620758610, "0.07298839709448537"], - [1620758625, "0.07301257421338406"], - [1620758640, "0.07304158515671498"], - [1620758655, "0.07311895518980911"], - [1620758670, "0.07325918868870857"], - [1620758685, "0.07340909025275498"], - [1620758700, "0.06640878600261439"], - [1620758715, "0.016943481796378928"], - [1620758730, "0.009846410786372045"], - [1620758745, "0.009846533933076818"], - [1620758760, "0.009865643995544734"], - [1620758775, "0.009877495333796778"], - [1620758790, "0.009894557340703772"], - [1620758805, "0.0098843910341446"], - [1620758820, "0.00990408341969324"], - [1620758835, "0.00989844441243741"], - [1620758850, "0.009889907575638773"], - [1620758865, "0.009918898761738633"], - [1620758880, "0.009937127911002756"], - [1620758895, "0.009940908363410796"], - [1620758910, "0.00998103477604732"], - [1620758925, "0.009972785096318881"], - [1620758940, "0.012851280416358784"], - [1620758955, "0.016073228821362785"], - [1620758970, "0.020414802032173343"], - [1620761580, "0.007599075245347286"], - [1620761595, "0.008931710803442608"], - [1620761610, "0.008726716914241494"], - [1620761625, "0.008200081743024097"], - [1620761640, "0.00855242238708798"], - [1620761655, "0.008286349295644651"], - [1620761670, "0.008226278261449314"], - [1620761685, "0.008195191146355274"], - [1620761700, "0.008187372718523614"], - [1620761715, "0.008513095070485845"], - [1620761730, "0.08239661322810221"], - [1620761745, "0.0859446307478243"], - [1620761760, "0.08307358128715034"], - [1620761775, "0.08068720480328369"], - [1620761790, "0.07619009806120529"], - [1620761805, "0.0750613052160521"], - [1620761820, "0.07146092807229597"], - [1620761835, "0.06898128960085806"] - ] - } - ] - } -} diff --git a/packages/grafana-e2e/cypress/fixtures/prometheus-query-response.json b/packages/grafana-e2e/cypress/fixtures/prometheus-query-response.json deleted file mode 100644 index 56df1d882b5..00000000000 --- a/packages/grafana-e2e/cypress/fixtures/prometheus-query-response.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "status": "success", - "data": { "resultType": "vector", "result": [{ "metric": {}, "value": [1620761849, "0.06765848222986065"] }] } -} diff --git a/packages/grafana-e2e/cypress/fixtures/tempo-response.json b/packages/grafana-e2e/cypress/fixtures/tempo-response.json deleted file mode 100644 index 30136a226a5..00000000000 --- a/packages/grafana-e2e/cypress/fixtures/tempo-response.json +++ /dev/null @@ -1,1181 +0,0 @@ -{ - "results": { - "A": { - "frames": [ - { - "schema": { - "name": "Trace", - "refId": "A", - "meta": { - "preferredVisualisationType": "trace" - }, - "fields": [ - { - "name": "traceID", - "type": "string", - "typeInfo": { - "frame": "string" - } - }, - { - "name": "spanID", - "type": "string", - "typeInfo": { - "frame": "string" - } - }, - { - "name": "parentSpanID", - "type": "string", - "typeInfo": { - "frame": "string" - } - }, - { - "name": "operationName", - "type": "string", - "typeInfo": { - "frame": "string" - } - }, - { - "name": "serviceName", - "type": "string", - "typeInfo": { - "frame": "string" - } - }, - { - "name": "serviceTags", - "type": "other", - "typeInfo": { - "frame": "other" - } - }, - { - "name": "startTime", - "type": "number", - "typeInfo": { - "frame": "float64" - } - }, - { - "name": "duration", - "type": "number", - "typeInfo": { - "frame": "float64" - } - }, - { - "name": "logs", - "type": "other", - "typeInfo": { - "frame": "other" - } - }, - { - "name": "tags", - "type": "other", - "typeInfo": { - "frame": "other" - } - } - ] - }, - "data": { - "values": [ - [ - "04829080550953998599", - "04829080550953998599", - "04829080550953998599", - "04829080550953998599", - "04829080550953998599", - "04829080550953998599", - "04829080550953998599", - "04829080550953998599", - "04829080550953998599", - "04829080550953998599", - "04829080550953998599" - ], - [ - "4829080550953998599", - "2017565205134657253", - "8342878680821632593", - "1224460974534844837", - "275963539390240345", - "8319197348330196958", - "6652870949263121722", - "607757742690372462", - "4351457163250103362", - "6171925700166476032", - "7554371986156978275" - ], - [ - null, - "4829080550953998599", - "2017565205134657253", - "8342878680821632593", - "1224460974534844837", - "275963539390240345", - "4829080550953998599", - "6652870949263121722", - "607757742690372462", - "4351457163250103362", - "6171925700166476032" - ], - [ - "HTTP Client", - "HTTP POST", - "HTTP POST - post", - "HTTP Client", - "HTTP POST", - "HTTP POST - post", - "HTTP GET", - "HTTP GET - root", - "HTTP Client", - "HTTP GET", - "HTTP GET - root" - ], - ["lb", "lb", "app", "app", "app", "db", "lb", "app", "app", "app", "db"], - [ - [ - { - "value": "lb", - "key": "service.name" - }, - { - "value": "tns/loadgen", - "key": "job" - }, - { - "value": "Jaeger-Go-2.22.1", - "key": "opencensus.exporterversion" - }, - { - "value": "f55ab7e317a6", - "key": "host.name" - }, - { - "value": "172.24.0.8", - "key": "ip" - }, - { - "value": "36042b5ca9f81d60", - "key": "client-uuid" - } - ], - [ - { - "value": "lb", - "key": "service.name" - }, - { - "value": "tns/loadgen", - "key": "job" - }, - { - "value": "Jaeger-Go-2.22.1", - "key": "opencensus.exporterversion" - }, - { - "value": "f55ab7e317a6", - "key": "host.name" - }, - { - "value": "172.24.0.8", - "key": "ip" - }, - { - "value": "36042b5ca9f81d60", - "key": "client-uuid" - } - ], - [ - { - "value": "app", - "key": "service.name" - }, - { - "value": "tns/app", - "key": "job" - }, - { - "value": "Jaeger-Go-2.22.1", - "key": "opencensus.exporterversion" - }, - { - "value": "7945a05e75db", - "key": "host.name" - }, - { - "value": "172.24.0.7", - "key": "ip" - }, - { - "value": "78f645af1e60163d", - "key": "client-uuid" - } - ], - [ - { - "value": "app", - "key": "service.name" - }, - { - "value": "tns/app", - "key": "job" - }, - { - "value": "Jaeger-Go-2.22.1", - "key": "opencensus.exporterversion" - }, - { - "value": "7945a05e75db", - "key": "host.name" - }, - { - "value": "172.24.0.7", - "key": "ip" - }, - { - "value": "78f645af1e60163d", - "key": "client-uuid" - } - ], - [ - { - "value": "app", - "key": "service.name" - }, - { - "value": "tns/app", - "key": "job" - }, - { - "value": "Jaeger-Go-2.22.1", - "key": "opencensus.exporterversion" - }, - { - "value": "7945a05e75db", - "key": "host.name" - }, - { - "value": "172.24.0.7", - "key": "ip" - }, - { - "value": "78f645af1e60163d", - "key": "client-uuid" - } - ], - [ - { - "value": "db", - "key": "service.name" - }, - { - "value": "tns/db", - "key": "job" - }, - { - "value": "Jaeger-Go-2.22.1", - "key": "opencensus.exporterversion" - }, - { - "value": "aae464791221", - "key": "host.name" - }, - { - "value": "172.24.0.2", - "key": "ip" - }, - { - "value": "18b1a6b5278dd643", - "key": "client-uuid" - } - ], - [ - { - "value": "lb", - "key": "service.name" - }, - { - "value": "tns/loadgen", - "key": "job" - }, - { - "value": "Jaeger-Go-2.22.1", - "key": "opencensus.exporterversion" - }, - { - "value": "f55ab7e317a6", - "key": "host.name" - }, - { - "value": "172.24.0.8", - "key": "ip" - }, - { - "value": "36042b5ca9f81d60", - "key": "client-uuid" - } - ], - [ - { - "value": "app", - "key": "service.name" - }, - { - "value": "tns/app", - "key": "job" - }, - { - "value": "Jaeger-Go-2.22.1", - "key": "opencensus.exporterversion" - }, - { - "value": "7945a05e75db", - "key": "host.name" - }, - { - "value": "172.24.0.7", - "key": "ip" - }, - { - "value": "78f645af1e60163d", - "key": "client-uuid" - } - ], - [ - { - "value": "app", - "key": "service.name" - }, - { - "value": "tns/app", - "key": "job" - }, - { - "value": "Jaeger-Go-2.22.1", - "key": "opencensus.exporterversion" - }, - { - "value": "7945a05e75db", - "key": "host.name" - }, - { - "value": "172.24.0.7", - "key": "ip" - }, - { - "value": "78f645af1e60163d", - "key": "client-uuid" - } - ], - [ - { - "value": "app", - "key": "service.name" - }, - { - "value": "tns/app", - "key": "job" - }, - { - "value": "Jaeger-Go-2.22.1", - "key": "opencensus.exporterversion" - }, - { - "value": "7945a05e75db", - "key": "host.name" - }, - { - "value": "172.24.0.7", - "key": "ip" - }, - { - "value": "78f645af1e60163d", - "key": "client-uuid" - } - ], - [ - { - "value": "db", - "key": "service.name" - }, - { - "value": "tns/db", - "key": "job" - }, - { - "value": "Jaeger-Go-2.22.1", - "key": "opencensus.exporterversion" - }, - { - "value": "aae464791221", - "key": "host.name" - }, - { - "value": "172.24.0.2", - "key": "ip" - }, - { - "value": "18b1a6b5278dd643", - "key": "client-uuid" - } - ] - ], - [ - 1620761543730.354, 1620761543730.3628, 1620761543732.044, 1620761543732.163, 1620761543732.191, - 1620761543733.925, 1620761543735.194, 1620761543738.197, 1620761543738.394, 1620761543738.398, - 1620761543744.6929 - ], - [17.826, 4.824, 2.588, 2.403, 2.401, 0.142, 13.266, 11.281, 8.593, 10.87, 0.136], - [ - null, - [ - { - "timestamp": 1620761543730.414, - "fields": [ - { - "value": "GetConn", - "key": "event" - } - ] - }, - { - "timestamp": 1620761543730.475, - "fields": [ - { - "value": "DNSStart", - "key": "event" - }, - { - "value": "app", - "key": "host" - } - ] - }, - { - "timestamp": 1620761543731.1738, - "fields": [ - { - "value": "DNSDone", - "key": "event" - }, - { - "value": "172.24.0.7", - "key": "addr" - } - ] - }, - { - "timestamp": 1620761543731.184, - "fields": [ - { - "value": "ConnectStart", - "key": "event" - }, - { - "value": "tcp", - "key": "network" - }, - { - "value": "172.24.0.7: 80", - "key": "addr" - } - ] - }, - { - "timestamp": 1620761543731.6228, - "fields": [ - { - "value": "ConnectDone", - "key": "event" - }, - { - "value": "tcp", - "key": "network" - }, - { - "value": "172.24.0.7: 80", - "key": "addr" - } - ] - }, - { - "timestamp": 1620761543731.678, - "fields": [ - { - "value": "GotConn", - "key": "event" - } - ] - }, - { - "timestamp": 1620761543731.737, - "fields": [ - { - "value": "WroteHeaders", - "key": "event" - } - ] - }, - { - "timestamp": 1620761543731.742, - "fields": [ - { - "value": "WroteRequest", - "key": "event" - } - ] - }, - { - "timestamp": 1620761543735.001, - "fields": [ - { - "value": "GotFirstResponseByte", - "key": "event" - } - ] - }, - { - "timestamp": 1620761543735.186, - "fields": [ - { - "value": "ClosedBody", - "key": "event" - } - ] - } - ], - null, - null, - [ - { - "timestamp": 1620761543732.209, - "fields": [ - { - "value": "GetConn", - "key": "event" - } - ] - }, - { - "timestamp": 1620761543732.236, - "fields": [ - { - "value": "DNSStart", - "key": "event" - }, - { - "value": "db", - "key": "host" - } - ] - }, - { - "timestamp": 1620761543733.196, - "fields": [ - { - "value": "DNSDone", - "key": "event" - }, - { - "value": "172.24.0.2", - "key": "addr" - } - ] - }, - { - "timestamp": 1620761543733.203, - "fields": [ - { - "value": "ConnectStart", - "key": "event" - }, - { - "value": "tcp", - "key": "network" - }, - { - "value": "172.24.0.2: 80", - "key": "addr" - } - ] - }, - { - "timestamp": 1620761543733.459, - "fields": [ - { - "value": "ConnectDone", - "key": "event" - }, - { - "value": "tcp", - "key": "network" - }, - { - "value": "172.24.0.2: 80", - "key": "addr" - } - ] - }, - { - "timestamp": 1620761543733.549, - "fields": [ - { - "value": "GotConn", - "key": "event" - } - ] - }, - { - "timestamp": 1620761543733.613, - "fields": [ - { - "value": "WroteHeaders", - "key": "event" - } - ] - }, - { - "timestamp": 1620761543733.618, - "fields": [ - { - "value": "WroteRequest", - "key": "event" - } - ] - }, - { - "timestamp": 1620761543734.241, - "fields": [ - { - "value": "GotFirstResponseByte", - "key": "event" - } - ] - }, - { - "timestamp": 1620761543734.592, - "fields": [ - { - "value": "ClosedBody", - "key": "event" - } - ] - } - ], - null, - [ - { - "timestamp": 1620761543735.23, - "fields": [ - { - "value": "GetConn", - "key": "event" - } - ] - }, - { - "timestamp": 1620761543735.277, - "fields": [ - { - "value": "DNSStart", - "key": "event" - }, - { - "value": "app", - "key": "host" - } - ] - }, - { - "timestamp": 1620761543736.9658, - "fields": [ - { - "value": "DNSDone", - "key": "event" - }, - { - "value": "172.24.0.7", - "key": "addr" - } - ] - }, - { - "timestamp": 1620761543736.9758, - "fields": [ - { - "value": "ConnectStart", - "key": "event" - }, - { - "value": "tcp", - "key": "network" - }, - { - "value": "172.24.0.7: 80", - "key": "addr" - } - ] - }, - { - "timestamp": 1620761543737.705, - "fields": [ - { - "value": "ConnectDone", - "key": "event" - }, - { - "value": "tcp", - "key": "network" - }, - { - "value": "172.24.0.7: 80", - "key": "addr" - } - ] - }, - { - "timestamp": 1620761543737.773, - "fields": [ - { - "value": "GotConn", - "key": "event" - } - ] - }, - { - "timestamp": 1620761543737.8098, - "fields": [ - { - "value": "WroteHeaders", - "key": "event" - } - ] - }, - { - "timestamp": 1620761543737.811, - "fields": [ - { - "value": "WroteRequest", - "key": "event" - } - ] - }, - { - "timestamp": 1620761543747.939, - "fields": [ - { - "value": "GotFirstResponseByte", - "key": "event" - } - ] - }, - { - "timestamp": 1620761543748.46, - "fields": [ - { - "value": "ClosedBody", - "key": "event" - } - ] - } - ], - null, - null, - [ - { - "timestamp": 1620761543738.446, - "fields": [ - { - "value": "GetConn", - "key": "event" - } - ] - }, - { - "timestamp": 1620761543738.502, - "fields": [ - { - "value": "DNSStart", - "key": "event" - }, - { - "value": "db", - "key": "host" - } - ] - }, - { - "timestamp": 1620761543743.861, - "fields": [ - { - "value": "DNSDone", - "key": "event" - }, - { - "value": "172.24.0.2", - "key": "addr" - } - ] - }, - { - "timestamp": 1620761543743.918, - "fields": [ - { - "value": "ConnectStart", - "key": "event" - }, - { - "value": "tcp", - "key": "network" - }, - { - "value": "172.24.0.2: 80", - "key": "addr" - } - ] - }, - { - "timestamp": 1620761543744.228, - "fields": [ - { - "value": "ConnectDone", - "key": "event" - }, - { - "value": "tcp", - "key": "network" - }, - { - "value": "172.24.0.2: 80", - "key": "addr" - } - ] - }, - { - "timestamp": 1620761543744.345, - "fields": [ - { - "value": "GotConn", - "key": "event" - } - ] - }, - { - "timestamp": 1620761543744.414, - "fields": [ - { - "value": "WroteHeaders", - "key": "event" - } - ] - }, - { - "timestamp": 1620761543744.416, - "fields": [ - { - "value": "WroteRequest", - "key": "event" - } - ] - }, - { - "timestamp": 1620761543746.825, - "fields": [ - { - "value": "GotFirstResponseByte", - "key": "event" - } - ] - }, - { - "timestamp": 1620761543749.2668, - "fields": [ - { - "value": "ClosedBody", - "key": "event" - } - ] - } - ], - null - ], - [ - [ - { - "value": "const", - "key": "sampler.type" - }, - { - "value": true, - "key": "sampler.param" - }, - { - "value": 0, - "key": "status.code" - } - ], - [ - { - "value": 302, - "key": "http.status_code" - }, - { - "value": "net/http", - "key": "component" - }, - { - "value": "POST", - "key": "http.method" - }, - { - "value": "app: 80", - "key": "http.url" - }, - { - "value": false, - "key": "net/http.reused" - }, - { - "value": false, - "key": "net/http.was_idle" - }, - { - "value": "client", - "key": "span.kind" - }, - { - "value": 0, - "key": "status.code" - } - ], - [ - { - "value": 302, - "key": "http.status_code" - }, - { - "value": "POST", - "key": "http.method" - }, - { - "value": "/post", - "key": "http.url" - }, - { - "value": "net/http", - "key": "component" - }, - { - "value": "server", - "key": "span.kind" - }, - { - "value": 0, - "key": "status.code" - } - ], - [ - { - "value": 0, - "key": "status.code" - } - ], - [ - { - "value": 204, - "key": "http.status_code" - }, - { - "value": "net/http", - "key": "component" - }, - { - "value": "POST", - "key": "http.method" - }, - { - "value": "db: 80", - "key": "http.url" - }, - { - "value": false, - "key": "net/http.reused" - }, - { - "value": false, - "key": "net/http.was_idle" - }, - { - "value": "client", - "key": "span.kind" - }, - { - "value": 0, - "key": "status.code" - } - ], - [ - { - "value": 204, - "key": "http.status_code" - }, - { - "value": "POST", - "key": "http.method" - }, - { - "value": "/post", - "key": "http.url" - }, - { - "value": "net/http", - "key": "component" - }, - { - "value": "server", - "key": "span.kind" - }, - { - "value": 0, - "key": "status.code" - } - ], - [ - { - "value": 200, - "key": "http.status_code" - }, - { - "value": "net/http", - "key": "component" - }, - { - "value": "GET", - "key": "http.method" - }, - { - "value": "app: 80", - "key": "http.url" - }, - { - "value": false, - "key": "net/http.reused" - }, - { - "value": false, - "key": "net/http.was_idle" - }, - { - "value": "client", - "key": "span.kind" - }, - { - "value": 0, - "key": "status.code" - } - ], - [ - { - "value": 200, - "key": "http.status_code" - }, - { - "value": "GET", - "key": "http.method" - }, - { - "value": "/", - "key": "http.url" - }, - { - "value": "net/http", - "key": "component" - }, - { - "value": "server", - "key": "span.kind" - }, - { - "value": 0, - "key": "status.code" - } - ], - [ - { - "value": 0, - "key": "status.code" - } - ], - [ - { - "value": 200, - "key": "http.status_code" - }, - { - "value": "net/http", - "key": "component" - }, - { - "value": "GET", - "key": "http.method" - }, - { - "value": "db: 80", - "key": "http.url" - }, - { - "value": false, - "key": "net/http.reused" - }, - { - "value": false, - "key": "net/http.was_idle" - }, - { - "value": "client", - "key": "span.kind" - }, - { - "value": 0, - "key": "status.code" - } - ], - [ - { - "value": 200, - "key": "http.status_code" - }, - { - "value": "GET", - "key": "http.method" - }, - { - "value": "/", - "key": "http.url" - }, - { - "value": "net/http", - "key": "component" - }, - { - "value": "server", - "key": "span.kind" - }, - { - "value": 0, - "key": "status.code" - } - ] - ] - ] - } - } - ] - } - } -} diff --git a/packages/grafana-e2e/cypress/plugins/benchmark/CDPDataCollector.ts b/packages/grafana-e2e/cypress/plugins/benchmark/CDPDataCollector.ts deleted file mode 100644 index 76e120a741c..00000000000 --- a/packages/grafana-e2e/cypress/plugins/benchmark/CDPDataCollector.ts +++ /dev/null @@ -1,136 +0,0 @@ -import CDP from 'chrome-remote-interface'; -import ProtocolProxyApi from 'devtools-protocol/types/protocol-proxy-api'; -import { countBy, mean } from 'lodash'; -import Tracelib, { TraceEvent } from 'tracelib'; - -import { CollectedData, DataCollector, DataCollectorName } from './DataCollector'; - -type CDPDataCollectorDeps = { - port: number; -}; - -export class CDPDataCollector implements DataCollector { - private tracingCategories: string[]; - - private state: { - client?: CDP.Client; - tracingPromise?: Promise; - traceEvents: TraceEvent[]; - }; - - constructor(private deps: CDPDataCollectorDeps) { - this.state = this.getDefaultState(); - this.tracingCategories = [ - 'disabled-by-default-v8.cpu_profile', - 'disabled-by-default-v8.cpu_profiler', - 'disabled-by-default-v8.cpu_profiler.hires', - 'disabled-by-default-devtools.timeline.frame', - 'disabled-by-default-devtools.timeline', - 'disabled-by-default-devtools.timeline.inputs', - 'disabled-by-default-devtools.timeline.stack', - 'disabled-by-default-devtools.timeline.invalidationTracking', - 'disabled-by-default-layout_shift.debug', - 'disabled-by-default-cc.debug.scheduler.frames', - 'disabled-by-default-blink.debug.display_lock', - ]; - } - - getName = () => DataCollectorName.CDP; - - private resetState = async () => { - if (this.state.client) { - await this.state.client.close(); - } - this.state = this.getDefaultState(); - }; - - private getDefaultState = () => ({ - traceEvents: [], - }); - - // workaround for type declaration issues in cdp lib - private asApis = ( - client: CDP.Client - ): { - Profiler: ProtocolProxyApi.ProfilerApi; - Page: ProtocolProxyApi.PageApi; - Tracing: ProtocolProxyApi.TracingApi; - } => client; - - private getClientApis = async () => this.asApis(await this.getClient()); - - private getClient = async () => { - if (this.state.client) { - return this.state.client; - } - - const client = await CDP({ port: this.deps.port }); - - const { Profiler, Page } = this.asApis(client); - await Promise.all([Page.enable(), Profiler.enable(), Profiler.setSamplingInterval({ interval: 100 })]); - - this.state.client = client; - - return client; - }; - - start: DataCollector['start'] = async ({ id }) => { - if (this.state.tracingPromise) { - throw new Error(`collection in progress - can't start another one! ${id}`); - } - - const { Tracing, Profiler } = await this.getClientApis(); - - await Promise.all([ - Tracing.start({ - bufferUsageReportingInterval: 1000, - traceConfig: { - includedCategories: this.tracingCategories, - }, - }), - Profiler.start(), - ]); - - Tracing.on('dataCollected', ({ value: events }) => { - this.state.traceEvents.push(...events); - }); - - let resolveFn: (data: CollectedData) => void; - this.state.tracingPromise = new Promise((resolve) => { - resolveFn = resolve; - }); - Tracing.on('tracingComplete', ({ dataLossOccurred }) => { - const t = new Tracelib(this.state.traceEvents); - - const eventCounts = countBy(this.state.traceEvents, (ev) => ev.name); - - const fps = t.getFPS(); - - resolveFn({ - eventCounts, - fps: mean(fps.values), - tracingDataLoss: dataLossOccurred ? 1 : 0, - warnings: t.getWarningCounts(), - }); - }); - }; - - stop: DataCollector['stop'] = async (req) => { - if (!this.state.tracingPromise) { - throw new Error(`collection was never started - there is nothing to stop!`); - } - - const { Tracing, Profiler } = await this.getClientApis(); - - // TODO: capture profiler data - const [, , traceData] = await Promise.all([Profiler.stop(), Tracing.end(), this.state.tracingPromise]); - - await this.resetState(); - - return traceData; - }; - - close: DataCollector['close'] = async () => { - await this.resetState(); - }; -} diff --git a/packages/grafana-e2e/cypress/plugins/benchmark/DataCollector.ts b/packages/grafana-e2e/cypress/plugins/benchmark/DataCollector.ts deleted file mode 100644 index bfff10312e2..00000000000 --- a/packages/grafana-e2e/cypress/plugins/benchmark/DataCollector.ts +++ /dev/null @@ -1,14 +0,0 @@ -export type CollectedData = Record; - -export enum DataCollectorName { - CDP = 'CDP', -} - -type DataCollectorRequest = { id: string }; - -export type DataCollector = { - start: (input: DataCollectorRequest) => Promise; - stop: (input: DataCollectorRequest) => Promise; - getName: () => DataCollectorName; - close: () => Promise; -}; diff --git a/packages/grafana-e2e/cypress/plugins/benchmark/formatting.ts b/packages/grafana-e2e/cypress/plugins/benchmark/formatting.ts deleted file mode 100644 index 3ecb983a027..00000000000 --- a/packages/grafana-e2e/cypress/plugins/benchmark/formatting.ts +++ /dev/null @@ -1,139 +0,0 @@ -import { fromPairs } from 'lodash'; - -import { CollectedData, DataCollectorName } from './DataCollector'; - -type Stats = { - sum: number; - min: number; - max: number; - count: number; - avg: number; - time: number; -}; - -export enum MeasurementName { - DataRenderDelay = 'DataRenderDelay', -} - -type LivePerformanceAppStats = Record; - -const isLivePerformanceAppStats = (data: CollectedData[]): data is LivePerformanceAppStats[] => - data.some((st) => { - const stat = st?.[MeasurementName.DataRenderDelay]; - return Array.isArray(stat) && Boolean(stat?.length); - }); - -type FormattedStats = { - total: { - count: number[]; - avg: number[]; - }; - lastInterval: { - avg: number[]; - min: number[]; - max: number[]; - count: number[]; - }; -}; - -export const formatAppStats = (allStats: CollectedData[]) => { - if (!isLivePerformanceAppStats(allStats)) { - return {}; - } - - const names = Object.keys(MeasurementName) as MeasurementName[]; - - return fromPairs( - names.map((name) => { - const statsForMeasurement = allStats.map((s) => s[name]); - const res: FormattedStats = { - total: { - count: [], - avg: [], - }, - lastInterval: { - avg: [], - min: [], - max: [], - count: [], - }, - }; - - statsForMeasurement.forEach((s) => { - const total = s.reduce( - (prev, next) => { - prev.count += next.count; - prev.avg += next.avg; - return prev; - }, - { count: 0, avg: 0 } - ); - res.total.count.push(Math.round(total.count)); - res.total.avg.push(Math.round(total.avg / s.length)); - - const lastInterval = s[s.length - 1]; - - res.lastInterval.avg.push(Math.round(lastInterval?.avg)); - res.lastInterval.min.push(Math.round(lastInterval?.min)); - res.lastInterval.max.push(Math.round(lastInterval?.max)); - res.lastInterval.count.push(Math.round(lastInterval?.count)); - }); - - return [name, res]; - }) - ); -}; - -type CDPData = { - eventCounts: Record; - fps: number; - tracingDataLoss: number; - warnings: Record; -}; - -const isCDPData = (data: any[]): data is CDPData[] => data.every((d) => typeof d.eventCounts === 'object'); - -type FormattedCDPData = { - minorGC: number[]; - majorGC: number[]; - droppedFrames: number[]; - fps: number[]; - tracingDataLossOccurred: boolean; - longTaskWarnings: number[]; -}; - -const emptyFormattedCDPData = (): FormattedCDPData => ({ - minorGC: [], - majorGC: [], - droppedFrames: [], - fps: [], - tracingDataLossOccurred: false, - longTaskWarnings: [], -}); - -const formatCDPData = (data: any): FormattedCDPData => { - if (!isCDPData(data)) { - return emptyFormattedCDPData(); - } - - return data.reduce((acc, next) => { - acc.majorGC.push((next.eventCounts.MajorGC as number) ?? 0); - acc.minorGC.push((next.eventCounts.MinorGC as number) ?? 0); - acc.fps.push(Math.round(next.fps) ?? 0); - acc.tracingDataLossOccurred = acc.tracingDataLossOccurred || Boolean(next.tracingDataLoss); - acc.droppedFrames.push((next.eventCounts.DroppedFrame as number) ?? 0); - acc.longTaskWarnings.push((next.warnings.LongTask as number) ?? 0); - return acc; - }, emptyFormattedCDPData()); -}; - -export const formatResults = ( - results: Array<{ appStats: CollectedData; collectorsData: CollectedData }> -): CollectedData => { - return { - ...formatAppStats(results.map(({ appStats }) => appStats)), - ...formatCDPData(results.map(({ collectorsData }) => collectorsData[DataCollectorName.CDP])), - - __raw: results, - }; -}; diff --git a/packages/grafana-e2e/cypress/plugins/benchmark/index.ts b/packages/grafana-e2e/cypress/plugins/benchmark/index.ts deleted file mode 100644 index 22307555d58..00000000000 --- a/packages/grafana-e2e/cypress/plugins/benchmark/index.ts +++ /dev/null @@ -1,88 +0,0 @@ -import fs from 'fs'; -import { fromPairs } from 'lodash'; - -import { CDPDataCollector } from './CDPDataCollector'; -import { CollectedData, DataCollector } from './DataCollector'; -import { formatResults } from './formatting'; -const remoteDebuggingPortOptionPrefix = '--remote-debugging-port='; - -const getOrAddRemoteDebuggingPort = (args: string[]) => { - const existing = args.find((arg) => arg.startsWith(remoteDebuggingPortOptionPrefix)); - - if (existing) { - return Number(existing.substring(remoteDebuggingPortOptionPrefix.length)); - } - - const port = 40000 + Math.round(Math.random() * 25000); - args.push(`${remoteDebuggingPortOptionPrefix}${port}`); - return port; -}; - -let collectors: DataCollector[] = []; -let results: Array<{ appStats: CollectedData; collectorsData: CollectedData }> = []; - -const startBenchmarking = async ({ testName }: { testName: string }) => { - await Promise.all(collectors.map((coll) => coll.start({ id: testName }))); - - return true; -}; - -const stopBenchmarking = async ({ testName, appStats }: { testName: string; appStats: CollectedData }) => { - const data = await Promise.all(collectors.map(async (coll) => [coll.getName(), await coll.stop({ id: testName })])); - - results.push({ - collectorsData: fromPairs(data), - appStats: appStats, - }); - - return true; -}; -const afterRun = async () => { - await Promise.all(collectors.map((coll) => coll.close())); - collectors = []; - results = []; -}; - -const afterSpec = (resultsFolder: string) => async (spec: { name: string }) => { - fs.writeFileSync(`${resultsFolder}/${spec.name}-${Date.now()}.json`, JSON.stringify(formatResults(results), null, 2)); - - results = []; -}; - -export const initialize: Cypress.PluginConfig = (on, config) => { - const resultsFolder = config.env['BENCHMARK_PLUGIN_RESULTS_FOLDER']; - - if (!fs.existsSync(resultsFolder)) { - fs.mkdirSync(resultsFolder, { recursive: true }); - console.log(`Created folder for benchmark results ${resultsFolder}`); - } - - on('before:browser:launch', async (browser, options) => { - if (browser.family !== 'chromium' || browser.name === 'electron') { - throw new Error('benchmarking plugin requires chrome'); - } - - const { args } = options; - - const port = getOrAddRemoteDebuggingPort(args); - collectors.push(new CDPDataCollector({ port })); - - args.push('--start-fullscreen'); - - console.log( - `initialized benchmarking plugin with ${collectors.length} collectors: ${collectors - .map((col) => col.getName()) - .join(', ')}` - ); - - return options; - }); - - on('task', { - startBenchmarking, - stopBenchmarking, - }); - - on('after:run', afterRun); - on('after:spec', afterSpec(resultsFolder)); -}; diff --git a/packages/grafana-e2e/cypress/plugins/benchmark/tracelib.d.ts b/packages/grafana-e2e/cypress/plugins/benchmark/tracelib.d.ts deleted file mode 100644 index 2ba83b35594..00000000000 --- a/packages/grafana-e2e/cypress/plugins/benchmark/tracelib.d.ts +++ /dev/null @@ -1,15 +0,0 @@ -type TraceEvent = { - name: string; -}; - -declare class Tracelib { - constructor(private events: TraceEvent[]) {} - - getFPS: () => { times: number[]; values: number[] }; - getWarningCounts: () => Record; -} -declare module 'tracelib' { - export = Tracelib; - - export { TraceEvent }; -} diff --git a/packages/grafana-e2e/cypress/plugins/compareScreenshots.js b/packages/grafana-e2e/cypress/plugins/compareScreenshots.js deleted file mode 100644 index 426d12adac5..00000000000 --- a/packages/grafana-e2e/cypress/plugins/compareScreenshots.js +++ /dev/null @@ -1,49 +0,0 @@ -'use strict'; -const BlinkDiff = require('blink-diff'); -const { resolve } = require('path'); - -// @todo use npmjs.com/pixelmatch or an available cypress plugin -const compareScreenshots = async ({ config, screenshotsFolder, specName }) => { - const name = config.name || config; // @todo use `??` - const threshold = config.threshold || 0.001; // @todo use `??` - - const imageAPath = `${screenshotsFolder}/${specName}/${name}.png`; - const imageBPath = resolve(`${screenshotsFolder}/../expected/${specName}/${name}.png`); - - const imageOutputPath = screenshotsFolder.endsWith('actual') ? imageAPath.replace('.png', '.diff.png') : undefined; - - const { code } = await new Promise((resolve, reject) => { - new BlinkDiff({ - imageAPath, - imageBPath, - imageOutputPath, - threshold, - thresholdType: BlinkDiff.THRESHOLD_PERCENT, - }).run((error, result) => { - if (error) { - reject(error); - } else { - resolve(result); - } - }); - }); - - if (code <= 1) { - let msg = `\nThe screenshot [${imageAPath}] differs from [${imageBPath}]`; - msg += '\n'; - msg += '\nCheck the Artifacts tab in the CircleCi build output for the actual screenshots.'; - msg += '\n'; - msg += '\n If the difference between expected and outcome is NOT acceptable then do the following:'; - msg += '\n - Check the code for changes that causes this difference, fix that and retry.'; - msg += '\n'; - msg += '\n If the difference between expected and outcome is acceptable then do the following:'; - msg += '\n - Replace the expected image with the outcome and retry.'; - msg += '\n'; - throw new Error(msg); - } else { - // Must return a value - return true; - } -}; - -module.exports = compareScreenshots; diff --git a/packages/grafana-e2e/cypress/plugins/extendConfig.js b/packages/grafana-e2e/cypress/plugins/extendConfig.js deleted file mode 100644 index 85b9088cd98..00000000000 --- a/packages/grafana-e2e/cypress/plugins/extendConfig.js +++ /dev/null @@ -1,79 +0,0 @@ -'use strict'; -const { - promises: { readFile }, -} = require('fs'); -const { resolve } = require('path'); - -// @todo use https://github.com/bahmutov/cypress-extends when possible -module.exports = async (baseConfig) => { - // From CLI - const { - env: { CWD, UPDATE_SCREENSHOTS }, - } = baseConfig; - - if (CWD) { - // @todo: https://github.com/cypress-io/cypress/issues/6406 - const jsonReporter = require.resolve('@mochajs/json-file-reporter'); - - // @todo `baseUrl: env.CYPRESS_BASEURL` - const projectConfig = { - fixturesFolder: `${CWD}/cypress/fixtures`, - integrationFolder: `${CWD}/cypress/integration`, - reporter: jsonReporter, - reporterOptions: { - output: `${CWD}/cypress/report.json`, - }, - screenshotsFolder: `${CWD}/cypress/screenshots/${UPDATE_SCREENSHOTS ? 'expected' : 'actual'}`, - videosFolder: `${CWD}/cypress/videos`, - }; - - const customProjectConfig = await readFile(`${CWD}/cypress.json`, 'utf8') - .then(JSON.parse) - .then((config) => { - const pathKeys = [ - 'fileServerFolder', - 'fixturesFolder', - 'ignoreTestFiles', - 'integrationFolder', - 'pluginsFile', - 'screenshotsFolder', - 'supportFile', - 'testFiles', - 'videosFolder', - ]; - - return Object.fromEntries( - Object.entries(config).map(([key, value]) => { - if (pathKeys.includes(key)) { - return [key, resolve(CWD, value)]; - } else { - return [key, value]; - } - }) - ); - }) - .catch((error) => { - if (error.code === 'ENOENT') { - // File is optional - return {}; - } else { - // Unexpected error - throw error; - } - }); - - return { - ...baseConfig, - ...projectConfig, - ...customProjectConfig, - reporterOptions: { - ...baseConfig.reporterOptions, - ...projectConfig.reporterOptions, - ...customProjectConfig.reporterOptions, - }, - }; - } else { - // Temporary legacy support for Grafana core (using `yarn start`) - return baseConfig; - } -}; diff --git a/packages/grafana-e2e/cypress/plugins/index.js b/packages/grafana-e2e/cypress/plugins/index.js deleted file mode 100644 index 7ec85fecae1..00000000000 --- a/packages/grafana-e2e/cypress/plugins/index.js +++ /dev/null @@ -1,73 +0,0 @@ -const fs = require('fs'); -const path = require('path'); - -const benchmarkPlugin = require('./benchmark'); -const compareScreenshots = require('./compareScreenshots'); -const extendConfig = require('./extendConfig'); -const readProvisions = require('./readProvisions'); -const typescriptPreprocessor = require('./typescriptPreprocessor'); - -module.exports = (on, config) => { - if (config.env['BENCHMARK_PLUGIN_ENABLED'] === true) { - benchmarkPlugin.initialize(on, config); - } - - on('file:preprocessor', typescriptPreprocessor); - on('task', { compareScreenshots, readProvisions }); - on('task', { - log({ message, optional }) { - optional ? console.log(message, optional) : console.log(message); - return null; - }, - }); - on('task', { - getJSONFilesFromDir: async ({ projectPath, relativePath }) => { - const directoryPath = path.join(projectPath, relativePath); - const jsonFiles = fs.readdirSync(directoryPath); - return jsonFiles - .filter((fileName) => /.json$/i.test(fileName)) - .map((fileName) => { - const fileBuffer = fs.readFileSync(path.join(directoryPath, fileName)); - return JSON.parse(fileBuffer); - }); - }, - }); - - // Make recordings higher resolution - // https://www.cypress.io/blog/2021/03/01/generate-high-resolution-videos-and-screenshots/ - on('before:browser:launch', (browser = {}, launchOptions) => { - console.log('launching browser %s is headless? %s', browser.name, browser.isHeadless); - - // the browser width and height we want to get - // our screenshots and videos will be of that resolution - const width = 1920; - const height = 1080; - - console.log('setting the browser window size to %d x %d', width, height); - - if (browser.name === 'chrome' && browser.isHeadless) { - launchOptions.args.push(`--window-size=${width},${height}`); - - // force screen to be non-retina and just use our given resolution - launchOptions.args.push('--force-device-scale-factor=1'); - } - - if (browser.name === 'electron' && browser.isHeadless) { - // might not work on CI for some reason - launchOptions.preferences.width = width; - launchOptions.preferences.height = height; - } - - if (browser.name === 'firefox' && browser.isHeadless) { - launchOptions.args.push(`--width=${width}`); - launchOptions.args.push(`--height=${height}`); - } - - // IMPORTANT: return the updated browser launch options - return launchOptions; - }); - - // Always extend with this library's config and return for diffing - // @todo remove this when possible: https://github.com/cypress-io/cypress/issues/5674 - return extendConfig(config); -}; diff --git a/packages/grafana-e2e/cypress/plugins/readProvisions.js b/packages/grafana-e2e/cypress/plugins/readProvisions.js deleted file mode 100644 index f82542d2070..00000000000 --- a/packages/grafana-e2e/cypress/plugins/readProvisions.js +++ /dev/null @@ -1,14 +0,0 @@ -'use strict'; -const { - promises: { readFile }, -} = require('fs'); -const { resolve: resolvePath } = require('path'); -const { parse: parseYml } = require('yaml'); - -const readProvision = (filePath) => readFile(filePath, 'utf8').then((contents) => parseYml(contents)); - -const readProvisions = (filePaths) => Promise.all(filePaths.map(readProvision)); - -// Paths are relative to /provisioning -module.exports = ({ CWD, filePaths }) => - readProvisions(filePaths.map((filePath) => resolvePath(CWD, 'provisioning', filePath))); diff --git a/packages/grafana-e2e/cypress/plugins/typescriptPreprocessor.js b/packages/grafana-e2e/cypress/plugins/typescriptPreprocessor.js deleted file mode 100644 index be506578188..00000000000 --- a/packages/grafana-e2e/cypress/plugins/typescriptPreprocessor.js +++ /dev/null @@ -1,42 +0,0 @@ -const wp = require('@cypress/webpack-preprocessor'); -const { resolve } = require('path'); - -const anyNodeModules = /node_modules/; -const packageRoot = resolve(`${__dirname}/../../`); -const packageModules = `${packageRoot}/node_modules`; - -const webpackOptions = { - module: { - rules: [ - { - include: (modulePath) => { - if (!anyNodeModules.test(modulePath)) { - // Is a file within the project - return true; - } else { - // Is a file within this package - return modulePath.startsWith(packageRoot) && !modulePath.startsWith(packageModules); - } - }, - test: /\.ts$/, - use: [ - { - loader: 'ts-loader', - options: { - transpileOnly: true, - }, - }, - ], - }, - ], - }, - resolve: { - extensions: ['.ts', '.js'], - }, -}; - -const options = { - webpackOptions, -}; - -module.exports = wp(options); diff --git a/packages/grafana-e2e/cypress/support/commands.ts b/packages/grafana-e2e/cypress/support/commands.ts deleted file mode 100644 index ce386d11d8f..00000000000 --- a/packages/grafana-e2e/cypress/support/commands.ts +++ /dev/null @@ -1,41 +0,0 @@ -import 'cypress-file-upload'; - -interface CompareScreenshotsConfig { - name: string; - threshold?: number; -} - -Cypress.Commands.add('compareScreenshots', (config: CompareScreenshotsConfig | string) => { - cy.task('compareScreenshots', { - config, - screenshotsFolder: Cypress.config('screenshotsFolder'), - specName: Cypress.spec.name, - }); -}); - -Cypress.Commands.add('logToConsole', (message: string, optional?: any) => { - cy.task('log', { message: '(' + new Date().toISOString() + ') ' + message, optional }); -}); - -Cypress.Commands.add('readProvisions', (filePaths: string[]) => { - cy.task('readProvisions', { - CWD: Cypress.env('CWD'), - filePaths, - }); -}); - -Cypress.Commands.add('getJSONFilesFromDir', (dirPath: string) => { - return cy.task('getJSONFilesFromDir', { - // CWD is set for plugins in the cli but not for the main grafana repo: https://github.com/grafana/grafana/blob/main/packages/grafana-e2e/cli.js#L12 - projectPath: Cypress.env('CWD') || Cypress.config().parentTestsFolder, - relativePath: dirPath, - }); -}); - -Cypress.Commands.add('startBenchmarking', (testName: string) => { - return cy.task('startBenchmarking', { testName }); -}); - -Cypress.Commands.add('stopBenchmarking', (testName: string, appStats: Record) => { - return cy.task('stopBenchmarking', { testName, appStats }); -}); diff --git a/packages/grafana-e2e/cypress/support/index.d.ts b/packages/grafana-e2e/cypress/support/index.d.ts deleted file mode 100644 index 3f8d1975936..00000000000 --- a/packages/grafana-e2e/cypress/support/index.d.ts +++ /dev/null @@ -1,12 +0,0 @@ -/// - -declare namespace Cypress { - interface Chainable { - compareScreenshots(config: CompareScreenshotsConfig | string): Chainable; - logToConsole(message: string, optional?: any): void; - readProvisions(filePaths: string[]): Chainable; - getJSONFilesFromDir(dirPath: string): Chainable; - startBenchmarking(testName: string): void; - stopBenchmarking(testName: string, appStats: Record): void; - } -} diff --git a/packages/grafana-e2e/cypress/support/index.ts b/packages/grafana-e2e/cypress/support/index.ts deleted file mode 100644 index f84817e99fb..00000000000 --- a/packages/grafana-e2e/cypress/support/index.ts +++ /dev/null @@ -1,49 +0,0 @@ -// yarn build fails with: -// >> /Users/hugo/go/src/github.com/grafana/grafana/node_modules/stringmap/stringmap.js:99 -// >> throw new Error("StringMap expected string key"); -// require('cypress-failed-log'); -import './commands'; - -Cypress.Screenshot.defaults({ - screenshotOnRunFailure: false, -}); - -const COMMAND_DELAY = 1000; - -if (Cypress.env('SLOWMO')) { - const commandsToModify = ['clear', 'click', 'contains', 'reload', 'then', 'trigger', 'type', 'visit']; - - commandsToModify.forEach((command) => { - // @ts-ignore -- https://github.com/cypress-io/cypress/issues/7807 - Cypress.Commands.overwrite(command, (originalFn, ...args) => { - const origVal = originalFn(...args); - - return new Promise((resolve) => { - setTimeout(() => resolve(origVal), COMMAND_DELAY); - }); - }); - }); -} - -// @todo remove when possible: https://github.com/cypress-io/cypress/issues/95 -Cypress.on('window:before:load', (win) => { - // @ts-ignore - delete win.fetch; -}); - -// See https://github.com/quasarframework/quasar/issues/2233 for details -const resizeObserverLoopErrRe = /^[^(ResizeObserver loop limit exceeded)]/; -Cypress.on('uncaught:exception', (err) => { - /* returning false here prevents Cypress from failing the test */ - if (resizeObserverLoopErrRe.test(err.message)) { - return false; - } - return true; -}); - -// uncomment below to prevent Cypress from failing tests when unhandled errors are thrown -// Cypress.on('uncaught:exception', (err, runnable) => { -// // returning false here prevents Cypress from -// // failing the test -// return false; -// }); diff --git a/packages/grafana-e2e/cypress/tsconfig.json b/packages/grafana-e2e/cypress/tsconfig.json deleted file mode 100644 index b3f735c508e..00000000000 --- a/packages/grafana-e2e/cypress/tsconfig.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "compilerOptions": { - "declaration": false, - "module": "commonjs", - "types": ["cypress", "cypress-file-upload", "node"] - }, - "extends": "@grafana/tsconfig", - "include": ["**/*.ts"] -} diff --git a/packages/grafana-e2e/package.json b/packages/grafana-e2e/package.json deleted file mode 100644 index ec76cbd7184..00000000000 --- a/packages/grafana-e2e/package.json +++ /dev/null @@ -1,89 +0,0 @@ -{ - "author": "Grafana Labs", - "license": "Apache-2.0", - "name": "@grafana/e2e", - "version": "11.1.0-pre", - "description": "Grafana End-to-End Test Library", - "keywords": [ - "cli", - "grafana", - "e2e", - "typescript" - ], - "repository": { - "type": "git", - "url": "http://github.com/grafana/grafana.git", - "directory": "packages/grafana-e2e" - }, - "main": "src/index.ts", - "types": "src/index.ts", - "bin": { - "grafana-e2e": "bin/grafana-e2e.js" - }, - "publishConfig": { - "main": "dist/index.js", - "types": "dist/index.d.ts", - "access": "public" - }, - "files": [ - "cypress", - "dist", - "cli.js", - "cypress.json", - "./README.md", - "./CHANGELOG.md", - "LICENSE_APACHE2" - ], - "scripts": { - "build": "tsc -p ./tsconfig.build.json && rollup -c rollup.config.ts", - "bundle": "rollup -c rollup.config.ts", - "clean": "rimraf ./dist ./compiled ./package.tgz", - "open": "cypress open", - "start": "cypress run --browser=chrome", - "start-benchmark": "CYPRESS_NO_COMMAND_LOG=1 yarn start", - "test": "pushd test && node ../dist/bin/grafana-e2e.js run", - "typecheck": "tsc --emitDeclarationOnly false --noEmit", - "prepack": "cp package.json package.json.bak && node ../../scripts/prepare-packagejson.js", - "postpack": "mv package.json.bak package.json" - }, - "devDependencies": { - "@rollup/plugin-node-resolve": "15.2.3", - "@types/chrome-remote-interface": "0.31.10", - "@types/lodash": "4.14.195", - "@types/node": "18.18.4", - "@types/uuid": "9.0.2", - "esbuild": "0.18.12", - "rollup": "2.79.1", - "rollup-plugin-dts": "^5.0.0", - "rollup-plugin-esbuild": "5.0.0", - "rollup-plugin-node-externals": "^5.0.0", - "webpack": "5.89.0" - }, - "dependencies": { - "@babel/core": "7.23.2", - "@babel/preset-env": "7.23.2", - "@cypress/webpack-preprocessor": "5.17.1", - "@grafana/e2e-selectors": "11.1.0-pre", - "@grafana/schema": "11.1.0-pre", - "@grafana/tsconfig": "^1.3.0-rc1", - "@mochajs/json-file-reporter": "^1.2.0", - "babel-loader": "9.1.3", - "blink-diff": "1.0.13", - "chrome-remote-interface": "0.33.0", - "commander": "8.3.0", - "cypress": "9.5.1", - "cypress-file-upload": "5.0.8", - "devtools-protocol": "0.0.1170333", - "execa": "5.1.1", - "lodash": "4.17.21", - "mocha": "10.2.0", - "resolve-bin": "1.0.1", - "rimraf": "5.0.1", - "tracelib": "1.0.1", - "ts-loader": "8.4.0", - "tslib": "2.6.0", - "typescript": "5.2.2", - "uuid": "9.0.0", - "yaml": "^2.0.0" - } -} diff --git a/packages/grafana-e2e/rollup.config.ts b/packages/grafana-e2e/rollup.config.ts deleted file mode 100644 index 95eeba012a7..00000000000 --- a/packages/grafana-e2e/rollup.config.ts +++ /dev/null @@ -1,29 +0,0 @@ -import resolve from '@rollup/plugin-node-resolve'; -import path from 'path'; -import dts from 'rollup-plugin-dts'; -import esbuild from 'rollup-plugin-esbuild'; -import { externals } from 'rollup-plugin-node-externals'; - -const pkg = require('./package.json'); - -export default [ - { - input: 'src/index.ts', - plugins: [externals({ deps: true, packagePath: './package.json' }), resolve(), esbuild({ target: 'node16' })], - output: [ - { - format: 'cjs', - sourcemap: true, - dir: path.dirname(pkg.publishConfig.main), - }, - ], - }, - { - input: './compiled/index.d.ts', - plugins: [dts()], - output: { - file: pkg.publishConfig.types, - format: 'es', - }, - }, -]; diff --git a/packages/grafana-e2e/src/flows/addDashboard.ts b/packages/grafana-e2e/src/flows/addDashboard.ts deleted file mode 100644 index 2526a03422e..00000000000 --- a/packages/grafana-e2e/src/flows/addDashboard.ts +++ /dev/null @@ -1,303 +0,0 @@ -import { v4 as uuidv4 } from 'uuid'; - -import { e2e } from '../index'; -import { getDashboardUid } from '../support/url'; - -import { DeleteDashboardConfig } from './deleteDashboard'; -import { selectOption } from './selectOption'; -import { setDashboardTimeRange, TimeRangeConfig } from './setDashboardTimeRange'; - -export interface AddAnnotationConfig { - dataSource: string; - dataSourceForm?: () => void; - name: string; -} - -export interface AddDashboardConfig { - annotations: AddAnnotationConfig[]; - timeRange: TimeRangeConfig; - title: string; - variables: PartialAddVariableConfig[]; -} - -interface AddVariableDefault { - hide: string; - type: string; -} - -interface AddVariableOptional { - constantValue?: string; - dataSource?: string; - label?: string; - query?: string; - regex?: string; - variableQueryForm?: (config: AddVariableConfig) => void; -} - -interface AddVariableRequired { - name: string; -} - -export type PartialAddVariableConfig = Partial & AddVariableOptional & AddVariableRequired; -export type AddVariableConfig = AddVariableDefault & AddVariableOptional & AddVariableRequired; - -/** - * This flow is used to add a dashboard with whatever configuration specified. - * @param config Configuration object. Currently supports configuring dashboard time range, annotations, and variables (support dependant on type). - * @see{@link AddDashboardConfig} - * - * @example - * ``` - * // Configuring a simple dashboard - * addDashboard({ - * timeRange: { - * from: '2022-10-03 00:00:00', - * to: '2022-10-03 23:59:59', - * zone: 'Coordinated Universal Time', - * }, - * title: 'Test Dashboard', - * }) - * ``` - * - * @example - * ``` - * // Configuring a dashboard with annotations - * addDashboard({ - * title: 'Test Dashboard', - * annotations: [ - * { - * // This should match the datasource name - * dataSource: 'azure-monitor', - * name: 'Test Annotation', - * dataSourceForm: () => { - * // Insert steps to create annotation using datasource form - * } - * } - * ] - * }) - * ``` - * - * @see{@link AddAnnotationConfig} - * - * @example - * ``` - * // Configuring a dashboard with variables - * addDashboard({ - * title: 'Test Dashboard', - * variables: [ - * { - * name: 'test-query-variable', - * label: 'Testing Query', - * hide: '', - * type: e2e.flows.VARIABLE_TYPE_QUERY, - * dataSource: 'azure-monitor', - * variableQueryForm: () => { - * // Insert steps to create variable using datasource form - * }, - * }, - * { - * name: 'test-constant-variable', - * label: 'Testing Constant', - * type: e2e.flows.VARIABLE_TYPE_CONSTANT, - * constantValue: 'constant', - * } - * ] - * }) - * ``` - * - * @see{@link AddVariableConfig} - * - * @see{@link https://github.com/grafana/grafana/blob/main/e2e/cloud-plugins-suite/azure-monitor.spec.ts Azure Monitor Tests for full examples} - */ -export const addDashboard = (config?: Partial) => { - const fullConfig: AddDashboardConfig = { - annotations: [], - title: `e2e-${uuidv4()}`, - variables: [], - ...config, - timeRange: { - from: '2020-01-01 00:00:00', - to: '2020-01-01 06:00:00', - zone: 'Coordinated Universal Time', - ...config?.timeRange, - }, - }; - - const { annotations, timeRange, title, variables } = fullConfig; - - e2e().logToConsole('Adding dashboard with title:', title); - - e2e.pages.AddDashboard.visit(); - - if (annotations.length > 0 || variables.length > 0) { - e2e.components.PageToolbar.item('Dashboard settings').click(); - addAnnotations(annotations); - - fullConfig.variables = addVariables(variables); - - e2e.components.BackButton.backArrow().should('be.visible').click({ force: true }); - } - - setDashboardTimeRange(timeRange); - - e2e.components.PageToolbar.item('Save dashboard').click(); - e2e.pages.SaveDashboardAsModal.newName().clear().type(title, { force: true }); - e2e.pages.SaveDashboardAsModal.save().click(); - e2e.flows.assertSuccessNotification(); - e2e.pages.AddDashboard.itemButton('Create new panel button').should('be.visible'); - - e2e().logToConsole('Added dashboard with title:', title); - - return e2e() - .url() - .should('contain', '/d/') - .then((url: string) => { - const uid = getDashboardUid(url); - - e2e.getScenarioContext().then(({ addedDashboards }: any) => { - e2e.setScenarioContext({ - addedDashboards: [...addedDashboards, { title, uid } as DeleteDashboardConfig], - }); - }); - - // @todo remove `wrap` when possible - return e2e().wrap( - { - config: fullConfig, - uid, - }, - { log: false } - ); - }); -}; - -const addAnnotation = (config: AddAnnotationConfig, isFirst: boolean) => { - if (isFirst) { - if (e2e.pages.Dashboard.Settings.Annotations.List.addAnnotationCTAV2) { - e2e.pages.Dashboard.Settings.Annotations.List.addAnnotationCTAV2().click(); - } else { - e2e.pages.Dashboard.Settings.Annotations.List.addAnnotationCTA().click(); - } - } else { - cy.contains('New query').click(); - } - - const { dataSource, dataSourceForm, name } = config; - - selectOption({ - container: e2e.components.DataSourcePicker.container(), - optionText: dataSource, - }); - - e2e.pages.Dashboard.Settings.Annotations.Settings.name().clear().type(name); - - if (dataSourceForm) { - dataSourceForm(); - } -}; - -const addAnnotations = (configs: AddAnnotationConfig[]) => { - if (configs.length > 0) { - e2e.pages.Dashboard.Settings.General.sectionItems('Annotations').click(); - } - - return configs.forEach((config, i) => addAnnotation(config, i === 0)); -}; - -export const VARIABLE_HIDE_LABEL = 'Label'; -export const VARIABLE_HIDE_NOTHING = ''; -export const VARIABLE_HIDE_VARIABLE = 'Variable'; - -export const VARIABLE_TYPE_AD_HOC_FILTERS = 'Ad hoc filters'; -export const VARIABLE_TYPE_CONSTANT = 'Constant'; -export const VARIABLE_TYPE_DATASOURCE = 'Datasource'; -export const VARIABLE_TYPE_QUERY = 'Query'; - -const addVariable = (config: PartialAddVariableConfig, isFirst: boolean): AddVariableConfig => { - const fullConfig = { - hide: VARIABLE_HIDE_NOTHING, - type: VARIABLE_TYPE_QUERY, - ...config, - }; - - if (isFirst) { - if (e2e.pages.Dashboard.Settings.Variables.List.addVariableCTAV2) { - e2e.pages.Dashboard.Settings.Variables.List.addVariableCTAV2().click(); - } else { - e2e.pages.Dashboard.Settings.Variables.List.addVariableCTA().click(); - } - } else { - e2e.pages.Dashboard.Settings.Variables.List.newButton().click(); - } - - const { constantValue, dataSource, label, name, query, regex, type, variableQueryForm } = fullConfig; - - // This field is key to many reactive changes - if (type !== VARIABLE_TYPE_QUERY) { - e2e.pages.Dashboard.Settings.Variables.Edit.General.generalTypeSelectV2() - .should('be.visible') - .within(() => { - e2e.components.Select.singleValue().should('have.text', 'Query').parent().click(); - }); - e2e.pages.Dashboard.Settings.Variables.Edit.General.generalTypeSelectV2().find('input').type(`${type}{enter}`); - } - - if (label) { - e2e.pages.Dashboard.Settings.Variables.Edit.General.generalLabelInputV2().type(label); - } - - e2e.pages.Dashboard.Settings.Variables.Edit.General.generalNameInputV2().clear().type(name); - - if ( - dataSource && - (type === VARIABLE_TYPE_AD_HOC_FILTERS || type === VARIABLE_TYPE_DATASOURCE || type === VARIABLE_TYPE_QUERY) - ) { - e2e.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsDataSourceSelect() - .should('be.visible') - .within(() => { - e2e.components.DataSourcePicker.inputV2().type(`${dataSource}{enter}`); - }); - } - - if (constantValue && type === VARIABLE_TYPE_CONSTANT) { - e2e.pages.Dashboard.Settings.Variables.Edit.ConstantVariable.constantOptionsQueryInputV2().type(constantValue); - } - - if (type === VARIABLE_TYPE_QUERY) { - if (query) { - e2e.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsQueryInput().type(query); - } - - if (regex) { - e2e.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsRegExInputV2().type(regex); - } - - if (variableQueryForm) { - variableQueryForm(fullConfig); - } - } - - // Avoid flakiness - e2e().focused().blur(); - - e2e.pages.Dashboard.Settings.Variables.Edit.General.previewOfValuesOption() - .should('exist') - .within((previewOfValues) => { - if (type === VARIABLE_TYPE_CONSTANT) { - expect(previewOfValues.text()).equals(constantValue); - } - }); - - e2e.pages.Dashboard.Settings.Variables.Edit.General.submitButton().click(); - e2e.pages.Dashboard.Settings.Variables.Edit.General.applyButton().click(); - - return fullConfig; -}; - -const addVariables = (configs: PartialAddVariableConfig[]): AddVariableConfig[] => { - if (configs.length > 0) { - e2e.components.Tab.title('Variables').click(); - } - - return configs.map((config, i) => addVariable(config, i === 0)); -}; diff --git a/packages/grafana-e2e/src/flows/addDataSource.ts b/packages/grafana-e2e/src/flows/addDataSource.ts deleted file mode 100644 index 38779572bd4..00000000000 --- a/packages/grafana-e2e/src/flows/addDataSource.ts +++ /dev/null @@ -1,116 +0,0 @@ -import { v4 as uuidv4 } from 'uuid'; - -import { e2e } from '../index'; - -import { DeleteDataSourceConfig } from './deleteDataSource'; - -export interface AddDataSourceConfig { - basicAuth: boolean; - basicAuthPassword: string; - basicAuthUser: string; - expectedAlertMessage: string | RegExp; - form: () => void; - name: string; - skipTlsVerify: boolean; - type: string; - timeout?: number; - awaitHealth?: boolean; -} - -// @todo this actually returns type `Cypress.Chainable` -export const addDataSource = (config?: Partial) => { - const fullConfig: AddDataSourceConfig = { - basicAuth: false, - basicAuthPassword: '', - basicAuthUser: '', - expectedAlertMessage: 'Data source is working', - form: () => {}, - name: `e2e-${uuidv4()}`, - skipTlsVerify: false, - type: 'TestData', - ...config, - }; - - const { - basicAuth, - basicAuthPassword, - basicAuthUser, - expectedAlertMessage, - form, - name, - skipTlsVerify, - type, - timeout, - awaitHealth, - } = fullConfig; - - if (awaitHealth) { - e2e() - .intercept(/health/) - .as('health'); - } - - e2e().logToConsole('Adding data source with name:', name); - e2e.pages.AddDataSource.visit(); - e2e.pages.AddDataSource.dataSourcePluginsV2(type) - .scrollIntoView() - .should('be.visible') // prevents flakiness - .click(); - - e2e.pages.DataSource.name().clear(); - e2e.pages.DataSource.name().type(name); - - if (basicAuth) { - e2e().contains('label', 'Basic auth').scrollIntoView().click(); - e2e() - .contains('.gf-form-group', 'Basic Auth Details') - .should('be.visible') - .scrollIntoView() - .within(() => { - if (basicAuthUser) { - e2e().get('[placeholder=user]').type(basicAuthUser); - } - if (basicAuthPassword) { - e2e().get('[placeholder=Password]').type(basicAuthPassword); - } - }); - } - - if (skipTlsVerify) { - e2e().contains('label', 'Skip TLS Verify').scrollIntoView().click(); - } - - form(); - - e2e.pages.DataSource.saveAndTest().click(); - - if (awaitHealth) { - e2e().wait('@health', { timeout: timeout ?? e2e.config().defaultCommandTimeout }); - } - - // use the timeout passed in if it exists, otherwise, continue to use the default - e2e.pages.DataSource.alert() - .should('exist') - .contains(expectedAlertMessage, { - timeout: timeout ?? e2e.config().defaultCommandTimeout, - }); - e2e().logToConsole('Added data source with name:', name); - - return e2e() - .url() - .then(() => { - e2e.getScenarioContext().then(({ addedDataSources }: any) => { - e2e.setScenarioContext({ - addedDataSources: [...addedDataSources, { name } as DeleteDataSourceConfig], - }); - }); - - // @todo remove `wrap` when possible - return e2e().wrap( - { - config: fullConfig, - }, - { log: false } - ); - }); -}; diff --git a/packages/grafana-e2e/src/flows/addPanel.ts b/packages/grafana-e2e/src/flows/addPanel.ts deleted file mode 100644 index 0f214e51835..00000000000 --- a/packages/grafana-e2e/src/flows/addPanel.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { v4 as uuidv4 } from 'uuid'; - -import { getScenarioContext } from '../support/scenarioContext'; - -import { configurePanel, PartialAddPanelConfig } from './configurePanel'; - -export const addPanel = (config?: Partial) => - getScenarioContext().then(({ lastAddedDataSource }: any) => - configurePanel({ - dataSourceName: lastAddedDataSource, - panelTitle: `e2e-${uuidv4()}`, - ...config, - isEdit: false, - }) - ); diff --git a/packages/grafana-e2e/src/flows/assertSuccessNotification.ts b/packages/grafana-e2e/src/flows/assertSuccessNotification.ts deleted file mode 100644 index 25f9228683c..00000000000 --- a/packages/grafana-e2e/src/flows/assertSuccessNotification.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { e2e } from '../index'; - -export const assertSuccessNotification = () => { - if (e2e.components.Alert.alertV2) { - e2e.components.Alert.alertV2('success').should('exist'); - } else { - e2e.components.Alert.alert('success').should('exist'); - } -}; diff --git a/packages/grafana-e2e/src/flows/configurePanel.ts b/packages/grafana-e2e/src/flows/configurePanel.ts deleted file mode 100644 index 59bc72216db..00000000000 --- a/packages/grafana-e2e/src/flows/configurePanel.ts +++ /dev/null @@ -1,192 +0,0 @@ -import { e2e } from '..'; -import { getScenarioContext } from '../support/scenarioContext'; - -import { setDashboardTimeRange } from './setDashboardTimeRange'; -import { TimeRangeConfig } from './setTimeRange'; - -interface AddPanelOverrides { - dataSourceName: string; - queriesForm: (config: AddPanelConfig) => void; - panelTitle: string; -} - -interface EditPanelOverrides { - queriesForm?: (config: EditPanelConfig) => void; - panelTitle: string; -} - -interface ConfigurePanelDefault { - chartData: { - method: string; - route: string | RegExp; - }; - dashboardUid: string; - matchScreenshot: boolean; - saveDashboard: boolean; - screenshotName: string; - visitDashboardAtStart: boolean; // @todo remove when possible -} - -interface ConfigurePanelOptional { - dataSourceName?: string; - queriesForm?: (config: ConfigurePanelConfig) => void; - panelTitle?: string; - timeRange?: TimeRangeConfig; - visualizationName?: string; - timeout?: number; -} - -interface ConfigurePanelRequired { - isEdit: boolean; -} - -export type PartialConfigurePanelConfig = Partial & - ConfigurePanelOptional & - ConfigurePanelRequired; - -export type ConfigurePanelConfig = ConfigurePanelDefault & ConfigurePanelOptional & ConfigurePanelRequired; - -export type PartialAddPanelConfig = PartialConfigurePanelConfig & AddPanelOverrides; -export type AddPanelConfig = ConfigurePanelConfig & AddPanelOverrides; - -export type PartialEditPanelConfig = PartialConfigurePanelConfig & EditPanelOverrides; -export type EditPanelConfig = ConfigurePanelConfig & EditPanelOverrides; - -// @todo this actually returns type `Cypress.Chainable` -export const configurePanel = (config: PartialAddPanelConfig | PartialEditPanelConfig | PartialConfigurePanelConfig) => - getScenarioContext().then(({ lastAddedDashboardUid }: any) => { - const fullConfig: AddPanelConfig | EditPanelConfig | ConfigurePanelConfig = { - chartData: { - method: 'POST', - route: '/api/ds/query', - }, - dashboardUid: lastAddedDashboardUid, - matchScreenshot: false, - saveDashboard: true, - screenshotName: 'panel-visualization', - visitDashboardAtStart: true, - ...config, - }; - - const { - chartData, - dashboardUid, - dataSourceName, - isEdit, - matchScreenshot, - panelTitle, - queriesForm, - screenshotName, - timeRange, - visitDashboardAtStart, - visualizationName, - timeout, - } = fullConfig; - - if (visitDashboardAtStart) { - e2e.flows.openDashboard({ uid: dashboardUid }); - } - - if (isEdit) { - e2e.components.Panels.Panel.title(panelTitle).click(); - e2e.components.Panels.Panel.headerItems('Edit').click(); - } else { - try { - e2e.components.PageToolbar.itemButton('Add button').should('be.visible'); - e2e.components.PageToolbar.itemButton('Add button').click(); - } catch (e) { - // Depending on the screen size, the "Add" button might be hidden - e2e.components.PageToolbar.item('Show more items').click(); - e2e.components.PageToolbar.item('Add button').last().click(); - } - e2e.pages.AddDashboard.itemButton('Add new visualization menu item').should('be.visible'); - e2e.pages.AddDashboard.itemButton('Add new visualization menu item').click(); - } - - if (timeRange) { - setDashboardTimeRange(timeRange); - } - - // @todo alias '/**/*.js*' as '@pluginModule' when possible: https://github.com/cypress-io/cypress/issues/1296 - - e2e().intercept(chartData.method, chartData.route).as('chartData'); - - if (dataSourceName) { - e2e.components.DataSourcePicker.container().click().type(`${dataSourceName}{downArrow}{enter}`); - } - - // @todo instead wait for '@pluginModule' if not already loaded - e2e().wait(2000); - - // `panelTitle` is needed to edit the panel, and unlikely to have its value changed at that point - const changeTitle = panelTitle && !isEdit; - - if (changeTitle || visualizationName) { - if (changeTitle && panelTitle) { - e2e.components.PanelEditor.OptionsPane.fieldLabel('Panel options Title').type(`{selectall}${panelTitle}`); - } - - if (visualizationName) { - e2e.components.PluginVisualization.item(visualizationName).scrollIntoView().click(); - - // @todo wait for '@pluginModule' if not a core visualization and not already loaded - e2e().wait(2000); - } - } else { - // Consistently closed - closeOptions(); - } - - if (queriesForm) { - queriesForm(fullConfig); - - // Wait for a possible complex visualization to render (or something related, as this isn't necessary on the dashboard page) - // Can't assert that its HTML changed because a new query could produce the same results - e2e().wait(1000); - } - - // @todo enable when plugins have this implemented - //e2e.components.QueryEditorRow.actionButton('Disable/enable query').click(); - //e2e().wait('@chartData'); - //e2e.components.Panels.Panel.containerByTitle(panelTitle).find('.panel-content').contains('No data'); - //e2e.components.QueryEditorRow.actionButton('Disable/enable query').click(); - //e2e().wait('@chartData'); - - // Avoid annotations flakiness - e2e.components.RefreshPicker.runButtonV2().first().click({ force: true }); - - // Wait for RxJS - e2e().wait(timeout ?? e2e.config().defaultCommandTimeout); - - if (matchScreenshot) { - let visualization; - - visualization = e2e.components.Panels.Panel.containerByTitle(panelTitle).find('.panel-content'); - - visualization.scrollIntoView().screenshot(screenshotName); - e2e().compareScreenshots(screenshotName); - } - - // @todo remove `wrap` when possible - return e2e().wrap({ config: fullConfig }, { log: false }); - }); - -// @todo this actually returns type `Cypress.Chainable` -const closeOptions = () => e2e.components.PanelEditor.toggleVizOptions().click(); - -export const VISUALIZATION_ALERT_LIST = 'Alert list'; -export const VISUALIZATION_BAR_GAUGE = 'Bar gauge'; -export const VISUALIZATION_CLOCK = 'Clock'; -export const VISUALIZATION_DASHBOARD_LIST = 'Dashboard list'; -export const VISUALIZATION_GAUGE = 'Gauge'; -export const VISUALIZATION_GRAPH = 'Graph'; -export const VISUALIZATION_HEAT_MAP = 'Heatmap'; -export const VISUALIZATION_LOGS = 'Logs'; -export const VISUALIZATION_NEWS = 'News'; -export const VISUALIZATION_PIE_CHART = 'Pie Chart'; -export const VISUALIZATION_PLUGIN_LIST = 'Plugin list'; -export const VISUALIZATION_POLYSTAT = 'Polystat'; -export const VISUALIZATION_STAT = 'Stat'; -export const VISUALIZATION_TABLE = 'Table'; -export const VISUALIZATION_TEXT = 'Text'; -export const VISUALIZATION_WORLD_MAP = 'Worldmap Panel'; diff --git a/packages/grafana-e2e/src/flows/deleteDashboard.ts b/packages/grafana-e2e/src/flows/deleteDashboard.ts deleted file mode 100644 index d69c3da7146..00000000000 --- a/packages/grafana-e2e/src/flows/deleteDashboard.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { e2e } from '../index'; -import { fromBaseUrl } from '../support/url'; - -export interface DeleteDashboardConfig { - quick?: boolean; - title: string; - uid: string; -} - -export const deleteDashboard = ({ quick = false, title, uid }: DeleteDashboardConfig) => { - e2e().logToConsole('Deleting dashboard with uid:', uid); - - if (quick) { - quickDelete(uid); - } else { - uiDelete(uid, title); - } - - e2e().logToConsole('Deleted dashboard with uid:', uid); - - e2e.getScenarioContext().then(({ addedDashboards }: any) => { - e2e.setScenarioContext({ - addedDashboards: addedDashboards.filter((dashboard: DeleteDashboardConfig) => { - return dashboard.title !== title && dashboard.uid !== uid; - }), - }); - }); -}; - -const quickDelete = (uid: string) => { - e2e().request('DELETE', fromBaseUrl(`/api/dashboards/uid/${uid}`)); -}; - -const uiDelete = (uid: string, title: string) => { - e2e.pages.Dashboard.visit(uid); - e2e.components.PageToolbar.item('Dashboard settings').click(); - e2e.pages.Dashboard.Settings.General.deleteDashBoard().click(); - e2e.pages.ConfirmModal.delete().click(); - e2e.flows.assertSuccessNotification(); - - e2e.pages.Dashboards.visit(); - - // @todo replace `e2e.pages.Dashboards.dashboards` with this when argument is empty - if (e2e.components.Search.dashboardItems) { - e2e.components.Search.dashboardItems().each((item) => e2e().wrap(item).should('not.contain', title)); - } else { - e2e() - .get('[aria-label^="Dashboard search item "]') - .each((item) => e2e().wrap(item).should('not.contain', title)); - } -}; diff --git a/packages/grafana-e2e/src/flows/deleteDataSource.ts b/packages/grafana-e2e/src/flows/deleteDataSource.ts deleted file mode 100644 index f68b38f3a79..00000000000 --- a/packages/grafana-e2e/src/flows/deleteDataSource.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { e2e } from '../index'; -import { fromBaseUrl } from '../support/url'; - -export interface DeleteDataSourceConfig { - id: string; - name: string; - quick?: boolean; -} - -export const deleteDataSource = ({ id, name, quick = false }: DeleteDataSourceConfig) => { - e2e().logToConsole('Deleting data source with name:', name); - - if (quick) { - quickDelete(name); - } else { - uiDelete(name); - } - - e2e().logToConsole('Deleted data source with name:', name); - - e2e.getScenarioContext().then(({ addedDataSources }: any) => { - e2e.setScenarioContext({ - addedDataSources: addedDataSources.filter((dataSource: DeleteDataSourceConfig) => { - return dataSource.id !== id && dataSource.name !== name; - }), - }); - }); -}; - -const quickDelete = (name: string) => { - e2e().request('DELETE', fromBaseUrl(`/api/datasources/name/${name}`)); -}; - -const uiDelete = (name: string) => { - e2e.pages.DataSources.visit(); - e2e.pages.DataSources.dataSources(name).click(); - e2e.pages.DataSource.delete().click(); - e2e.pages.ConfirmModal.delete().click(); - - e2e.pages.DataSources.visit(); - - // @todo replace `e2e.pages.DataSources.dataSources` with this when argument is empty - e2e() - .get('[aria-label^="Data source list item "]') - .each((item) => e2e().wrap(item).should('not.contain', name)); -}; diff --git a/packages/grafana-e2e/src/flows/editPanel.ts b/packages/grafana-e2e/src/flows/editPanel.ts deleted file mode 100644 index c213465902a..00000000000 --- a/packages/grafana-e2e/src/flows/editPanel.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { configurePanel, PartialEditPanelConfig } from './configurePanel'; - -export const editPanel = (config: Partial) => - configurePanel({ - ...config, - isEdit: true, - }); diff --git a/packages/grafana-e2e/src/flows/importDashboard.ts b/packages/grafana-e2e/src/flows/importDashboard.ts deleted file mode 100644 index 2d9edeee690..00000000000 --- a/packages/grafana-e2e/src/flows/importDashboard.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { e2e } from '../index'; -import { fromBaseUrl, getDashboardUid } from '../support/url'; - -import { DeleteDashboardConfig } from '.'; - -type Panel = { - title: string; - [key: string]: unknown; -}; - -export type Dashboard = { title: string; panels: Panel[]; uid: string; [key: string]: unknown }; - -/** - * Smoke test a particular dashboard by quickly importing a json file and validate that all the panels finish loading - * @param dashboardToImport a sample dashboard - * @param queryTimeout a number of ms to wait for the imported dashboard to finish loading - * @param skipPanelValidation skip panel validation - */ -export const importDashboard = (dashboardToImport: Dashboard, queryTimeout?: number, skipPanelValidation?: boolean) => { - e2e().visit(fromBaseUrl('/dashboard/import')); - - // Note: normally we'd use 'click' and then 'type' here, but the json object is so big that using 'val' is much faster - e2e.components.DashboardImportPage.textarea().should('be.visible'); - e2e.components.DashboardImportPage.textarea().click(); - e2e.components.DashboardImportPage.textarea().invoke('val', JSON.stringify(dashboardToImport)); - e2e.components.DashboardImportPage.submit().should('be.visible').click(); - e2e.components.ImportDashboardForm.name().should('be.visible').click().clear().type(dashboardToImport.title); - e2e.components.ImportDashboardForm.submit().should('be.visible').click(); - - // wait for dashboard to load - e2e().wait(queryTimeout || 6000); - - // save the newly imported dashboard to context so it'll get properly deleted later - e2e() - .url() - .should('contain', '/d/') - .then((url: string) => { - const uid = getDashboardUid(url); - - e2e.getScenarioContext().then(({ addedDashboards }: { addedDashboards: DeleteDashboardConfig[] }) => { - e2e.setScenarioContext({ - addedDashboards: [...addedDashboards, { title: dashboardToImport.title, uid }], - }); - }); - - expect(dashboardToImport.uid).to.equal(uid); - }); - - if (!skipPanelValidation) { - dashboardToImport.panels.forEach((panel) => { - // Look at the json data - e2e.components.Panels.Panel.menu(panel.title).click({ force: true }); // force click because menu is hidden and show on hover - e2e.components.Panels.Panel.menuItems('Inspect').should('be.visible').click(); - e2e.components.Tab.title('JSON').should('be.visible').click(); - e2e.components.PanelInspector.Json.content().should('be.visible').contains('Panel JSON').click({ force: true }); - e2e.components.Select.option().should('be.visible').contains('Panel data').click(); - - // ensures that panel has loaded without knowingly hitting an error - // note: this does not prove that data came back as we expected it, - // it could get `state: Done` for no data for example - // but it ensures we didn't hit a 401 or 500 or something like that - e2e.components.CodeEditor.container() - .should('be.visible') - .contains(/"state": "(Done|Streaming)"/); - - // need to close panel - e2e.components.Drawer.General.close().click(); - }); - } -}; diff --git a/packages/grafana-e2e/src/flows/importDashboards.ts b/packages/grafana-e2e/src/flows/importDashboards.ts deleted file mode 100644 index 90a126b947a..00000000000 --- a/packages/grafana-e2e/src/flows/importDashboards.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { e2e } from '../index'; - -import { importDashboard, Dashboard } from './importDashboard'; - -/** - * Smoke test several dashboard json files from a test directory - * and validate that all the panels in each import finish loading their queries - * @param dirPath the relative path to a directory which contains json files representing dashboards, - * for example if your dashboards live in `cypress/testDashboards` you can pass `/testDashboards` - * @param queryTimeout a number of ms to wait for the imported dashboard to finish loading - * @param skipPanelValidation skips panel validation - */ -export const importDashboards = async (dirPath: string, queryTimeout?: number, skipPanelValidation?: boolean) => { - e2e() - .getJSONFilesFromDir(dirPath) - .then((jsonFiles: Dashboard[]) => { - jsonFiles.forEach((file) => { - importDashboard(file, queryTimeout || 6000, skipPanelValidation); - }); - }); -}; diff --git a/packages/grafana-e2e/src/flows/index.ts b/packages/grafana-e2e/src/flows/index.ts deleted file mode 100644 index e7fbfb3ce82..00000000000 --- a/packages/grafana-e2e/src/flows/index.ts +++ /dev/null @@ -1,36 +0,0 @@ -export * from './addDashboard'; -export * from './addDataSource'; -export * from './addPanel'; -export * from './assertSuccessNotification'; -export * from './deleteDashboard'; -export * from './deleteDataSource'; -export * from './editPanel'; -export * from './login'; -export * from './openDashboard'; -export * from './openPanelMenuItem'; -export * from './revertAllChanges'; -export * from './saveDashboard'; -export * from './selectOption'; -export * from './setTimeRange'; -export * from './importDashboard'; -export * from './importDashboards'; -export * from './userPreferences'; - -export { - VISUALIZATION_ALERT_LIST, - VISUALIZATION_BAR_GAUGE, - VISUALIZATION_CLOCK, - VISUALIZATION_DASHBOARD_LIST, - VISUALIZATION_GAUGE, - VISUALIZATION_GRAPH, - VISUALIZATION_HEAT_MAP, - VISUALIZATION_LOGS, - VISUALIZATION_NEWS, - VISUALIZATION_PIE_CHART, - VISUALIZATION_PLUGIN_LIST, - VISUALIZATION_POLYSTAT, - VISUALIZATION_STAT, - VISUALIZATION_TABLE, - VISUALIZATION_TEXT, - VISUALIZATION_WORLD_MAP, -} from './configurePanel'; diff --git a/packages/grafana-e2e/src/flows/login.ts b/packages/grafana-e2e/src/flows/login.ts deleted file mode 100644 index 744e4332754..00000000000 --- a/packages/grafana-e2e/src/flows/login.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { e2e } from '../index'; -import { fromBaseUrl } from '../support/url'; - -const DEFAULT_USERNAME = 'admin'; -const DEFAULT_PASSWORD = 'admin'; - -const loginApi = (username: string, password: string) => { - cy.request({ - method: 'POST', - url: fromBaseUrl('/login'), - body: { - user: username, - password, - }, - }); -}; - -const loginUi = (username: string, password: string) => { - e2e().logToConsole('Logging in with username:', username); - e2e.pages.Login.visit(); - e2e.pages.Login.username() - .should('be.visible') // prevents flakiness - .type(username); - e2e.pages.Login.password().type(password); - e2e.pages.Login.submit().click(); - - // Local tests will have insecure credentials - if (password === DEFAULT_PASSWORD) { - e2e.pages.Login.skip().should('be.visible').click(); - } - - e2e().get('.login-page').should('not.exist'); -}; - -export const login = (username = DEFAULT_USERNAME, password = DEFAULT_PASSWORD, loginViaApi = true) => { - if (loginViaApi) { - loginApi(username, password); - } else { - loginUi(username, password); - } - e2e().logToConsole('Logged in with username:', username); -}; diff --git a/packages/grafana-e2e/src/flows/openDashboard.ts b/packages/grafana-e2e/src/flows/openDashboard.ts deleted file mode 100644 index c535099e63a..00000000000 --- a/packages/grafana-e2e/src/flows/openDashboard.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { e2e } from '../index'; -import { getScenarioContext } from '../support/scenarioContext'; - -import { setDashboardTimeRange, TimeRangeConfig } from './setDashboardTimeRange'; - -interface OpenDashboardDefault { - uid: string; -} - -interface OpenDashboardOptional { - timeRange?: TimeRangeConfig; - queryParams?: object; -} - -export type PartialOpenDashboardConfig = Partial & OpenDashboardOptional; -export type OpenDashboardConfig = OpenDashboardDefault & OpenDashboardOptional; - -// @todo this actually returns type `Cypress.Chainable` -export const openDashboard = (config?: PartialOpenDashboardConfig) => - getScenarioContext().then(({ lastAddedDashboardUid }: any) => { - const fullConfig: OpenDashboardConfig = { - uid: lastAddedDashboardUid, - ...config, - }; - - const { timeRange, uid, queryParams } = fullConfig; - - e2e.pages.Dashboard.visit(uid, queryParams); - - if (timeRange) { - setDashboardTimeRange(timeRange); - } - - // @todo remove `wrap` when possible - return e2e().wrap({ config: fullConfig }, { log: false }); - }); diff --git a/packages/grafana-e2e/src/flows/openPanelMenuItem.ts b/packages/grafana-e2e/src/flows/openPanelMenuItem.ts deleted file mode 100644 index 1ccd5206826..00000000000 --- a/packages/grafana-e2e/src/flows/openPanelMenuItem.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { e2e } from '../index'; - -export enum PanelMenuItems { - Edit = 'Edit', - Inspect = 'Inspect', - More = 'More...', - Extensions = 'Extensions', -} - -export const openPanelMenuItem = (menu: PanelMenuItems, panelTitle = 'Panel Title') => { - // we changed the way we open the panel menu in react panels with the new panel header - detectPanelType(panelTitle, (isAngularPanel) => { - if (isAngularPanel) { - e2e.components.Panels.Panel.title(panelTitle).should('be.visible').click(); - e2e.components.Panels.Panel.headerItems(menu).should('be.visible').click(); - } else { - e2e.components.Panels.Panel.menu(panelTitle).click({ force: true }); // force click because menu is hidden and show on hover - e2e.components.Panels.Panel.menuItems(menu).should('be.visible').click(); - } - }); -}; - -export const openPanelMenuExtension = (extensionTitle: string, panelTitle = 'Panel Title') => { - const menuItem = PanelMenuItems.Extensions; - // we changed the way we open the panel menu in react panels with the new panel header - detectPanelType(panelTitle, (isAngularPanel) => { - if (isAngularPanel) { - e2e.components.Panels.Panel.title(panelTitle).should('be.visible').click(); - e2e.components.Panels.Panel.headerItems(menuItem) - .should('be.visible') - .parent() - .parent() - .invoke('addClass', 'open'); - e2e.components.Panels.Panel.headerItems(extensionTitle).should('be.visible').click(); - } else { - e2e.components.Panels.Panel.menu(panelTitle).click({ force: true }); // force click because menu is hidden and show on hover - e2e.components.Panels.Panel.menuItems(menuItem).trigger('mouseover', { force: true }); - e2e.components.Panels.Panel.menuItems(extensionTitle).click({ force: true }); - } - }); -}; - -function detectPanelType(panelTitle: string, detected: (isAngularPanel: boolean) => void) { - e2e.components.Panels.Panel.title(panelTitle).then((el) => { - const isAngularPanel = el.find('plugin-component.ng-scope').length > 0; - - if (isAngularPanel) { - Cypress.log({ - name: 'detectPanelType', - displayName: 'detector', - message: 'Angular panel detected, will use legacy selectors.', - }); - } - - detected(isAngularPanel); - }); -} diff --git a/packages/grafana-e2e/src/flows/revertAllChanges.ts b/packages/grafana-e2e/src/flows/revertAllChanges.ts deleted file mode 100644 index 94b2e966bd6..00000000000 --- a/packages/grafana-e2e/src/flows/revertAllChanges.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { e2e } from '../index'; - -export const revertAllChanges = () => { - e2e.getScenarioContext().then(({ addedDashboards, addedDataSources, hasChangedUserPreferences }) => { - addedDashboards.forEach((dashboard: any) => e2e.flows.deleteDashboard({ ...dashboard, quick: true })); - addedDataSources.forEach((dataSource: any) => e2e.flows.deleteDataSource({ ...dataSource, quick: true })); - - if (hasChangedUserPreferences) { - e2e.flows.setDefaultUserPreferences(); - } - }); -}; diff --git a/packages/grafana-e2e/src/flows/saveDashboard.ts b/packages/grafana-e2e/src/flows/saveDashboard.ts deleted file mode 100644 index 37d5238f431..00000000000 --- a/packages/grafana-e2e/src/flows/saveDashboard.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { e2e } from '../index'; - -export const saveDashboard = () => { - e2e.components.PageToolbar.item('Save dashboard').click(); - - e2e.pages.SaveDashboardModal.save().click(); - - e2e.flows.assertSuccessNotification(); -}; diff --git a/packages/grafana-e2e/src/flows/selectOption.ts b/packages/grafana-e2e/src/flows/selectOption.ts deleted file mode 100644 index d47918c9fb5..00000000000 --- a/packages/grafana-e2e/src/flows/selectOption.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { e2e } from '../index'; - -export interface SelectOptionConfig { - clickToOpen?: boolean; - container: any; - forceClickOption?: boolean; - optionText: string | RegExp; -} - -// @todo this actually returns type `Cypress.Chainable` -export const selectOption = (config: SelectOptionConfig): any => { - const fullConfig: SelectOptionConfig = { - clickToOpen: true, - forceClickOption: false, - ...config, - }; - - const { clickToOpen, container, forceClickOption, optionText } = fullConfig; - - container.within(() => { - if (clickToOpen) { - e2e() - .get('[class$="-input-suffix"]', { timeout: 1000 }) - .then((element) => { - expect(Cypress.dom.isAttached(element)).to.eq(true); - e2e().get('[class$="-input-suffix"]', { timeout: 1000 }).click({ force: true }); - }); - } - }); - - return e2e.components.Select.option() - .filter((_, { textContent }) => { - if (textContent === null) { - return false; - } else if (typeof optionText === 'string') { - return textContent.includes(optionText); - } else { - return optionText.test(textContent); - } - }) - .scrollIntoView() - .click({ force: forceClickOption }); -}; diff --git a/packages/grafana-e2e/src/flows/setDashboardTimeRange.ts b/packages/grafana-e2e/src/flows/setDashboardTimeRange.ts deleted file mode 100644 index 566934c24e7..00000000000 --- a/packages/grafana-e2e/src/flows/setDashboardTimeRange.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { setTimeRange, TimeRangeConfig } from './setTimeRange'; - -export type { TimeRangeConfig }; - -export const setDashboardTimeRange = (config: TimeRangeConfig) => setTimeRange(config); diff --git a/packages/grafana-e2e/src/flows/setTimeRange.ts b/packages/grafana-e2e/src/flows/setTimeRange.ts deleted file mode 100644 index fe3528fea19..00000000000 --- a/packages/grafana-e2e/src/flows/setTimeRange.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { e2e } from '../index'; - -import { selectOption } from './selectOption'; - -export interface TimeRangeConfig { - from: string; - to: string; - zone?: string; -} - -export const setTimeRange = ({ from, to, zone }: TimeRangeConfig) => { - e2e.components.TimePicker.openButton().click(); - - if (zone) { - e2e().contains('button', 'Change time settings').click(); - e2e().log('setting time zone to ' + zone); - - if (e2e.components.TimeZonePicker.containerV2) { - selectOption({ - clickToOpen: true, - container: e2e.components.TimeZonePicker.containerV2(), - optionText: zone, - }); - } else { - selectOption({ - clickToOpen: true, - container: e2e.components.TimeZonePicker.container(), - optionText: zone, - }); - } - } - - // For smaller screens - e2e.components.TimePicker.absoluteTimeRangeTitle().click(); - - e2e.components.TimePicker.fromField().clear().type(from); - e2e.components.TimePicker.toField().clear().type(to); - - e2e.components.TimePicker.applyTimeRange().click(); -}; diff --git a/packages/grafana-e2e/src/flows/userPreferences.ts b/packages/grafana-e2e/src/flows/userPreferences.ts deleted file mode 100644 index 621bff04ce6..00000000000 --- a/packages/grafana-e2e/src/flows/userPreferences.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { Preferences as UserPreferencesDTO } from '@grafana/schema/src/raw/preferences/x/preferences_types.gen'; - -import { e2e } from '..'; -import { fromBaseUrl } from '../support/url'; - -const defaultUserPreferences = { - timezone: '', // "Default" option -} as const; // TODO: when we update typescript >4.9 change to `as const satisfies UserPreferencesDTO` - -// Only accept preferences we have defaults for as arguments. To allow a new preference to be set, add a default for it -type UserPreferences = Pick; - -export function setUserPreferences(prefs: UserPreferences) { - e2e.setScenarioContext({ hasChangedUserPreferences: prefs !== defaultUserPreferences }); - - return cy.request({ - method: 'PUT', - url: fromBaseUrl('/api/user/preferences'), - body: prefs, - }); -} - -export function setDefaultUserPreferences() { - return setUserPreferences(defaultUserPreferences); -} diff --git a/packages/grafana-e2e/src/index.ts b/packages/grafana-e2e/src/index.ts deleted file mode 100644 index a7df2bec40f..00000000000 --- a/packages/grafana-e2e/src/index.ts +++ /dev/null @@ -1,31 +0,0 @@ -/** - * A library for writing end-to-end tests for Grafana and its ecosystem. - * - * @packageDocumentation - */ -import { E2ESelectors, Selectors, selectors } from '@grafana/e2e-selectors'; - -import * as flows from './flows'; -import { e2eFactory } from './support'; -import { benchmark } from './support/benchmark'; -import { e2eScenario, ScenarioArguments } from './support/scenario'; -import { getScenarioContext, setScenarioContext } from './support/scenarioContext'; -import * as typings from './typings'; - -const e2eObject = { - env: (args: string) => Cypress.env(args), - config: () => Cypress.config(), - blobToBase64String: (blob: Blob) => Cypress.Blob.blobToBase64String(blob), - imgSrcToBlob: (url: string) => Cypress.Blob.imgSrcToBlob(url), - scenario: (args: ScenarioArguments) => e2eScenario(args), - benchmark, - pages: e2eFactory({ selectors: selectors.pages }), - typings, - components: e2eFactory({ selectors: selectors.components }), - flows, - getScenarioContext, - setScenarioContext, - getSelectors: (selectors: E2ESelectors) => e2eFactory({ selectors }), -}; - -export const e2e: (() => Cypress.cy) & typeof e2eObject = Object.assign(() => cy, e2eObject); diff --git a/packages/grafana-e2e/src/support/benchmark.ts b/packages/grafana-e2e/src/support/benchmark.ts deleted file mode 100644 index 70dbfb45028..00000000000 --- a/packages/grafana-e2e/src/support/benchmark.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { e2e } from '../'; - -export interface BenchmarkArguments { - name: string; - dashboard: { - folder: string; - delayAfterOpening: number; - skipPanelValidation: boolean; - }; - repeat: number; - duration: number; - appStats?: { - startCollecting?: (window: Window) => void; - collect: (window: Window) => Record; - }; - skipScenario?: boolean; -} - -export const benchmark = ({ - name, - skipScenario = false, - repeat, - duration, - appStats, - dashboard, -}: BenchmarkArguments) => { - if (skipScenario) { - describe(name, () => { - it.skip(name, () => {}); - }); - } - - describe(name, () => { - before(() => { - e2e.flows.login(e2e.env('USERNAME'), e2e.env('PASSWORD')); - }); - - beforeEach(() => { - e2e.flows.importDashboards(dashboard.folder, 1000, dashboard.skipPanelValidation); - Cypress.Cookies.preserveOnce('grafana_session'); - }); - - afterEach(() => e2e.flows.revertAllChanges()); - after(() => { - e2e().clearCookies(); - }); - - Array(repeat) - .fill(0) - .map((_, i) => { - const testName = `${name}-${i}`; - return it(testName, () => { - e2e.flows.openDashboard(); - - e2e().wait(dashboard.delayAfterOpening); - - if (appStats) { - const startCollecting = appStats.startCollecting; - if (startCollecting) { - e2e() - .window() - .then((win) => startCollecting(win)); - } - - e2e().startBenchmarking(testName); - e2e().wait(duration); - - e2e() - .window() - .then((win) => { - e2e().stopBenchmarking(testName, appStats.collect(win)); - }); - } else { - e2e().startBenchmarking(testName); - e2e().wait(duration); - e2e().stopBenchmarking(testName, {}); - } - }); - }); - }); -}; diff --git a/packages/grafana-e2e/src/support/index.ts b/packages/grafana-e2e/src/support/index.ts deleted file mode 100644 index 1ebbc3f881e..00000000000 --- a/packages/grafana-e2e/src/support/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export * from './localStorage'; -export * from './scenarioContext'; -export * from './selector'; -export * from './types'; diff --git a/packages/grafana-e2e/src/support/localStorage.ts b/packages/grafana-e2e/src/support/localStorage.ts deleted file mode 100644 index eec99559ef6..00000000000 --- a/packages/grafana-e2e/src/support/localStorage.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { e2e } from '../index'; - -// @todo this actually returns type `Cypress.Chainable` -const get = (key: string): any => - e2e() - .wrap({ getLocalStorage: () => localStorage.getItem(key) }, { log: false }) - .invoke('getLocalStorage'); - -// @todo this actually returns type `Cypress.Chainable` -export const getLocalStorage = (key: string): any => - get(key).then((value: any) => { - if (value === null) { - return value; - } else { - return JSON.parse(value); - } - }); - -// @todo this actually returns type `Cypress.Chainable` -export const requireLocalStorage = (key: string): any => - get(key) // `getLocalStorage()` would turn 'null' into `null` - .should('not.equal', null) - .then((value: any) => JSON.parse(value as string)); diff --git a/packages/grafana-e2e/src/support/scenario.ts b/packages/grafana-e2e/src/support/scenario.ts deleted file mode 100644 index 85155ef365f..00000000000 --- a/packages/grafana-e2e/src/support/scenario.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { e2e } from '../'; - -export interface ScenarioArguments { - describeName: string; - itName: string; - scenario: Function; - skipScenario?: boolean; - addScenarioDataSource?: boolean; - addScenarioDashBoard?: boolean; - loginViaApi?: boolean; -} - -export const e2eScenario = ({ - describeName, - itName, - scenario, - skipScenario = false, - addScenarioDataSource = false, - addScenarioDashBoard = false, - loginViaApi = true, -}: ScenarioArguments) => { - describe(describeName, () => { - if (skipScenario) { - it.skip(itName, () => scenario()); - } else { - before(() => { - e2e.flows.login(e2e.env('USERNAME'), e2e.env('PASSWORD'), loginViaApi); - e2e.flows.setDefaultUserPreferences(); - }); - - beforeEach(() => { - Cypress.Cookies.preserveOnce('grafana_session'); - - if (addScenarioDataSource) { - e2e.flows.addDataSource(); - } - if (addScenarioDashBoard) { - e2e.flows.addDashboard(); - } - }); - - afterEach(() => e2e.flows.revertAllChanges()); - after(() => e2e().clearCookies()); - - it(itName, () => scenario()); - - // @todo remove when possible: https://github.com/cypress-io/cypress/issues/2831 - it('temporary', () => {}); - } - }); -}; diff --git a/packages/grafana-e2e/src/support/scenarioContext.ts b/packages/grafana-e2e/src/support/scenarioContext.ts deleted file mode 100644 index 741588da523..00000000000 --- a/packages/grafana-e2e/src/support/scenarioContext.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { DeleteDashboardConfig } from '../flows/deleteDashboard'; -import { DeleteDataSourceConfig } from '../flows/deleteDataSource'; -import { e2e } from '../index'; - -export interface ScenarioContext { - addedDashboards: DeleteDashboardConfig[]; - addedDataSources: DeleteDataSourceConfig[]; - lastAddedDashboard: string; // @todo rename to `lastAddedDashboardTitle` - lastAddedDashboardUid: string; - lastAddedDataSource: string; // @todo rename to `lastAddedDataSourceName` - lastAddedDataSourceId: string; - hasChangedUserPreferences: boolean; - [key: string]: any; -} - -const scenarioContext: ScenarioContext = { - addedDashboards: [], - addedDataSources: [], - hasChangedUserPreferences: false, - get lastAddedDashboard() { - return lastProperty(this.addedDashboards, 'title'); - }, - get lastAddedDashboardUid() { - return lastProperty(this.addedDashboards, 'uid'); - }, - get lastAddedDataSource() { - return lastProperty(this.addedDataSources, 'name'); - }, - get lastAddedDataSourceId() { - return lastProperty(this.addedDataSources, 'id'); - }, -}; - -const lastProperty = ( - items: T[], - key: K -) => items[items.length - 1]?.[key] ?? ''; - -export const getScenarioContext = (): Cypress.Chainable => - e2e() - .wrap( - { - getScenarioContext: (): ScenarioContext => ({ ...scenarioContext }), - }, - { log: false } - ) - .invoke({ log: false }, 'getScenarioContext'); - -export const setScenarioContext = (newContext: Partial): Cypress.Chainable => - e2e() - .wrap( - { - setScenarioContext: () => { - Object.entries(newContext).forEach(([key, value]) => { - scenarioContext[key] = value; - }); - }, - }, - { log: false } - ) - .invoke({ log: false }, 'setScenarioContext'); diff --git a/packages/grafana-e2e/src/support/selector.ts b/packages/grafana-e2e/src/support/selector.ts deleted file mode 100644 index 8e49d54afa1..00000000000 --- a/packages/grafana-e2e/src/support/selector.ts +++ /dev/null @@ -1,11 +0,0 @@ -export interface SelectorApi { - fromAriaLabel: (selector: string) => string; - fromDataTestId: (selector: string) => string; - fromSelector: (selector: string) => string; -} - -export const Selector: SelectorApi = { - fromAriaLabel: (selector: string) => `[aria-label="${selector}"]`, - fromDataTestId: (selector: string) => `[data-testid="${selector}"]`, - fromSelector: (selector: string) => selector, -}; diff --git a/packages/grafana-e2e/src/support/types.ts b/packages/grafana-e2e/src/support/types.ts deleted file mode 100644 index 60a275882ad..00000000000 --- a/packages/grafana-e2e/src/support/types.ts +++ /dev/null @@ -1,138 +0,0 @@ -import { CssSelector, FunctionSelector, Selectors, StringSelector, UrlSelector } from '@grafana/e2e-selectors'; - -import { e2e } from '../index'; - -import { Selector } from './selector'; -import { fromBaseUrl } from './url'; - -export type VisitFunction = (args?: string, queryParams?: object) => Cypress.Chainable; -export type E2EVisit = { visit: VisitFunction }; -export type E2EFunction = ((text?: string, options?: CypressOptions) => Cypress.Chainable>) & - E2EFunctionWithOnlyOptions; -export type E2EFunctionWithOnlyOptions = (options?: CypressOptions) => Cypress.Chainable>; - -export type TypeSelectors = S extends StringSelector - ? E2EFunctionWithOnlyOptions - : S extends FunctionSelector - ? E2EFunction - : S extends CssSelector - ? E2EFunction - : S extends UrlSelector - ? E2EVisit & Omit, 'url'> - : S extends Record - ? E2EFunctions - : S; - -export type E2EFunctions = { - [P in keyof S]: TypeSelectors; -}; - -export type E2EObjects = E2EFunctions; - -export type E2EFactoryArgs = { selectors: S }; - -export type CypressOptions = Partial; - -const processSelectors = (e2eObjects: E2EFunctions, selectors: S): E2EFunctions => { - const logOutput = (data: any) => e2e().logToConsole('Retrieving Selector:', data); - const keys = Object.keys(selectors); - for (let index = 0; index < keys.length; index++) { - const key = keys[index]; - const value = selectors[key]; - - if (key === 'url') { - // @ts-ignore - e2eObjects['visit'] = (args?: string, queryParams?: object) => { - let parsedUrl = ''; - if (typeof value === 'string') { - parsedUrl = fromBaseUrl(value); - } - - if (typeof value === 'function' && args) { - parsedUrl = fromBaseUrl(value(args)); - } - - e2e().logToConsole('Visiting', parsedUrl); - if (queryParams) { - return e2e().visit({ url: parsedUrl, qs: queryParams }); - } else { - return e2e().visit(parsedUrl); - } - }; - - continue; - } - - if (typeof value === 'string') { - // @ts-ignore - e2eObjects[key] = (options?: CypressOptions) => { - logOutput(value); - const selector = value.startsWith('data-testid') - ? Selector.fromDataTestId(value) - : Selector.fromAriaLabel(value); - - return e2e().get(selector, options); - }; - - continue; - } - - if (typeof value === 'function') { - // @ts-ignore - e2eObjects[key] = function (textOrOptions?: string | CypressOptions, options?: CypressOptions) { - // the input can only be () - if (arguments.length === 0) { - const selector = value(undefined as unknown as string); - - logOutput(selector); - return e2e().get(selector); - } - - // the input can be (text) or (options) - if (arguments.length === 1) { - if (typeof textOrOptions === 'string') { - const selectorText = value(textOrOptions); - const selector = selectorText.startsWith('data-testid') - ? Selector.fromDataTestId(selectorText) - : Selector.fromAriaLabel(selectorText); - - logOutput(selector); - return e2e().get(selector); - } - const selector = value(undefined as unknown as string); - - logOutput(selector); - return e2e().get(selector, textOrOptions); - } - - // the input can only be (text, options) - if (arguments.length === 2 && typeof textOrOptions === 'string') { - const text = textOrOptions; - const selectorText = value(text); - const selector = text.startsWith('data-testid') - ? Selector.fromDataTestId(selectorText) - : Selector.fromAriaLabel(selectorText); - - logOutput(selector); - return e2e().get(selector, options); - } - }; - - continue; - } - - if (typeof value === 'object') { - // @ts-ignore - e2eObjects[key] = processSelectors({}, value); - } - } - - return e2eObjects; -}; - -export const e2eFactory = ({ selectors }: E2EFactoryArgs): E2EObjects => { - const e2eObjects: E2EFunctions = {} as E2EFunctions; - processSelectors(e2eObjects, selectors); - - return { ...e2eObjects }; -}; diff --git a/packages/grafana-e2e/src/support/url.ts b/packages/grafana-e2e/src/support/url.ts deleted file mode 100644 index 7af06d000ac..00000000000 --- a/packages/grafana-e2e/src/support/url.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { e2e } from '../index'; - -const getBaseUrl = () => e2e.env('BASE_URL') || e2e.config().baseUrl || 'http://localhost:3000'; - -export const fromBaseUrl = (url = '') => new URL(url, getBaseUrl()).href; - -export const getDashboardUid = (url: string): string => { - const matches = new URL(url).pathname.match(/\/d\/([^/]+)/); - if (!matches) { - throw new Error(`Couldn't parse uid from ${url}`); - } else { - return matches[1]; - } -}; diff --git a/packages/grafana-e2e/src/typings/index.ts b/packages/grafana-e2e/src/typings/index.ts deleted file mode 100644 index d4a4805a21f..00000000000 --- a/packages/grafana-e2e/src/typings/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { undo } from './undo'; diff --git a/packages/grafana-e2e/src/typings/undo.ts b/packages/grafana-e2e/src/typings/undo.ts deleted file mode 100644 index 9ee3c1cda40..00000000000 --- a/packages/grafana-e2e/src/typings/undo.ts +++ /dev/null @@ -1,19 +0,0 @@ -// https://nodejs.org/api/os.html#os_os_platform -enum Platform { - osx = 'darwin', - windows = 'win32', - linux = 'linux', - aix = 'aix', - freebsd = 'freebsd', - openbsd = 'openbsd', - sunos = 'sunos', -} - -export const undo = () => { - switch (Cypress.platform) { - case Platform.osx: - return '{cmd}z'; - default: - return '{ctrl}z'; - } -}; diff --git a/packages/grafana-e2e/test/cypress/integration/0.cli.ts b/packages/grafana-e2e/test/cypress/integration/0.cli.ts deleted file mode 100644 index 52d23a4f242..00000000000 --- a/packages/grafana-e2e/test/cypress/integration/0.cli.ts +++ /dev/null @@ -1,3 +0,0 @@ -describe('CLI', () => { - it('compiles this file and runs it', () => {}); -}); diff --git a/packages/grafana-e2e/test/cypress/integration/1.api.ts b/packages/grafana-e2e/test/cypress/integration/1.api.ts deleted file mode 100644 index 00e790fab8e..00000000000 --- a/packages/grafana-e2e/test/cypress/integration/1.api.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { e2e } from '../../../dist'; - -describe('API', () => { - it('can be imported', () => { - expect(e2e).to.be.a('function'); - }); -}); diff --git a/packages/grafana-e2e/test/cypress/tsconfig.json b/packages/grafana-e2e/test/cypress/tsconfig.json deleted file mode 100644 index 16a03900977..00000000000 --- a/packages/grafana-e2e/test/cypress/tsconfig.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "extends": "@grafana/tsconfig", - "include": ["**/*.ts"], - "compilerOptions": { - "baseUrl": "../node_modules", - "types": ["cypress", "cypress-file-upload"] - } -} diff --git a/packages/grafana-e2e/tsconfig.build.json b/packages/grafana-e2e/tsconfig.build.json deleted file mode 100644 index 9ec189c28ea..00000000000 --- a/packages/grafana-e2e/tsconfig.build.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "exclude": ["dist", "node_modules", "**/*.test.ts*"], - "extends": "./tsconfig.json" -} diff --git a/packages/grafana-e2e/tsconfig.json b/packages/grafana-e2e/tsconfig.json deleted file mode 100644 index de9e076e2eb..00000000000 --- a/packages/grafana-e2e/tsconfig.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "compilerOptions": { - "declarationDir": "./compiled", - "emitDeclarationOnly": true, - "isolatedModules": true, - "rootDirs": ["."], - "types": ["cypress"] - }, - "exclude": ["dist/**/*"], - "extends": "@grafana/tsconfig", - "include": ["src/**/*.ts", "cypress/support/index.d.ts"] -} diff --git a/packages/grafana-eslint-rules/README.md b/packages/grafana-eslint-rules/README.md index 187e1d2deb9..1fb043a4ec0 100644 --- a/packages/grafana-eslint-rules/README.md +++ b/packages/grafana-eslint-rules/README.md @@ -24,7 +24,89 @@ Avoid direct use of `animation*` or `transition*` properties. To account for users with motion sensitivities, these should always be wrapped in a [`prefers-reduced-motion`](https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-reduced-motion) media query. -`@grafana/ui` exposes a `handledReducedMotion` utility function that can be used to handle this. +There is a `handleMotion` utility function exposed on the theme that can help with this. + +#### Examples + +```tsx +// Bad ❌ +const getStyles = (theme: GrafanaTheme2) => ({ + loading: css({ + animationName: rotate, + animationDuration: '2s', + animationIterationCount: 'infinite', + }), +}); + +// Good ✅ +const getStyles = (theme: GrafanaTheme2) => ({ + loading: css({ + [theme.transitions.handleMotion('no-preference')]: { + animationName: rotate, + animationDuration: '2s', + animationIterationCount: 'infinite', + }, + [theme.transitions.handleMotion('reduce')]: { + animationName: pulse, + animationDuration: '2s', + animationIterationCount: 'infinite', + }, + }), +}); + +// Good ✅ +const getStyles = (theme: GrafanaTheme2) => ({ + loading: css({ + '@media (prefers-reduced-motion: no-preference)': { + animationName: rotate, + animationDuration: '2s', + animationIterationCount: 'infinite', + }, + '@media (prefers-reduced-motion: reduce)': { + animationName: pulse, + animationDuration: '2s', + animationIterationCount: 'infinite', + }, + }), +}); +``` + +Note we've switched the potentially sensitive rotating animation to a less intense pulse animation when `prefers-reduced-motion` is set. + +Animations that involve only non-moving properties, like opacity, color, and blurs, are unlikely to be problematic. In those cases, you still need to wrap the animation in a `prefers-reduced-motion` media query, but you can use the same animation for both cases: + +```tsx +// Bad ❌ +const getStyles = (theme: GrafanaTheme2) => ({ + card: css({ + transition: theme.transitions.create(['background-color'], { + duration: theme.transitions.duration.short, + }), + }), +}); + +// Good ✅ +const getStyles = (theme: GrafanaTheme2) => ({ + card: css({ + [theme.transitions.handleMotion('no-preference', 'reduce')]: { + transition: theme.transitions.create(['background-color'], { + duration: theme.transitions.duration.short, + }), + }, + }), +}); + +// Good ✅ +const getStyles = (theme: GrafanaTheme2) => ({ + card: css({ + '@media (prefers-reduced-motion: no-preference), @media (prefers-reduced-motion: reduce)': { + transition: theme.transitions.create(['background-color'], { + duration: theme.transitions.duration.short, + }), + }, + }), +}); +``` ### `theme-token-usage` diff --git a/packages/grafana-eslint-rules/rules/no-unreduced-motion.cjs b/packages/grafana-eslint-rules/rules/no-unreduced-motion.cjs index a8f673a4be2..a1b279fb861 100644 --- a/packages/grafana-eslint-rules/rules/no-unreduced-motion.cjs +++ b/packages/grafana-eslint-rules/rules/no-unreduced-motion.cjs @@ -55,7 +55,7 @@ const rule = createRule({ description: 'Check if animation or transition properties are used directly.', }, messages: { - noUnreducedMotion: 'Avoid direct use of `animation*` or `transition*` properties. Use the `handleReducedMotion` utility function or wrap in a `prefers-reduced-motion` media query.', + noUnreducedMotion: 'Avoid direct use of `animation*` or `transition*` properties. Use the `handleMotion` utility function from theme.transitions or wrap in a `prefers-reduced-motion` media query.', }, schema: [], }, diff --git a/packages/grafana-flamegraph/package.json b/packages/grafana-flamegraph/package.json index aec4c058d74..95e2c9747c3 100644 --- a/packages/grafana-flamegraph/package.json +++ b/packages/grafana-flamegraph/package.json @@ -61,16 +61,19 @@ "@babel/preset-react": "7.24.1", "@grafana/tsconfig": "^1.3.0-rc1", "@rollup/plugin-node-resolve": "15.2.3", + "@testing-library/dom": "10.0.0", "@testing-library/jest-dom": "^6.1.2", "@testing-library/react": "15.0.2", "@testing-library/user-event": "14.5.2", "@types/d3": "^7", "@types/jest": "^29.5.4", "@types/lodash": "4.17.0", + "@types/node": "20.12.7", "@types/react": "18.2.79", "@types/react-virtualized-auto-sizer": "1.0.4", "@types/tinycolor2": "1.4.6", "babel-jest": "29.7.0", + "esbuild": "0.20.2", "jest": "^29.6.4", "jest-canvas-mock": "2.5.2", "rollup": "2.79.1", diff --git a/packages/grafana-flamegraph/src/FlameGraph/FlameGraph.tsx b/packages/grafana-flamegraph/src/FlameGraph/FlameGraph.tsx index 0a207cb94ea..5340bd9d122 100644 --- a/packages/grafana-flamegraph/src/FlameGraph/FlameGraph.tsx +++ b/packages/grafana-flamegraph/src/FlameGraph/FlameGraph.tsx @@ -185,33 +185,32 @@ const FlameGraph = ({ }; const getStyles = () => ({ - graph: css` - label: graph; - overflow: auto; - flex-grow: 1; - flex-basis: 50%; - `, - sandwichCanvasWrapper: css` - label: sandwichCanvasWrapper; - display: flex; - margin-bottom: ${PIXELS_PER_LEVEL / window.devicePixelRatio}px; - `, - sandwichMarker: css` - label: sandwichMarker; - writing-mode: vertical-lr; - transform: rotate(180deg); - overflow: hidden; - white-space: nowrap; - `, - - sandwichMarkerCalees: css` - label: sandwichMarkerCalees; - text-align: right; - `, - sandwichMarkerIcon: css` - label: sandwichMarkerIcon; - vertical-align: baseline; - `, + graph: css({ + label: 'graph', + overflow: 'auto', + flexGrow: 1, + flexBasis: '50%', + }), + sandwichCanvasWrapper: css({ + label: 'sandwichCanvasWrapper', + display: 'flex', + marginBottom: `${PIXELS_PER_LEVEL / window.devicePixelRatio}px`, + }), + sandwichMarker: css({ + label: 'sandwichMarker', + writingMode: 'vertical-lr', + transform: 'rotate(180deg)', + overflow: 'hidden', + whiteSpace: 'nowrap', + }), + sandwichMarkerCalees: css({ + label: 'sandwichMarkerCalees', + textAlign: 'right', + }), + sandwichMarkerIcon: css({ + label: 'sandwichMarkerIcon', + verticalAlign: 'baseline', + }), }); export default FlameGraph; diff --git a/packages/grafana-flamegraph/src/FlameGraph/FlameGraphMetadata.tsx b/packages/grafana-flamegraph/src/FlameGraph/FlameGraphMetadata.tsx index 9a896f33a0d..d385b7dd148 100644 --- a/packages/grafana-flamegraph/src/FlameGraph/FlameGraphMetadata.tsx +++ b/packages/grafana-flamegraph/src/FlameGraph/FlameGraphMetadata.tsx @@ -89,36 +89,35 @@ const FlameGraphMetadata = React.memo( FlameGraphMetadata.displayName = 'FlameGraphMetadata'; const getStyles = (theme: GrafanaTheme2) => ({ - metadataPill: css` - label: metadataPill; - display: inline-flex; - align-items: center; - background: ${theme.colors.background.secondary}; - border-radius: ${theme.shape.borderRadius(8)}; - padding: ${theme.spacing(0.5, 1)}; - font-size: ${theme.typography.bodySmall.fontSize}; - font-weight: ${theme.typography.fontWeightMedium}; - line-height: ${theme.typography.bodySmall.lineHeight}; - color: ${theme.colors.text.secondary}; - `, - - pillCloseButton: css` - label: pillCloseButton; - vertical-align: text-bottom; - margin: ${theme.spacing(0, 0.5)}; - `, - metadata: css` - margin: 8px 0; - text-align: center; - `, - metadataPillName: css` - label: metadataPillName; - max-width: 200px; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - margin-left: ${theme.spacing(0.5)}; - `, + metadataPill: css({ + label: 'metadataPill', + display: 'inline-flex', + alignItems: 'center', + background: theme.colors.background.secondary, + borderRadius: theme.shape.borderRadius(8), + padding: theme.spacing(0.5, 1), + fontSize: theme.typography.bodySmall.fontSize, + fontWeight: theme.typography.fontWeightMedium, + lineHeight: theme.typography.bodySmall.lineHeight, + color: theme.colors.text.secondary, + }), + pillCloseButton: css({ + label: 'pillCloseButton', + verticalAlign: 'text-bottom', + margin: theme.spacing(0, 0.5), + }), + metadata: css({ + margin: '8px 0', + textAlign: 'center', + }), + metadataPillName: css({ + label: 'metadataPillName', + maxWidth: '200px', + overflow: 'hidden', + textOverflow: 'ellipsis', + whiteSpace: 'nowrap', + marginLeft: theme.spacing(0.5), + }), }); export default FlameGraphMetadata; diff --git a/packages/grafana-flamegraph/src/FlameGraph/FlameGraphTooltip.tsx b/packages/grafana-flamegraph/src/FlameGraph/FlameGraphTooltip.tsx index d2fe2866606..62f4a53343a 100644 --- a/packages/grafana-flamegraph/src/FlameGraph/FlameGraphTooltip.tsx +++ b/packages/grafana-flamegraph/src/FlameGraph/FlameGraphTooltip.tsx @@ -182,33 +182,33 @@ function getValueWithUnit(data: FlameGraphDataContainer, displayValue: DisplayVa } const getStyles = (theme: GrafanaTheme2) => ({ - tooltipContainer: css` - title: tooltipContainer; - overflow: hidden; - `, - tooltipContent: css` - title: tooltipContent; - font-size: ${theme.typography.bodySmall.fontSize}; - width: 100%; - `, - tooltipName: css` - title: tooltipName; - margin-top: 0; - word-break: break-all; - `, - lastParagraph: css` - title: lastParagraph; - margin-bottom: 0; - `, - name: css` - title: name; - margin-bottom: 10px; - `, + tooltipContainer: css({ + title: 'tooltipContainer', + overflow: 'hidden', + }), + tooltipContent: css({ + title: 'tooltipContent', + fontSize: theme.typography.bodySmall.fontSize, + width: '100%', + }), + tooltipName: css({ + title: 'tooltipName', + marginTop: 0, + wordBreak: 'break-all', + }), + lastParagraph: css({ + title: 'lastParagraph', + marginBottom: 0, + }), + name: css({ + title: 'name', + marginBottom: '10px', + }), - tooltipTable: css` - title: tooltipTable; - max-width: 400px; - `, + tooltipTable: css({ + title: 'tooltipTable', + maxWidth: '400px', + }), }); export default FlameGraphTooltip; diff --git a/packages/grafana-flamegraph/src/FlameGraphHeader.tsx b/packages/grafana-flamegraph/src/FlameGraphHeader.tsx index 1e372a93660..20606e0637c 100644 --- a/packages/grafana-flamegraph/src/FlameGraphHeader.tsx +++ b/packages/grafana-flamegraph/src/FlameGraphHeader.tsx @@ -46,7 +46,7 @@ const FlameGraphHeader = ({ vertical, isDiffMode, }: Props) => { - const styles = useStyles2(getStyles, stickyHeader); + const styles = useStyles2(getStyles); const [localSearch, setLocalSearch] = useSearchInput(search, setSearch); const suffix = @@ -66,7 +66,7 @@ const FlameGraphHeader = ({ ) : null; return ( -
+
props.onChange(ColorScheme.PackageBased)} /> @@ -213,79 +213,77 @@ function useSearchInput( return [localSearchState, setLocalSearchState]; } -const getStyles = (theme: GrafanaTheme2, sticky?: boolean) => ({ - header: css` - label: header; - display: flex; - flex-wrap: wrap; - justify-content: space-between; - width: 100%; - top: 0; - ${sticky - ? css` - z-index: ${theme.zIndex.navbarFixed}; - position: sticky; - padding-bottom: ${theme.spacing(1)}; - padding-top: ${theme.spacing(1)}; - background: ${theme.colors.background.primary}; - ` - : ''}; - `, - inputContainer: css` - label: inputContainer; - margin-right: 20px; - flex-grow: 1; - min-width: 150px; - max-width: 350px; - `, - rightContainer: css` - label: rightContainer; - display: flex; - align-items: flex-start; - flex-wrap: wrap; - `, - buttonSpacing: css` - label: buttonSpacing; - margin-right: ${theme.spacing(1)}; - `, - - resetButton: css` - label: resetButton; - display: flex; - margin-right: ${theme.spacing(2)}; - `, - resetButtonIconWrapper: css` - label: resetButtonIcon; - padding: 0 5px; - color: ${theme.colors.text.disabled}; - `, - colorDot: css` - label: colorDot; - display: inline-block; - width: 10px; - height: 10px; - border-radius: 50%; - `, - colorDotByValue: css` - label: colorDotByValue; - background: ${byValueGradient}; - `, - colorDotByPackage: css` - label: colorDotByPackage; - background: ${byPackageGradient}; - `, - colorDotDiffDefault: css` - label: colorDotDiffDefault; - background: ${diffDefaultGradient}; - `, - colorDotDiffColorBlind: css` - label: colorDotDiffColorBlind; - background: ${diffColorBlindGradient}; - `, - extraElements: css` - label: extraElements; - margin-left: ${theme.spacing(1)}; - `, +const getStyles = (theme: GrafanaTheme2) => ({ + header: css({ + label: 'header', + display: 'flex', + flexWrap: 'wrap', + justifyContent: 'space-between', + width: '100%', + top: 0, + }), + stickyHeader: css({ + zIndex: theme.zIndex.navbarFixed, + position: 'sticky', + paddingBottom: theme.spacing(1), + paddingTop: theme.spacing(1), + background: theme.colors.background.primary, + }), + inputContainer: css({ + label: 'inputContainer', + marginRight: '20px', + flexGrow: 1, + minWidth: '150px', + maxWidth: '350px', + }), + rightContainer: css({ + label: 'rightContainer', + display: 'flex', + alignItems: 'flex-start', + flexWrap: 'wrap', + }), + buttonSpacing: css({ + label: 'buttonSpacing', + marginRight: theme.spacing(1), + }), + resetButton: css({ + label: 'resetButton', + display: 'flex', + marginRight: theme.spacing(2), + }), + resetButtonIconWrapper: css({ + label: 'resetButtonIcon', + padding: '0 5px', + color: theme.colors.text.disabled, + }), + colorDot: css({ + label: 'colorDot', + display: 'inline-block', + width: '10px', + height: '10px', + // eslint-disable-next-line @grafana/no-border-radius-literal + borderRadius: '50%', + }), + colorDotByValue: css({ + label: 'colorDotByValue', + background: byValueGradient, + }), + colorDotByPackage: css({ + label: 'colorDotByPackage', + background: byPackageGradient, + }), + colorDotDiffDefault: css({ + label: 'colorDotDiffDefault', + background: diffDefaultGradient, + }), + colorDotDiffColorBlind: css({ + label: 'colorDotDiffColorBlind', + background: diffColorBlindGradient, + }), + extraElements: css({ + label: 'extraElements', + marginLeft: theme.spacing(1), + }), }); export default FlameGraphHeader; diff --git a/packages/grafana-flamegraph/src/TopTable/FlameGraphTopTableContainer.tsx b/packages/grafana-flamegraph/src/TopTable/FlameGraphTopTableContainer.tsx index 936be013806..2630e7dcf9d 100644 --- a/packages/grafana-flamegraph/src/TopTable/FlameGraphTopTableContainer.tsx +++ b/packages/grafana-flamegraph/src/TopTable/FlameGraphTopTableContainer.tsx @@ -332,17 +332,16 @@ const getStyles = (theme: GrafanaTheme2) => { const getStylesActionCell = () => { return { - actionCellWrapper: css` - label: actionCellWrapper; - display: flex; - height: 24px; - `, - - actionCellButton: css` - label: actionCellButton; - margin-right: 0; - width: 24px; - `, + actionCellWrapper: css({ + label: 'actionCellWrapper', + display: 'flex', + height: '24px', + }), + actionCellButton: css({ + label: 'actionCellButton', + marginRight: 0, + width: '24px', + }), }; }; diff --git a/packages/grafana-icons/package.json b/packages/grafana-icons/package.json index f2a8dcee0f3..343369b7bad 100644 --- a/packages/grafana-icons/package.json +++ b/packages/grafana-icons/package.json @@ -34,18 +34,21 @@ "build": "yarn generate && rollup -c rollup.config.ts" }, "devDependencies": { + "@babel/core": "7.24.4", "@grafana/tsconfig": "^1.3.0-rc1", "@rollup/plugin-node-resolve": "^15.2.3", "@rollup/plugin-typescript": "^11.1.6", "@svgr/babel-plugin-remove-jsx-attribute": "^8.0.0", "@svgr/cli": "^8.1.0", + "@svgr/core": "8.1.0", "@svgr/plugin-jsx": "^8.1.0", "@svgr/plugin-prettier": "^8.1.0", "@svgr/plugin-svgo": "^8.1.0", + "@types/babel__core": "^7", "@types/node": "20.12.7", "@types/react": "18.2.79", "@types/react-dom": "18.2.25", - "esbuild": "0.18.12", + "esbuild": "0.20.2", "prettier": "3.2.5", "react": "18.2.0", "react-dom": "18.2.0", diff --git a/packages/grafana-icons/src/IconBase.tsx b/packages/grafana-icons/src/IconBase.tsx index cc4149e7413..5c3983f0442 100644 --- a/packages/grafana-icons/src/IconBase.tsx +++ b/packages/grafana-icons/src/IconBase.tsx @@ -33,8 +33,6 @@ export const IconBase = ({ title, size = 'md', color = 'currentColor', ...props return ( { jsx.openingElement.name.name = 'IconBase'; jsx.openingElement.attributes = [ + ...jsx.openingElement.attributes, { type: 'JSXSpreadAttribute', argument: { diff --git a/packages/grafana-o11y-ds-frontend/package.json b/packages/grafana-o11y-ds-frontend/package.json index 24e31cb00fe..b37b712bc6f 100644 --- a/packages/grafana-o11y-ds-frontend/package.json +++ b/packages/grafana-o11y-ds-frontend/package.json @@ -24,16 +24,19 @@ "@grafana/runtime": "11.1.0-pre", "@grafana/schema": "11.1.0-pre", "@grafana/ui": "11.1.0-pre", + "react-select": "5.8.0", "react-use": "17.5.0", "rxjs": "7.8.1", "tslib": "2.6.2" }, "devDependencies": { "@grafana/tsconfig": "^1.3.0-rc1", + "@testing-library/dom": "10.0.0", "@testing-library/jest-dom": "^6.1.2", "@testing-library/react": "15.0.2", "@testing-library/user-event": "14.5.2", "@types/jest": "^29.5.4", + "@types/node": "20.12.7", "@types/react": "18.2.79", "@types/systemjs": "6.13.5", "@types/testing-library__jest-dom": "5.14.9", diff --git a/packages/grafana-plugin-configs/package.json b/packages/grafana-plugin-configs/package.json index 8ee714497c5..18861fbc444 100644 --- a/packages/grafana-plugin-configs/package.json +++ b/packages/grafana-plugin-configs/package.json @@ -8,12 +8,16 @@ }, "devDependencies": { "@grafana/tsconfig": "^1.3.0-rc1", + "@swc/core": "1.4.2", + "@types/eslint": "8.56.10", "copy-webpack-plugin": "12.0.2", + "eslint": "8.57.0", "eslint-webpack-plugin": "4.1.0", "fork-ts-checker-webpack-plugin": "9.0.2", "glob": "10.3.12", "replace-in-file-webpack-plugin": "1.0.6", "swc-loader": "0.2.6", + "typescript": "5.4.5", "webpack": "5.91.0" }, "packageManager": "yarn@4.1.1" diff --git a/packages/grafana-prometheus/package.json b/packages/grafana-prometheus/package.json index dc2299d2f41..29fa45e6ad6 100644 --- a/packages/grafana-prometheus/package.json +++ b/packages/grafana-prometheus/package.json @@ -106,12 +106,12 @@ "@typescript-eslint/eslint-plugin": "6.21.0", "@typescript-eslint/parser": "6.21.0", "copy-webpack-plugin": "12.0.2", - "css-loader": "6.10.0", - "esbuild": "0.18.12", + "css-loader": "7.1.1", + "esbuild": "0.20.2", "eslint": "8.57.0", "eslint-config-prettier": "9.1.0", "eslint-plugin-import": "^2.26.0", - "eslint-plugin-jest": "27.9.0", + "eslint-plugin-jest": "28.2.0", "eslint-plugin-jsdoc": "48.2.3", "eslint-plugin-jsx-a11y": "6.8.0", "eslint-plugin-lodash": "7.4.0", @@ -134,7 +134,7 @@ "rollup-plugin-node-externals": "^5.0.0", "sass": "1.75.0", "sass-loader": "14.2.1", - "style-loader": "3.3.4", + "style-loader": "4.0.0", "testing-library-selector": "0.3.1", "ts-node": "10.9.2", "typescript": "5.4.5", diff --git a/packages/grafana-prometheus/src/querybuilder/shared/OperationEditor.tsx b/packages/grafana-prometheus/src/querybuilder/shared/OperationEditor.tsx index 405b40b7cd2..a2d23aacdc9 100644 --- a/packages/grafana-prometheus/src/querybuilder/shared/OperationEditor.tsx +++ b/packages/grafana-prometheus/src/querybuilder/shared/OperationEditor.tsx @@ -238,7 +238,9 @@ const getStyles = (theme: GrafanaTheme2) => { borderRadius: theme.shape.radius.default, marginBottom: theme.spacing(1), position: 'relative', - transition: 'all 0.5s ease-in 0s', + [theme.transitions.handleMotion('no-preference', 'reduce')]: { + transition: 'all 0.5s ease-in 0s', + }, height: '100%', }), cardError: css({ diff --git a/packages/grafana-runtime/package.json b/packages/grafana-runtime/package.json index 0b40a487e4a..e5c488ff35a 100644 --- a/packages/grafana-runtime/package.json +++ b/packages/grafana-runtime/package.json @@ -61,7 +61,7 @@ "@types/react": "18.2.79", "@types/react-dom": "18.2.25", "@types/systemjs": "6.13.5", - "esbuild": "0.18.12", + "esbuild": "0.20.2", "lodash": "4.17.21", "react": "18.2.0", "react-dom": "18.2.0", diff --git a/packages/grafana-runtime/src/services/index.ts b/packages/grafana-runtime/src/services/index.ts index 56208d451f8..f60715cbe5d 100644 --- a/packages/grafana-runtime/src/services/index.ts +++ b/packages/grafana-runtime/src/services/index.ts @@ -15,5 +15,15 @@ export { getPluginLinkExtensions, getPluginComponentExtensions, type GetPluginExtensions, + type GetPluginExtensionsOptions, + type GetPluginExtensionsResult, + type UsePluginExtensions, + type UsePluginExtensionsResult, } from './pluginExtensions/getPluginExtensions'; +export { + setPluginExtensionsHook, + usePluginExtensions, + usePluginLinkExtensions, + usePluginComponentExtensions, +} from './pluginExtensions/usePluginExtensions'; export { isPluginExtensionLink, isPluginExtensionComponent } from './pluginExtensions/utils'; diff --git a/packages/grafana-runtime/src/services/pluginExtensions/getPluginExtensions.ts b/packages/grafana-runtime/src/services/pluginExtensions/getPluginExtensions.ts index dcd52b200b2..eff1975e8f4 100644 --- a/packages/grafana-runtime/src/services/pluginExtensions/getPluginExtensions.ts +++ b/packages/grafana-runtime/src/services/pluginExtensions/getPluginExtensions.ts @@ -2,18 +2,29 @@ import type { PluginExtension, PluginExtensionLink, PluginExtensionComponent } f import { isPluginExtensionComponent, isPluginExtensionLink } from './utils'; -export type GetPluginExtensions = ({ - extensionPointId, - context, - limitPerPlugin, -}: { +export type GetPluginExtensions = ( + options: GetPluginExtensionsOptions +) => GetPluginExtensionsResult; + +export type UsePluginExtensions = ( + options: GetPluginExtensionsOptions +) => UsePluginExtensionsResult; + +export type GetPluginExtensionsOptions = { extensionPointId: string; context?: object | Record; limitPerPlugin?: number; -}) => { +}; + +export type GetPluginExtensionsResult = { extensions: T[]; }; +export type UsePluginExtensionsResult = { + extensions: T[]; + isLoading: boolean; +}; + let singleton: GetPluginExtensions | undefined; export function setPluginExtensionGetter(instance: GetPluginExtensions): void { diff --git a/packages/grafana-runtime/src/services/pluginExtensions/usePluginExtensions.test.tsx b/packages/grafana-runtime/src/services/pluginExtensions/usePluginExtensions.test.tsx new file mode 100644 index 00000000000..de943427e64 --- /dev/null +++ b/packages/grafana-runtime/src/services/pluginExtensions/usePluginExtensions.test.tsx @@ -0,0 +1,262 @@ +import { renderHook } from '@testing-library/react-hooks'; + +import { PluginExtension, PluginExtensionTypes } from '@grafana/data'; + +import { UsePluginExtensions } from './getPluginExtensions'; +import { + setPluginExtensionsHook, + usePluginComponentExtensions, + usePluginExtensions, + usePluginLinkExtensions, +} from './usePluginExtensions'; + +describe('Plugin Extensions / usePluginExtensions', () => { + afterEach(() => { + process.env.NODE_ENV = 'test'; + }); + + test('should always return the same extension-hook function that was previously set', () => { + const hook: UsePluginExtensions = jest.fn().mockReturnValue({ extensions: [], isLoading: false }); + + setPluginExtensionsHook(hook); + usePluginExtensions({ extensionPointId: 'panel-menu' }); + + expect(hook).toHaveBeenCalledTimes(1); + expect(hook).toHaveBeenCalledWith({ extensionPointId: 'panel-menu' }); + }); + + test('should throw an error when trying to redefine the app-wide extension-hook function', () => { + // By default, NODE_ENV is set to 'test' in jest.config.js, which allows to override the registry in tests. + process.env.NODE_ENV = 'production'; + + const hook: UsePluginExtensions = () => ({ extensions: [], isLoading: false }); + + expect(() => { + setPluginExtensionsHook(hook); + setPluginExtensionsHook(hook); + }).toThrow(); + }); + + test('should throw an error when trying to access the extension-hook function before it was set', () => { + // "Unsetting" the registry + // @ts-ignore + setPluginExtensionsHook(undefined); + + expect(() => { + usePluginExtensions({ extensionPointId: 'panel-menu' }); + }).toThrow(); + }); + + describe('usePluginExtensionLinks()', () => { + test('should return only links extensions', () => { + const usePluginExtensionsMock: UsePluginExtensions = () => ({ + extensions: [ + { + id: '1', + pluginId: '', + title: '', + description: '', + type: PluginExtensionTypes.component, + component: () => undefined, + }, + { + id: '2', + pluginId: '', + title: '', + description: '', + path: '', + type: PluginExtensionTypes.link, + }, + { + id: '3', + pluginId: '', + title: '', + description: '', + path: '', + type: PluginExtensionTypes.link, + }, + ], + isLoading: false, + }); + + setPluginExtensionsHook(usePluginExtensionsMock); + + const { result } = renderHook(() => usePluginLinkExtensions({ extensionPointId: 'panel-menu' })); + const { extensions } = result.current; + + expect(extensions).toHaveLength(2); + expect(extensions[0].type).toBe('link'); + expect(extensions[1].type).toBe('link'); + expect(extensions.find(({ id }) => id === '2')).toBeDefined(); + expect(extensions.find(({ id }) => id === '3')).toBeDefined(); + }); + + test('should return the same object if the extensions do not change', () => { + const extensionPointId = 'foo'; + const extensions: PluginExtension[] = [ + { + id: '1', + pluginId: '', + title: '', + description: '', + path: '', + type: PluginExtensionTypes.link, + }, + ]; + + // Mimicing that the extensions do not change between renders + const usePluginExtensionsMock: UsePluginExtensions = () => ({ + extensions, + isLoading: false, + }); + + setPluginExtensionsHook(usePluginExtensionsMock); + + const { result, rerender } = renderHook(() => usePluginLinkExtensions({ extensionPointId })); + const firstExtensions = result.current.extensions; + + rerender(); + + const secondExtensions = result.current.extensions; + + expect(firstExtensions === secondExtensions).toBe(true); + }); + + test('should return a different object if the extensions do change', () => { + const extensionPointId = 'foo'; + + // Mimicing that the extensions is a new array object every time + const usePluginExtensionsMock: UsePluginExtensions = () => ({ + extensions: [ + { + id: '1', + pluginId: '', + title: '', + description: '', + path: '', + type: PluginExtensionTypes.link, + }, + ], + isLoading: false, + }); + + setPluginExtensionsHook(usePluginExtensionsMock); + + const { result, rerender } = renderHook(() => usePluginLinkExtensions({ extensionPointId })); + const firstExtensions = result.current.extensions; + + rerender(); + + const secondExtensions = result.current.extensions; + + // The results differ + expect(firstExtensions === secondExtensions).toBe(false); + }); + }); + + describe('usePluginExtensionComponents()', () => { + test('should return only component extensions', () => { + const hook: UsePluginExtensions = () => ({ + extensions: [ + { + id: '1', + pluginId: '', + title: '', + description: '', + type: PluginExtensionTypes.component, + component: () => undefined, + }, + { + id: '2', + pluginId: '', + title: '', + description: '', + path: '', + type: PluginExtensionTypes.link, + }, + { + id: '3', + pluginId: '', + title: '', + description: '', + path: '', + type: PluginExtensionTypes.link, + }, + ], + isLoading: false, + }); + + setPluginExtensionsHook(hook); + + const hookRender = renderHook(() => usePluginComponentExtensions({ extensionPointId: 'panel-menu' })); + const { extensions } = hookRender.result.current; + + expect(extensions).toHaveLength(1); + expect(extensions[0].type).toBe('component'); + expect(extensions.find(({ id }) => id === '1')).toBeDefined(); + }); + + test('should return the same object if the extensions do not change', () => { + const extensionPointId = 'foo'; + const extensions: PluginExtension[] = [ + { + id: '1', + pluginId: '', + title: '', + description: '', + type: PluginExtensionTypes.component, + component: () => undefined, + }, + ]; + + // Mimicing that the extensions do not change between renders + const usePluginExtensionsMock: UsePluginExtensions = () => ({ + extensions, + isLoading: false, + }); + + setPluginExtensionsHook(usePluginExtensionsMock); + + const { result, rerender } = renderHook(() => usePluginComponentExtensions({ extensionPointId })); + const firstExtensions = result.current.extensions; + + rerender(); + + const secondExtensions = result.current.extensions; + + // The results are the same + expect(firstExtensions === secondExtensions).toBe(true); + }); + + test('should return a different object if the extensions do change', () => { + const extensionPointId = 'foo'; + + // Mimicing that the extensions is a new array object every time + const usePluginExtensionsMock: UsePluginExtensions = () => ({ + extensions: [ + { + id: '1', + pluginId: '', + title: '', + description: '', + type: PluginExtensionTypes.component, + component: () => undefined, + }, + ], + isLoading: false, + }); + + setPluginExtensionsHook(usePluginExtensionsMock); + + const { result, rerender } = renderHook(() => usePluginComponentExtensions({ extensionPointId })); + const firstExtensions = result.current.extensions; + + rerender(); + + const secondExtensions = result.current.extensions; + + // The results differ + expect(firstExtensions === secondExtensions).toBe(false); + }); + }); +}); diff --git a/packages/grafana-runtime/src/services/pluginExtensions/usePluginExtensions.ts b/packages/grafana-runtime/src/services/pluginExtensions/usePluginExtensions.ts new file mode 100644 index 00000000000..4629fc7baab --- /dev/null +++ b/packages/grafana-runtime/src/services/pluginExtensions/usePluginExtensions.ts @@ -0,0 +1,50 @@ +import { useMemo } from 'react'; + +import { PluginExtensionComponent, PluginExtensionLink } from '@grafana/data'; + +import { GetPluginExtensionsOptions, UsePluginExtensions, UsePluginExtensionsResult } from './getPluginExtensions'; +import { isPluginExtensionComponent, isPluginExtensionLink } from './utils'; + +let singleton: UsePluginExtensions | undefined; + +export function setPluginExtensionsHook(hook: UsePluginExtensions): void { + // We allow overriding the registry in tests + if (singleton && process.env.NODE_ENV !== 'test') { + throw new Error('setPluginExtensionsHook() function should only be called once, when Grafana is starting.'); + } + singleton = hook; +} + +export function usePluginExtensions(options: GetPluginExtensionsOptions): UsePluginExtensionsResult { + if (!singleton) { + throw new Error('usePluginExtensions(options) can only be used after the Grafana instance has started.'); + } + return singleton(options); +} + +export function usePluginLinkExtensions( + options: GetPluginExtensionsOptions +): UsePluginExtensionsResult { + const { extensions, isLoading } = usePluginExtensions(options); + + return useMemo(() => { + return { + extensions: extensions.filter(isPluginExtensionLink), + isLoading, + }; + }, [extensions, isLoading]); +} + +export function usePluginComponentExtensions( + options: GetPluginExtensionsOptions +): { extensions: Array>; isLoading: boolean } { + const { extensions, isLoading } = usePluginExtensions(options); + + return useMemo( + () => ({ + extensions: extensions.filter(isPluginExtensionComponent) as Array>, + isLoading, + }), + [extensions, isLoading] + ); +} diff --git a/packages/grafana-schema/package.json b/packages/grafana-schema/package.json index dacc87e3583..138a11501be 100644 --- a/packages/grafana-schema/package.json +++ b/packages/grafana-schema/package.json @@ -38,7 +38,7 @@ "devDependencies": { "@grafana/tsconfig": "^1.3.0-rc1", "@rollup/plugin-node-resolve": "15.2.3", - "esbuild": "0.18.12", + "esbuild": "0.20.2", "glob": "^10.2.7", "rimraf": "5.0.5", "rollup": "2.79.1", diff --git a/packages/grafana-schema/src/common/common.gen.ts b/packages/grafana-schema/src/common/common.gen.ts index a0c69ebbb06..6fca2bf56fd 100644 --- a/packages/grafana-schema/src/common/common.gen.ts +++ b/packages/grafana-schema/src/common/common.gen.ts @@ -601,6 +601,7 @@ export enum SortOrder { export interface GraphFieldConfig extends LineConfig, FillConfig, PointsConfig, AxisConfig, BarConfig, StackableFieldConfig, HideableFieldConfig { drawStyle?: GraphDrawStyle; gradientMode?: GraphGradientMode; + insertNulls?: (boolean | number); thresholdsStyle?: GraphThresholdsStyleConfig; transform?: GraphTransform; } diff --git a/packages/grafana-schema/src/common/mudball.cue b/packages/grafana-schema/src/common/mudball.cue index d08fbeff153..a8fe6f5c291 100644 --- a/packages/grafana-schema/src/common/mudball.cue +++ b/packages/grafana-schema/src/common/mudball.cue @@ -224,6 +224,7 @@ GraphFieldConfig: { gradientMode?: GraphGradientMode thresholdsStyle?: GraphThresholdsStyleConfig transform?: GraphTransform + insertNulls?: bool | number } @cuetsy(kind="interface") // TODO docs diff --git a/packages/grafana-sql/package.json b/packages/grafana-sql/package.json index 749c8f356d5..9a480005a35 100644 --- a/packages/grafana-sql/package.json +++ b/packages/grafana-sql/package.json @@ -16,15 +16,16 @@ "dependencies": { "@emotion/css": "11.11.2", "@grafana/data": "11.1.0-pre", + "@grafana/e2e-selectors": "11.1.0-pre", "@grafana/experimental": "1.7.10", "@grafana/runtime": "11.1.0-pre", "@grafana/ui": "11.1.0-pre", "@react-awesome-query-builder/ui": "6.4.2", - "@types/lodash": "4.17.0", - "@types/react-virtualized-auto-sizer": "1.0.4", - "@types/uuid": "9.0.8", "immutable": "4.3.5", "lodash": "4.17.21", + "react": "18.2.0", + "react-dom": "18.2.0", + "react-select": "5.8.0", "react-use": "17.5.0", "react-virtualized-auto-sizer": "1.0.24", "rxjs": "7.8.1", @@ -34,16 +35,21 @@ }, "devDependencies": { "@grafana/tsconfig": "^1.3.0-rc1", + "@testing-library/dom": "10.0.0", "@testing-library/jest-dom": "^6.1.2", "@testing-library/react": "15.0.2", "@testing-library/react-hooks": "^8.0.1", "@testing-library/user-event": "14.5.2", "@types/jest": "^29.5.4", + "@types/lodash": "4.17.0", + "@types/node": "20.12.7", "@types/react": "18.2.79", + "@types/react-dom": "18.2.25", + "@types/react-virtualized-auto-sizer": "1.0.4", "@types/systemjs": "6.13.5", "@types/testing-library__jest-dom": "5.14.9", + "@types/uuid": "9.0.8", "jest": "^29.6.4", - "react": "18.2.0", "ts-jest": "29.1.2", "ts-node": "10.9.2", "typescript": "5.4.5" diff --git a/packages/grafana-sql/src/components/query-editor-raw/QueryToolbox.tsx b/packages/grafana-sql/src/components/query-editor-raw/QueryToolbox.tsx index a8817e102f6..5798b65b567 100644 --- a/packages/grafana-sql/src/components/query-editor-raw/QueryToolbox.tsx +++ b/packages/grafana-sql/src/components/query-editor-raw/QueryToolbox.tsx @@ -20,31 +20,31 @@ export function QueryToolbox({ showTools, onFormatCode, onExpand, isExpanded, .. const styles = useMemo(() => { return { - container: css` - border: 1px solid ${theme.colors.border.medium}; - border-top: none; - padding: ${theme.spacing(0.5, 0.5, 0.5, 0.5)}; - display: flex; - flex-grow: 1; - justify-content: space-between; - font-size: ${theme.typography.bodySmall.fontSize}; - `, - error: css` - color: ${theme.colors.error.text}; - font-size: ${theme.typography.bodySmall.fontSize}; - font-family: ${theme.typography.fontFamilyMonospace}; - `, - valid: css` - color: ${theme.colors.success.text}; - `, - info: css` - color: ${theme.colors.text.secondary}; - `, - hint: css` - color: ${theme.colors.text.disabled}; - white-space: nowrap; - cursor: help; - `, + container: css({ + border: `1px solid ${theme.colors.border.medium}`, + borderTop: 'none', + padding: theme.spacing(0.5, 0.5, 0.5, 0.5), + display: 'flex', + flexGrow: 1, + justifyContent: 'space-between', + fontSize: theme.typography.bodySmall.fontSize, + }), + error: css({ + color: theme.colors.error.text, + fontSize: theme.typography.bodySmall.fontSize, + fontFamily: theme.typography.fontFamilyMonospace, + }), + valid: css({ + color: theme.colors.success.text, + }), + info: css({ + color: theme.colors.text.secondary, + }), + hint: css({ + color: theme.colors.text.disabled, + whiteSpace: 'nowrap', + cursor: 'help', + }), }; }, [theme]); diff --git a/packages/grafana-sql/src/components/query-editor-raw/QueryValidator.tsx b/packages/grafana-sql/src/components/query-editor-raw/QueryValidator.tsx index 0cbad6702a8..c787229b9d9 100644 --- a/packages/grafana-sql/src/components/query-editor-raw/QueryValidator.tsx +++ b/packages/grafana-sql/src/components/query-editor-raw/QueryValidator.tsx @@ -22,17 +22,17 @@ export function QueryValidator({ db, query, onValidate, range }: QueryValidatorP const styles = useMemo(() => { return { - error: css` - color: ${theme.colors.error.text}; - font-size: ${theme.typography.bodySmall.fontSize}; - font-family: ${theme.typography.fontFamilyMonospace}; - `, - valid: css` - color: ${theme.colors.success.text}; - `, - info: css` - color: ${theme.colors.text.secondary}; - `, + error: css({ + color: theme.colors.error.text, + fontSize: theme.typography.bodySmall.fontSize, + fontFamily: theme.typography.fontFamilyMonospace, + }), + valid: css({ + color: theme.colors.success.text, + }), + info: css({ + color: theme.colors.text.secondary, + }), }; }, [theme]); diff --git a/packages/grafana-sql/src/components/query-editor-raw/RawEditor.tsx b/packages/grafana-sql/src/components/query-editor-raw/RawEditor.tsx index 19215827b74..0c6883adaa9 100644 --- a/packages/grafana-sql/src/components/query-editor-raw/RawEditor.tsx +++ b/packages/grafana-sql/src/components/query-editor-raw/RawEditor.tsx @@ -114,13 +114,13 @@ export function RawEditor({ db, query, onChange, onRunQuery, onValidate, queryTo function getStyles(theme: GrafanaTheme2) { return { - modal: css` - width: 95vw; - height: 95vh; - `, - modalContent: css` - height: 100%; - padding-top: 0; - `, + modal: css({ + width: '95vw', + height: '95vh', + }), + modalContent: css({ + height: '100%', + paddingTop: 0, + }), }; } diff --git a/packages/grafana-ui/.eslintrc b/packages/grafana-ui/.eslintrc index 995c2caa896..13011848702 100644 --- a/packages/grafana-ui/.eslintrc +++ b/packages/grafana-ui/.eslintrc @@ -4,24 +4,24 @@ "no-restricted-imports": [ "error", { - "patterns": ["@grafana/runtime", "@grafana/data/*", "@grafana/ui", "@grafana/e2e", "@grafana/e2e-selectors/*"], + "patterns": ["@grafana/runtime", "@grafana/data/*", "@grafana/ui", "@grafana/e2e-selectors/*"], "paths": [ { "name": "react-i18next", "importNames": ["Trans", "t"], - "message": "Please import from grafana-ui/src/utils/i18n instead" - } - ] - } - ] + "message": "Please import from grafana-ui/src/utils/i18n instead", + }, + ], + }, + ], }, "overrides": [ { "files": ["**/*.{test,story}.{ts,tsx}"], "rules": { "no-restricted-imports": "off", - "react/prop-types": "off" - } - } - ] + "react/prop-types": "off", + }, + }, + ], } diff --git a/packages/grafana-ui/.storybook/preview.ts b/packages/grafana-ui/.storybook/preview.ts index e58cfed7230..1e7e84f797c 100644 --- a/packages/grafana-ui/.storybook/preview.ts +++ b/packages/grafana-ui/.storybook/preview.ts @@ -1,5 +1,6 @@ import { Preview } from '@storybook/react'; import 'jquery'; +import { getTimeZone, getTimeZones } from '@grafana/data'; import '../../../public/vendor/flot/jquery.flot.js'; import '../../../public/vendor/flot/jquery.flot.selection'; @@ -12,13 +13,14 @@ import '../../../public/vendor/flot/jquery.flot.dashes'; import '../../../public/vendor/flot/jquery.flot.gauge'; import { withTheme } from '../src/utils/storybook/withTheme'; +import { withTimeZone } from '../src/utils/storybook/withTimeZone'; import { ThemedDocsContainer } from '../src/utils/storybook/ThemedDocsContainer'; // @ts-ignore import lightTheme from './grafana.light.scss'; // @ts-ignore import darkTheme from './grafana.dark.scss'; -import { GrafanaLight, GrafanaDark } from './storybookTheme'; +import { GrafanaDark, GrafanaLight } from './storybookTheme'; const handleThemeChange = (theme: any) => { if (theme !== 'light') { @@ -31,7 +33,7 @@ const handleThemeChange = (theme: any) => { }; const preview: Preview = { - decorators: [withTheme(handleThemeChange)], + decorators: [withTheme(handleThemeChange), withTimeZone()], parameters: { actions: { argTypesRegex: '^on[A-Z].*' }, darkMode: { @@ -66,6 +68,21 @@ const preview: Preview = { }, }, }, + globalTypes: { + timeZone: { + description: 'Set the timezone for the storybook preview', + defaultValue: getTimeZone(), + toolbar: { + icon: 'globe', + items: getTimeZones(true) + .filter((timezone) => !!timezone) + .map((timezone) => ({ + title: timezone, + value: timezone, + })), + }, + }, + }, }; export default preview; diff --git a/packages/grafana-ui/package.json b/packages/grafana-ui/package.json index 8601c4ce5a9..fcc36ee8ab0 100644 --- a/packages/grafana-ui/package.json +++ b/packages/grafana-ui/package.json @@ -80,7 +80,7 @@ "prismjs": "1.29.0", "rc-cascader": "3.24.1", "rc-drawer": "7.1.0", - "rc-slider": "10.5.0", + "rc-slider": "10.6.2", "rc-time-picker": "^3.7.3", "rc-tooltip": "6.2.0", "react-beautiful-dnd": "13.1.1", @@ -90,7 +90,7 @@ "react-dropzone": "14.2.3", "react-highlight-words": "0.20.0", "react-hook-form": "^7.49.2", - "react-i18next": "^12.0.0", + "react-i18next": "^14.0.0", "react-inlinesvg": "3.0.2", "react-loading-skeleton": "3.4.0", "react-router-dom": "5.3.3", @@ -158,10 +158,10 @@ "@types/tinycolor2": "1.4.6", "@types/uuid": "9.0.8", "common-tags": "1.8.2", - "core-js": "3.36.1", - "css-loader": "6.10.0", + "core-js": "3.37.0", + "css-loader": "7.1.1", "csstype": "3.1.3", - "esbuild": "0.18.12", + "esbuild": "0.20.2", "expose-loader": "5.0.0", "mock-raf": "1.0.1", "process": "^0.11.10", @@ -180,7 +180,7 @@ "storybook": "7.4.5", "storybook-addon-turbo-build": "2.0.1", "storybook-dark-mode": "3.0.1", - "style-loader": "3.3.4", + "style-loader": "4.0.0", "typescript": "5.4.5", "webpack": "5.91.0" }, diff --git a/packages/grafana-ui/src/components/AutoSaveField/EllipsisAnimated.tsx b/packages/grafana-ui/src/components/AutoSaveField/EllipsisAnimated.tsx index 1a91db3120d..bac0fe4445d 100644 --- a/packages/grafana-ui/src/components/AutoSaveField/EllipsisAnimated.tsx +++ b/packages/grafana-ui/src/components/AutoSaveField/EllipsisAnimated.tsx @@ -1,6 +1,8 @@ import { css, keyframes } from '@emotion/css'; import React from 'react'; +import { GrafanaTheme2 } from '@grafana/data'; + import { useStyles2 } from '../../themes'; export const EllipsisAnimated = React.memo(() => { @@ -16,19 +18,25 @@ export const EllipsisAnimated = React.memo(() => { EllipsisAnimated.displayName = 'EllipsisAnimated'; -const getStyles = () => { +const getStyles = (theme: GrafanaTheme2) => { return { ellipsis: css({ display: 'inline', }), firstDot: css({ - animation: `${firstDot} 2s linear infinite`, + [theme.transitions.handleMotion('no-preference', 'reduce')]: { + animation: `${firstDot} 2s linear infinite`, + }, }), secondDot: css({ - animation: `${secondDot} 2s linear infinite`, + [theme.transitions.handleMotion('no-preference', 'reduce')]: { + animation: `${secondDot} 2s linear infinite`, + }, }), thirdDot: css({ - animation: `${thirdDot} 2s linear infinite`, + [theme.transitions.handleMotion('no-preference', 'reduce')]: { + animation: `${thirdDot} 2s linear infinite`, + }, }), }; }; diff --git a/packages/grafana-ui/src/components/BrowserLabel/Label.tsx b/packages/grafana-ui/src/components/BrowserLabel/Label.tsx index 1b83a57bc26..69d09b4aafd 100644 --- a/packages/grafana-ui/src/components/BrowserLabel/Label.tsx +++ b/packages/grafana-ui/src/components/BrowserLabel/Label.tsx @@ -120,7 +120,9 @@ const getLabelStyles = (theme: GrafanaTheme2) => ({ fontWeight: theme.typography.fontWeightMedium, backgroundColor: theme.colors.primary.shade, color: theme.colors.text.primary, - animation: 'pulse 3s ease-out 0s infinite normal forwards', + [theme.transitions.handleMotion('no-preference', 'reduce')]: { + animation: 'pulse 3s ease-out 0s infinite normal forwards', + }, '@keyframes pulse': { '0%': { color: theme.colors.text.primary, diff --git a/packages/grafana-ui/src/components/Card/CardContainer.tsx b/packages/grafana-ui/src/components/Card/CardContainer.tsx index 8879a87ed4f..15ba01c9651 100644 --- a/packages/grafana-ui/src/components/Card/CardContainer.tsx +++ b/packages/grafana-ui/src/components/Card/CardContainer.tsx @@ -94,9 +94,11 @@ export const getCardContainerStyles = ( borderRadius: theme.shape.radius.default, marginBottom: '8px', pointerEvents: disabled ? 'none' : 'auto', - transition: theme.transitions.create(['background-color', 'box-shadow', 'border-color', 'color'], { - duration: theme.transitions.duration.short, - }), + [theme.transitions.handleMotion('no-preference', 'reduce')]: { + transition: theme.transitions.create(['background-color', 'box-shadow', 'border-color', 'color'], { + duration: theme.transitions.duration.short, + }), + }, ...(!disableHover && { '&:hover': { @@ -123,9 +125,11 @@ export const getCardContainerStyles = ( position: 'relative', pointerEvents: disabled ? 'none' : 'auto', marginBottom: theme.spacing(1), - transition: theme.transitions.create(['background-color', 'box-shadow', 'border-color', 'color'], { - duration: theme.transitions.duration.short, - }), + [theme.transitions.handleMotion('no-preference', 'reduce')]: { + transition: theme.transitions.create(['background-color', 'box-shadow', 'border-color', 'color'], { + duration: theme.transitions.duration.short, + }), + }, ...(!disableHover && { '&:hover': { diff --git a/packages/grafana-ui/src/components/Collapse/Collapse.tsx b/packages/grafana-ui/src/components/Collapse/Collapse.tsx index a1526af6154..d934ee52285 100644 --- a/packages/grafana-ui/src/components/Collapse/Collapse.tsx +++ b/packages/grafana-ui/src/components/Collapse/Collapse.tsx @@ -71,7 +71,6 @@ const getStyles = (theme: GrafanaTheme2) => ({ label: 'collapse__header', padding: theme.spacing(1, 2, 1, 2), display: 'flex', - transition: 'all 0.1s linear', }), headerCollapsed: css({ label: 'collapse__header--collapsed', diff --git a/packages/grafana-ui/src/components/ColorPicker/ColorSwatch.tsx b/packages/grafana-ui/src/components/ColorPicker/ColorSwatch.tsx index f3827df404e..97cdabdc1c1 100644 --- a/packages/grafana-ui/src/components/ColorPicker/ColorSwatch.tsx +++ b/packages/grafana-ui/src/components/ColorPicker/ColorSwatch.tsx @@ -80,9 +80,11 @@ const getStyles = ( boxShadow: isSelected ? `inset 0 0 0 2px ${color}, inset 0 0 0 4px ${theme.colors.getContrastText(color)}` : 'none', - transition: theme.transitions.create(['transform'], { - duration: theme.transitions.duration.short, - }), + [theme.transitions.handleMotion('no-preference')]: { + transition: theme.transitions.create(['transform'], { + duration: theme.transitions.duration.short, + }), + }, '&:hover': { transform: 'scale(1.1)', }, diff --git a/packages/grafana-ui/src/components/ConfirmButton/ConfirmButton.tsx b/packages/grafana-ui/src/components/ConfirmButton/ConfirmButton.tsx index 850da68b49e..d8c539fc43c 100644 --- a/packages/grafana-ui/src/components/ConfirmButton/ConfirmButton.tsx +++ b/packages/grafana-ui/src/components/ConfirmButton/ConfirmButton.tsx @@ -133,18 +133,22 @@ const getStyles = (theme: GrafanaTheme2) => { }), mainButton: css({ opacity: 1, - transition: theme.transitions.create(['opacity'], { - duration: theme.transitions.duration.shortest, - easing: theme.transitions.easing.easeOut, - }), + [theme.transitions.handleMotion('no-preference')]: { + transition: theme.transitions.create(['opacity'], { + duration: theme.transitions.duration.shortest, + easing: theme.transitions.easing.easeOut, + }), + }, zIndex: 2, }), mainButtonHide: css({ opacity: 0, - transition: theme.transitions.create(['opacity', 'visibility'], { - duration: theme.transitions.duration.shortest, - easing: theme.transitions.easing.easeIn, - }), + [theme.transitions.handleMotion('no-preference')]: { + transition: theme.transitions.create(['opacity', 'visibility'], { + duration: theme.transitions.duration.shortest, + easing: theme.transitions.easing.easeIn, + }), + }, visibility: 'hidden', zIndex: 0, }), @@ -164,19 +168,23 @@ const getStyles = (theme: GrafanaTheme2) => { display: 'flex', opacity: 1, transform: 'translateX(0)', - transition: theme.transitions.create(['opacity', 'transform'], { - duration: theme.transitions.duration.shortest, - easing: theme.transitions.easing.easeOut, - }), + [theme.transitions.handleMotion('no-preference')]: { + transition: theme.transitions.create(['opacity', 'transform'], { + duration: theme.transitions.duration.shortest, + easing: theme.transitions.easing.easeOut, + }), + }, zIndex: 1, }), confirmButtonHide: css({ opacity: 0, transform: 'translateX(100%)', - transition: theme.transitions.create(['opacity', 'transform', 'visibility'], { - duration: theme.transitions.duration.shortest, - easing: theme.transitions.easing.easeIn, - }), + [theme.transitions.handleMotion('no-preference')]: { + transition: theme.transitions.create(['opacity', 'transform', 'visibility'], { + duration: theme.transitions.duration.shortest, + easing: theme.transitions.easing.easeIn, + }), + }, visibility: 'hidden', }), }; diff --git a/packages/grafana-ui/src/components/CustomScrollbar/ScrollIndicators.tsx b/packages/grafana-ui/src/components/CustomScrollbar/ScrollIndicators.tsx index b22711457ff..ef80d3df6a1 100644 --- a/packages/grafana-ui/src/components/CustomScrollbar/ScrollIndicators.tsx +++ b/packages/grafana-ui/src/components/CustomScrollbar/ScrollIndicators.tsx @@ -65,7 +65,9 @@ const getStyles = (theme: GrafanaTheme2) => { pointerEvents: 'none', position: 'absolute', right: 0, - transition: theme.transitions.create('opacity'), + [theme.transitions.handleMotion('no-preference', 'reduce')]: { + transition: theme.transitions.create('opacity'), + }, zIndex: 1, }), scrollTopIndicator: css({ diff --git a/packages/grafana-ui/src/components/DateTimePickers/DateTimePicker/DateTimePicker.test.tsx b/packages/grafana-ui/src/components/DateTimePickers/DateTimePicker/DateTimePicker.test.tsx index 1f4bed87550..5258ee22ad0 100644 --- a/packages/grafana-ui/src/components/DateTimePickers/DateTimePicker/DateTimePicker.test.tsx +++ b/packages/grafana-ui/src/components/DateTimePickers/DateTimePicker/DateTimePicker.test.tsx @@ -2,15 +2,23 @@ import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import React from 'react'; -import { dateTime } from '@grafana/data'; +import { dateTime, dateTimeAsMoment, dateTimeForTimeZone, getTimeZone, setTimeZoneResolver } from '@grafana/data'; import { Components } from '@grafana/e2e-selectors'; import { DateTimePicker, Props } from './DateTimePicker'; +// An assortment of timezones that we will test the behavior of the DateTimePicker with different timezones +const TEST_TIMEZONES = ['browser', 'Europe/Stockholm', 'America/Indiana/Marengo']; + +const defaultTimeZone = getTimeZone(); +afterAll(() => { + return setTimeZoneResolver(() => defaultTimeZone); +}); + const renderDatetimePicker = (props?: Props) => { const combinedProps = Object.assign( { - date: dateTime('2021-05-05 12:00:00'), + date: dateTimeForTimeZone(getTimeZone(), '2021-05-05 12:00:00'), onChange: () => {}, }, props @@ -26,12 +34,22 @@ describe('Date time picker', () => { expect(screen.queryByTestId('date-time-picker')).toBeInTheDocument(); }); - it('input should have a value', () => { + it.each(TEST_TIMEZONES)('input should have a value (timezone: %s)', (timeZone) => { + setTimeZoneResolver(() => timeZone); renderDatetimePicker(); - expect(screen.queryByDisplayValue('2021-05-05 12:00:00')).toBeInTheDocument(); + const dateTimeInput = screen.getByTestId(Components.DateTimePicker.input); + expect(dateTimeInput).toHaveDisplayValue('2021-05-05 12:00:00'); }); - it('should update date onblur', async () => { + it.each(TEST_TIMEZONES)('should render (timezone %s)', (timeZone) => { + setTimeZoneResolver(() => timeZone); + renderDatetimePicker(); + const dateTimeInput = screen.getByTestId(Components.DateTimePicker.input); + expect(dateTimeInput).toHaveDisplayValue('2021-05-05 12:00:00'); + }); + + it.each(TEST_TIMEZONES)('should update date onblur (timezone: %)', async (timeZone) => { + setTimeZoneResolver(() => timeZone); const onChangeInput = jest.fn(); render(); const dateTimeInput = screen.getByTestId(Components.DateTimePicker.input); @@ -42,7 +60,8 @@ describe('Date time picker', () => { expect(onChangeInput).toHaveBeenCalled(); }); - it('should not update onblur if invalid date', async () => { + it.each(TEST_TIMEZONES)('should not update onblur if invalid date (timezone: %s)', async (timeZone) => { + setTimeZoneResolver(() => timeZone); const onChangeInput = jest.fn(); render(); const dateTimeInput = screen.getByTestId(Components.DateTimePicker.input); @@ -53,31 +72,167 @@ describe('Date time picker', () => { expect(onChangeInput).not.toHaveBeenCalled(); }); - it('should be able to select values in TimeOfDayPicker without blurring the element', async () => { - renderDatetimePicker(); + it.each(TEST_TIMEZONES)( + 'should not change the day at times near the day boundary (timezone: %s)', + async (timeZone) => { + setTimeZoneResolver(() => timeZone); + const onChangeInput = jest.fn(); + render(); - // open the calendar + time picker - await userEvent.click(screen.getByLabelText('Time picker')); + // Click the calendar button + await userEvent.click(screen.getByRole('button', { name: 'Time picker' })); - // open the time of day overlay - await userEvent.click(screen.getAllByRole('textbox')[1]); + // Check the active day is the 5th + expect(screen.getByRole('button', { name: 'May 5, 2021' })).toHaveClass('react-calendar__tile--active'); - // check the hour element is visible - const hourElement = screen.getAllByRole('button', { - name: '00', - })[0]; - expect(hourElement).toBeVisible(); + // open the time of day overlay + await userEvent.click(screen.getAllByRole('textbox')[1]); - // select the hour value and check it's still visible - await userEvent.click(hourElement); - expect(hourElement).toBeVisible(); + // change the hour + await userEvent.click( + screen.getAllByRole('button', { + name: '00', + })[0] + ); - // click outside the overlay and check the hour element is no longer visible + // Check the active day is the 5th + expect(screen.getByRole('button', { name: 'May 5, 2021' })).toHaveClass('react-calendar__tile--active'); + + // change the hour + await userEvent.click( + screen.getAllByRole('button', { + name: '23', + })[0] + ); + + // Check the active day is the 5th + expect(screen.getByRole('button', { name: 'May 5, 2021' })).toHaveClass('react-calendar__tile--active'); + } + ); + + it.each(TEST_TIMEZONES)( + 'should not reset the time when selecting a different day (timezone: %s)', + async (timeZone) => { + setTimeZoneResolver(() => timeZone); + const onChangeInput = jest.fn(); + render(); + + // Click the calendar button + await userEvent.click(screen.getByRole('button', { name: 'Time picker' })); + + // Select a different day in the calendar + await userEvent.click(screen.getByRole('button', { name: 'May 15, 2021' })); + + const timeInput = screen.getAllByRole('textbox')[1]; + expect(timeInput).toHaveClass('rc-time-picker-input'); + expect(timeInput).not.toHaveDisplayValue('00:00:00'); + } + ); + + it.each(TEST_TIMEZONES)( + 'should always show the correct matching day in the calendar (timezone: %s)', + async (timeZone) => { + setTimeZoneResolver(() => timeZone); + const onChangeInput = jest.fn(); + render(); + + const dateTimeInputValue = screen.getByTestId(Components.DateTimePicker.input).getAttribute('value')!; + + // takes the string from the input + // depending on the timezone, this will look something like 2024-04-05 19:59:41 + // parses out the day value and strips the leading 0 + const day = parseInt(dateTimeInputValue.split(' ')[0].split('-')[2], 10); + + // Click the calendar button + await userEvent.click(screen.getByRole('button', { name: 'Time picker' })); + + // Check the active day matches the input + expect(screen.getByRole('button', { name: `May ${day}, 2021` })).toHaveClass('react-calendar__tile--active'); + } + ); + + it.each(TEST_TIMEZONES)( + 'should always show the correct matching day when selecting a date in the calendar (timezone: %s)', + async (timeZone) => { + setTimeZoneResolver(() => timeZone); + const onChangeInput = jest.fn(); + render(); + + // Click the calendar button + await userEvent.click(screen.getByRole('button', { name: 'Time picker' })); + + // Select a new day + const day = 8; + await userEvent.click(screen.getByRole('button', { name: `May ${day}, 2021` })); + await userEvent.click(screen.getByRole('button', { name: 'Apply' })); + + const onChangeInputArg = onChangeInput.mock.calls[0][0]; + + expect(dateTimeAsMoment(dateTimeForTimeZone(timeZone, onChangeInputArg)).date()).toBe(day); + } + ); + + it.each(TEST_TIMEZONES)('should not alter a UTC time when blurring (timezone: %s)', async (timeZone) => { + setTimeZoneResolver(() => timeZone); + const onChangeInput = jest.fn(); + + // render with a UTC value + const { rerender } = render( + + ); + + const inputValue = screen.getByTestId(Components.DateTimePicker.input).getAttribute('value')!; + + // blur the input to trigger an onChange + await userEvent.click(screen.getByTestId(Components.DateTimePicker.input)); await userEvent.click(document.body); - expect( - screen.queryByRole('button', { - name: '00', - }) - ).not.toBeInTheDocument(); + + const onChangeValue = onChangeInput.mock.calls[0][0]; + expect(onChangeInput).toHaveBeenCalledWith(onChangeValue); + + // now rerender with the "changed" value + rerender(); + + // expect the input to show the same value + expect(screen.getByTestId(Components.DateTimePicker.input)).toHaveDisplayValue(inputValue); + + // blur the input to trigger an onChange + await userEvent.click(screen.getByTestId(Components.DateTimePicker.input)); + await userEvent.click(document.body); + + // expect the onChange to be called with the same value + expect(onChangeInput).toHaveBeenCalledWith(onChangeValue); }); + + it.each(TEST_TIMEZONES)( + 'should be able to select values in TimeOfDayPicker without blurring the element (timezone: %s)', + async (timeZone) => { + setTimeZoneResolver(() => timeZone); + renderDatetimePicker(); + + // open the calendar + time picker + await userEvent.click(screen.getByLabelText('Time picker')); + + // open the time of day overlay + await userEvent.click(screen.getAllByRole('textbox')[1]); + + // check the hour element is visible + const hourElement = screen.getAllByRole('button', { + name: '00', + })[0]; + expect(hourElement).toBeVisible(); + + // select the hour value and check it's still visible + await userEvent.click(hourElement); + expect(hourElement).toBeVisible(); + + // click outside the overlay and check the hour element is no longer visible + await userEvent.click(document.body); + expect( + screen.queryByRole('button', { + name: '00', + }) + ).not.toBeInTheDocument(); + } + ); }); diff --git a/packages/grafana-ui/src/components/DateTimePickers/DateTimePicker/DateTimePicker.tsx b/packages/grafana-ui/src/components/DateTimePickers/DateTimePicker/DateTimePicker.tsx index 3df2c3b7565..60d8ba4bdb2 100644 --- a/packages/grafana-ui/src/components/DateTimePickers/DateTimePicker/DateTimePicker.tsx +++ b/packages/grafana-ui/src/components/DateTimePickers/DateTimePicker/DateTimePicker.tsx @@ -7,7 +7,15 @@ import React, { FormEvent, ReactNode, useCallback, useEffect, useRef, useState } import Calendar from 'react-calendar'; import { useMedia } from 'react-use'; -import { dateTimeFormat, DateTime, dateTime, GrafanaTheme2, isDateTime } from '@grafana/data'; +import { + dateTimeFormat, + DateTime, + dateTime, + GrafanaTheme2, + isDateTime, + dateTimeForTimeZone, + getTimeZone, +} from '@grafana/data'; import { Components } from '@grafana/e2e-selectors'; import { useStyles2, useTheme2 } from '../../../themes'; @@ -21,6 +29,7 @@ import { Portal } from '../../Portal/Portal'; import { TimeOfDayPicker, POPUP_CLASS_NAME } from '../TimeOfDayPicker'; import { getBodyStyles } from '../TimeRangePicker/CalendarBody'; import { isValid } from '../utils'; +import { adjustDateForReactCalendar } from '../utils/adjustDateForReactCalendar'; export interface Props { /** Input date for the component */ @@ -227,7 +236,7 @@ const DateTimeInput = React.forwardRef( const onBlur = useCallback(() => { if (!internalDate.invalid) { - const date = dateTime(internalDate.value); + const date = dateTimeForTimeZone(getTimeZone(), internalDate.value); onChange(date); } }, [internalDate, onChange]); @@ -276,9 +285,18 @@ const DateTimeCalendar = React.forwardRef ) => { const calendarStyles = useStyles2(getBodyStyles); const styles = useStyles2(getStyles); - const [internalDate, setInternalDate] = useState(() => { + + // need to keep these 2 separate in state since react-calendar doesn't support different timezones + const [timeOfDayDateTime, setTimeOfDayDateTime] = useState(() => { if (date && date.isValid()) { - return date.toDate(); + return dateTimeForTimeZone(getTimeZone(), date); + } + + return dateTimeForTimeZone(getTimeZone(), new Date()); + }); + const [reactCalendarDate, setReactCalendarDate] = useState(() => { + if (date && date.isValid()) { + return adjustDateForReactCalendar(date.toDate(), getTimeZone()); } return new Date(); @@ -286,28 +304,33 @@ const DateTimeCalendar = React.forwardRef const onChangeDate = useCallback['onChange']>>((date) => { if (date && !Array.isArray(date)) { - setInternalDate((prevState) => { - // If we don't use time from prevState - // the time will be reset to 00:00:00 - date.setHours(prevState.getHours()); - date.setMinutes(prevState.getMinutes()); - date.setSeconds(prevState.getSeconds()); - - return date; - }); + setReactCalendarDate(date); } }, []); const onChangeTime = useCallback((date: DateTime) => { - setInternalDate(date.toDate()); + setTimeOfDayDateTime(date); }, []); + // here we need to stitch the 2 date objects back together + const handleApply = () => { + // we take the date that's set by TimeOfDayPicker + const newDate = dateTime(timeOfDayDateTime); + + // and apply the date/month/year set by react-calendar + newDate.set('date', reactCalendarDate.getDate()); + newDate.set('month', reactCalendarDate.getMonth()); + newDate.set('year', reactCalendarDate.getFullYear()); + + onChange(newDate); + }; + return (
} nextAriaLabel="Next month" prevLabel={} @@ -323,14 +346,14 @@ const DateTimeCalendar = React.forwardRef
- +
); } else { return img; diff --git a/packages/grafana-ui/src/components/Table/styles.ts b/packages/grafana-ui/src/components/Table/styles.ts index 9ef525ac005..a7647c978aa 100644 --- a/packages/grafana-ui/src/components/Table/styles.ts +++ b/packages/grafana-ui/src/components/Table/styles.ts @@ -263,7 +263,9 @@ export function useTableStyles(theme: GrafanaTheme2, cellHeightOption: TableCell display: 'inline-block', background: resizerColor, opacity: 0, - transition: 'opacity 0.2s ease-in-out', + [theme.transitions.handleMotion('no-preference', 'reduce')]: { + transition: 'opacity 0.2s ease-in-out', + }, width: '8px', height: '100%', position: 'absolute', diff --git a/packages/grafana-ui/src/components/ToolbarButton/ToolbarButton.tsx b/packages/grafana-ui/src/components/ToolbarButton/ToolbarButton.tsx index 9bebc40b83c..767ecd7d81b 100644 --- a/packages/grafana-ui/src/components/ToolbarButton/ToolbarButton.tsx +++ b/packages/grafana-ui/src/components/ToolbarButton/ToolbarButton.tsx @@ -152,9 +152,11 @@ const getStyles = (theme: GrafanaTheme2) => { fontWeight: theme.typography.fontWeightMedium, border: `1px solid ${theme.colors.secondary.border}`, whiteSpace: 'nowrap', - transition: theme.transitions.create(['background', 'box-shadow', 'border-color', 'color'], { - duration: theme.transitions.duration.short, - }), + [theme.transitions.handleMotion('no-preference', 'reduce')]: { + transition: theme.transitions.create(['background', 'box-shadow', 'border-color', 'color'], { + duration: theme.transitions.duration.short, + }), + }, '&:focus, &:focus-visible': { ...getFocusStyles(theme), diff --git a/packages/grafana-ui/src/components/Typeahead/TypeaheadItem.tsx b/packages/grafana-ui/src/components/Typeahead/TypeaheadItem.tsx index b230f7f6cd7..de90392647b 100644 --- a/packages/grafana-ui/src/components/Typeahead/TypeaheadItem.tsx +++ b/packages/grafana-ui/src/components/Typeahead/TypeaheadItem.tsx @@ -36,8 +36,10 @@ const getStyles = (theme: GrafanaTheme2) => ({ display: 'block', whiteSpace: 'nowrap', cursor: 'pointer', - transition: - 'color 0.3s cubic-bezier(0.645, 0.045, 0.355, 1), border-color 0.3s cubic-bezier(0.645, 0.045, 0.355, 1), background 0.3s cubic-bezier(0.645, 0.045, 0.355, 1), padding 0.15s cubic-bezier(0.645, 0.045, 0.355, 1)', + [theme.transitions.handleMotion('no-preference', 'reduce')]: { + transition: + 'color 0.3s cubic-bezier(0.645, 0.045, 0.355, 1), border-color 0.3s cubic-bezier(0.645, 0.045, 0.355, 1), background 0.3s cubic-bezier(0.645, 0.045, 0.355, 1), padding 0.15s cubic-bezier(0.645, 0.045, 0.355, 1)', + }, }), typeaheadItemSelected: css({ diff --git a/packages/grafana-ui/src/components/transitions/FadeTransition.tsx b/packages/grafana-ui/src/components/transitions/FadeTransition.tsx index c1d9d6be223..501a0dfe40e 100644 --- a/packages/grafana-ui/src/components/transitions/FadeTransition.tsx +++ b/packages/grafana-ui/src/components/transitions/FadeTransition.tsx @@ -23,7 +23,7 @@ export function FadeTransition(props: Props) { ); } -const getStyles = (_theme: GrafanaTheme2, duration: number) => ({ +const getStyles = (theme: GrafanaTheme2, duration: number) => ({ enter: css({ label: 'enter', opacity: 0, @@ -31,7 +31,9 @@ const getStyles = (_theme: GrafanaTheme2, duration: number) => ({ enterActive: css({ label: 'enterActive', opacity: 1, - transition: `opacity ${duration}ms ease-out`, + [theme.transitions.handleMotion('no-preference', 'reduce')]: { + transition: `opacity ${duration}ms ease-out`, + }, }), exit: css({ label: 'exit', @@ -40,6 +42,8 @@ const getStyles = (_theme: GrafanaTheme2, duration: number) => ({ exitActive: css({ label: 'exitActive', opacity: 0, - transition: `opacity ${duration}ms ease-out`, + [theme.transitions.handleMotion('no-preference', 'reduce')]: { + transition: `opacity ${duration}ms ease-out`, + }, }), }); diff --git a/packages/grafana-ui/src/components/transitions/SlideOutTransition.tsx b/packages/grafana-ui/src/components/transitions/SlideOutTransition.tsx index 5981f28d49e..a12a4ba1700 100644 --- a/packages/grafana-ui/src/components/transitions/SlideOutTransition.tsx +++ b/packages/grafana-ui/src/components/transitions/SlideOutTransition.tsx @@ -26,7 +26,7 @@ export function SlideOutTransition(props: Props) { ); } -const getStyles = (_theme: GrafanaTheme2, duration: number, measurement: 'width' | 'height', size: number) => ({ +const getStyles = (theme: GrafanaTheme2, duration: number, measurement: 'width' | 'height', size: number) => ({ enter: css({ label: 'enter', [`${measurement}`]: 0, @@ -36,7 +36,12 @@ const getStyles = (_theme: GrafanaTheme2, duration: number, measurement: 'width' label: 'enterActive', [`${measurement}`]: `${size}px`, opacity: 1, - transition: `opacity ${duration}ms ease-out, ${measurement} ${duration}ms ease-out`, + [theme.transitions.handleMotion('no-preference')]: { + transition: `opacity ${duration}ms ease-out, ${measurement} ${duration}ms ease-out`, + }, + [theme.transitions.handleMotion('reduce')]: { + transition: `opacity ${duration}ms ease-out`, + }, }), exit: css({ label: 'exit', @@ -47,6 +52,11 @@ const getStyles = (_theme: GrafanaTheme2, duration: number, measurement: 'width' label: 'exitActive', opacity: 0, [`${measurement}`]: 0, - transition: `opacity ${duration}ms ease-out, ${measurement} ${duration}ms ease-out`, + [theme.transitions.handleMotion('no-preference')]: { + transition: `opacity ${duration}ms ease-out, ${measurement} ${duration}ms ease-out`, + }, + [theme.transitions.handleMotion('reduce')]: { + transition: `opacity ${duration}ms ease-out`, + }, }), }); diff --git a/packages/grafana-ui/src/components/uPlot/plugins/TooltipPlugin2.tsx b/packages/grafana-ui/src/components/uPlot/plugins/TooltipPlugin2.tsx index df90a10bc44..daa3f42a12d 100644 --- a/packages/grafana-ui/src/components/uPlot/plugins/TooltipPlugin2.tsx +++ b/packages/grafana-ui/src/components/uPlot/plugins/TooltipPlugin2.tsx @@ -1,12 +1,11 @@ import { css, cx } from '@emotion/css'; -import React, { useLayoutEffect, useRef, useReducer, CSSProperties, useContext } from 'react'; +import React, { useLayoutEffect, useRef, useReducer, CSSProperties } from 'react'; import { createPortal } from 'react-dom'; import uPlot from 'uplot'; import { GrafanaTheme2 } from '@grafana/data'; import { DashboardCursorSync } from '@grafana/schema'; -import { ModalsContext } from '../../../components/Modal/ModalsContext'; import { useStyles2 } from '../../../themes'; import { getPortalContainer } from '../../Portal/Portal'; import { UPlotConfigBuilder } from '../config/UPlotConfigBuilder'; @@ -123,10 +122,8 @@ export const TooltipPlugin2 = ({ const sizeRef = useRef(); - const isWithinModal = useContext(ModalsContext).component !== null; - maxWidth = isPinned ? DEFAULT_TOOLTIP_WIDTH : maxWidth ?? DEFAULT_TOOLTIP_WIDTH; - const styles = useStyles2(getStyles, maxWidth, isWithinModal); + const styles = useStyles2(getStyles, maxWidth); const renderRef = useRef(render); renderRef.current = render; @@ -583,11 +580,11 @@ export const TooltipPlugin2 = ({ return null; }; -const getStyles = (theme: GrafanaTheme2, maxWidth?: number, isWithinModal?: boolean) => ({ +const getStyles = (theme: GrafanaTheme2, maxWidth?: number) => ({ tooltipWrapper: css({ top: 0, left: 0, - zIndex: !isWithinModal ? theme.zIndex.tooltip : theme.zIndex.tooltipWithinModal, + zIndex: theme.zIndex.portal, whiteSpace: 'pre', borderRadius: theme.shape.radius.default, position: 'fixed', diff --git a/packages/grafana-ui/src/utils/handleReducedMotion.ts b/packages/grafana-ui/src/utils/handleReducedMotion.ts deleted file mode 100644 index 1ca85249dbe..00000000000 --- a/packages/grafana-ui/src/utils/handleReducedMotion.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { CSSInterpolation } from '@emotion/css'; - -/** - * @param styles - Styles to apply when no `prefers-reduced-motion` preference is set. - * @param reducedMotionStyles - Styles to apply when `prefers-reduced-motion` is enabled. - * Applies one of `styles` or `reducedMotionStyles` depending on a users `prefers-reduced-motion` setting. Omitting `reducedMotionStyles` entirely will result in no styles being applied when `prefers-reduced-motion` is enabled. In most cases this is a reasonable default. - */ -export const handleReducedMotion = (styles: CSSInterpolation, reducedMotionStyles?: CSSInterpolation) => { - const result: Record = { - '@media (prefers-reduced-motion: no-preference)': styles, - }; - if (reducedMotionStyles) { - result['@media (prefers-reduced-motion: reduce)'] = reducedMotionStyles; - } - return result; -}; diff --git a/packages/grafana-ui/src/utils/index.ts b/packages/grafana-ui/src/utils/index.ts index 55a1694e8b7..b4ec365d717 100644 --- a/packages/grafana-ui/src/utils/index.ts +++ b/packages/grafana-ui/src/utils/index.ts @@ -19,6 +19,5 @@ export { createLogger } from './logger'; export { attachDebugger } from './debug'; export * from './nodeGraph'; export { fuzzyMatch } from './fuzzy'; -export { handleReducedMotion } from './handleReducedMotion'; export { ReactUtils }; diff --git a/packages/grafana-ui/src/utils/storybook/withTimeZone.tsx b/packages/grafana-ui/src/utils/storybook/withTimeZone.tsx new file mode 100644 index 00000000000..27445b3c24e --- /dev/null +++ b/packages/grafana-ui/src/utils/storybook/withTimeZone.tsx @@ -0,0 +1,12 @@ +import { Decorator } from '@storybook/react'; +import { useEffect } from 'react'; + +import { setTimeZoneResolver } from '@grafana/data'; + +export const withTimeZone = (): Decorator => (Story, context) => { + useEffect(() => { + setTimeZoneResolver(() => context.globals.timeZone ?? 'browser'); + }, [context.globals.timeZone]); + + return Story(); +}; diff --git a/packages/grafana-ui/src/utils/tooltipUtils.ts b/packages/grafana-ui/src/utils/tooltipUtils.ts index b666ca85b29..821b9de5f7f 100644 --- a/packages/grafana-ui/src/utils/tooltipUtils.ts +++ b/packages/grafana-ui/src/utils/tooltipUtils.ts @@ -37,7 +37,9 @@ export function buildTooltipTheme( color: tooltipText, fontSize: theme.typography.bodySmall.fontSize, padding: theme.spacing(tooltipPadding.topBottom, tooltipPadding.rightLeft), - transition: 'opacity 0.3s', + [theme.transitions.handleMotion('no-preference', 'reduce')]: { + transition: 'opacity 0.3s', + }, zIndex: theme.zIndex.tooltip, maxWidth: '400px', overflowWrap: 'break-word', diff --git a/pkg/api/dashboard_test.go b/pkg/api/dashboard_test.go index be403723bb3..19f4e58c43f 100644 --- a/pkg/api/dashboard_test.go +++ b/pkg/api/dashboard_test.go @@ -814,9 +814,9 @@ func getDashboardShouldReturn200WithConfig(t *testing.T, sc *scenarioContext, pr features := featuremgmt.WithFeatures() var err error if dashboardStore == nil { - sql := db.InitTestDB(t) + sql, cfg := db.InitTestDBWithCfg(t) quotaService := quotatest.New(false, nil) - dashboardStore, err = database.ProvideDashboardStore(sql, sql.Cfg, features, tagimpl.ProvideService(sql), quotaService) + dashboardStore, err = database.ProvideDashboardStore(sql, cfg, features, tagimpl.ProvideService(sql), quotaService) require.NoError(t, err) } diff --git a/pkg/api/folder_bench_test.go b/pkg/api/folder_bench_test.go index b53c9098d01..7ff5c315ea8 100644 --- a/pkg/api/folder_bench_test.go +++ b/pkg/api/folder_bench_test.go @@ -201,21 +201,23 @@ func BenchmarkFolderListAndSearch(b *testing.B) { func setupDB(b testing.TB) benchScenario { b.Helper() - db := sqlstore.InitTestDB(b) + db, cfg := sqlstore.InitTestDB(b) IDs := map[int64]struct{}{} opts := sqlstore.NativeSettingsForDialect(db.GetDialect()) quotaService := quotatest.New(false, nil) - cfg := setting.NewCfg() - teamSvc, err := teamimpl.ProvideService(db, cfg) + teamSvc, err := teamimpl.ProvideService(db, cfg, tracing.InitializeTracerForTest()) require.NoError(b, err) orgService, err := orgimpl.ProvideService(db, cfg, quotaService) require.NoError(b, err) cache := localcache.ProvideService() - userSvc, err := userimpl.ProvideService(db, orgService, cfg, teamSvc, cache, "atest.FakeQuotaService{}, bundleregistry.ProvideService()) + userSvc, err := userimpl.ProvideService( + db, orgService, cfg, teamSvc, cache, tracing.InitializeTracerForTest(), + "atest.FakeQuotaService{}, bundleregistry.ProvideService(), + ) require.NoError(b, err) var orgID int64 = 1 diff --git a/pkg/api/login_test.go b/pkg/api/login_test.go index 1d4c725f23c..c0aed770605 100644 --- a/pkg/api/login_test.go +++ b/pkg/api/login_test.go @@ -331,7 +331,7 @@ func TestLoginPostRedirect(t *testing.T) { HooksService: &hooks.HooksService{}, License: &licensing.OSSLicensingService{}, authnService: &authntest.FakeService{ - ExpectedIdentity: &authn.Identity{ID: "user:42", SessionToken: &usertoken.UserToken{}}, + ExpectedIdentity: &authn.Identity{ID: authn.MustParseNamespaceID("user:42"), SessionToken: &usertoken.UserToken{}}, }, AuthTokenService: authtest.NewFakeUserAuthTokenService(), Features: featuremgmt.WithFeatures(), diff --git a/pkg/api/org_invite.go b/pkg/api/org_invite.go index 51054a22ada..ee41146eacf 100644 --- a/pkg/api/org_invite.go +++ b/pkg/api/org_invite.go @@ -338,7 +338,7 @@ func (hs *HTTPServer) applyUserInvite(ctx context.Context, usr *user.User, invit if setActive { // set org to active - if err := hs.userService.SetUsingOrg(ctx, &user.SetUsingOrgCommand{OrgID: invite.OrgID, UserID: usr.ID}); err != nil { + if err := hs.userService.Update(ctx, &user.UpdateUserCommand{OrgID: &invite.OrgID, UserID: usr.ID}); err != nil { return false, response.Error(http.StatusInternalServerError, "Failed to set org as active", err) } } diff --git a/pkg/api/org_test.go b/pkg/api/org_test.go index 39acc89cbd2..fcbc7f79bbf 100644 --- a/pkg/api/org_test.go +++ b/pkg/api/org_test.go @@ -266,7 +266,7 @@ func TestAPIEndpoint_GetOrg(t *testing.T) { for _, tt := range tests { t.Run(tt.desc, func(t *testing.T) { expectedIdentity := &authn.Identity{ - ID: "user:1", + ID: authn.MustParseNamespaceID("user:1"), OrgID: 1, Permissions: map[int64]map[string][]string{ 0: accesscontrol.GroupScopesByAction(tt.permissions), diff --git a/pkg/api/org_users_test.go b/pkg/api/org_users_test.go index 34a52f549b0..f3a3834b22f 100644 --- a/pkg/api/org_users_test.go +++ b/pkg/api/org_users_test.go @@ -15,6 +15,7 @@ import ( "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/db/dbtest" + "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/login/social" "github.com/grafana/grafana/pkg/login/social/socialtest" "github.com/grafana/grafana/pkg/models/roletype" @@ -27,6 +28,7 @@ import ( "github.com/grafana/grafana/pkg/services/org/orgimpl" "github.com/grafana/grafana/pkg/services/org/orgtest" "github.com/grafana/grafana/pkg/services/quota/quotaimpl" + "github.com/grafana/grafana/pkg/services/sqlstore" "github.com/grafana/grafana/pkg/services/supportbundles/supportbundlestest" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/services/user/userimpl" @@ -42,7 +44,10 @@ func setUpGetOrgUsersDB(t *testing.T, sqlStore db.DB, cfg *setting.Cfg) { quotaService := quotaimpl.ProvideService(sqlStore, cfg) orgService, err := orgimpl.ProvideService(sqlStore, cfg, quotaService) require.NoError(t, err) - usrSvc, err := userimpl.ProvideService(sqlStore, orgService, cfg, nil, nil, quotaService, supportbundlestest.NewFakeBundleService()) + usrSvc, err := userimpl.ProvideService( + sqlStore, orgService, cfg, nil, nil, tracing.InitializeTracerForTest(), + quotaService, supportbundlestest.NewFakeBundleService(), + ) require.NoError(t, err) id, err := orgService.GetOrCreate(context.Background(), "testOrg") @@ -61,8 +66,7 @@ func TestOrgUsersAPIEndpoint_userLoggedIn(t *testing.T) { hs := setupSimpleHTTPServer(featuremgmt.WithFeatures()) settings := hs.Cfg - sqlStore := db.InitTestDB(t) - sqlStore.Cfg = settings + sqlStore := db.InitTestDB(t, sqlstore.InitTestDBOpt{Cfg: settings}) hs.SQLStore = sqlStore orgService := orgtest.NewOrgServiceFake() orgService.ExpectedSearchOrgUsersResult = &org.SearchOrgUsersQueryResult{} diff --git a/pkg/api/pluginproxy/ds_proxy.go b/pkg/api/pluginproxy/ds_proxy.go index 8037c79a123..ac50243cd1d 100644 --- a/pkg/api/pluginproxy/ds_proxy.go +++ b/pkg/api/pluginproxy/ds_proxy.go @@ -19,6 +19,7 @@ import ( glog "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/plugins" + "github.com/grafana/grafana/pkg/services/accesscontrol" contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/datasources" "github.com/grafana/grafana/pkg/services/featuremgmt" @@ -304,10 +305,8 @@ func (proxy *DataSourceProxy) validateRequest() error { continue } - if route.ReqRole.IsValid() { - if !proxy.ctx.HasUserRole(route.ReqRole) { - return errors.New("plugin proxy route access denied") - } + if !proxy.hasAccessToRoute(route) { + return errors.New("plugin proxy route access denied") } proxy.matchedRoute = route @@ -330,6 +329,22 @@ func (proxy *DataSourceProxy) validateRequest() error { return nil } +func (proxy *DataSourceProxy) hasAccessToRoute(route *plugins.Route) bool { + useRBAC := proxy.features.IsEnabled(proxy.ctx.Req.Context(), featuremgmt.FlagAccessControlOnCall) && route.ReqAction != "" + if useRBAC { + routeEval := accesscontrol.EvalPermission(route.ReqAction) + ok := routeEval.Evaluate(proxy.ctx.GetPermissions()) + if !ok { + proxy.ctx.Logger.Debug("plugin route is covered by RBAC, user doesn't have access", "route", proxy.ctx.Req.URL.Path) + } + return ok + } + if route.ReqRole.IsValid() { + return proxy.ctx.HasUserRole(route.ReqRole) + } + return true +} + func (proxy *DataSourceProxy) logRequest() { if !proxy.cfg.DataProxyLogging { return diff --git a/pkg/api/pluginproxy/pluginproxy.go b/pkg/api/pluginproxy/pluginproxy.go index c61a6a284b9..5a959d97ab6 100644 --- a/pkg/api/pluginproxy/pluginproxy.go +++ b/pkg/api/pluginproxy/pluginproxy.go @@ -122,7 +122,7 @@ func (proxy *PluginProxy) HandleRequest() { } func (proxy *PluginProxy) hasAccessToRoute(route *plugins.Route) bool { - useRBAC := proxy.features.IsEnabled(proxy.ctx.Req.Context(), featuremgmt.FlagAccessControlOnCall) && route.RequiresRBACAction() + useRBAC := proxy.features.IsEnabled(proxy.ctx.Req.Context(), featuremgmt.FlagAccessControlOnCall) && route.ReqAction != "" if useRBAC { hasAccess := ac.HasAccess(proxy.accessControl, proxy.ctx)(ac.EvalPermission(route.ReqAction)) if !hasAccess { diff --git a/pkg/api/user.go b/pkg/api/user.go index f774d8ae133..d64ff7a15ca 100644 --- a/pkg/api/user.go +++ b/pkg/api/user.go @@ -215,9 +215,7 @@ func (hs *HTTPServer) UpdateUserActiveOrg(c *contextmodel.ReqContext) response.R return response.Error(http.StatusUnauthorized, "Not a valid organization", nil) } - cmd := user.SetUsingOrgCommand{UserID: userID, OrgID: orgID} - - if err := hs.userService.SetUsingOrg(c.Req.Context(), &cmd); err != nil { + if err := hs.userService.Update(c.Req.Context(), &user.UpdateUserCommand{UserID: userID, OrgID: &orgID}); err != nil { return response.Error(http.StatusInternalServerError, "Failed to change active organization", err) } @@ -493,9 +491,7 @@ func (hs *HTTPServer) UserSetUsingOrg(c *contextmodel.ReqContext) response.Respo return response.Error(http.StatusUnauthorized, "Not a valid organization", nil) } - cmd := user.SetUsingOrgCommand{UserID: userID, OrgID: orgID} - - if err := hs.userService.SetUsingOrg(c.Req.Context(), &cmd); err != nil { + if err := hs.userService.Update(c.Req.Context(), &user.UpdateUserCommand{UserID: userID, OrgID: &orgID}); err != nil { return response.Error(http.StatusInternalServerError, "Failed to change active organization", err) } @@ -527,8 +523,7 @@ func (hs *HTTPServer) ChangeActiveOrgAndRedirectToHome(c *contextmodel.ReqContex return } - cmd := user.SetUsingOrgCommand{UserID: userID, OrgID: orgID} - if err := hs.userService.SetUsingOrg(c.Req.Context(), &cmd); err != nil { + if err := hs.userService.Update(c.Req.Context(), &user.UpdateUserCommand{UserID: userID, OrgID: &orgID}); err != nil { hs.NotFoundHandler(c) return } @@ -606,16 +601,11 @@ func (hs *HTTPServer) SetHelpFlag(c *contextmodel.ReqContext) response.Response bitmask := &usr.HelpFlags1 bitmask.AddFlag(user.HelpFlags1(flag)) - cmd := user.SetUserHelpFlagCommand{ - UserID: userID, - HelpFlags1: *bitmask, - } - - if err := hs.userService.SetUserHelpFlag(c.Req.Context(), &cmd); err != nil { + if err := hs.userService.Update(c.Req.Context(), &user.UpdateUserCommand{UserID: userID, HelpFlags1: bitmask}); err != nil { return response.Error(http.StatusInternalServerError, "Failed to update help flag", err) } - return response.JSON(http.StatusOK, &util.DynMap{"message": "Help flag set", "helpFlags1": cmd.HelpFlags1}) + return response.JSON(http.StatusOK, &util.DynMap{"message": "Help flag set", "helpFlags1": *bitmask}) } // swagger:route GET /user/helpflags/clear signed_in_user clearHelpFlags @@ -633,16 +623,12 @@ func (hs *HTTPServer) ClearHelpFlags(c *contextmodel.ReqContext) response.Respon return errResponse } - cmd := user.SetUserHelpFlagCommand{ - UserID: userID, - HelpFlags1: user.HelpFlags1(0), - } - - if err := hs.userService.SetUserHelpFlag(c.Req.Context(), &cmd); err != nil { + flags := user.HelpFlags1(0) + if err := hs.userService.Update(c.Req.Context(), &user.UpdateUserCommand{UserID: userID, HelpFlags1: &flags}); err != nil { return response.Error(http.StatusInternalServerError, "Failed to update help flag", err) } - return response.JSON(http.StatusOK, &util.DynMap{"message": "Help flag set", "helpFlags1": cmd.HelpFlags1}) + return response.JSON(http.StatusOK, &util.DynMap{"message": "Help flag set", "helpFlags1": flags}) } func getUserID(c *contextmodel.ReqContext) (int64, *response.NormalResponse) { diff --git a/pkg/api/user_test.go b/pkg/api/user_test.go index 55bd2dc61d3..be9f4f129a2 100644 --- a/pkg/api/user_test.go +++ b/pkg/api/user_test.go @@ -21,6 +21,7 @@ import ( "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/db/dbtest" "github.com/grafana/grafana/pkg/infra/remotecache" + "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/login/social" "github.com/grafana/grafana/pkg/login/social/socialtest" "github.com/grafana/grafana/pkg/services/accesscontrol" @@ -39,6 +40,7 @@ import ( "github.com/grafana/grafana/pkg/services/secrets/database" "github.com/grafana/grafana/pkg/services/secrets/fakes" secretsManager "github.com/grafana/grafana/pkg/services/secrets/manager" + "github.com/grafana/grafana/pkg/services/sqlstore" "github.com/grafana/grafana/pkg/services/supportbundles/supportbundlestest" tempuser "github.com/grafana/grafana/pkg/services/temp_user" "github.com/grafana/grafana/pkg/services/temp_user/tempuserimpl" @@ -53,8 +55,7 @@ const newEmail = "newemail@localhost" func TestUserAPIEndpoint_userLoggedIn(t *testing.T) { settings := setting.NewCfg() - sqlStore := db.InitTestDB(t) - sqlStore.Cfg = settings + sqlStore := db.InitTestDB(t, sqlstore.InitTestDBOpt{Cfg: settings}) hs := &HTTPServer{ Cfg: settings, SQLStore: sqlStore, @@ -78,9 +79,12 @@ func TestUserAPIEndpoint_userLoggedIn(t *testing.T) { srv := authinfoimpl.ProvideService( authInfoStore, remotecache.NewFakeCacheStorage(), secretsService) hs.authInfoService = srv - orgSvc, err := orgimpl.ProvideService(sqlStore, sqlStore.Cfg, quotatest.New(false, nil)) + orgSvc, err := orgimpl.ProvideService(sqlStore, settings, quotatest.New(false, nil)) require.NoError(t, err) - userSvc, err := userimpl.ProvideService(sqlStore, orgSvc, sc.cfg, nil, nil, quotatest.New(false, nil), supportbundlestest.NewFakeBundleService()) + userSvc, err := userimpl.ProvideService( + sqlStore, orgSvc, sc.cfg, nil, nil, tracing.InitializeTracerForTest(), + quotatest.New(false, nil), supportbundlestest.NewFakeBundleService(), + ) require.NoError(t, err) hs.userService = userSvc @@ -148,9 +152,12 @@ func TestUserAPIEndpoint_userLoggedIn(t *testing.T) { Login: "admin", IsAdmin: true, } - orgSvc, err := orgimpl.ProvideService(sqlStore, sqlStore.Cfg, quotatest.New(false, nil)) + orgSvc, err := orgimpl.ProvideService(sqlStore, sc.cfg, quotatest.New(false, nil)) require.NoError(t, err) - userSvc, err := userimpl.ProvideService(sqlStore, orgSvc, sc.cfg, nil, nil, quotatest.New(false, nil), supportbundlestest.NewFakeBundleService()) + userSvc, err := userimpl.ProvideService( + sqlStore, orgSvc, sc.cfg, nil, nil, tracing.InitializeTracerForTest(), + quotatest.New(false, nil), supportbundlestest.NewFakeBundleService(), + ) require.NoError(t, err) _, err = userSvc.Create(context.Background(), &createUserCmd) require.Nil(t, err) @@ -379,13 +386,15 @@ func TestHTTPServer_UpdateUser(t *testing.T) { func setupUpdateEmailTests(t *testing.T, cfg *setting.Cfg) (*user.User, *HTTPServer, *notifications.NotificationServiceMock) { t.Helper() - sqlStore := db.InitTestDB(t) - sqlStore.Cfg = cfg + sqlStore := db.InitTestDB(t, sqlstore.InitTestDBOpt{Cfg: cfg}) tempUserService := tempuserimpl.ProvideService(sqlStore, cfg) orgSvc, err := orgimpl.ProvideService(sqlStore, cfg, quotatest.New(false, nil)) require.NoError(t, err) - userSvc, err := userimpl.ProvideService(sqlStore, orgSvc, cfg, nil, nil, quotatest.New(false, nil), supportbundlestest.NewFakeBundleService()) + userSvc, err := userimpl.ProvideService( + sqlStore, orgSvc, cfg, nil, nil, tracing.InitializeTracerForTest(), + quotatest.New(false, nil), supportbundlestest.NewFakeBundleService(), + ) require.NoError(t, err) // Create test user @@ -606,13 +615,15 @@ func TestUser_UpdateEmail(t *testing.T) { } nsMock := notifications.MockNotificationService() - sqlStore := db.InitTestDB(t) - sqlStore.Cfg = settings + sqlStore := db.InitTestDB(t, sqlstore.InitTestDBOpt{Cfg: settings}) tempUserSvc := tempuserimpl.ProvideService(sqlStore, settings) orgSvc, err := orgimpl.ProvideService(sqlStore, settings, quotatest.New(false, nil)) require.NoError(t, err) - userSvc, err := userimpl.ProvideService(sqlStore, orgSvc, settings, nil, nil, quotatest.New(false, nil), supportbundlestest.NewFakeBundleService()) + userSvc, err := userimpl.ProvideService( + sqlStore, orgSvc, settings, nil, nil, tracing.InitializeTracerForTest(), + quotatest.New(false, nil), supportbundlestest.NewFakeBundleService(), + ) require.NoError(t, err) server := SetupAPITestServer(t, func(hs *HTTPServer) { diff --git a/pkg/apimachinery/go.mod b/pkg/apimachinery/go.mod index c2e41f53e24..74ed02f5b1f 100644 --- a/pkg/apimachinery/go.mod +++ b/pkg/apimachinery/go.mod @@ -4,7 +4,7 @@ go 1.21.0 require ( k8s.io/apimachinery v0.29.2 - k8s.io/kube-openapi v0.0.0-20240220201932-37d671a357a5 + k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 ) require ( @@ -24,8 +24,9 @@ require ( github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/rogpeppe/go-internal v1.12.0 // indirect github.com/stretchr/testify v1.9.0 // indirect - golang.org/x/net v0.22.0 // indirect + golang.org/x/net v0.24.0 // indirect golang.org/x/text v0.14.0 // indirect google.golang.org/protobuf v1.33.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect diff --git a/pkg/apimachinery/go.sum b/pkg/apimachinery/go.sum index c82e0a06cc0..f12745cd37b 100644 --- a/pkg/apimachinery/go.sum +++ b/pkg/apimachinery/go.sum @@ -43,8 +43,7 @@ github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjY 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.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= -github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= +github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= 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= @@ -62,8 +61,7 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn 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.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc= -golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= +golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w= 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= @@ -98,8 +96,7 @@ k8s.io/apimachinery v0.29.2 h1:EWGpfJ856oj11C52NRCHuU7rFDwxev48z+6DSlGNsV8= k8s.io/apimachinery v0.29.2/go.mod h1:6HVkd1FwxIagpYrHSwJlQqZI3G9LfYWRPAkUvLnXTKU= k8s.io/klog/v2 v2.120.1 h1:QXU6cPEOIslTGvZaXvFWiP9VKyeet3sawzTOvdXb4Vw= k8s.io/klog/v2 v2.120.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= -k8s.io/kube-openapi v0.0.0-20240220201932-37d671a357a5 h1:QSpdNrZ9uRlV0VkqLvVO0Rqg8ioKi3oSw7O5P7pJV8M= -k8s.io/kube-openapi v0.0.0-20240220201932-37d671a357a5/go.mod h1:Pa1PvrP7ACSkuX6I7KYomY6cmMA0Tx86waBhDUgoKPw= +k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 h1:BZqlfIlq5YbRMFko6/PM7FjZpUb45WallggurYhKGag= k8s.io/utils v0.0.0-20230726121419-3b25d923346b h1:sgn3ZU783SCgtaSJjpcVVlRqd6GSnlTLKgpAAttJvpI= k8s.io/utils v0.0.0-20230726121419-3b25d923346b/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo= diff --git a/pkg/apiserver/go.mod b/pkg/apiserver/go.mod index 1fbc37c28f5..104d63af55c 100644 --- a/pkg/apiserver/go.mod +++ b/pkg/apiserver/go.mod @@ -5,34 +5,34 @@ go 1.21.0 require ( github.com/bwmarrin/snowflake v0.3.0 github.com/gorilla/mux v1.8.1 - github.com/grafana/grafana-plugin-sdk-go v0.224.0 + github.com/grafana/grafana-plugin-sdk-go v0.228.0 github.com/grafana/grafana/pkg/apimachinery v0.0.0-20240409140820-518d3341d58f github.com/stretchr/testify v1.9.0 - golang.org/x/mod v0.14.0 + golang.org/x/mod v0.15.0 k8s.io/apimachinery v0.29.2 k8s.io/apiserver v0.29.2 k8s.io/client-go v0.29.2 k8s.io/component-base v0.29.2 k8s.io/klog v1.0.0 k8s.io/klog/v2 v2.120.1 - k8s.io/kube-openapi v0.0.0-20240220201932-37d671a357a5 + k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 ) require ( github.com/BurntSushi/toml v1.3.2 // indirect github.com/NYTimes/gziphandler v1.1.1 // indirect - github.com/antlr/antlr4/runtime/Go/antlr/v4 v4.0.0-20230305170008-8188dc5388df // indirect + github.com/antlr/antlr4/runtime/Go/antlr/v4 v4.0.0-20230512164433-5d1fd1a340c9 // indirect github.com/apache/arrow/go/v15 v15.0.2 // indirect github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/blang/semver/v4 v4.0.0 // indirect - github.com/cenkalti/backoff/v4 v4.2.1 // indirect - github.com/cespare/xxhash/v2 v2.2.0 // indirect + github.com/cenkalti/backoff/v4 v4.3.0 // 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-20230802225258-3cf4e6d46a89 // indirect github.com/coreos/go-semver v0.3.1 // indirect github.com/coreos/go-systemd/v22 v22.5.0 // indirect - github.com/cpuguy83/go-md2man/v2 v2.0.3 // indirect + github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/elazarl/goproxy v0.0.0-20230731152917-f99041a5c027 // indirect github.com/emicklei/go-restful/v3 v3.11.0 // indirect @@ -40,9 +40,10 @@ require ( github.com/fatih/color v1.15.0 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect - github.com/getkin/kin-openapi v0.120.0 // indirect + github.com/getkin/kin-openapi v0.124.0 // indirect github.com/go-logr/logr v1.4.1 // indirect github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-logr/zapr v1.3.0 // indirect github.com/go-openapi/jsonpointer v0.20.2 // indirect github.com/go-openapi/jsonreference v0.20.4 // indirect github.com/go-openapi/swag v0.22.9 // indirect @@ -59,8 +60,10 @@ require ( github.com/google/pprof v0.0.0-20231205033806-a5a03c77bf08 // indirect github.com/google/uuid v1.6.0 // indirect github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 // indirect + github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.0.1 // indirect + github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.1.0 // indirect github.com/grpc-ecosystem/go-grpc-prometheus v1.2.1-0.20191002090509-6af20e3a5340 // indirect - github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.1 // indirect github.com/hashicorp/go-hclog v1.6.3 // indirect github.com/hashicorp/go-plugin v1.6.0 // indirect github.com/hashicorp/yamux v0.1.1 // indirect @@ -76,7 +79,7 @@ require ( github.com/mailru/easyjson v0.7.7 // indirect github.com/mattetti/filebuffer v1.0.1 // indirect github.com/mattn/go-colorable v0.1.13 // indirect - github.com/mattn/go-isatty v0.0.19 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-runewidth v0.0.15 // indirect github.com/mitchellh/go-testing-interface v1.14.1 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect @@ -89,10 +92,10 @@ require ( github.com/pierrec/lz4/v4 v4.1.18 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect - github.com/prometheus/client_golang v1.18.0 // indirect - github.com/prometheus/client_model v0.5.0 // indirect - github.com/prometheus/common v0.46.0 // indirect - github.com/prometheus/procfs v0.12.0 // indirect + github.com/prometheus/client_golang v1.19.0 // indirect + github.com/prometheus/client_model v0.6.1 // indirect + github.com/prometheus/common v0.53.0 // indirect + github.com/prometheus/procfs v0.14.0 // indirect github.com/rivo/uniseg v0.3.4 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/sirupsen/logrus v1.9.3 // indirect @@ -105,40 +108,38 @@ require ( github.com/unknwon/bra v0.0.0-20200517080246-1e3013ecaff8 // indirect github.com/unknwon/com v1.0.1 // indirect github.com/unknwon/log v0.0.0-20150304194804-e617c87089d3 // indirect - github.com/urfave/cli v1.22.14 // indirect + github.com/urfave/cli v1.22.15 // indirect github.com/zeebo/xxh3 v1.0.2 // indirect go.etcd.io/etcd/api/v3 v3.5.10 // indirect go.etcd.io/etcd/client/pkg/v3 v3.5.10 // indirect go.etcd.io/etcd/client/v3 v3.5.10 // indirect - go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0 // indirect - go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.49.0 // indirect - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect - go.opentelemetry.io/contrib/propagators/jaeger v1.22.0 // indirect - go.opentelemetry.io/contrib/samplers/jaegerremote v0.18.0 // indirect - go.opentelemetry.io/otel v1.24.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.24.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.24.0 // indirect - go.opentelemetry.io/otel/metric v1.24.0 // indirect - go.opentelemetry.io/otel/sdk v1.24.0 // indirect - go.opentelemetry.io/otel/trace v1.24.0 // indirect - go.opentelemetry.io/proto/otlp v1.1.0 // indirect + go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.51.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.51.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.51.0 // indirect + go.opentelemetry.io/contrib/propagators/jaeger v1.26.0 // indirect + go.opentelemetry.io/contrib/samplers/jaegerremote v0.20.0 // indirect + go.opentelemetry.io/otel v1.26.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.26.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.26.0 // indirect + go.opentelemetry.io/otel/metric v1.26.0 // indirect + go.opentelemetry.io/otel/sdk v1.26.0 // indirect + go.opentelemetry.io/otel/trace v1.26.0 // indirect + go.opentelemetry.io/proto/otlp v1.2.0 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.26.0 // indirect - golang.org/x/crypto v0.21.0 // indirect + golang.org/x/crypto v0.22.0 // indirect golang.org/x/exp v0.0.0-20231206192017-f3f8817b8deb // indirect - golang.org/x/net v0.22.0 // indirect - golang.org/x/oauth2 v0.18.0 // indirect - golang.org/x/sync v0.6.0 // indirect - golang.org/x/sys v0.18.0 // indirect - golang.org/x/term v0.18.0 // indirect + golang.org/x/net v0.24.0 // indirect + golang.org/x/oauth2 v0.19.0 // indirect + golang.org/x/sync v0.7.0 // indirect + golang.org/x/sys v0.19.0 // indirect + golang.org/x/term v0.19.0 // indirect golang.org/x/text v0.14.0 // indirect golang.org/x/time v0.5.0 // indirect - golang.org/x/tools v0.17.0 // indirect + golang.org/x/tools v0.18.0 // indirect golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect - google.golang.org/appengine v1.6.8 // indirect - google.golang.org/genproto v0.0.0-20240227224415-6ceb2ff114de // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20240227224415-6ceb2ff114de // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20240227224415-6ceb2ff114de // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20240415180920-8c6c420018be // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240415180920-8c6c420018be // indirect google.golang.org/grpc v1.63.2 // indirect google.golang.org/protobuf v1.33.0 // indirect gopkg.in/fsnotify/fsnotify.v1 v1.4.7 // indirect diff --git a/pkg/apiserver/go.sum b/pkg/apiserver/go.sum index 0efc1c878d3..703e5d9c0ef 100644 --- a/pkg/apiserver/go.sum +++ b/pkg/apiserver/go.sum @@ -4,8 +4,7 @@ github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8 github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= github.com/NYTimes/gziphandler v1.1.1 h1:ZUDjpQae29j0ryrS0u/B8HZfJBtBQHjqw2rQ2cqUQ3I= github.com/NYTimes/gziphandler v1.1.1/go.mod h1:n/CVRwUEOgIxrgPvAQhUUr9oeUtvrhMomdKFjzJNB0c= -github.com/antlr/antlr4/runtime/Go/antlr/v4 v4.0.0-20230305170008-8188dc5388df h1:7RFfzj4SSt6nnvCPbCqijJi1nWCd+TqAT3bYCStRC18= -github.com/antlr/antlr4/runtime/Go/antlr/v4 v4.0.0-20230305170008-8188dc5388df/go.mod h1:pSwJ0fSY5KhvocuWSx4fz3BA8OrA1bQn+K1Eli3BRwM= +github.com/antlr/antlr4/runtime/Go/antlr/v4 v4.0.0-20230512164433-5d1fd1a340c9 h1:goHVqTbFX3AIo0tzGr14pgfAW2ZfPChKO21Z9MGf/gk= github.com/apache/arrow/go/v15 v15.0.2 h1:60IliRbiyTWCWjERBCkO1W4Qun9svcYoZrSLcyOsMLE= github.com/apache/arrow/go/v15 v15.0.2/go.mod h1:DGXsR3ajT524njufqf95822i+KTh+yea1jass9YXgjA= github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so= @@ -20,11 +19,9 @@ github.com/bufbuild/protocompile v0.4.0 h1:LbFKd2XowZvQ/kajzguUp2DC9UEIQhIq77fZZ github.com/bufbuild/protocompile v0.4.0/go.mod h1:3v93+mbWn/v3xzN+31nwkJfrEpAUwp+BagBSZWx+TP8= github.com/bwmarrin/snowflake v0.3.0 h1:xm67bEhkKh6ij1790JB83OujPR5CzNe8QuQqAgISZN0= github.com/bwmarrin/snowflake v0.3.0/go.mod h1:NdZxfVWX+oR6y2K0o6qAYv6gIOP9rjG0/E9WsDpxqwE= -github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM= -github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= -github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= -github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= 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 h1:aPflPkRFkVwbW6dmcVqfgwp1i+UWGFH6VgR1Jim5Ygc= @@ -36,9 +33,8 @@ github.com/coreos/go-semver v0.3.1/go.mod h1:irMmmIw/7yzSRPWryHsK7EYSg09caPQL03V github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= -github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= -github.com/cpuguy83/go-md2man/v2 v2.0.3 h1:qMCsGGgs+MAzDFyp9LpAe1Lqy/fY/qCovCm0qnXZOBM= github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/cpuguy83/go-md2man/v2 v2.0.4 h1:wfIWP927BUkWJb2NmU/kNDYIBTh/ziUX91+lVfRxZq4= 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= @@ -66,8 +62,8 @@ github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSw github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= -github.com/getkin/kin-openapi v0.120.0 h1:MqJcNJFrMDFNc07iwE8iFC5eT2k/NPUFDIpNeiZv8Jg= -github.com/getkin/kin-openapi v0.120.0/go.mod h1:PCWw/lfBrJY4HcdqE3jj+QFkaFK8ABoqo7PvqVhXXqw= +github.com/getkin/kin-openapi v0.124.0 h1:VSFNMB9C9rTKBnQ/fpyDU8ytMTr4dWI9QovSKj9kz/M= +github.com/getkin/kin-openapi v0.124.0/go.mod h1:wb1aSZA/iWmorQP9KTAS/phLj/t17B5jT7+fS8ed9NM= github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas= @@ -76,8 +72,7 @@ github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= -github.com/go-logr/zapr v1.2.3 h1:a9vnzlIBPQBBkeaR9IuMUfmVOrQlkoC4YfPoFkX3T7A= -github.com/go-logr/zapr v1.2.3/go.mod h1:eIauM6P8qSvTw5o2ez6UEAfGjQKrxQTl5EoK+Qa2oG4= +github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ= github.com/go-openapi/jsonpointer v0.20.2 h1:mQc3nmndL8ZBzStEo3JYF8wzmeWffDH4VbXz58sAx6Q= github.com/go-openapi/jsonpointer v0.20.2/go.mod h1:bHen+N0u1KEO3YlmqOjTT9Adn1RfD91Ar825/PuiRVs= github.com/go-openapi/jsonreference v0.20.4 h1:bKlDxQxQJgwpUSgOENiMPzCTBVuc7vTdXSSgNeAhojU= @@ -103,8 +98,6 @@ github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfb github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= -github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= -github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU= github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= @@ -115,7 +108,6 @@ github.com/google/flatbuffers v23.5.26+incompatible/go.mod h1:1AeVuKshWv4vARoZat github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= -github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= @@ -133,17 +125,18 @@ github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/grafana/grafana-plugin-sdk-go v0.224.0 h1:WBpKJEhzEcGBAmmNB/OGGF/pHrt654O3m7idjnpSc40= +github.com/grafana/grafana-plugin-sdk-go v0.228.0 h1:LlPqyB+RZTtDy8RVYD7iQVJW5A0gMoGSI/+Ykz8HebQ= github.com/grafana/grafana/pkg/apimachinery v0.0.0-20240409140820-518d3341d58f h1:+CK3tH3XrAAqx5urmVqpgSxMrL2MlpTOnLVSU4w4IjY= github.com/grafana/grafana/pkg/apimachinery v0.0.0-20240409140820-518d3341d58f/go.mod h1:ZxIaCOlDmFupiL55aLU+Qp7O1dgwkDMBAQBK7wnEVBg= github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 h1:UH//fgunKIs4JdUbpDl1VZCDaL56wXCB/5+wF6uHfaI= github.com/grpc-ecosystem/go-grpc-middleware v1.4.0/go.mod h1:g5qyo/la0ALbONm6Vbp88Yd8NsDy6rZz+RcrMPxvld8= +github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.0.1 h1:qnpSQwGEnkcRpTqNOIR6bJbR0gAorgP9CSALpRcKoAA= +github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.1.0 h1:pRhl55Yx1eC7BZ1N+BBWwnKaMyD8uC+34TLdndZMAKk= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.1-0.20191002090509-6af20e3a5340 h1:uGoIog/wiQHI9GAxXO5TJbT0wWKH3O9HhOJW1F9c3fY= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.1-0.20191002090509-6af20e3a5340/go.mod h1:3bDW6wMZJB7tiONtC/1Xpicra6Wp5GgbTbQWCbI5fkc= github.com/grpc-ecosystem/grpc-gateway v1.16.0 h1:gmcG1KaJ57LophUzW0Hy8NmPhnMZb4M0+kPpLofRdBo= github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0 h1:Wqo399gCIufwto+VfwCSvsnfGpF/w5E9CNxSwbpD6No= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0/go.mod h1:qmOFXW2epJhM0qSnUUYpldc7gVz2KMQwJ/QYCDIa7XU= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.1 h1:/c3QmbOGMGTOumP2iT/rCwB7b0QDGLKzqOmktBjT+Is= github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k= github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= github.com/hashicorp/go-plugin v1.6.0 h1:wgd4KxHJTVGGqWBq4QPB1i5BZNEx9BR8+OFmHDmTk8A= @@ -194,8 +187,7 @@ github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovk github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= -github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= -github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 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= @@ -233,25 +225,20 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN 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/prometheus/client_golang v0.9.2/go.mod h1:OsXs2jCmiKlQ1lTBmv21f2mNfw4xf/QclQDMrYNZzcM= -github.com/prometheus/client_golang v1.18.0 h1:HzFfmkOzH5Q8L8G+kSJKUx5dtG87sewO+FoDDqP5Tbk= -github.com/prometheus/client_golang v1.18.0/go.mod h1:T+GXkCk5wSJyOqMIzVgvvjFDlkOQntgjkJWKrN5txjA= +github.com/prometheus/client_golang v1.19.0 h1:ygXvpU1AoN1MhdzckN+PyD9QJOSD4x7kmXYlnfbA6JU= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw= -github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI= +github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= github.com/prometheus/common v0.0.0-20181126121408-4724e9255275/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= -github.com/prometheus/common v0.46.0 h1:doXzt5ybi1HBKpsZOL0sSkaNHJJqkyfEWZGGqqScV0Y= -github.com/prometheus/common v0.46.0/go.mod h1:Tp0qkxpb9Jsg54QMe+EAmqXkSV7Evdy1BTn+g2pa/hQ= +github.com/prometheus/common v0.53.0 h1:U2pL9w9nmJwJDa4qqLQ3ZaePJ6ZTwt7cMD3AG3+aLCE= github.com/prometheus/procfs v0.0.0-20181204211112-1dc9a6cbc91a/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= -github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo= -github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= +github.com/prometheus/procfs v0.14.0 h1:Lw4VdGGoKEZilJsayHf0B+9YgLGREba2C6xr+Fdfq6s= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.3.4 h1:3Z3Eu6FGHZWSfNKJTOUiPatWwfc7DzJRU04jFUqJODw= github.com/rivo/uniseg v0.3.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rogpeppe/go-charset v0.0.0-20180617210344-2471d30d28b4/go.mod h1:qgYeAmZ5ZIpBWTGllZSQnw97Dj+woV0toclVaRGI8pc= -github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= -github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= +github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= @@ -300,13 +287,11 @@ github.com/unknwon/com v1.0.1/go.mod h1:tOOxU81rwgoCLoOVVPHb6T/wt8HZygqH5id+GNnl github.com/unknwon/log v0.0.0-20150304194804-e617c87089d3 h1:4EYQaWAatQokdji3zqZloVIW/Ke1RQjYw2zHULyrHJg= github.com/unknwon/log v0.0.0-20150304194804-e617c87089d3/go.mod h1:1xEUf2abjfP92w2GZTV+GgaRxXErwRXcClbUwrNJffU= github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= -github.com/urfave/cli v1.22.14 h1:ebbhrRiGK2i4naQJr+1Xj92HXZCrK7MsyTS/ob3HnAk= -github.com/urfave/cli v1.22.14/go.mod h1:X0eDS6pD6Exaclxm99NJ3FiCDRED7vIHpx2mDOHLvkA= +github.com/urfave/cli v1.22.15 h1:nuqt+pdC/KqswQKhETJjo7pvn/k4xMUxgW6liI7XpnM= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2 h1:eY9dn8+vbi4tKz5Qo6v2eYzo7kUS51QINcR5jNpbZS8= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ= github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0= github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0= @@ -327,30 +312,18 @@ go.etcd.io/etcd/raft/v3 v3.5.10 h1:cgNAYe7xrsrn/5kXMSaH8kM/Ky8mAdMqGOxyYwpP0LA= go.etcd.io/etcd/raft/v3 v3.5.10/go.mod h1:odD6kr8XQXTy9oQnyMPBOr0TVe+gT0neQhElQ6jbGRc= go.etcd.io/etcd/server/v3 v3.5.10 h1:4NOGyOwD5sUZ22PiWYKmfxqoeh72z6EhYjNosKGLmZg= go.etcd.io/etcd/server/v3 v3.5.10/go.mod h1:gBplPHfs6YI0L+RpGkTQO7buDbHv5HJGG/Bst0/zIPo= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0 h1:4Pp6oUg3+e/6M4C0A/3kJ2VYa++dsWVTtGgLVj5xtHg= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0/go.mod h1:Mjt1i1INqiaoZOMGR1RIUJN+i3ChKoFRqzrRQhlkbs0= -go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.49.0 h1:RtcvQ4iw3w9NBB5yRwgA4sSa82rfId7n4atVpvKx3bY= -go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.49.0/go.mod h1:f/PbKbRd4cdUICWell6DmzvVJ7QrmBgFrRHjXmAXbK4= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 h1:jq9TW8u3so/bN+JPT166wjOI6/vQPF6Xe7nMNIltagk= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0/go.mod h1:p8pYQP+m5XfbZm9fxtSKAbM6oIllS7s2AfxrChvc7iw= -go.opentelemetry.io/contrib/propagators/jaeger v1.22.0 h1:bAHX+zN/inu+Rbqk51REmC8oXLl+Dw6pp9ldQf/onaY= -go.opentelemetry.io/contrib/propagators/jaeger v1.22.0/go.mod h1:bH9GkgkN21mscXcQP6lQJYI8XnEPDxlTN/ZOBuHDjqE= -go.opentelemetry.io/contrib/samplers/jaegerremote v0.18.0 h1:Q9PrD94WoMolBx44ef5UWWvufpVSME0MiSymXZfedso= -go.opentelemetry.io/contrib/samplers/jaegerremote v0.18.0/go.mod h1:tjp49JHNvreAAoWjdCHIVD7NXMjuJ3Dp/9iNOuPPlC8= -go.opentelemetry.io/otel v1.24.0 h1:0LAOdjNmQeSTzGBzduGe/rU4tZhMwL5rWgtp9Ku5Jfo= -go.opentelemetry.io/otel v1.24.0/go.mod h1:W7b9Ozg4nkF5tWI5zsXkaKKDjdVjpD4oAt9Qi/MArHo= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.24.0 h1:t6wl9SPayj+c7lEIFgm4ooDBZVb01IhLB4InpomhRw8= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.24.0/go.mod h1:iSDOcsnSA5INXzZtwaBPrKp/lWu/V14Dd+llD0oI2EA= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.24.0 h1:Mw5xcxMwlqoJd97vwPxA8isEaIoxsta9/Q51+TTJLGE= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.24.0/go.mod h1:CQNu9bj7o7mC6U7+CA/schKEYakYXWr79ucDHTMGhCM= -go.opentelemetry.io/otel/metric v1.24.0 h1:6EhoGWWK28x1fbpA4tYTOWBkPefTDQnb8WSGXlc88kI= -go.opentelemetry.io/otel/metric v1.24.0/go.mod h1:VYhLe1rFfxuTXLgj4CBiyz+9WYBA8pNGJgDcSFRKBco= -go.opentelemetry.io/otel/sdk v1.24.0 h1:YMPPDNymmQN3ZgczicBY3B6sf9n62Dlj9pWD3ucgoDw= -go.opentelemetry.io/otel/sdk v1.24.0/go.mod h1:KVrIYw6tEubO9E96HQpcmpTKDVn9gdv35HoYiQWGDFg= -go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y1YELI= -go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU= -go.opentelemetry.io/proto/otlp v1.1.0 h1:2Di21piLrCqJ3U3eXGCTPHE9R8Nh+0uglSnOyxikMeI= -go.opentelemetry.io/proto/otlp v1.1.0/go.mod h1:GpBHCBWiqvVLDqmHZsoMM3C5ySeKTC7ej/RNTae6MdY= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.51.0 h1:A3SayB3rNyt+1S6qpI9mHPkeHTZbD7XILEqWnYZb2l0= +go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.51.0 h1:974XTyIwHI4nHa1+uSLxHtUnlJ2DiVtAJjk7fd07p/8= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.51.0 h1:Xs2Ncz0gNihqu9iosIZ5SkBbWo5T8JhhLJFMQL1qmLI= +go.opentelemetry.io/contrib/propagators/jaeger v1.26.0 h1:RH76Cl2pfOLLoCtxAPax9c7oYzuL1tiI7/ZPJEmEmOw= +go.opentelemetry.io/contrib/samplers/jaegerremote v0.20.0 h1:ja+d7Aea/9PgGxB63+E0jtRFpma717wubS0KFkZpmYw= +go.opentelemetry.io/otel v1.26.0 h1:LQwgL5s/1W7YiiRwxf03QGnWLb2HW4pLiAhaA5cZXBs= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.26.0 h1:1u/AyyOqAWzy+SkPxDpahCNZParHV8Vid1RnI2clyDE= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.26.0 h1:Waw9Wfpo/IXzOI8bCB7DIk+0JZcqqsyn1JFnAc+iam8= +go.opentelemetry.io/otel/metric v1.26.0 h1:7S39CLuY5Jgg9CrnA9HHiEjGMF/X2VHvoXGgSllRz30= +go.opentelemetry.io/otel/sdk v1.26.0 h1:Y7bumHf5tAiDlRYFmGqetNcLaVUZmh4iYfmGxtmz7F8= +go.opentelemetry.io/otel/trace v1.26.0 h1:1ieeAUb4y0TE26jUFrCIXKpTuVK7uJGN9/Z/2LP5sQA= +go.opentelemetry.io/proto/otlp v1.2.0 h1:pVeZGk7nXDC9O2hncA6nHldxEjm6LByfA2aN8IOkz94= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= @@ -364,9 +337,7 @@ go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so= 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/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA= -golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= +golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20231206192017-f3f8817b8deb h1:c0vyKkb6yr3KR7jEfJaOSv4lG7xPkbN6r52aJz1d8a8= golang.org/x/exp v0.0.0-20231206192017-f3f8817b8deb/go.mod h1:iRJReGqOEeBhDZGkGbynYwcHlctCvnjTYIamk7uXpHI= @@ -376,9 +347,7 @@ golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHl golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 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/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0= -golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.15.0 h1:SernR4v+D55NyBH2QiEQrlBAnj1ECL6AGrA5+dPaMY8= 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= golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -388,21 +357,15 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn 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.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc= -golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= +golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.18.0 h1:09qnuIAgzdx1XplqJvW6CQqMCtGZykZWcXzPMPUusvI= -golang.org/x/oauth2 v0.18.0/go.mod h1:Wf7knwG0MPoWIMMBgFlEaSUDaKskp0dCfrlJRJXbBi8= +golang.org/x/oauth2 v0.19.0 h1:9+E/EZBCbTLNrbN35fHv/a/d/mOBatymz1zbtQrXpIg= 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-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/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ= -golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 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= @@ -411,28 +374,18 @@ golang.org/x/sys v0.0.0-20191020152052-9984515f0562/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= -golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.18.0 h1:FcHjZXDMxI8mM3nwhX9HlKop4C0YQvCVCdwYl2wOtE8= -golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58= +golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= +golang.org/x/term v0.19.0 h1:+ThwsDv+tYfnJFhF4L8jITxu1tdTWRTZpdsWgEgjL6Q= 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.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= @@ -448,9 +401,7 @@ golang.org/x/tools v0.0.0-20191108193012-7d206e10da11/go.mod h1:b+2E5dAYhXwXZwtn 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/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.17.0 h1:FvmRgNOcs3kOa+T20R1uhfP9F6HgG2mfxDv1vrx1Htc= -golang.org/x/tools v0.17.0/go.mod h1:xsh6VxdV005rRVaS6SSAf9oiAqljS7UZUacMZ8Bnsps= +golang.org/x/tools v0.18.0 h1:k8NLag8AGHnn+PHbl7g43CtqZAwG60vZkLqgyZgIHgQ= 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= @@ -461,14 +412,12 @@ gonum.org/v1/gonum v0.12.0 h1:xKuo6hzt+gMav00meVPUlXwSdoEJP46BR+wdxQEFK2o= gonum.org/v1/gonum v0.12.0/go.mod h1:73TDxJfAAHeA8Mk9mf8NlIppyhQNo5GLTcYeqgo2lvY= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM= -google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20200423170343-7949de9c1215/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20240227224415-6ceb2ff114de h1:F6qOa9AZTYJXOUEr4jDysRDLrm4PHePlge4v4TGAlxY= -google.golang.org/genproto/googleapis/api v0.0.0-20240227224415-6ceb2ff114de h1:jFNzHPIeuzhdRwVhbZdiym9q0ory/xY3sA+v2wPg8I0= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240227224415-6ceb2ff114de h1:cZGRis4/ot9uVm639a+rHCUaG0JJHEsdyzSQTMX+suY= +google.golang.org/genproto/googleapis/api v0.0.0-20240415180920-8c6c420018be h1:Zz7rLWqp0ApfsR/l7+zSHhY3PMiH2xqgxlfYfAfNpoU= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240415180920-8c6c420018be h1:LG9vZxsWGOmUKieR8wPAUR3u3MpnYFQZROPIMaXh7/A= google.golang.org/grpc v1.18.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= @@ -476,8 +425,6 @@ google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQ google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= google.golang.org/grpc v1.63.2 h1:MUeiw1B2maTVZthpU5xvASfTh3LDbxHd6IJ6QQVU+xM= -google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= -google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -516,8 +463,7 @@ k8s.io/klog v1.0.0 h1:Pt+yjF5aB1xDSVbau4VsWe+dQNzA0qv1LlXdC2dF6Q8= k8s.io/klog v1.0.0/go.mod h1:4Bi6QPql/J/LkTDqv7R/cd3hPo4k2DG6Ptcz060Ez5I= k8s.io/klog/v2 v2.120.1 h1:QXU6cPEOIslTGvZaXvFWiP9VKyeet3sawzTOvdXb4Vw= k8s.io/klog/v2 v2.120.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= -k8s.io/kube-openapi v0.0.0-20240220201932-37d671a357a5 h1:QSpdNrZ9uRlV0VkqLvVO0Rqg8ioKi3oSw7O5P7pJV8M= -k8s.io/kube-openapi v0.0.0-20240220201932-37d671a357a5/go.mod h1:Pa1PvrP7ACSkuX6I7KYomY6cmMA0Tx86waBhDUgoKPw= +k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 h1:BZqlfIlq5YbRMFko6/PM7FjZpUb45WallggurYhKGag= k8s.io/utils v0.0.0-20230726121419-3b25d923346b h1:sgn3ZU783SCgtaSJjpcVVlRqd6GSnlTLKgpAAttJvpI= k8s.io/utils v0.0.0-20230726121419-3b25d923346b/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.28.0 h1:TgtAeesdhpm2SGwkQasmbeqDo8th5wOBA5h/AjTKA4I= diff --git a/pkg/build/npm/npm.go b/pkg/build/npm/npm.go index 5b1a895ec49..04bcc53b322 100644 --- a/pkg/build/npm/npm.go +++ b/pkg/build/npm/npm.go @@ -22,7 +22,6 @@ var packages = []string{ "@grafana/ui", "@grafana/data", "@grafana/runtime", - "@grafana/e2e", "@grafana/e2e-selectors", "@grafana/schema", "@grafana/flamegraph", diff --git a/pkg/build/wire/go.mod b/pkg/build/wire/go.mod index 0b2868ee974..86688b89207 100644 --- a/pkg/build/wire/go.mod +++ b/pkg/build/wire/go.mod @@ -6,5 +6,6 @@ require ( github.com/google/go-cmp v0.6.0 github.com/google/subcommands v1.2.0 github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 - golang.org/x/tools v0.17.0 + golang.org/x/sync v0.7.0 // indirect + golang.org/x/tools v0.18.0 ) diff --git a/pkg/build/wire/go.sum b/pkg/build/wire/go.sum index 371f59f5482..5d5c60f18f5 100644 --- a/pkg/build/wire/go.sum +++ b/pkg/build/wire/go.sum @@ -8,25 +8,22 @@ github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5t golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= -golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0= -golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.15.0 h1:SernR4v+D55NyBH2QiEQrlBAnj1ECL6AGrA5+dPaMY8= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= -golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= -golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ= golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -35,13 +32,11 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= -golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY= 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.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= @@ -54,6 +49,5 @@ golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtn golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= -golang.org/x/tools v0.17.0 h1:FvmRgNOcs3kOa+T20R1uhfP9F6HgG2mfxDv1vrx1Htc= -golang.org/x/tools v0.17.0/go.mod h1:xsh6VxdV005rRVaS6SSAf9oiAqljS7UZUacMZ8Bnsps= +golang.org/x/tools v0.18.0 h1:k8NLag8AGHnn+PHbl7g43CtqZAwG60vZkLqgyZgIHgQ= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/pkg/cmd/grafana-cli/commands/conflict_user_command.go b/pkg/cmd/grafana-cli/commands/conflict_user_command.go index 5e188f6b797..f9d592bb2b3 100644 --- a/pkg/cmd/grafana-cli/commands/conflict_user_command.go +++ b/pkg/cmd/grafana-cli/commands/conflict_user_command.go @@ -33,7 +33,7 @@ import ( "github.com/grafana/grafana/pkg/setting" ) -func initConflictCfg(cmd *utils.ContextCommandLine) (*setting.Cfg, featuremgmt.FeatureToggles, error) { +func initConflictCfg(cmd *utils.ContextCommandLine) (*setting.Cfg, tracing.Tracer, featuremgmt.FeatureToggles, error) { configOptions := strings.Split(cmd.String("configOverrides"), " ") configOptions = append(configOptions, cmd.Args().Slice()...) cfg, err := setting.NewCfgFromArgs(setting.CommandLineArgs{ @@ -43,19 +43,33 @@ func initConflictCfg(cmd *utils.ContextCommandLine) (*setting.Cfg, featuremgmt.F }) if err != nil { - return nil, nil, err + return nil, nil, nil, err } features, err := featuremgmt.ProvideManagerService(cfg) - return cfg, features, err + if err != nil { + return nil, nil, nil, err + } + + tracingCfg, err := tracing.ProvideTracingConfig(cfg) + if err != nil { + return nil, nil, nil, fmt.Errorf("%v: %w", "failed to initialize tracer config", err) + } + + tracer, err := tracing.ProvideService(tracingCfg) + if err != nil { + return nil, nil, nil, fmt.Errorf("%v: %w", "failed to initialize tracer service", err) + } + + return cfg, tracer, features, err } func initializeConflictResolver(cmd *utils.ContextCommandLine, f Formatter, ctx *cli.Context) (*ConflictResolver, error) { - cfg, features, err := initConflictCfg(cmd) + cfg, tracer, features, err := initConflictCfg(cmd) if err != nil { return nil, fmt.Errorf("%v: %w", "failed to load configuration", err) } - s, err := getSqlStore(cfg, features) + s, err := getSqlStore(cfg, tracer, features) if err != nil { return nil, fmt.Errorf("%v: %w", "failed to get to sql", err) } @@ -64,7 +78,7 @@ func initializeConflictResolver(cmd *utils.ContextCommandLine, f Formatter, ctx return nil, fmt.Errorf("%v: %w", "failed to get users with conflicting logins", err) } quotaService := quotaimpl.ProvideService(s, cfg) - userService, err := userimpl.ProvideService(s, nil, cfg, nil, nil, quotaService, supportbundlestest.NewFakeBundleService()) + userService, err := userimpl.ProvideService(s, nil, cfg, nil, nil, tracer, quotaService, supportbundlestest.NewFakeBundleService()) if err != nil { return nil, fmt.Errorf("%v: %w", "failed to get user service", err) } @@ -78,16 +92,7 @@ func initializeConflictResolver(cmd *utils.ContextCommandLine, f Formatter, ctx return &resolver, nil } -func getSqlStore(cfg *setting.Cfg, features featuremgmt.FeatureToggles) (*sqlstore.SQLStore, error) { - tracingCfg, err := tracing.ProvideTracingConfig(cfg) - if err != nil { - return nil, fmt.Errorf("%v: %w", "failed to initialize tracer config", err) - } - - tracer, err := tracing.ProvideService(tracingCfg) - if err != nil { - return nil, fmt.Errorf("%v: %w", "failed to initialize tracer service", err) - } +func getSqlStore(cfg *setting.Cfg, tracer tracing.Tracer, features featuremgmt.FeatureToggles) (*sqlstore.SQLStore, error) { bus := bus.ProvideBus(tracer) return sqlstore.ProvideService(cfg, features, &migrations.OSSMigrations{}, bus, tracer) } @@ -771,7 +776,7 @@ ORDER BY func notServiceAccount(ss *sqlstore.SQLStore) string { return fmt.Sprintf("is_service_account = %s", - ss.Dialect.BooleanStr(false)) + ss.GetDialect().BooleanStr(false)) } // confirm function asks for user input diff --git a/pkg/cmd/grafana-cli/commands/conflict_user_command_test.go b/pkg/cmd/grafana-cli/commands/conflict_user_command_test.go index 3d0f600dc88..c0aafacd3c0 100644 --- a/pkg/cmd/grafana-cli/commands/conflict_user_command_test.go +++ b/pkg/cmd/grafana-cli/commands/conflict_user_command_test.go @@ -12,6 +12,7 @@ import ( "github.com/urfave/cli/v2" "github.com/grafana/grafana/pkg/infra/db" + "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/services/accesscontrol/actest" "github.com/grafana/grafana/pkg/services/sqlstore" "github.com/grafana/grafana/pkg/services/sqlstore/migrator" @@ -109,9 +110,9 @@ func TestBuildConflictBlock(t *testing.T) { for _, tc := range testCases { t.Run(tc.desc, func(t *testing.T) { // Restore after destructive operation - sqlStore := db.InitTestDB(t) + sqlStore, cfg := db.InitTestDBWithCfg(t) if sqlStore.GetDialect().DriverName() != ignoredDatabase { - userStore := userimpl.ProvideStore(sqlStore, sqlStore.Cfg) + userStore := userimpl.ProvideStore(sqlStore, cfg) for _, u := range tc.users { u := user.User{ Email: u.Email, @@ -217,9 +218,9 @@ conflict: test2 for _, tc := range testCases { t.Run(tc.desc, func(t *testing.T) { // Restore after destructive operation - sqlStore := db.InitTestDB(t) + sqlStore, cfg := db.InitTestDBWithCfg(t) if sqlStore.GetDialect().DriverName() != ignoredDatabase { - userStore := userimpl.ProvideStore(sqlStore, sqlStore.Cfg) + userStore := userimpl.ProvideStore(sqlStore, cfg) for _, u := range tc.users { u := user.User{ Email: u.Email, @@ -398,9 +399,9 @@ func TestGetConflictingUsers(t *testing.T) { for _, tc := range testCases { t.Run(tc.desc, func(t *testing.T) { // Restore after destructive operation - sqlStore := db.InitTestDB(t) + sqlStore, cfg := db.InitTestDBWithCfg(t) if sqlStore.GetDialect().DriverName() != ignoredDatabase { - userStore := userimpl.ProvideStore(sqlStore, sqlStore.Cfg) + userStore := userimpl.ProvideStore(sqlStore, cfg) for _, u := range tc.users { u := user.User{ Email: u.Email, @@ -510,9 +511,9 @@ func TestGenerateConflictingUsersFile(t *testing.T) { for _, tc := range testCases { t.Run(tc.desc, func(t *testing.T) { // Restore after destructive operation - sqlStore := db.InitTestDB(t) + sqlStore, cfg := db.InitTestDBWithCfg(t) if sqlStore.GetDialect().DriverName() != ignoredDatabase { - userStore := userimpl.ProvideStore(sqlStore, sqlStore.Cfg) + userStore := userimpl.ProvideStore(sqlStore, cfg) for _, u := range tc.users { cmd := user.User{ Email: u.Email, @@ -580,7 +581,7 @@ func TestRunValidateConflictUserFile(t *testing.T) { rawSQL := fmt.Sprintf( "INSERT INTO %s (email, login, org_id, version, is_admin, created, updated) VALUES (?,?,?,0,%s,\"2024-03-18T15:25:32\",\"2024-03-18T15:25:32\")", sqlStore.Quote("user"), - sqlStore.Dialect.BooleanStr(false), + sqlStore.GetDialect().BooleanStr(false), ) result, err := sess.Exec(rawSQL, dupUserLogincmd.Email, dupUserLogincmd.Login, dupUserLogincmd.OrgID) if err != nil { @@ -636,9 +637,9 @@ func TestIntegrationMergeUser(t *testing.T) { t.Run("should be able to merge user", func(t *testing.T) { // Restore after destructive operation sqlStore := db.InitTestDB(t) - teamSvc, err := teamimpl.ProvideService(sqlStore, setting.NewCfg()) + teamSvc, err := teamimpl.ProvideService(sqlStore, setting.NewCfg(), tracing.InitializeTracerForTest()) require.NoError(t, err) - team1, err := teamSvc.CreateTeam("team1 name", "", 1) + team1, err := teamSvc.CreateTeam(context.Background(), "team1 name", "", 1) require.NoError(t, err) const testOrgID int64 = 1 @@ -660,7 +661,7 @@ func TestIntegrationMergeUser(t *testing.T) { rawSQL := fmt.Sprintf( "INSERT INTO %s (email, login, org_id, version, is_admin, created, updated) VALUES (?,?,?,0,%s,?,?)", sqlStore.Quote("user"), - sqlStore.Dialect.BooleanStr(false), + sqlStore.GetDialect().BooleanStr(false), ) result, err := sess.Exec(rawSQL, cmd.Email, cmd.Login, cmd.OrgID, cmd.Created, cmd.Updated) if err != nil { @@ -692,7 +693,7 @@ func TestIntegrationMergeUser(t *testing.T) { rawSQL := fmt.Sprintf( "INSERT INTO %s (email, login, org_id, version, is_admin, created, updated) VALUES (?,?,?,0,%s,?,?)", sqlStore.Quote("user"), - sqlStore.Dialect.BooleanStr(false), + sqlStore.GetDialect().BooleanStr(false), ) result, err := sess.Exec(rawSQL, cmd.Email, cmd.Login, cmd.OrgID, cmd.Created, cmd.Updated) if err != nil { @@ -705,14 +706,12 @@ func TestIntegrationMergeUser(t *testing.T) { return user.ErrUserNotFound } require.NoError(t, err) - return nil + // this is the user we want to update to another team + return teamimpl.AddOrUpdateTeamMemberHook(sess, 1, testOrgID, team1.ID, false, 0) }) if err != nil { t.Error(err) } - // this is the user we want to update to another team - err = teamSvc.AddTeamMember(context.Background(), 1, testOrgID, team1.ID, false, 0) - require.NoError(t, err) // get users conflictUsers, err := GetUsersWithConflictingEmailsOrLogins(&cli.Context{Context: context.Background()}, sqlStore) @@ -860,7 +859,7 @@ conflict: test2 rawSQL := fmt.Sprintf( "INSERT INTO %s (email, login, org_id, version, is_admin, created, updated) VALUES (?,?,?,0,%s,?,?)", sqlStore.Quote("user"), - sqlStore.Dialect.BooleanStr(false), + sqlStore.GetDialect().BooleanStr(false), ) result, err := sess.Exec(rawSQL, cmd.Email, cmd.Login, cmd.OrgID, cmd.Created, cmd.Updated) if err != nil { diff --git a/pkg/codegen/generators/go_generator.go b/pkg/codegen/generators/go_generator.go index a3744803dff..0f6d6b89bd0 100644 --- a/pkg/codegen/generators/go_generator.go +++ b/pkg/codegen/generators/go_generator.go @@ -12,7 +12,7 @@ import ( "cuelang.org/go/pkg/encoding/yaml" "github.com/dave/dst/decorator" "github.com/dave/dst/dstutil" - "github.com/deepmap/oapi-codegen/pkg/codegen" + "github.com/deepmap/oapi-codegen/v2/pkg/codegen" "github.com/getkin/kin-openapi/openapi3" "golang.org/x/tools/imports" ) @@ -182,8 +182,8 @@ import ( "strings" "time" - "github.com/deepmap/oapi-codegen/pkg/runtime" - openapi_types "github.com/deepmap/oapi-codegen/pkg/types" + "github.com/oapi-codegen/runtime" + openapi_types "github.com/oapi-codegen/runtime/types" "github.com/getkin/kin-openapi/openapi3" "github.com/go-chi/chi/v5" "github.com/labstack/echo/v4" diff --git a/pkg/components/loki/lokigrpc/client.go b/pkg/components/loki/lokigrpc/client.go index e8a0c11808c..921d14b2af5 100644 --- a/pkg/components/loki/lokigrpc/client.go +++ b/pkg/components/loki/lokigrpc/client.go @@ -5,7 +5,7 @@ import ( "crypto/tls" "errors" - grpcretry "github.com/grpc-ecosystem/go-grpc-middleware/retry" + grpcretry "github.com/grpc-ecosystem/go-grpc-middleware/v2/interceptors/retry" "google.golang.org/grpc" "google.golang.org/grpc/credentials" "google.golang.org/grpc/credentials/insecure" diff --git a/pkg/extensions/main.go b/pkg/extensions/main.go index 2a8e7d3005d..8c94c29ef29 100644 --- a/pkg/extensions/main.go +++ b/pkg/extensions/main.go @@ -18,7 +18,8 @@ import ( _ "github.com/grafana/dskit/backoff" _ "github.com/grafana/dskit/flagext" _ "github.com/grafana/gofpdf" - _ "github.com/grpc-ecosystem/go-grpc-middleware" + _ "github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus" + _ "github.com/grpc-ecosystem/go-grpc-middleware/v2" _ "github.com/hashicorp/go-multierror" _ "github.com/hashicorp/golang-lru/v2" _ "github.com/linkedin/goavro/v2" @@ -28,6 +29,7 @@ import ( _ "github.com/stretchr/testify/require" _ "github.com/vectordotdev/go-datemath" _ "golang.org/x/time/rate" + _ "xorm.io/builder" ) var IsEnterprise bool = false diff --git a/pkg/infra/db/db.go b/pkg/infra/db/db.go index 08cea4a761f..574354fa605 100644 --- a/pkg/infra/db/db.go +++ b/pkg/infra/db/db.go @@ -54,13 +54,16 @@ type Session = sqlstore.DBSession type InitTestDBOpt = sqlstore.InitTestDBOpt var SetupTestDB = sqlstore.SetupTestDB -var InitTestDB = sqlstore.InitTestDB var CleanupTestDB = sqlstore.CleanupTestDB var ProvideService = sqlstore.ProvideService -func InitTestDBwithCfg(t sqlutil.ITestDB, opts ...InitTestDBOpt) (*sqlstore.SQLStore, *setting.Cfg) { - store := InitTestDB(t, opts...) - return store, store.Cfg +func InitTestDB(t sqlutil.ITestDB, opts ...InitTestDBOpt) *sqlstore.SQLStore { + db, _ := InitTestDBWithCfg(t, opts...) + return db +} + +func InitTestDBWithCfg(t sqlutil.ITestDB, opts ...InitTestDBOpt) (*sqlstore.SQLStore, *setting.Cfg) { + return sqlstore.InitTestDB(t, opts...) } func IsTestDbSQLite() bool { diff --git a/pkg/infra/httpclient/httpclientprovider/http_client_provider.go b/pkg/infra/httpclient/httpclientprovider/http_client_provider.go index 5447999a1fd..13ef9674c1c 100644 --- a/pkg/infra/httpclient/httpclientprovider/http_client_provider.go +++ b/pkg/infra/httpclient/httpclientprovider/http_client_provider.go @@ -32,10 +32,6 @@ func New(cfg *setting.Cfg, validator validations.PluginRequestValidator, tracer RedirectLimitMiddleware(validator), } - if cfg.SigV4AuthEnabled { - middlewares = append(middlewares, awssdk.SigV4Middleware(cfg.SigV4VerboseLogging)) - } - if httpLoggingEnabled(cfg.PluginSettings) { middlewares = append(middlewares, HTTPLoggerMiddleware(cfg.PluginSettings)) } @@ -44,6 +40,11 @@ func New(cfg *setting.Cfg, validator validations.PluginRequestValidator, tracer middlewares = append(middlewares, GrafanaRequestIDHeaderMiddleware(cfg, logger)) } + // SigV4 signing should be performed after all headers are added + if cfg.SigV4AuthEnabled { + middlewares = append(middlewares, awssdk.SigV4Middleware(cfg.SigV4VerboseLogging)) + } + setDefaultTimeoutOptions(cfg) return newProviderFunc(sdkhttpclient.ProviderOptions{ diff --git a/pkg/infra/remotecache/remotecache_test.go b/pkg/infra/remotecache/remotecache_test.go index a51a836fa6c..b05d47fb3d7 100644 --- a/pkg/infra/remotecache/remotecache_test.go +++ b/pkg/infra/remotecache/remotecache_test.go @@ -12,6 +12,7 @@ import ( "github.com/grafana/grafana/pkg/infra/usagestats" "github.com/grafana/grafana/pkg/services/secrets" "github.com/grafana/grafana/pkg/services/secrets/fakes" + "github.com/grafana/grafana/pkg/services/sqlstore" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/tests/testsuite" ) @@ -33,15 +34,15 @@ func createTestClient(t *testing.T, opts *setting.RemoteCacheOptions, sqlstore d } func TestCachedBasedOnConfig(t *testing.T) { - cfg := setting.NewCfg() + db, cfg := sqlstore.InitTestDB(t) err := cfg.Load(setting.CommandLineArgs{ HomePath: "../../../", }) require.Nil(t, err, "Failed to load config") - client := createTestClient(t, cfg.RemoteCacheOptions, db.InitTestDB(t)) + client := createTestClient(t, cfg.RemoteCacheOptions, db) runTestsForClient(t, client) - runCountTestsForClient(t, cfg.RemoteCacheOptions, db.InitTestDB(t)) + runCountTestsForClient(t, cfg.RemoteCacheOptions, db) } func TestInvalidCacheTypeReturnsError(t *testing.T) { diff --git a/pkg/infra/usagestats/statscollector/concurrent_users_test.go b/pkg/infra/usagestats/statscollector/concurrent_users_test.go index 33ffcb3244a..decb1642657 100644 --- a/pkg/infra/usagestats/statscollector/concurrent_users_test.go +++ b/pkg/infra/usagestats/statscollector/concurrent_users_test.go @@ -24,7 +24,7 @@ func TestMain(m *testing.M) { } func TestConcurrentUsersMetrics(t *testing.T) { - sqlStore, cfg := db.InitTestDBwithCfg(t) + sqlStore, cfg := db.InitTestDBWithCfg(t) statsService := statsimpl.ProvideService(&setting.Cfg{}, sqlStore) s := createService(t, cfg, sqlStore, statsService) @@ -42,7 +42,7 @@ func TestConcurrentUsersMetrics(t *testing.T) { } func TestConcurrentUsersStats(t *testing.T) { - sqlStore, cfg := db.InitTestDBwithCfg(t) + sqlStore, cfg := db.InitTestDBWithCfg(t) statsService := statsimpl.ProvideService(&setting.Cfg{}, sqlStore) s := createService(t, cfg, sqlStore, statsService) diff --git a/pkg/login/social/socialimpl/service.go b/pkg/login/social/socialimpl/service.go index 3dc66ff819e..97dd10af931 100644 --- a/pkg/login/social/socialimpl/service.go +++ b/pkg/login/social/socialimpl/service.go @@ -8,6 +8,7 @@ import ( "net" "net/http" "os" + "slices" "strings" "time" @@ -58,6 +59,11 @@ func ProvideService(cfg *setting.Cfg, } for _, ssoSetting := range allSettings { + // ignore non-oauth2 providers + if !slices.Contains(ssosettings.AllOAuthProviders, ssoSetting.Provider) { + continue + } + info, err := connectors.CreateOAuthInfoFromKeyValues(ssoSetting.Settings) if err != nil { ss.log.Error("Failed to create OAuthInfo for provider", "error", err, "provider", ssoSetting.Provider) diff --git a/pkg/middleware/auth_test.go b/pkg/middleware/auth_test.go index bc45f07ac9c..c5fc39ae508 100644 --- a/pkg/middleware/auth_test.go +++ b/pkg/middleware/auth_test.go @@ -94,7 +94,7 @@ func TestAuth_Middleware(t *testing.T) { desc: "ReqSignedInNoAnonymous should return 200 for authenticated user", path: "/api/secure", authMiddleware: ReqSignedInNoAnonymous, - identity: &authn.Identity{ID: "user:1"}, + identity: &authn.Identity{ID: authn.MustParseNamespaceID("user:1")}, expecedReached: true, expectedCode: http.StatusOK, }, @@ -102,7 +102,7 @@ func TestAuth_Middleware(t *testing.T) { desc: "snapshot public mode disabled should return 200 for authenticated user", path: "/api/secure", authMiddleware: SnapshotPublicModeOrSignedIn(&setting.Cfg{SnapshotPublicMode: false}), - identity: &authn.Identity{ID: "user:1"}, + identity: &authn.Identity{ID: authn.MustParseNamespaceID("user:1")}, expecedReached: true, expectedCode: http.StatusOK, }, diff --git a/pkg/middleware/org_redirect.go b/pkg/middleware/org_redirect.go index 1199f1852cc..36a1081f415 100644 --- a/pkg/middleware/org_redirect.go +++ b/pkg/middleware/org_redirect.go @@ -32,8 +32,7 @@ func OrgRedirect(cfg *setting.Cfg, userSvc user.Service) web.Handler { return } - cmd := user.SetUsingOrgCommand{UserID: ctx.UserID, OrgID: orgId} - if err := userSvc.SetUsingOrg(ctx.Req.Context(), &cmd); err != nil { + if err := userSvc.Update(ctx.Req.Context(), &user.UpdateUserCommand{UserID: ctx.UserID, OrgID: &orgId}); err != nil { if ctx.IsApiRequest() { ctx.JsonApiErr(404, "Not found", nil) } else { diff --git a/pkg/middleware/org_redirect_test.go b/pkg/middleware/org_redirect_test.go index 23e1ebb412d..e2bc6d81075 100644 --- a/pkg/middleware/org_redirect_test.go +++ b/pkg/middleware/org_redirect_test.go @@ -55,7 +55,7 @@ func TestOrgRedirectMiddleware(t *testing.T) { middlewareScenario(t, "when setting an invalid org for user", func(t *testing.T, sc *scenarioContext) { sc.withIdentity(&authn.Identity{}) - sc.userService.ExpectedSetUsingOrgError = fmt.Errorf("") + sc.userService.ExpectedError = fmt.Errorf("") sc.m.Get("/", sc.defaultHandler) sc.fakeReq("GET", "/?orgId=1").exec() diff --git a/pkg/middleware/quota_test.go b/pkg/middleware/quota_test.go index 0cbc27da4e2..811871de525 100644 --- a/pkg/middleware/quota_test.go +++ b/pkg/middleware/quota_test.go @@ -52,7 +52,7 @@ func TestMiddlewareQuota(t *testing.T) { t.Run("with user logged in", func(t *testing.T) { setUp := func(sc *scenarioContext) { - sc.withIdentity(&authn.Identity{ID: "user:1", SessionToken: &auth.UserToken{UserId: 12}}) + sc.withIdentity(&authn.Identity{ID: authn.MustParseNamespaceID("user:1"), SessionToken: &auth.UserToken{UserId: 12}}) } middlewareScenario(t, "global datasource quota reached", func(t *testing.T, sc *scenarioContext) { diff --git a/pkg/plugins/backendplugin/grpcplugin/client.go b/pkg/plugins/backendplugin/grpcplugin/client.go index eacd1611e2b..5d352314e75 100644 --- a/pkg/plugins/backendplugin/grpcplugin/client.go +++ b/pkg/plugins/backendplugin/grpcplugin/client.go @@ -4,7 +4,6 @@ import ( "os/exec" "github.com/grafana/grafana-plugin-sdk-go/backend/grpcplugin" - grpc_opentracing "github.com/grpc-ecosystem/go-grpc-middleware/tracing/opentracing" goplugin "github.com/hashicorp/go-plugin" "go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc" "google.golang.org/grpc" @@ -54,12 +53,6 @@ func newClientConfig(executablePath string, args []string, env []string, skipHos Logger: logWrapper{Logger: logger}, AllowedProtocols: []goplugin.Protocol{goplugin.ProtocolGRPC}, GRPCDialOptions: []grpc.DialOption{ - grpc.WithChainUnaryInterceptor( - grpc_opentracing.UnaryClientInterceptor(), - ), - grpc.WithChainStreamInterceptor( - grpc_opentracing.StreamClientInterceptor(), - ), grpc.WithStatsHandler(otelgrpc.NewClientHandler()), }, } diff --git a/pkg/plugins/backendplugin/grpcplugin/log_wrapper.go b/pkg/plugins/backendplugin/grpcplugin/log_wrapper.go index 38052b9bc1d..25b975f0c58 100644 --- a/pkg/plugins/backendplugin/grpcplugin/log_wrapper.go +++ b/pkg/plugins/backendplugin/grpcplugin/log_wrapper.go @@ -149,6 +149,11 @@ func (lw logWrapper) ResetNamed(name string) hclog.Logger { } } +// No-op. The wrapped logger implementation cannot update the level on the fly. +func (lw logWrapper) GetLevel() hclog.Level { + return hclog.Trace +} + // No-op. The wrapped logger implementation cannot update the level on the fly. func (lw logWrapper) SetLevel(level hclog.Level) {} diff --git a/pkg/plugins/backendplugin/pluginextensionv2/rendererv2.pb.go b/pkg/plugins/backendplugin/pluginextensionv2/rendererv2.pb.go index 4f235a4f61d..ab2a9b8e673 100644 --- a/pkg/plugins/backendplugin/pluginextensionv2/rendererv2.pb.go +++ b/pkg/plugins/backendplugin/pluginextensionv2/rendererv2.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.32.0 -// protoc v4.25.2 +// protoc-gen-go v1.33.0 +// protoc v5.26.1 // source: rendererv2.proto package pluginextensionv2 diff --git a/pkg/plugins/backendplugin/pluginextensionv2/rendererv2_grpc.pb.go b/pkg/plugins/backendplugin/pluginextensionv2/rendererv2_grpc.pb.go index 7625f9655e2..5875a69e090 100644 --- a/pkg/plugins/backendplugin/pluginextensionv2/rendererv2_grpc.pb.go +++ b/pkg/plugins/backendplugin/pluginextensionv2/rendererv2_grpc.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go-grpc. DO NOT EDIT. // versions: // - protoc-gen-go-grpc v1.3.0 -// - protoc v4.25.2 +// - protoc v5.26.1 // source: rendererv2.proto package pluginextensionv2 diff --git a/pkg/plugins/backendplugin/pluginextensionv2/sanitizer.pb.go b/pkg/plugins/backendplugin/pluginextensionv2/sanitizer.pb.go index a420647a687..b464bbdc65c 100644 --- a/pkg/plugins/backendplugin/pluginextensionv2/sanitizer.pb.go +++ b/pkg/plugins/backendplugin/pluginextensionv2/sanitizer.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.32.0 -// protoc v4.25.2 +// protoc-gen-go v1.33.0 +// protoc v5.26.1 // source: sanitizer.proto package pluginextensionv2 diff --git a/pkg/plugins/backendplugin/pluginextensionv2/sanitizer_grpc.pb.go b/pkg/plugins/backendplugin/pluginextensionv2/sanitizer_grpc.pb.go index 80a3f117df3..a56ffbffc82 100644 --- a/pkg/plugins/backendplugin/pluginextensionv2/sanitizer_grpc.pb.go +++ b/pkg/plugins/backendplugin/pluginextensionv2/sanitizer_grpc.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go-grpc. DO NOT EDIT. // versions: // - protoc-gen-go-grpc v1.3.0 -// - protoc v4.25.2 +// - protoc v5.26.1 // source: sanitizer.proto package pluginextensionv2 diff --git a/pkg/plugins/backendplugin/secretsmanagerplugin/secretsmanager.pb.go b/pkg/plugins/backendplugin/secretsmanagerplugin/secretsmanager.pb.go index 777de3c42f2..09afbab3654 100644 --- a/pkg/plugins/backendplugin/secretsmanagerplugin/secretsmanager.pb.go +++ b/pkg/plugins/backendplugin/secretsmanagerplugin/secretsmanager.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.32.0 -// protoc v4.25.2 +// protoc-gen-go v1.33.0 +// protoc v5.26.1 // source: secretsmanager.proto package secretsmanagerplugin diff --git a/pkg/plugins/backendplugin/secretsmanagerplugin/secretsmanager_grpc.pb.go b/pkg/plugins/backendplugin/secretsmanagerplugin/secretsmanager_grpc.pb.go index 264020b216e..945d6fa9f7f 100644 --- a/pkg/plugins/backendplugin/secretsmanagerplugin/secretsmanager_grpc.pb.go +++ b/pkg/plugins/backendplugin/secretsmanagerplugin/secretsmanager_grpc.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go-grpc. DO NOT EDIT. // versions: // - protoc-gen-go-grpc v1.3.0 -// - protoc v4.25.2 +// - protoc v5.26.1 // source: secretsmanager.proto package secretsmanagerplugin diff --git a/pkg/plugins/config/config.go b/pkg/plugins/config/config.go index 9fcf3bba71a..a8081f728a7 100644 --- a/pkg/plugins/config/config.go +++ b/pkg/plugins/config/config.go @@ -18,7 +18,7 @@ type PluginManagementCfg struct { PluginsCDNURLTemplate string - GrafanaComURL string + GrafanaComAPIURL string GrafanaAppURL string @@ -37,7 +37,7 @@ type Features struct { // NewPluginManagementCfg returns a new PluginManagementCfg. func NewPluginManagementCfg(devMode bool, pluginsPath string, pluginSettings setting.PluginSettings, pluginsAllowUnsigned []string, pluginsCDNURLTemplate string, appURL string, features Features, angularSupportEnabled bool, - grafanaComURL string, disablePlugins []string, hideAngularDeprecation []string, forwardHostEnvVars []string, + grafanaComAPIURL string, disablePlugins []string, hideAngularDeprecation []string, forwardHostEnvVars []string, ) *PluginManagementCfg { return &PluginManagementCfg{ PluginsPath: pluginsPath, @@ -46,7 +46,7 @@ func NewPluginManagementCfg(devMode bool, pluginsPath string, pluginSettings set PluginsAllowUnsigned: pluginsAllowUnsigned, DisablePlugins: disablePlugins, PluginsCDNURLTemplate: pluginsCDNURLTemplate, - GrafanaComURL: grafanaComURL, + GrafanaComAPIURL: grafanaComAPIURL, GrafanaAppURL: appURL, Features: features, AngularSupportEnabled: angularSupportEnabled, diff --git a/pkg/plugins/manager/signature/manifest.go b/pkg/plugins/manager/signature/manifest.go index f2c64917f31..4bb035f011e 100644 --- a/pkg/plugins/manager/signature/manifest.go +++ b/pkg/plugins/manager/signature/manifest.go @@ -169,6 +169,7 @@ func (s *Signature) Calculate(ctx context.Context, src plugins.PluginSource, plu // Make sure the versions all match if manifest.Plugin != plugin.JSONData.ID || manifest.Version != plugin.JSONData.Info.Version { + s.log.Debug("Plugin signature invalid because ID or Version mismatch", "pluginId", plugin.JSONData.ID, "manifestPluginId", manifest.Plugin, "pluginVersion", plugin.JSONData.Info.Version, "manifestPluginVersion", manifest.Version) return plugins.Signature{ Status: plugins.SignatureStatusModified, }, nil @@ -194,6 +195,7 @@ func (s *Signature) Calculate(ctx context.Context, src plugins.PluginSource, plu for p, hash := range manifest.Files { err = verifyHash(s.log, plugin, p, hash) if err != nil { + s.log.Debug("Plugin signature invalid", "pluginId", plugin.JSONData.ID, "error", err) return plugins.Signature{ Status: plugins.SignatureStatusModified, }, nil @@ -259,7 +261,7 @@ func verifyHash(mlog log.Logger, plugin plugins.FoundPlugin, path, hash string) h := sha256.New() if _, err := io.Copy(h, f); err != nil { - return errors.New("could not calculate plugin file checksum") + return fmt.Errorf("could not calculate plugin file checksum. Path: %s. Error: %w", path, err) } sum := hex.EncodeToString(h.Sum(nil)) if sum != hash { diff --git a/pkg/plugins/plugins.go b/pkg/plugins/plugins.go index 1a045651277..3c281ee86bf 100644 --- a/pkg/plugins/plugins.go +++ b/pkg/plugins/plugins.go @@ -204,10 +204,6 @@ type Route struct { Body json.RawMessage `json:"body"` } -func (r *Route) RequiresRBACAction() bool { - return r.ReqAction != "" -} - // Header describes an HTTP header that is forwarded with // the proxied request for a plugin route type Header struct { diff --git a/pkg/plugins/repo/service.go b/pkg/plugins/repo/service.go index 704411e2bb4..dea206ef5af 100644 --- a/pkg/plugins/repo/service.go +++ b/pkg/plugins/repo/service.go @@ -22,7 +22,7 @@ type Manager struct { } func ProvideService(cfg *config.PluginManagementCfg) (*Manager, error) { - baseURL, err := url.JoinPath(cfg.GrafanaComURL, "/api/plugins") + baseURL, err := url.JoinPath(cfg.GrafanaComAPIURL, "/plugins") if err != nil { return nil, err } diff --git a/pkg/promlib/go.mod b/pkg/promlib/go.mod index 012c44cb4c1..86bbf410526 100644 --- a/pkg/promlib/go.mod +++ b/pkg/promlib/go.mod @@ -3,15 +3,15 @@ module github.com/grafana/grafana/pkg/promlib go 1.21.0 require ( - github.com/grafana/grafana-plugin-sdk-go v0.224.0 + github.com/grafana/grafana-plugin-sdk-go v0.228.0 github.com/json-iterator/go v1.1.12 github.com/patrickmn/go-cache v2.1.0+incompatible - github.com/prometheus/client_golang v1.18.0 - github.com/prometheus/common v0.46.0 + github.com/prometheus/client_golang v1.19.0 + github.com/prometheus/common v0.53.0 github.com/prometheus/prometheus v1.8.2-0.20221021121301-51a44e6657c3 github.com/stretchr/testify v1.9.0 - go.opentelemetry.io/otel v1.24.0 - go.opentelemetry.io/otel/trace v1.24.0 + go.opentelemetry.io/otel v1.26.0 + go.opentelemetry.io/otel/trace v1.26.0 golang.org/x/exp v0.0.0-20231206192017-f3f8817b8deb ) @@ -20,22 +20,22 @@ require ( github.com/alecthomas/units v0.0.0-20231202071711-9a357b53e9c9 // indirect github.com/apache/arrow/go/v15 v15.0.2 // indirect github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect - github.com/aws/aws-sdk-go v1.50.8 // indirect + github.com/aws/aws-sdk-go v1.50.29 // indirect github.com/bahlo/generic-list-go v0.2.0 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/buger/jsonparser v1.1.1 // indirect - github.com/cenkalti/backoff/v4 v4.2.1 // indirect - github.com/cespare/xxhash/v2 v2.2.0 // indirect + github.com/cenkalti/backoff/v4 v4.3.0 // 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-20230802225258-3cf4e6d46a89 // indirect - github.com/cpuguy83/go-md2man/v2 v2.0.3 // indirect + github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/dennwc/varint v1.0.0 // indirect github.com/elazarl/goproxy v0.0.0-20230731152917-f99041a5c027 // indirect github.com/emicklei/go-restful/v3 v3.11.0 // indirect github.com/fatih/color v1.15.0 // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect - github.com/getkin/kin-openapi v0.120.0 // indirect + github.com/getkin/kin-openapi v0.124.0 // indirect github.com/go-kit/log v0.2.1 // indirect github.com/go-logfmt/logfmt v0.6.0 // indirect github.com/go-logr/logr v1.4.1 // indirect @@ -53,9 +53,9 @@ require ( github.com/google/uuid v1.6.0 // indirect github.com/gorilla/mux v1.8.1 // indirect github.com/grafana/regexp v0.0.0-20221123153739-15dc172cd2db // 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 - github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0 // indirect + github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.0.1 // indirect + github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.1.0 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.1 // indirect github.com/hashicorp/go-hclog v1.6.3 // indirect github.com/hashicorp/go-plugin v1.6.0 // indirect github.com/hashicorp/yamux v0.1.1 // indirect @@ -68,7 +68,7 @@ require ( github.com/mailru/easyjson v0.7.7 // indirect github.com/mattetti/filebuffer v1.0.1 // indirect github.com/mattn/go-colorable v0.1.13 // indirect - github.com/mattn/go-isatty v0.0.19 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-runewidth v0.0.15 // indirect github.com/mitchellh/go-testing-interface v1.14.1 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect @@ -80,8 +80,8 @@ require ( github.com/pierrec/lz4/v4 v4.1.18 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect - github.com/prometheus/client_model v0.5.0 // indirect - github.com/prometheus/procfs v0.12.0 // indirect + github.com/prometheus/client_model v0.6.1 // indirect + github.com/prometheus/procfs v0.14.0 // indirect github.com/rivo/uniseg v0.3.4 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/smartystreets/goconvey v1.6.4 // indirect @@ -89,33 +89,33 @@ require ( github.com/unknwon/bra v0.0.0-20200517080246-1e3013ecaff8 // indirect github.com/unknwon/com v1.0.1 // indirect github.com/unknwon/log v0.0.0-20150304194804-e617c87089d3 // indirect - github.com/urfave/cli v1.22.14 // indirect + github.com/urfave/cli v1.22.15 // indirect github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect github.com/zeebo/xxh3 v1.0.2 // indirect - go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0 // indirect - go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.49.0 // indirect - go.opentelemetry.io/contrib/propagators/jaeger v1.22.0 // indirect - go.opentelemetry.io/contrib/samplers/jaegerremote v0.18.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.24.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.24.0 // indirect - go.opentelemetry.io/otel/metric v1.24.0 // indirect - go.opentelemetry.io/otel/sdk v1.24.0 // indirect - go.opentelemetry.io/proto/otlp v1.1.0 // indirect + go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.51.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.51.0 // indirect + go.opentelemetry.io/contrib/propagators/jaeger v1.26.0 // indirect + go.opentelemetry.io/contrib/samplers/jaegerremote v0.20.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.26.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.26.0 // indirect + go.opentelemetry.io/otel/metric v1.26.0 // indirect + go.opentelemetry.io/otel/sdk v1.26.0 // indirect + go.opentelemetry.io/proto/otlp v1.2.0 // indirect go.uber.org/atomic v1.11.0 // indirect go.uber.org/goleak v1.3.0 // indirect - golang.org/x/mod v0.14.0 // indirect - golang.org/x/net v0.22.0 // indirect - golang.org/x/sys v0.18.0 // indirect + golang.org/x/mod v0.15.0 // indirect + golang.org/x/net v0.24.0 // indirect + golang.org/x/oauth2 v0.19.0 // indirect + golang.org/x/sys v0.19.0 // indirect golang.org/x/text v0.14.0 // indirect - golang.org/x/tools v0.17.0 // indirect + golang.org/x/tools v0.18.0 // indirect golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect - google.golang.org/genproto v0.0.0-20240227224415-6ceb2ff114de // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20240227224415-6ceb2ff114de // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20240227224415-6ceb2ff114de // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20240415180920-8c6c420018be // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240415180920-8c6c420018be // indirect google.golang.org/grpc v1.63.2 // indirect google.golang.org/protobuf v1.33.0 // indirect gopkg.in/fsnotify/fsnotify.v1 v1.4.7 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - k8s.io/kube-openapi v0.0.0-20240220201932-37d671a357a5 // indirect + k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 // indirect k8s.io/utils v0.0.0-20230726121419-3b25d923346b // indirect ) diff --git a/pkg/promlib/go.sum b/pkg/promlib/go.sum index 717ed270802..65973bdb9a4 100644 --- a/pkg/promlib/go.sum +++ b/pkg/promlib/go.sum @@ -1,4 +1,3 @@ -cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 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= @@ -8,33 +7,23 @@ github.com/apache/arrow/go/v15 v15.0.2 h1:60IliRbiyTWCWjERBCkO1W4Qun9svcYoZrSLcy github.com/apache/arrow/go/v15 v15.0.2/go.mod h1:DGXsR3ajT524njufqf95822i+KTh+yea1jass9YXgjA= github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so= github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= -github.com/aws/aws-sdk-go v1.50.8 h1:gY0WoOW+/Wz6XmYSgDH9ge3wnAevYDSQWPxxJvqAkP4= -github.com/aws/aws-sdk-go v1.50.8/go.mod h1:LF8svs817+Nz+DmiMQKTO3ubZ/6IaTpq3TjupRn3Eqk= +github.com/aws/aws-sdk-go v1.50.29 h1:Ol2FYzesF2tsQrgVSnDWRFI60+FsSqKKdt7MLlZKubc= github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk= github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg= -github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= -github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/bufbuild/protocompile v0.4.0 h1:LbFKd2XowZvQ/kajzguUp2DC9UEIQhIq77fZZlaQsNA= github.com/bufbuild/protocompile v0.4.0/go.mod h1:3v93+mbWn/v3xzN+31nwkJfrEpAUwp+BagBSZWx+TP8= github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs= github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= -github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM= -github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= -github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= -github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= -github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= 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 h1:aPflPkRFkVwbW6dmcVqfgwp1i+UWGFH6VgR1Jim5Ygc= github.com/chromedp/cdproto v0.0.0-20230802225258-3cf4e6d46a89/go.mod h1:GKljq0VrfU4D5yc+2qA6OVr8pmO/MBbPEWqWQ/oqGEs= -github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= -github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= -github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= -github.com/cpuguy83/go-md2man/v2 v2.0.3 h1:qMCsGGgs+MAzDFyp9LpAe1Lqy/fY/qCovCm0qnXZOBM= -github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/cpuguy83/go-md2man/v2 v2.0.4 h1:wfIWP927BUkWJb2NmU/kNDYIBTh/ziUX91+lVfRxZq4= 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= @@ -48,10 +37,6 @@ github.com/elazarl/goproxy/ext v0.0.0-20220115173737-adb46da277ac h1:9yrT5tmn9Zc github.com/elazarl/goproxy/ext v0.0.0-20220115173737-adb46da277ac/go.mod h1:gNh8nYJoAm43RfaxurUnxr+N1PwuFV3ZMl/efxlIlY8= github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= -github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= -github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs= github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw= @@ -60,12 +45,9 @@ github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSw github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= -github.com/getkin/kin-openapi v0.120.0 h1:MqJcNJFrMDFNc07iwE8iFC5eT2k/NPUFDIpNeiZv8Jg= -github.com/getkin/kin-openapi v0.120.0/go.mod h1:PCWw/lfBrJY4HcdqE3jj+QFkaFK8ABoqo7PvqVhXXqw= -github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= +github.com/getkin/kin-openapi v0.124.0 h1:VSFNMB9C9rTKBnQ/fpyDU8ytMTr4dWI9QovSKj9kz/M= github.com/go-kit/log v0.2.1 h1:MRVx0/zhvdseW+Gza6N9rVzU/IVzaeE1SFI4raAhmBU= github.com/go-kit/log v0.2.1/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0= -github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4= github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= @@ -79,18 +61,12 @@ github.com/go-openapi/jsonreference v0.20.4 h1:bKlDxQxQJgwpUSgOENiMPzCTBVuc7vTdX github.com/go-openapi/jsonreference v0.20.4/go.mod h1:5pZJyJP2MnYCpoeoMAql78cCHauHj0V9Lhc506VOpw4= github.com/go-openapi/swag v0.22.9 h1:XX2DssF+mQKM2DHsbgZK74y/zj4mo9I99+89xUmuZCE= github.com/go-openapi/swag v0.22.9/go.mod h1:3/OXnFfnMAwBD099SwYRk7GD3xOrr1iL7d/XNLXVVwE= -github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM= github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= -github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= @@ -98,7 +74,6 @@ github.com/google/flatbuffers v23.5.26+incompatible h1:M9dgRyhJemaM4Sw8+66GHBu8i github.com/google/flatbuffers v23.5.26+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= 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.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 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= @@ -111,15 +86,12 @@ github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e h1:JKmoR8x90Iww1 github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= -github.com/grafana/grafana-plugin-sdk-go v0.224.0 h1:WBpKJEhzEcGBAmmNB/OGGF/pHrt654O3m7idjnpSc40= +github.com/grafana/grafana-plugin-sdk-go v0.228.0 h1:LlPqyB+RZTtDy8RVYD7iQVJW5A0gMoGSI/+Ykz8HebQ= github.com/grafana/regexp v0.0.0-20221123153739-15dc172cd2db h1:7aN5cccjIqCLTzedH7MZzRZt5/lsAHch6Z3L2ZGn5FA= github.com/grafana/regexp v0.0.0-20221123153739-15dc172cd2db/go.mod h1:M5qHK+eWfAv8VR/265dIuEpL3fNfeC21tXXp9itM24A= -github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 h1:UH//fgunKIs4JdUbpDl1VZCDaL56wXCB/5+wF6uHfaI= -github.com/grpc-ecosystem/go-grpc-middleware v1.4.0/go.mod h1:g5qyo/la0ALbONm6Vbp88Yd8NsDy6rZz+RcrMPxvld8= -github.com/grpc-ecosystem/go-grpc-prometheus v1.2.1-0.20191002090509-6af20e3a5340 h1:uGoIog/wiQHI9GAxXO5TJbT0wWKH3O9HhOJW1F9c3fY= -github.com/grpc-ecosystem/go-grpc-prometheus v1.2.1-0.20191002090509-6af20e3a5340/go.mod h1:3bDW6wMZJB7tiONtC/1Xpicra6Wp5GgbTbQWCbI5fkc= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0 h1:Wqo399gCIufwto+VfwCSvsnfGpF/w5E9CNxSwbpD6No= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0/go.mod h1:qmOFXW2epJhM0qSnUUYpldc7gVz2KMQwJ/QYCDIa7XU= +github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.0.1 h1:qnpSQwGEnkcRpTqNOIR6bJbR0gAorgP9CSALpRcKoAA= +github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.1.0 h1:pRhl55Yx1eC7BZ1N+BBWwnKaMyD8uC+34TLdndZMAKk= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.1 h1:/c3QmbOGMGTOumP2iT/rCwB7b0QDGLKzqOmktBjT+Is= github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k= github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= github.com/hashicorp/go-plugin v1.6.0 h1:wgd4KxHJTVGGqWBq4QPB1i5BZNEx9BR8+OFmHDmTk8A= @@ -149,12 +121,8 @@ github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM= github.com/klauspost/cpuid/v2 v2.2.5 h1:0E5MSMDEoAulmXNFquVs//DdoomxaoTY1kUhbc/qbZg= github.com/klauspost/cpuid/v2 v2.2.5/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= -github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/magefile/mage v1.15.0 h1:BvGheCMAsG3bWUDbZ8AyXXpCNwU9u5CB6sM+HNb9HYg= @@ -170,12 +138,10 @@ github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovk github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= -github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= -github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 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/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/mitchellh/go-testing-interface v1.14.1 h1:jrgshOhYAUVNMAJiKbEu7EqAwgJJ2JqpQmpLJOu07cU= github.com/mitchellh/go-testing-interface v1.14.1/go.mod h1:gfgS7OtZj6MA4U1UrDRp04twqAjfvlZyCfX3sDjEym8= github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= @@ -195,48 +161,34 @@ github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4= github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= -github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc= github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX50IvK2s= github.com/perimeterx/marshmallow v1.1.5/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw= github.com/pierrec/lz4/v4 v4.1.18 h1:xaKrnTkyoqfh1YItXl56+6KJNVYWlEEPuAQW9xsplYQ= github.com/pierrec/lz4/v4 v4.1.18/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= -github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 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/prometheus/client_golang v0.9.2/go.mod h1:OsXs2jCmiKlQ1lTBmv21f2mNfw4xf/QclQDMrYNZzcM= -github.com/prometheus/client_golang v1.18.0 h1:HzFfmkOzH5Q8L8G+kSJKUx5dtG87sewO+FoDDqP5Tbk= -github.com/prometheus/client_golang v1.18.0/go.mod h1:T+GXkCk5wSJyOqMIzVgvvjFDlkOQntgjkJWKrN5txjA= -github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= -github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw= -github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI= -github.com/prometheus/common v0.0.0-20181126121408-4724e9255275/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= -github.com/prometheus/common v0.46.0 h1:doXzt5ybi1HBKpsZOL0sSkaNHJJqkyfEWZGGqqScV0Y= -github.com/prometheus/common v0.46.0/go.mod h1:Tp0qkxpb9Jsg54QMe+EAmqXkSV7Evdy1BTn+g2pa/hQ= +github.com/prometheus/client_golang v1.19.0 h1:ygXvpU1AoN1MhdzckN+PyD9QJOSD4x7kmXYlnfbA6JU= +github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= +github.com/prometheus/common v0.53.0 h1:U2pL9w9nmJwJDa4qqLQ3ZaePJ6ZTwt7cMD3AG3+aLCE= github.com/prometheus/common/sigv4 v0.1.0 h1:qoVebwtwwEhS85Czm2dSROY5fTo2PAPEVdDeppTwGX4= github.com/prometheus/common/sigv4 v0.1.0/go.mod h1:2Jkxxk9yYvCkE5G1sQT7GuEXm57JrvHu9k5YwTjsNtI= -github.com/prometheus/procfs v0.0.0-20181204211112-1dc9a6cbc91a/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= -github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo= -github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= +github.com/prometheus/procfs v0.14.0 h1:Lw4VdGGoKEZilJsayHf0B+9YgLGREba2C6xr+Fdfq6s= github.com/prometheus/prometheus v1.8.2-0.20221021121301-51a44e6657c3 h1:etRZv4bJf9YAuyPWbyFufjkijfeoPSmyA5xNcd4DoyI= github.com/prometheus/prometheus v1.8.2-0.20221021121301-51a44e6657c3/go.mod h1:plwr4+63Q1xL8oIdBDeU854um7Cct0Av8dhP44lutMw= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.3.4 h1:3Z3Eu6FGHZWSfNKJTOUiPatWwfc7DzJRU04jFUqJODw= github.com/rivo/uniseg v0.3.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rogpeppe/go-charset v0.0.0-20180617210344-2471d30d28b4/go.mod h1:qgYeAmZ5ZIpBWTGllZSQnw97Dj+woV0toclVaRGI8pc= -github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= -github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= +github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= -github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/assertions v0.0.0-20190116191733-b6c0e53d7304 h1:Jpy1PXuP99tXNrhbq2BaPz9B+jNAvH1JPQQpG/9GCXY= github.com/smartystreets/assertions v0.0.0-20190116191733-b6c0e53d7304/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= @@ -244,13 +196,10 @@ github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c/go.mod h1:X github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s= github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= -github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= @@ -266,8 +215,7 @@ github.com/unknwon/com v1.0.1/go.mod h1:tOOxU81rwgoCLoOVVPHb6T/wt8HZygqH5id+GNnl github.com/unknwon/log v0.0.0-20150304194804-e617c87089d3 h1:4EYQaWAatQokdji3zqZloVIW/Ke1RQjYw2zHULyrHJg= github.com/unknwon/log v0.0.0-20150304194804-e617c87089d3/go.mod h1:1xEUf2abjfP92w2GZTV+GgaRxXErwRXcClbUwrNJffU= github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= -github.com/urfave/cli v1.22.14 h1:ebbhrRiGK2i4naQJr+1Xj92HXZCrK7MsyTS/ob3HnAk= -github.com/urfave/cli v1.22.14/go.mod h1:X0eDS6pD6Exaclxm99NJ3FiCDRED7vIHpx2mDOHLvkA= +github.com/urfave/cli v1.22.15 h1:nuqt+pdC/KqswQKhETJjo7pvn/k4xMUxgW6liI7XpnM= github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc= github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= @@ -276,107 +224,64 @@ github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ= github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0= github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0= github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0 h1:4Pp6oUg3+e/6M4C0A/3kJ2VYa++dsWVTtGgLVj5xtHg= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0/go.mod h1:Mjt1i1INqiaoZOMGR1RIUJN+i3ChKoFRqzrRQhlkbs0= -go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.49.0 h1:RtcvQ4iw3w9NBB5yRwgA4sSa82rfId7n4atVpvKx3bY= -go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.49.0/go.mod h1:f/PbKbRd4cdUICWell6DmzvVJ7QrmBgFrRHjXmAXbK4= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 h1:jq9TW8u3so/bN+JPT166wjOI6/vQPF6Xe7nMNIltagk= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0/go.mod h1:p8pYQP+m5XfbZm9fxtSKAbM6oIllS7s2AfxrChvc7iw= -go.opentelemetry.io/contrib/propagators/jaeger v1.22.0 h1:bAHX+zN/inu+Rbqk51REmC8oXLl+Dw6pp9ldQf/onaY= -go.opentelemetry.io/contrib/propagators/jaeger v1.22.0/go.mod h1:bH9GkgkN21mscXcQP6lQJYI8XnEPDxlTN/ZOBuHDjqE= -go.opentelemetry.io/contrib/samplers/jaegerremote v0.18.0 h1:Q9PrD94WoMolBx44ef5UWWvufpVSME0MiSymXZfedso= -go.opentelemetry.io/contrib/samplers/jaegerremote v0.18.0/go.mod h1:tjp49JHNvreAAoWjdCHIVD7NXMjuJ3Dp/9iNOuPPlC8= -go.opentelemetry.io/otel v1.24.0 h1:0LAOdjNmQeSTzGBzduGe/rU4tZhMwL5rWgtp9Ku5Jfo= -go.opentelemetry.io/otel v1.24.0/go.mod h1:W7b9Ozg4nkF5tWI5zsXkaKKDjdVjpD4oAt9Qi/MArHo= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.24.0 h1:t6wl9SPayj+c7lEIFgm4ooDBZVb01IhLB4InpomhRw8= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.24.0/go.mod h1:iSDOcsnSA5INXzZtwaBPrKp/lWu/V14Dd+llD0oI2EA= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.24.0 h1:Mw5xcxMwlqoJd97vwPxA8isEaIoxsta9/Q51+TTJLGE= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.24.0/go.mod h1:CQNu9bj7o7mC6U7+CA/schKEYakYXWr79ucDHTMGhCM= -go.opentelemetry.io/otel/metric v1.24.0 h1:6EhoGWWK28x1fbpA4tYTOWBkPefTDQnb8WSGXlc88kI= -go.opentelemetry.io/otel/metric v1.24.0/go.mod h1:VYhLe1rFfxuTXLgj4CBiyz+9WYBA8pNGJgDcSFRKBco= -go.opentelemetry.io/otel/sdk v1.24.0 h1:YMPPDNymmQN3ZgczicBY3B6sf9n62Dlj9pWD3ucgoDw= -go.opentelemetry.io/otel/sdk v1.24.0/go.mod h1:KVrIYw6tEubO9E96HQpcmpTKDVn9gdv35HoYiQWGDFg= -go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y1YELI= -go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU= -go.opentelemetry.io/proto/otlp v1.1.0 h1:2Di21piLrCqJ3U3eXGCTPHE9R8Nh+0uglSnOyxikMeI= -go.opentelemetry.io/proto/otlp v1.1.0/go.mod h1:GpBHCBWiqvVLDqmHZsoMM3C5ySeKTC7ej/RNTae6MdY= -go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.51.0 h1:A3SayB3rNyt+1S6qpI9mHPkeHTZbD7XILEqWnYZb2l0= +go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.51.0 h1:974XTyIwHI4nHa1+uSLxHtUnlJ2DiVtAJjk7fd07p/8= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.51.0 h1:Xs2Ncz0gNihqu9iosIZ5SkBbWo5T8JhhLJFMQL1qmLI= +go.opentelemetry.io/contrib/propagators/jaeger v1.26.0 h1:RH76Cl2pfOLLoCtxAPax9c7oYzuL1tiI7/ZPJEmEmOw= +go.opentelemetry.io/contrib/samplers/jaegerremote v0.20.0 h1:ja+d7Aea/9PgGxB63+E0jtRFpma717wubS0KFkZpmYw= +go.opentelemetry.io/otel v1.26.0 h1:LQwgL5s/1W7YiiRwxf03QGnWLb2HW4pLiAhaA5cZXBs= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.26.0 h1:1u/AyyOqAWzy+SkPxDpahCNZParHV8Vid1RnI2clyDE= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.26.0 h1:Waw9Wfpo/IXzOI8bCB7DIk+0JZcqqsyn1JFnAc+iam8= +go.opentelemetry.io/otel/metric v1.26.0 h1:7S39CLuY5Jgg9CrnA9HHiEjGMF/X2VHvoXGgSllRz30= +go.opentelemetry.io/otel/sdk v1.26.0 h1:Y7bumHf5tAiDlRYFmGqetNcLaVUZmh4iYfmGxtmz7F8= +go.opentelemetry.io/otel/trace v1.26.0 h1:1ieeAUb4y0TE26jUFrCIXKpTuVK7uJGN9/Z/2LP5sQA= +go.opentelemetry.io/proto/otlp v1.2.0 h1:pVeZGk7nXDC9O2hncA6nHldxEjm6LByfA2aN8IOkz94= go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= -go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= -go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= -go.uber.org/zap v1.18.1/go.mod h1:xg/QME4nWcxGxrpdeYfq7UvYrLh66cuVKdrbD1XF/NI= 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/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20231206192017-f3f8817b8deb h1:c0vyKkb6yr3KR7jEfJaOSv4lG7xPkbN6r52aJz1d8a8= golang.org/x/exp v0.0.0-20231206192017-f3f8817b8deb/go.mod h1:iRJReGqOEeBhDZGkGbynYwcHlctCvnjTYIamk7uXpHI= -golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= -golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= -golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 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/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0= -golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= -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= -golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/mod v0.15.0 h1:SernR4v+D55NyBH2QiEQrlBAnj1ECL6AGrA5+dPaMY8= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 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.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc= -golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= -golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.18.0 h1:09qnuIAgzdx1XplqJvW6CQqMCtGZykZWcXzPMPUusvI= -golang.org/x/oauth2 v0.18.0/go.mod h1:Wf7knwG0MPoWIMMBgFlEaSUDaKskp0dCfrlJRJXbBi8= -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/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w= +golang.org/x/oauth2 v0.19.0 h1:9+E/EZBCbTLNrbN35fHv/a/d/mOBatymz1zbtQrXpIg= 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/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ= -golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= 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-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191020152052-9984515f0562/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= -golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= 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.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= -golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20191108193012-7d206e10da11/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 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/tools v0.17.0 h1:FvmRgNOcs3kOa+T20R1uhfP9F6HgG2mfxDv1vrx1Htc= -golang.org/x/tools v0.17.0/go.mod h1:xsh6VxdV005rRVaS6SSAf9oiAqljS7UZUacMZ8Bnsps= +golang.org/x/tools v0.18.0 h1:k8NLag8AGHnn+PHbl7g43CtqZAwG60vZkLqgyZgIHgQ= 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= @@ -385,45 +290,24 @@ golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 h1:+cNy6SZtPcJQH3LJVLOSm golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= gonum.org/v1/gonum v0.12.0 h1:xKuo6hzt+gMav00meVPUlXwSdoEJP46BR+wdxQEFK2o= gonum.org/v1/gonum v0.12.0/go.mod h1:73TDxJfAAHeA8Mk9mf8NlIppyhQNo5GLTcYeqgo2lvY= -google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= -google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM= -google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds= -google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20200423170343-7949de9c1215/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20240227224415-6ceb2ff114de h1:F6qOa9AZTYJXOUEr4jDysRDLrm4PHePlge4v4TGAlxY= -google.golang.org/genproto/googleapis/api v0.0.0-20240227224415-6ceb2ff114de h1:jFNzHPIeuzhdRwVhbZdiym9q0ory/xY3sA+v2wPg8I0= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240227224415-6ceb2ff114de h1:cZGRis4/ot9uVm639a+rHCUaG0JJHEsdyzSQTMX+suY= -google.golang.org/grpc v1.18.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= -google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= -google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= -google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= -google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= +google.golang.org/genproto/googleapis/api v0.0.0-20240415180920-8c6c420018be h1:Zz7rLWqp0ApfsR/l7+zSHhY3PMiH2xqgxlfYfAfNpoU= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240415180920-8c6c420018be h1:LG9vZxsWGOmUKieR8wPAUR3u3MpnYFQZROPIMaXh7/A= google.golang.org/grpc v1.63.2 h1:MUeiw1B2maTVZthpU5xvASfTh3LDbxHd6IJ6QQVU+xM= google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/fsnotify/fsnotify.v1 v1.4.7 h1:XNNYLJHt73EyYiCZi6+xjupS9CpvmiDgjPTAjrBlQbo= gopkg.in/fsnotify/fsnotify.v1 v1.4.7/go.mod h1:Fyux9zXlo4rWoMSIzpn9fDAYjalPqJ/K1qJ27s+7ltE= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -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.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -k8s.io/kube-openapi v0.0.0-20240220201932-37d671a357a5 h1:QSpdNrZ9uRlV0VkqLvVO0Rqg8ioKi3oSw7O5P7pJV8M= -k8s.io/kube-openapi v0.0.0-20240220201932-37d671a357a5/go.mod h1:Pa1PvrP7ACSkuX6I7KYomY6cmMA0Tx86waBhDUgoKPw= +k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 h1:BZqlfIlq5YbRMFko6/PM7FjZpUb45WallggurYhKGag= k8s.io/utils v0.0.0-20230726121419-3b25d923346b h1:sgn3ZU783SCgtaSJjpcVVlRqd6GSnlTLKgpAAttJvpI= k8s.io/utils v0.0.0-20230726121419-3b25d923346b/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo= diff --git a/pkg/server/module_server_test.go b/pkg/server/module_server_test.go index 23d8a89a20d..ecfb9c36d7a 100644 --- a/pkg/server/module_server_test.go +++ b/pkg/server/module_server_test.go @@ -33,8 +33,7 @@ func TestIntegrationWillRunInstrumentationServerWhenTargetHasNoHttpServer(t *tes t.Skip("skipping - sqlite not supported for storage server target") } - testdb := db.InitTestDB(t) - cfg := testdb.Cfg + _, cfg := db.InitTestDBWithCfg(t) cfg.GRPCServerNetwork = "tcp" cfg.GRPCServerAddress = "localhost:10000" addStorageServerToConfig(t, cfg, dbType) diff --git a/pkg/services/accesscontrol/authorize_in_org_test.go b/pkg/services/accesscontrol/authorize_in_org_test.go index 81d53dab2b8..c0c91734cba 100644 --- a/pkg/services/accesscontrol/authorize_in_org_test.go +++ b/pkg/services/accesscontrol/authorize_in_org_test.go @@ -186,7 +186,7 @@ func TestAuthorizeInOrgMiddleware(t *testing.T) { req := httptest.NewRequest(http.MethodGet, "/api/endpoint", nil) expectedIdentity := &authn.Identity{ - ID: fmt.Sprintf("user:%v", tc.ctxSignedInUser.UserID), + ID: authn.MustNewNamespaceID(authn.NamespaceUser, tc.ctxSignedInUser.UserID), OrgID: tc.targetOrgId, Permissions: map[int64]map[string][]string{}, } diff --git a/pkg/services/accesscontrol/database/database_test.go b/pkg/services/accesscontrol/database/database_test.go index 4c3c2e78aca..fda8acff224 100644 --- a/pkg/services/accesscontrol/database/database_test.go +++ b/pkg/services/accesscontrol/database/database_test.go @@ -11,6 +11,7 @@ import ( "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/localcache" + "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/services/accesscontrol" rs "github.com/grafana/grafana/pkg/services/accesscontrol/resourcepermissions" "github.com/grafana/grafana/pkg/services/auth/identity" @@ -90,9 +91,9 @@ func TestAccessControlStore_GetUserPermissions(t *testing.T) { } for _, tt := range tests { t.Run(tt.desc, func(t *testing.T) { - store, permissionStore, sql, teamSvc, _ := setupTestEnv(t) + store, permissionStore, usrSvc, teamSvc, _ := setupTestEnv(t) - user, team := createUserAndTeam(t, sql, teamSvc, tt.orgID) + user, team := createUserAndTeam(t, store.sql, usrSvc, teamSvc, tt.orgID) for _, id := range tt.userPermissions { _, err := permissionStore.SetUserResourcePermission(context.Background(), tt.orgID, accesscontrol.User{ID: user.ID}, rs.SetResourcePermissionCommand{ @@ -163,8 +164,8 @@ func TestAccessControlStore_GetUserPermissions(t *testing.T) { func TestAccessControlStore_DeleteUserPermissions(t *testing.T) { t.Run("expect permissions in all orgs to be deleted", func(t *testing.T) { - store, permissionsStore, sql, teamSvc, _ := setupTestEnv(t) - user, _ := createUserAndTeam(t, sql, teamSvc, 1) + store, permissionsStore, usrSvc, teamSvc, _ := setupTestEnv(t) + user, _ := createUserAndTeam(t, store.sql, usrSvc, teamSvc, 1) // generate permissions in org 1 _, err := permissionsStore.SetUserResourcePermission(context.Background(), 1, accesscontrol.User{ID: user.ID}, rs.SetResourcePermissionCommand{ @@ -203,8 +204,8 @@ func TestAccessControlStore_DeleteUserPermissions(t *testing.T) { }) t.Run("expect permissions in org 1 to be deleted", func(t *testing.T) { - store, permissionsStore, sql, teamSvc, _ := setupTestEnv(t) - user, _ := createUserAndTeam(t, sql, teamSvc, 1) + store, permissionsStore, usrSvc, teamSvc, _ := setupTestEnv(t) + user, _ := createUserAndTeam(t, store.sql, usrSvc, teamSvc, 1) // generate permissions in org 1 _, err := permissionsStore.SetUserResourcePermission(context.Background(), 1, accesscontrol.User{ID: user.ID}, rs.SetResourcePermissionCommand{ @@ -245,8 +246,8 @@ func TestAccessControlStore_DeleteUserPermissions(t *testing.T) { func TestAccessControlStore_DeleteTeamPermissions(t *testing.T) { t.Run("expect permissions related to team to be deleted", func(t *testing.T) { - store, permissionsStore, sql, teamSvc, _ := setupTestEnv(t) - user, team := createUserAndTeam(t, sql, teamSvc, 1) + store, permissionsStore, usrSvc, teamSvc, _ := setupTestEnv(t) + user, team := createUserAndTeam(t, store.sql, usrSvc, teamSvc, 1) // grant permission to the team _, err := permissionsStore.SetTeamResourcePermission(context.Background(), 1, team.ID, rs.SetResourcePermissionCommand{ @@ -279,8 +280,8 @@ func TestAccessControlStore_DeleteTeamPermissions(t *testing.T) { assert.Len(t, permissions, 0) }) t.Run("expect permissions not related to team to be kept", func(t *testing.T) { - store, permissionsStore, sql, teamSvc, _ := setupTestEnv(t) - user, team := createUserAndTeam(t, sql, teamSvc, 1) + store, permissionsStore, usrSvc, teamSvc, _ := setupTestEnv(t) + user, team := createUserAndTeam(t, store.sql, usrSvc, teamSvc, 1) // grant permission to the team _, err := permissionsStore.SetTeamResourcePermission(context.Background(), 1, team.ID, rs.SetResourcePermissionCommand{ @@ -314,7 +315,7 @@ func TestAccessControlStore_DeleteTeamPermissions(t *testing.T) { }) } -func createUserAndTeam(t *testing.T, userSrv user.Service, teamSvc team.Service, orgID int64) (*user.User, team.Team) { +func createUserAndTeam(t *testing.T, store db.DB, userSrv user.Service, teamSvc team.Service, orgID int64) (*user.User, team.Team) { t.Helper() user, err := userSrv.Create(context.Background(), &user.CreateUserCommand{ @@ -323,10 +324,12 @@ func createUserAndTeam(t *testing.T, userSrv user.Service, teamSvc team.Service, }) require.NoError(t, err) - team, err := teamSvc.CreateTeam("team", "", orgID) + team, err := teamSvc.CreateTeam(context.Background(), "team", "", orgID) require.NoError(t, err) - err = teamSvc.AddTeamMember(context.Background(), user.ID, orgID, team.ID, false, dashboardaccess.PERMISSION_VIEW) + err = store.WithDbSession(context.Background(), func(sess *db.Session) error { + return teamimpl.AddOrUpdateTeamMemberHook(sess, user.ID, orgID, team.ID, false, dashboardaccess.PERMISSION_VIEW) + }) require.NoError(t, err) return user, team @@ -348,7 +351,7 @@ type dbUser struct { teamID int64 } -func createUsersAndTeams(t *testing.T, svcs helperServices, orgID int64, users []testUser) []dbUser { +func createUsersAndTeams(t *testing.T, store db.DB, svcs helperServices, orgID int64, users []testUser) []dbUser { t.Helper() res := []dbUser{} @@ -371,10 +374,12 @@ func createUsersAndTeams(t *testing.T, svcs helperServices, orgID int64, users [ continue } - team, err := svcs.teamSvc.CreateTeam(fmt.Sprintf("team%v", i+1), "", orgID) + team, err := svcs.teamSvc.CreateTeam(context.Background(), fmt.Sprintf("team%v", i+1), "", orgID) require.NoError(t, err) - err = svcs.teamSvc.AddTeamMember(context.Background(), user.ID, orgID, team.ID, false, dashboardaccess.PERMISSION_VIEW) + err = store.WithDbSession(context.Background(), func(sess *db.Session) error { + return teamimpl.AddOrUpdateTeamMemberHook(sess, user.ID, orgID, team.ID, false, dashboardaccess.PERMISSION_VIEW) + }) require.NoError(t, err) err = svcs.orgSvc.UpdateOrgUser(context.Background(), @@ -388,14 +393,14 @@ func createUsersAndTeams(t *testing.T, svcs helperServices, orgID int64, users [ } func setupTestEnv(t testing.TB) (*AccessControlStore, rs.Store, user.Service, team.Service, org.Service) { - sql, cfg := db.InitTestDBwithCfg(t) + sql, cfg := db.InitTestDBWithCfg(t) cfg.AutoAssignOrg = true cfg.AutoAssignOrgRole = "Viewer" cfg.AutoAssignOrgId = 1 acstore := ProvideService(sql) asService := rs.NewActionSetService() permissionStore := rs.NewStore(sql, featuremgmt.WithFeatures(), &asService) - teamService, err := teamimpl.ProvideService(sql, cfg) + teamService, err := teamimpl.ProvideService(sql, cfg, tracing.InitializeTracerForTest()) require.NoError(t, err) orgService, err := orgimpl.ProvideService(sql, cfg, quotatest.New(false, nil)) require.NoError(t, err) @@ -404,7 +409,10 @@ func setupTestEnv(t testing.TB) (*AccessControlStore, rs.Store, user.Service, te require.Equal(t, int64(1), orgID) require.NoError(t, err) - userService, err := userimpl.ProvideService(sql, orgService, cfg, teamService, localcache.ProvideService(), quotatest.New(false, nil), supportbundlestest.NewFakeBundleService()) + userService, err := userimpl.ProvideService( + sql, orgService, cfg, teamService, localcache.ProvideService(), tracing.InitializeTracerForTest(), + quotatest.New(false, nil), supportbundlestest.NewFakeBundleService(), + ) require.NoError(t, err) return acstore, permissionStore, userService, teamService, orgService } @@ -652,7 +660,7 @@ func TestIntegrationAccessControlStore_SearchUsersPermissions(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { acStore, permissionsStore, userSvc, teamSvc, orgSvc := setupTestEnv(t) - dbUsers := createUsersAndTeams(t, helperServices{userSvc, teamSvc, orgSvc}, 1, tt.users) + dbUsers := createUsersAndTeams(t, acStore.sql, helperServices{userSvc, teamSvc, orgSvc}, 1, tt.users) // Switch userID and TeamID by the real stored ones for i := range tt.permCmds { @@ -732,7 +740,7 @@ func TestAccessControlStore_GetUsersBasicRoles(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { acStore, _, userSvc, teamSvc, orgSvc := setupTestEnv(t) - dbUsers := createUsersAndTeams(t, helperServices{userSvc, teamSvc, orgSvc}, 1, tt.users) + dbUsers := createUsersAndTeams(t, acStore.sql, helperServices{userSvc, teamSvc, orgSvc}, 1, tt.users) // Test dbRoles, err := acStore.GetUsersBasicRoles(ctx, tt.userFilter, 1) diff --git a/pkg/services/accesscontrol/ossaccesscontrol/permissions_services.go b/pkg/services/accesscontrol/ossaccesscontrol/permissions_services.go index 693efe7053f..5101c049f36 100644 --- a/pkg/services/accesscontrol/ossaccesscontrol/permissions_services.go +++ b/pkg/services/accesscontrol/ossaccesscontrol/permissions_services.go @@ -14,6 +14,7 @@ import ( "github.com/grafana/grafana/pkg/services/auth/identity" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/dashboards/dashboardaccess" + "github.com/grafana/grafana/pkg/services/datasources" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/folder" "github.com/grafana/grafana/pkg/services/libraryelements" @@ -280,13 +281,24 @@ func ProvideFolderPermissions( return &FolderPermissionsService{srv}, nil } -func ProvideDatasourcePermissionsService() *DatasourcePermissionsService { - return &DatasourcePermissionsService{} +// DatasourceQueryActions contains permissions to read information +// about a data source and submit arbitrary queries to it. +var DatasourceQueryActions = []string{ + datasources.ActionRead, + datasources.ActionQuery, +} + +func ProvideDatasourcePermissionsService(features featuremgmt.FeatureToggles, db db.DB, actionSetService resourcepermissions.ActionSetService) *DatasourcePermissionsService { + return &DatasourcePermissionsService{ + store: resourcepermissions.NewStore(db, features, &actionSetService), + } } var _ accesscontrol.DatasourcePermissionsService = new(DatasourcePermissionsService) -type DatasourcePermissionsService struct{} +type DatasourcePermissionsService struct { + store resourcepermissions.Store +} func (e DatasourcePermissionsService) GetPermissions(ctx context.Context, user identity.Requester, resourceID string) ([]accesscontrol.ResourcePermission, error) { return nil, nil @@ -304,12 +316,39 @@ func (e DatasourcePermissionsService) SetBuiltInRolePermission(ctx context.Conte return nil, nil } +// SetPermissions sets managed permissions for a datasource in OSS. This ensures that Viewers and Editors maintain query access to a data source +// if an OSS/unlicensed instance is upgraded to Enterprise/licensed. +// https://github.com/grafana/identity-access-team/issues/672 func (e DatasourcePermissionsService) SetPermissions(ctx context.Context, orgID int64, resourceID string, commands ...accesscontrol.SetResourcePermissionCommand) ([]accesscontrol.ResourcePermission, error) { - return nil, nil + var dbCommands []resourcepermissions.SetResourcePermissionsCommand + for _, cmd := range commands { + // Only set query permissions for built-in roles + if cmd.Permission != "Query" || cmd.BuiltinRole == "" { + continue + } + actions := DatasourceQueryActions + + dbCommands = append(dbCommands, resourcepermissions.SetResourcePermissionsCommand{ + BuiltinRole: cmd.BuiltinRole, + SetResourcePermissionCommand: resourcepermissions.SetResourcePermissionCommand{ + Actions: actions, + Resource: datasources.ScopeRoot, + ResourceID: resourceID, + ResourceAttribute: "uid", + Permission: cmd.Permission, + }, + }) + } + + return e.store.SetResourcePermissions(ctx, orgID, dbCommands, resourcepermissions.ResourceHooks{}) } func (e DatasourcePermissionsService) DeleteResourcePermissions(ctx context.Context, orgID int64, resourceID string) error { - return nil + return e.store.DeleteResourcePermissions(ctx, orgID, &resourcepermissions.DeleteResourcePermissionsCmd{ + Resource: datasources.ScopeRoot, + ResourceAttribute: "uid", + ResourceID: resourceID, + }) } func (e DatasourcePermissionsService) MapActions(permission accesscontrol.ResourcePermission) string { diff --git a/pkg/services/accesscontrol/resourcepermissions/api_test.go b/pkg/services/accesscontrol/resourcepermissions/api_test.go index d7628f413af..42732a3388a 100644 --- a/pkg/services/accesscontrol/resourcepermissions/api_test.go +++ b/pkg/services/accesscontrol/resourcepermissions/api_test.go @@ -13,18 +13,12 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/contexthandler/ctxkey" contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" - "github.com/grafana/grafana/pkg/services/org/orgimpl" - "github.com/grafana/grafana/pkg/services/quota/quotatest" - "github.com/grafana/grafana/pkg/services/supportbundles/supportbundlestest" - "github.com/grafana/grafana/pkg/services/team/teamimpl" + "github.com/grafana/grafana/pkg/services/team" "github.com/grafana/grafana/pkg/services/user" - "github.com/grafana/grafana/pkg/services/user/userimpl" - "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/web" ) @@ -116,7 +110,7 @@ func TestApi_getDescription(t *testing.T) { for _, tt := range tests { t.Run(tt.desc, func(t *testing.T) { - service, _, _, _ := setupTestEnvironment(t, tt.options) + service, _, _ := setupTestEnvironment(t, tt.options) server := setupTestServer(t, &user.SignedInUser{OrgID: 1, Permissions: map[int64]map[string][]string{1: accesscontrol.GroupScopesByAction(tt.permissions)}}, service) req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("/api/access-control/%s/description", tt.options.Resource), nil) @@ -163,10 +157,10 @@ func TestApi_getPermissions(t *testing.T) { for _, tt := range tests { t.Run(tt.desc, func(t *testing.T) { - service, sql, cfg, _ := setupTestEnvironment(t, testOptions) + service, usrSvc, teamSvc := setupTestEnvironment(t, testOptions) server := setupTestServer(t, &user.SignedInUser{OrgID: 1, Permissions: map[int64]map[string][]string{1: accesscontrol.GroupScopesByAction(tt.permissions)}}, service) - seedPermissions(t, tt.resourceID, sql, cfg, service) + seedPermissions(t, tt.resourceID, usrSvc, teamSvc, service) permissions, recorder := getPermission(t, server, testOptions.Resource, tt.resourceID) assert.Equal(t, tt.expectedStatus, recorder.Code) @@ -240,7 +234,7 @@ func TestApi_setBuiltinRolePermission(t *testing.T) { for _, tt := range tests { t.Run(tt.desc, func(t *testing.T) { - service, _, _, _ := setupTestEnvironment(t, testOptions) + service, _, _ := setupTestEnvironment(t, testOptions) server := setupTestServer(t, &user.SignedInUser{OrgID: 1, Permissions: map[int64]map[string][]string{1: accesscontrol.GroupScopesByAction(tt.permissions)}}, service) recorder := setPermission(t, server, testOptions.Resource, tt.resourceID, tt.permission, "builtInRoles", tt.builtInRole) @@ -318,11 +312,11 @@ func TestApi_setTeamPermission(t *testing.T) { for _, tt := range tests { t.Run(tt.desc, func(t *testing.T) { - service, _, _, teamSvc := setupTestEnvironment(t, testOptions) + service, _, teamSvc := setupTestEnvironment(t, testOptions) server := setupTestServer(t, &user.SignedInUser{OrgID: 1, Permissions: map[int64]map[string][]string{1: accesscontrol.GroupScopesByAction(tt.permissions)}}, service) // seed team - _, err := teamSvc.CreateTeam("test", "test@test.com", 1) + _, err := teamSvc.CreateTeam(context.Background(), "test", "test@test.com", 1) require.NoError(t, err) recorder := setPermission(t, server, testOptions.Resource, tt.resourceID, tt.permission, "teams", strconv.Itoa(int(tt.teamID))) @@ -401,18 +395,13 @@ func TestApi_setUserPermission(t *testing.T) { for _, tt := range tests { t.Run(tt.desc, func(t *testing.T) { - service, sql, cfg, _ := setupTestEnvironment(t, testOptions) + service, usrSvc, _ := setupTestEnvironment(t, testOptions) server := setupTestServer(t, &user.SignedInUser{ OrgID: 1, Permissions: map[int64]map[string][]string{1: accesscontrol.GroupScopesByAction(tt.permissions)}, }, service) - // seed user - orgSvc, err := orgimpl.ProvideService(sql, cfg, quotatest.New(false, nil)) - require.NoError(t, err) - usrSvc, err := userimpl.ProvideService(sql, orgSvc, cfg, nil, nil, "atest.FakeQuotaService{}, supportbundlestest.NewFakeBundleService()) - require.NoError(t, err) - _, err = usrSvc.Create(context.Background(), &user.CreateUserCommand{Login: "test", OrgID: 1}) + _, err := usrSvc.Create(context.Background(), &user.CreateUserCommand{Login: "test", OrgID: 1}) require.NoError(t, err) recorder := setPermission(t, server, testOptions.Resource, tt.resourceID, tt.permission, "users", strconv.Itoa(int(tt.userID))) @@ -506,20 +495,15 @@ func checkSeededPermissions(t *testing.T, permissions []resourcePermissionDTO) { } } -func seedPermissions(t *testing.T, resourceID string, sql db.DB, cfg *setting.Cfg, service *Service) { +func seedPermissions(t *testing.T, resourceID string, usrSvc user.Service, teamSvc team.Service, service *Service) { t.Helper() + // seed team 1 with "Edit" permission on dashboard 1 - teamSvc, err := teamimpl.ProvideService(sql, cfg) - require.NoError(t, err) - team, err := teamSvc.CreateTeam("test", "test@test.com", 1) + team, err := teamSvc.CreateTeam(context.Background(), "test", "test@test.com", 1) require.NoError(t, err) _, err = service.SetTeamPermission(context.Background(), team.OrgID, team.ID, resourceID, "Edit") require.NoError(t, err) // seed user 1 with "View" permission on dashboard 1 - orgSvc, err := orgimpl.ProvideService(sql, cfg, quotatest.New(false, nil)) - require.NoError(t, err) - usrSvc, err := userimpl.ProvideService(sql, orgSvc, cfg, nil, nil, "atest.FakeQuotaService{}, supportbundlestest.NewFakeBundleService()) - require.NoError(t, err) u, err := usrSvc.Create(context.Background(), &user.CreateUserCommand{Login: "test", OrgID: 1}) require.NoError(t, err) _, err = service.SetUserPermission(context.Background(), u.OrgID, accesscontrol.User{ID: u.ID}, resourceID, "View") diff --git a/pkg/services/accesscontrol/resourcepermissions/service_test.go b/pkg/services/accesscontrol/resourcepermissions/service_test.go index 8aa2108dc2d..44bc6a98514 100644 --- a/pkg/services/accesscontrol/resourcepermissions/service_test.go +++ b/pkg/services/accesscontrol/resourcepermissions/service_test.go @@ -9,6 +9,7 @@ import ( "github.com/grafana/grafana/pkg/api/routing" "github.com/grafana/grafana/pkg/infra/db" + "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/accesscontrol/acimpl" "github.com/grafana/grafana/pkg/services/accesscontrol/actest" @@ -43,17 +44,13 @@ func TestService_SetUserPermission(t *testing.T) { for _, tt := range tests { t.Run(tt.desc, func(t *testing.T) { - service, sql, cfg, _ := setupTestEnvironment(t, Options{ + service, usrSvc, _ := setupTestEnvironment(t, Options{ Resource: "dashboards", Assignments: Assignments{Users: true}, PermissionsToActions: nil, }) // seed user - orgSvc, err := orgimpl.ProvideService(sql, cfg, quotatest.New(false, nil)) - require.NoError(t, err) - usrSvc, err := userimpl.ProvideService(sql, orgSvc, cfg, nil, nil, "atest.FakeQuotaService{}, supportbundlestest.NewFakeBundleService()) - require.NoError(t, err) user, err := usrSvc.Create(context.Background(), &user.CreateUserCommand{Login: "test", OrgID: 1}) require.NoError(t, err) @@ -91,14 +88,14 @@ func TestService_SetTeamPermission(t *testing.T) { for _, tt := range tests { t.Run(tt.desc, func(t *testing.T) { - service, _, _, teamSvc := setupTestEnvironment(t, Options{ + service, _, teamSvc := setupTestEnvironment(t, Options{ Resource: "dashboards", Assignments: Assignments{Teams: true}, PermissionsToActions: nil, }) // seed team - team, err := teamSvc.CreateTeam("test", "test@test.com", 1) + team, err := teamSvc.CreateTeam(context.Background(), "test", "test@test.com", 1) require.NoError(t, err) var hookCalled bool @@ -135,7 +132,7 @@ func TestService_SetBuiltInRolePermission(t *testing.T) { for _, tt := range tests { t.Run(tt.desc, func(t *testing.T) { - service, _, _, _ := setupTestEnvironment(t, Options{ + service, _, _ := setupTestEnvironment(t, Options{ Resource: "dashboards", Assignments: Assignments{BuiltInRoles: true}, PermissionsToActions: nil, @@ -208,16 +205,12 @@ func TestService_SetPermissions(t *testing.T) { for _, tt := range tests { t.Run(tt.desc, func(t *testing.T) { - service, sql, cfg, teamSvc := setupTestEnvironment(t, tt.options) + service, usrSvc, teamSvc := setupTestEnvironment(t, tt.options) // seed user - orgSvc, err := orgimpl.ProvideService(sql, cfg, quotatest.New(false, nil)) + _, err := usrSvc.Create(context.Background(), &user.CreateUserCommand{Login: "user", OrgID: 1}) require.NoError(t, err) - usrSvc, err := userimpl.ProvideService(sql, orgSvc, cfg, nil, nil, "atest.FakeQuotaService{}, supportbundlestest.NewFakeBundleService()) - require.NoError(t, err) - _, err = usrSvc.Create(context.Background(), &user.CreateUserCommand{Login: "user", OrgID: 1}) - require.NoError(t, err) - _, err = teamSvc.CreateTeam("team", "", 1) + _, err = teamSvc.CreateTeam(context.Background(), "team", "", 1) require.NoError(t, err) permissions, err := service.SetPermissions(context.Background(), 1, "1", tt.commands...) @@ -231,15 +224,25 @@ func TestService_SetPermissions(t *testing.T) { } } -func setupTestEnvironment(t *testing.T, ops Options) (*Service, db.DB, *setting.Cfg, team.Service) { +func setupTestEnvironment(t *testing.T, ops Options) (*Service, user.Service, team.Service) { t.Helper() sql := db.InitTestDB(t) cfg := setting.NewCfg() - teamSvc, err := teamimpl.ProvideService(sql, cfg) + tracer := tracing.InitializeTracerForTest() + + teamSvc, err := teamimpl.ProvideService(sql, cfg, tracer) require.NoError(t, err) - userSvc, err := userimpl.ProvideService(sql, nil, cfg, teamSvc, nil, quotatest.New(false, nil), supportbundlestest.NewFakeBundleService()) + + orgSvc, err := orgimpl.ProvideService(sql, cfg, quotatest.New(false, nil)) require.NoError(t, err) + + userSvc, err := userimpl.ProvideService( + sql, orgSvc, cfg, teamSvc, nil, tracer, + quotatest.New(false, nil), supportbundlestest.NewFakeBundleService(), + ) + require.NoError(t, err) + license := licensingtest.NewFakeLicensing() license.On("FeatureEnabled", "accesscontrol.enforcement").Return(true).Maybe() ac := acimpl.ProvideAccessControl(cfg) @@ -250,5 +253,5 @@ func setupTestEnvironment(t *testing.T, ops Options) (*Service, db.DB, *setting. ) require.NoError(t, err) - return service, sql, cfg, teamSvc + return service, userSvc, teamSvc } diff --git a/pkg/services/accesscontrol/resourcepermissions/store_bench_test.go b/pkg/services/accesscontrol/resourcepermissions/store_bench_test.go index 2d0f5834a6e..51a63a5deba 100644 --- a/pkg/services/accesscontrol/resourcepermissions/store_bench_test.go +++ b/pkg/services/accesscontrol/resourcepermissions/store_bench_test.go @@ -12,6 +12,7 @@ import ( "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/datasources" datasourcesService "github.com/grafana/grafana/pkg/services/datasources/service" @@ -138,15 +139,17 @@ func GenerateDatasourcePermissions(b *testing.B, db db.DB, cfg *setting.Cfg, ac return dataSources } -func generateTeamsAndUsers(b *testing.B, db db.DB, cfg *setting.Cfg, users int) ([]int64, []int64) { - teamSvc, err := teamimpl.ProvideService(db, cfg) +func generateTeamsAndUsers(b *testing.B, store db.DB, cfg *setting.Cfg, users int) ([]int64, []int64) { + teamSvc, err := teamimpl.ProvideService(store, cfg, tracing.InitializeTracerForTest()) require.NoError(b, err) numberOfTeams := int(math.Ceil(float64(users) / UsersPerTeam)) globalUserId := 0 qs := quotatest.New(false, nil) - orgSvc, err := orgimpl.ProvideService(db, cfg, qs) + orgSvc, err := orgimpl.ProvideService(store, cfg, qs) require.NoError(b, err) - usrSvc, err := userimpl.ProvideService(db, orgSvc, cfg, nil, nil, qs, supportbundlestest.NewFakeBundleService()) + usrSvc, err := userimpl.ProvideService( + store, orgSvc, cfg, nil, nil, tracing.InitializeTracerForTest(), + qs, supportbundlestest.NewFakeBundleService()) require.NoError(b, err) userIds := make([]int64, 0) teamIds := make([]int64, 0) @@ -154,7 +157,7 @@ func generateTeamsAndUsers(b *testing.B, db db.DB, cfg *setting.Cfg, users int) // Create team teamName := fmt.Sprintf("%s%v", "team", i) teamEmail := fmt.Sprintf("%s@example.org", teamName) - team, err := teamSvc.CreateTeam(teamName, teamEmail, 1) + team, err := teamSvc.CreateTeam(context.Background(), teamName, teamEmail, 1) require.NoError(b, err) teamId := team.ID teamIds = append(teamIds, teamId) @@ -171,7 +174,9 @@ func generateTeamsAndUsers(b *testing.B, db db.DB, cfg *setting.Cfg, users int) globalUserId++ userIds = append(userIds, userId) - err = teamSvc.AddTeamMember(context.Background(), userId, 1, teamId, false, 1) + err = store.WithDbSession(context.Background(), func(sess *db.Session) error { + return teamimpl.AddOrUpdateTeamMemberHook(sess, userId, 1, teamId, false, 1) + }) require.NoError(b, err) } } diff --git a/pkg/services/accesscontrol/resourcepermissions/store_test.go b/pkg/services/accesscontrol/resourcepermissions/store_test.go index 5774e74adad..0c92a48162e 100644 --- a/pkg/services/accesscontrol/resourcepermissions/store_test.go +++ b/pkg/services/accesscontrol/resourcepermissions/store_test.go @@ -10,6 +10,7 @@ import ( "github.com/stretchr/testify/require" "github.com/grafana/grafana/pkg/infra/db" + "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/featuremgmt" @@ -527,7 +528,10 @@ func seedResourcePermissions( orgID, err := orgService.GetOrCreate(context.Background(), "test") require.NoError(t, err) - usrSvc, err := userimpl.ProvideService(sql, orgService, cfg, nil, nil, quotatest.New(false, nil), supportbundlestest.NewFakeBundleService()) + usrSvc, err := userimpl.ProvideService( + sql, orgService, cfg, nil, nil, tracing.InitializeTracerForTest(), + quotatest.New(false, nil), supportbundlestest.NewFakeBundleService(), + ) require.NoError(t, err) create := func(login string, isServiceAccount bool) { @@ -558,9 +562,9 @@ func seedResourcePermissions( } func setupTestEnv(t testing.TB) (*store, db.DB, *setting.Cfg) { - sql := db.InitTestDB(t) + sql, cfg := db.InitTestDBWithCfg(t) asService := NewActionSetService() - return NewStore(sql, featuremgmt.WithFeatures(), &asService), sql, sql.Cfg + return NewStore(sql, featuremgmt.WithFeatures(), &asService), sql, cfg } func TestStore_IsInherited(t *testing.T) { diff --git a/pkg/services/annotations/accesscontrol/accesscontrol_test.go b/pkg/services/annotations/accesscontrol/accesscontrol_test.go index f74bd3b945e..ad845b89967 100644 --- a/pkg/services/annotations/accesscontrol/accesscontrol_test.go +++ b/pkg/services/annotations/accesscontrol/accesscontrol_test.go @@ -27,8 +27,7 @@ func TestIntegrationAuthorize(t *testing.T) { t.Skip("skipping integration test") } - sql := db.InitTestDB(t) - cfg := sql.Cfg + sql, cfg := db.InitTestDBWithCfg(t) dash1 := testutil.CreateDashboard(t, sql, cfg, featuremgmt.WithFeatures(), dashboards.SaveDashboardCommand{ UserID: 1, diff --git a/pkg/services/annotations/annotationsimpl/annotations_test.go b/pkg/services/annotations/annotationsimpl/annotations_test.go index c96a5efb67d..8850cac78a8 100644 --- a/pkg/services/annotations/annotationsimpl/annotations_test.go +++ b/pkg/services/annotations/annotationsimpl/annotations_test.go @@ -209,14 +209,14 @@ func TestIntegrationAnnotationListingWithInheritedRBAC(t *testing.T) { annotationsTexts := make([]string, 0, folder.MaxNestedFolderDepth+1) setupFolderStructure := func() db.DB { - sql := db.InitTestDB(t) + sql, cfg := db.InitTestDBWithCfg(t) // enable nested folders so that the folder table is populated for all the tests features := featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders) tagService := tagimpl.ProvideService(sql) - dashStore, err := dashboardstore.ProvideDashboardStore(sql, sql.Cfg, features, tagService, quotatest.New(false, nil)) + dashStore, err := dashboardstore.ProvideDashboardStore(sql, cfg, features, tagService, quotatest.New(false, nil)) require.NoError(t, err) origNewGuardian := guardian.New @@ -225,10 +225,9 @@ func TestIntegrationAnnotationListingWithInheritedRBAC(t *testing.T) { guardian.New = origNewGuardian }) - ac := acimpl.ProvideAccessControl(sql.Cfg) - folderSvc := folderimpl.ProvideService(ac, bus.ProvideBus(tracing.InitializeTracerForTest()), sql.Cfg, dashStore, folderimpl.ProvideDashboardFolderStore(sql), sql, features, supportbundlestest.NewFakeBundleService(), nil) + ac := acimpl.ProvideAccessControl(cfg) + folderSvc := folderimpl.ProvideService(ac, bus.ProvideBus(tracing.InitializeTracerForTest()), cfg, dashStore, folderimpl.ProvideDashboardFolderStore(sql), sql, features, supportbundlestest.NewFakeBundleService(), nil) - cfg := setting.NewCfg() cfg.AnnotationMaximumTagsLength = 60 store := NewXormStore(cfg, log.New("annotation.test"), sql, tagService) diff --git a/pkg/services/annotations/annotationsimpl/loki/historian_store_test.go b/pkg/services/annotations/annotationsimpl/loki/historian_store_test.go index a42256e0b4b..5202952cda9 100644 --- a/pkg/services/annotations/annotationsimpl/loki/historian_store_test.go +++ b/pkg/services/annotations/annotationsimpl/loki/historian_store_test.go @@ -7,10 +7,11 @@ import ( "math/rand" "net/url" "strconv" - "sync" "testing" "time" + "github.com/prometheus/client_golang/prometheus" + "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/log" @@ -28,7 +29,6 @@ import ( historymodel "github.com/grafana/grafana/pkg/services/ngalert/state/historian/model" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/tests/testsuite" - "github.com/prometheus/client_golang/prometheus" "github.com/stretchr/testify/require" ) @@ -42,8 +42,7 @@ func TestIntegrationAlertStateHistoryStore(t *testing.T) { t.Skip("skipping integration test") } - sql := db.InitTestDB(t) - cfg := sql.Cfg + sql, cfg := db.InitTestDBWithCfg(t) dashboard1 := testutil.CreateDashboard(t, sql, cfg, featuremgmt.WithFeatures(), dashboards.SaveDashboardCommand{ UserID: 1, @@ -60,21 +59,15 @@ func TestIntegrationAlertStateHistoryStore(t *testing.T) { "title": "Dashboard 2", }), }) - - knownUIDs := &sync.Map{} - generator := ngmodels.AlertRuleGen( - ngmodels.WithUniqueUID(knownUIDs), - ngmodels.WithUniqueID(), - ngmodels.WithOrgID(1), - ) + gen := ngmodels.RuleGen.With(ngmodels.RuleGen.WithOrgID(1)) dashboardRules := map[string][]*ngmodels.AlertRule{ dashboard1.UID: { - createAlertRuleFromDashboard(t, sql, "Test Rule 1", *dashboard1, generator), - createAlertRuleFromDashboard(t, sql, "Test Rule 2", *dashboard1, generator), + createAlertRuleFromDashboard(t, sql, "Test Rule 1", *dashboard1, gen), + createAlertRuleFromDashboard(t, sql, "Test Rule 2", *dashboard1, gen), }, dashboard2.UID: { - createAlertRuleFromDashboard(t, sql, "Test Rule 3", *dashboard2, generator), + createAlertRuleFromDashboard(t, sql, "Test Rule 3", *dashboard2, gen), }, } @@ -344,7 +337,7 @@ func TestIntegrationAlertStateHistoryStore(t *testing.T) { rule := dashboardRules[dashboard1.UID][0] stream1 := historian.StatesToStream(ruleMetaFromRule(t, rule), transitions, map[string]string{}, log.NewNopLogger()) - rule = createAlertRule(t, sql, "Test rule", generator) + rule = createAlertRule(t, sql, "Test rule", gen) stream2 := historian.StatesToStream(ruleMetaFromRule(t, rule), transitions, map[string]string{}, log.NewNopLogger()) stream := historian.Stream{ @@ -592,14 +585,15 @@ func createTestLokiStore(t *testing.T, sql db.DB, client lokiQueryClient) *LokiH // createAlertRule creates an alert rule in the database and returns it. // If a generator is not specified, uniqueness of primary key is not guaranteed. -func createAlertRule(t *testing.T, sql db.DB, title string, generator func() *ngmodels.AlertRule) *ngmodels.AlertRule { +func createAlertRule(t *testing.T, sql db.DB, title string, generator *ngmodels.AlertRuleGenerator) *ngmodels.AlertRule { t.Helper() if generator == nil { - generator = ngmodels.AlertRuleGen(ngmodels.WithTitle(title), withDashboardUID(nil), withPanelID(nil), ngmodels.WithOrgID(1)) + g := ngmodels.RuleGen + generator = g.With(g.WithTitle(title), g.WithDashboardAndPanel(nil, nil), g.WithOrgID(1)) } - rule := generator() + rule := generator.GenerateRef() // ensure rule has correct values if rule.Title != title { rule.Title = title @@ -633,17 +627,18 @@ func createAlertRule(t *testing.T, sql db.DB, title string, generator func() *ng // createAlertRuleFromDashboard creates an alert rule with a linked dashboard and panel in the database and returns it. // If a generator is not specified, uniqueness of primary key is not guaranteed. -func createAlertRuleFromDashboard(t *testing.T, sql db.DB, title string, dashboard dashboards.Dashboard, generator func() *ngmodels.AlertRule) *ngmodels.AlertRule { +func createAlertRuleFromDashboard(t *testing.T, sql db.DB, title string, dashboard dashboards.Dashboard, generator *ngmodels.AlertRuleGenerator) *ngmodels.AlertRule { t.Helper() panelID := new(int64) *panelID = 123 if generator == nil { - generator = ngmodels.AlertRuleGen(ngmodels.WithTitle(title), ngmodels.WithOrgID(1), withDashboardUID(&dashboard.UID), withPanelID(panelID)) + g := ngmodels.RuleGen + generator = g.With(g.WithTitle(title), g.WithDashboardAndPanel(&dashboard.UID, panelID), g.WithOrgID(1)) } - rule := generator() + rule := generator.GenerateRef() // ensure rule has correct values if rule.Title != title { rule.Title = title @@ -742,18 +737,6 @@ func genStateTransitions(t *testing.T, num int, start time.Time) []state.StateTr return transitions } -func withDashboardUID(dashboardUID *string) ngmodels.AlertRuleMutator { - return func(rule *ngmodels.AlertRule) { - rule.DashboardUID = dashboardUID - } -} - -func withPanelID(panelID *int64) ngmodels.AlertRuleMutator { - return func(rule *ngmodels.AlertRule) { - rule.PanelID = panelID - } -} - func compareAnnotationItem(t *testing.T, expected, actual *annotations.ItemDTO) { require.Equal(t, expected.AlertID, actual.AlertID) require.Equal(t, expected.AlertName, actual.AlertName) diff --git a/pkg/services/auth/identity/error.go b/pkg/services/auth/identity/error.go new file mode 100644 index 00000000000..1637af46402 --- /dev/null +++ b/pkg/services/auth/identity/error.go @@ -0,0 +1,13 @@ +package identity + +import ( + "errors" + + "github.com/grafana/grafana/pkg/util/errutil" +) + +var ( + ErrInvalidNamespaceID = errutil.BadRequest("auth.identity.invalid-namespace-id") + ErrNotIntIdentifier = errors.New("identifier is not an int64") + ErrIdentifierNotInitialized = errors.New("identifier is not initialized") +) diff --git a/pkg/services/auth/identity/namespace.go b/pkg/services/auth/identity/namespace.go new file mode 100644 index 00000000000..bcceffd8c8b --- /dev/null +++ b/pkg/services/auth/identity/namespace.go @@ -0,0 +1,113 @@ +package identity + +import ( + "fmt" + "strconv" + "strings" +) + +const ( + NamespaceUser = "user" + NamespaceAPIKey = "api-key" + NamespaceServiceAccount = "service-account" + NamespaceAnonymous = "anonymous" + NamespaceRenderService = "render" + NamespaceAccessPolicy = "access-policy" +) + +var AnonymousNamespaceID = MustNewNamespaceID(NamespaceAnonymous, 0) + +var namespaceLookup = map[string]struct{}{ + NamespaceUser: {}, + NamespaceAPIKey: {}, + NamespaceServiceAccount: {}, + NamespaceAnonymous: {}, + NamespaceRenderService: {}, + NamespaceAccessPolicy: {}, +} + +func ParseNamespaceID(str string) (NamespaceID, error) { + var namespaceID NamespaceID + + parts := strings.Split(str, ":") + if len(parts) != 2 { + return namespaceID, ErrInvalidNamespaceID.Errorf("expected namespace id to have 2 parts") + } + + namespace, id := parts[0], parts[1] + + if _, ok := namespaceLookup[namespace]; !ok { + return namespaceID, ErrInvalidNamespaceID.Errorf("got invalid namespace %s", namespace) + } + + namespaceID.id = id + namespaceID.namespace = namespace + + return namespaceID, nil +} + +// MustParseNamespaceID parses namespace id, it will panic if it fails to do so. +// Suitable to use in tests or when we can guarantee that we pass a correct format. +func MustParseNamespaceID(str string) NamespaceID { + namespaceID, err := ParseNamespaceID(str) + if err != nil { + panic(err) + } + return namespaceID +} + +// NewNamespaceID creates a new NamespaceID, will fail for invalid namespace. +func NewNamespaceID(namespace string, id int64) (NamespaceID, error) { + var namespaceID NamespaceID + if _, ok := namespaceLookup[namespace]; !ok { + return namespaceID, ErrInvalidNamespaceID.Errorf("got invalid namespace %s", namespace) + } + namespaceID.id = strconv.FormatInt(id, 10) + namespaceID.namespace = namespace + return namespaceID, nil +} + +// MustNewNamespaceID creates a new NamespaceID, will panic for invalid namespace. +// Suitable to use in tests or when we can guarantee that we pass a correct format. +func MustNewNamespaceID(namespace string, id int64) NamespaceID { + namespaceID, err := NewNamespaceID(namespace, id) + if err != nil { + panic(err) + } + return namespaceID +} + +// NewNamespaceIDUnchecked creates a new NamespaceID without checking if namespace is valid. +// It us up to the caller to ensure that namespace is valid. +func NewNamespaceIDUnchecked(namespace string, id int64) NamespaceID { + return NamespaceID{ + id: strconv.FormatInt(id, 10), + namespace: namespace, + } +} + +// FIXME: use this instead of encoded string through the codebase +type NamespaceID struct { + id string + namespace string +} + +func (ni NamespaceID) ID() string { + return ni.id +} + +func (ni NamespaceID) ParseInt() (int64, error) { + return strconv.ParseInt(ni.id, 10, 64) +} + +func (ni NamespaceID) Namespace() string { + return ni.namespace +} + +func (ni NamespaceID) IsNamespace(expected ...string) bool { + return IsNamespace(ni.namespace, expected...) +} + +func (ni NamespaceID) String() string { + return fmt.Sprintf("%s:%s", ni.namespace, ni.id) +} diff --git a/pkg/services/auth/identity/requester.go b/pkg/services/auth/identity/requester.go index 57c82e9ecd9..87140b85d4f 100644 --- a/pkg/services/auth/identity/requester.go +++ b/pkg/services/auth/identity/requester.go @@ -1,25 +1,12 @@ package identity import ( - "errors" "fmt" "strconv" "github.com/grafana/grafana/pkg/models/roletype" ) -const ( - NamespaceUser = "user" - NamespaceAPIKey = "api-key" - NamespaceServiceAccount = "service-account" - NamespaceAnonymous = "anonymous" - NamespaceRenderService = "render" - NamespaceAccessPolicy = "access-policy" -) - -var ErrNotIntIdentifier = errors.New("identifier is not an int64") -var ErrIdentifierNotInitialized = errors.New("identifier is not initialized") - type Requester interface { // GetID returns namespaced id for the entity GetID() string diff --git a/pkg/services/auth/idimpl/service_test.go b/pkg/services/auth/idimpl/service_test.go index 37e89229460..94495f54873 100644 --- a/pkg/services/auth/idimpl/service_test.go +++ b/pkg/services/auth/idimpl/service_test.go @@ -69,7 +69,7 @@ func TestService_SignIdentity(t *testing.T) { featuremgmt.WithFeatures(featuremgmt.FlagIdForwarding), &authntest.FakeService{}, nil, ) - token, err := s.SignIdentity(context.Background(), &authn.Identity{ID: "user:1"}) + token, err := s.SignIdentity(context.Background(), &authn.Identity{ID: authn.MustParseNamespaceID("user:1")}) require.NoError(t, err) require.NotEmpty(t, token) }) @@ -80,7 +80,7 @@ func TestService_SignIdentity(t *testing.T) { featuremgmt.WithFeatures(featuremgmt.FlagIdForwarding), &authntest.FakeService{}, nil, ) - token, err := s.SignIdentity(context.Background(), &authn.Identity{ID: "user:1", AuthenticatedBy: login.AzureADAuthModule}) + token, err := s.SignIdentity(context.Background(), &authn.Identity{ID: authn.MustParseNamespaceID("user:1"), AuthenticatedBy: login.AzureADAuthModule}) require.NoError(t, err) parsed, err := jwt.ParseSigned(token) diff --git a/pkg/services/authn/authn.go b/pkg/services/authn/authn.go index c2ec983b72a..83cbc622bf9 100644 --- a/pkg/services/authn/authn.go +++ b/pkg/services/authn/authn.go @@ -153,7 +153,7 @@ type RedirectClient interface { // that should happen during logout and supports client specific redirect URL. type LogoutClient interface { Client - Logout(ctx context.Context, user identity.Requester) (*Redirect, bool) + Logout(ctx context.Context, user Requester) (*Redirect, bool) } type PasswordClient interface { diff --git a/pkg/services/authn/authnimpl/service.go b/pkg/services/authn/authnimpl/service.go index ad8bb42c15f..2b12e1021f6 100644 --- a/pkg/services/authn/authnimpl/service.go +++ b/pkg/services/authn/authnimpl/service.go @@ -9,6 +9,7 @@ import ( "github.com/prometheus/client_golang/prometheus" "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/codes" "go.opentelemetry.io/otel/trace" "github.com/grafana/grafana/pkg/infra/log" @@ -123,12 +124,31 @@ func (s *Service) Authenticate(ctx context.Context, r *authn.Request) (*authn.Id } func (s *Service) authenticate(ctx context.Context, c authn.Client, r *authn.Request) (*authn.Identity, error) { + ctx, span := s.tracer.Start(ctx, "authn.authenticate") + defer span.End() + identity, err := c.Authenticate(ctx, r) if err != nil { + span.SetStatus(codes.Error, "authenticate failed on client") + span.RecordError(err) s.errorLogFunc(ctx, err)("Failed to authenticate request", "client", c.Name(), "error", err) return nil, err } + span.SetAttributes( + attribute.String("identity.ID", identity.ID.String()), + attribute.String("identity.AuthID", identity.AuthID), + attribute.String("identity.AuthenticatedBy", identity.AuthenticatedBy), + ) + + if len(identity.ClientParams.FetchPermissionsParams.ActionsLookup) > 0 { + span.SetAttributes(attribute.StringSlice("identity.ClientParams.FetchPermissionsParams.ActionsLookup", identity.ClientParams.FetchPermissionsParams.ActionsLookup)) + } + + if len(identity.ClientParams.FetchPermissionsParams.Roles) > 0 { + span.SetAttributes(attribute.StringSlice("identity.ClientParams.FetchPermissionsParams.Roles", identity.ClientParams.FetchPermissionsParams.Roles)) + } + if err := s.runPostAuthHooks(ctx, identity, r); err != nil { s.errorLogFunc(ctx, err)("Failed to run post auth hook", "client", c.Name(), "id", identity.ID, "error", err) return nil, err @@ -188,14 +208,13 @@ func (s *Service) Login(ctx context.Context, client string, r *authn.Request) (i return nil, err } - namespace, namespaceID := id.GetNamespacedID() // Login is only supported for users - if namespace != authn.NamespaceUser { + if !id.ID.IsNamespace(authn.NamespaceUser) { s.metrics.failedLogin.WithLabelValues(client).Inc() - return nil, authn.ErrUnsupportedIdentity.Errorf("expected identity of type user but got: %s", namespace) + return nil, authn.ErrUnsupportedIdentity.Errorf("expected identity of type user but got: %s", id.ID.Namespace()) } - intId, err := identity.IntIdentifier(namespace, namespaceID) + userID, err := id.ID.ParseInt() if err != nil { return nil, err } @@ -206,7 +225,7 @@ func (s *Service) Login(ctx context.Context, client string, r *authn.Request) (i s.log.FromContext(ctx).Debug("Failed to parse ip from address", "client", c.Name(), "id", id.ID, "addr", addr, "error", err) } - sessionToken, err := s.sessionService.CreateToken(ctx, &user.User{ID: intId}, ip, r.HTTPRequest.UserAgent()) + sessionToken, err := s.sessionService.CreateToken(ctx, &user.User{ID: userID}, ip, r.HTTPRequest.UserAgent()) if err != nil { s.metrics.failedLogin.WithLabelValues(client).Inc() s.log.FromContext(ctx).Error("Failed to create session", "client", client, "id", id.ID, "err", err) @@ -241,7 +260,7 @@ func (s *Service) RedirectURL(ctx context.Context, client string, r *authn.Reque return redirectClient.RedirectURL(ctx, r) } -func (s *Service) Logout(ctx context.Context, user identity.Requester, sessionToken *auth.UserToken) (*authn.Redirect, error) { +func (s *Service) Logout(ctx context.Context, user authn.Requester, sessionToken *auth.UserToken) (*authn.Redirect, error) { ctx, span := s.tracer.Start(ctx, "authn.Logout") defer span.End() @@ -340,7 +359,7 @@ func (s *Service) resolveIdenity(ctx context.Context, orgID int64, namespaceID a if namespaceID.IsNamespace(authn.NamespaceUser) { return &authn.Identity{ OrgID: orgID, - ID: namespaceID.String(), + ID: namespaceID, ClientParams: authn.ClientParams{ AllowGlobalOrg: true, FetchSyncedUser: true, @@ -350,7 +369,7 @@ func (s *Service) resolveIdenity(ctx context.Context, orgID int64, namespaceID a if namespaceID.IsNamespace(authn.NamespaceServiceAccount) { return &authn.Identity{ - ID: namespaceID.String(), + ID: namespaceID, OrgID: orgID, ClientParams: authn.ClientParams{ AllowGlobalOrg: true, diff --git a/pkg/services/authn/authnimpl/service_test.go b/pkg/services/authn/authnimpl/service_test.go index 045ebe6accf..dd2592f89d7 100644 --- a/pkg/services/authn/authnimpl/service_test.go +++ b/pkg/services/authn/authnimpl/service_test.go @@ -6,10 +6,14 @@ import ( "net" "net/http" "net/url" + "slices" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "go.opentelemetry.io/otel/codes" + sdktrace "go.opentelemetry.io/otel/sdk/trace" + "go.opentelemetry.io/otel/sdk/trace/tracetest" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/tracing" @@ -40,26 +44,67 @@ func TestService_Authenticate(t *testing.T) { { desc: "should succeed with authentication for configured client", clients: []authn.Client{ - &authntest.FakeClient{ExpectedTest: true, ExpectedIdentity: &authn.Identity{ID: "user:1"}}, + &authntest.FakeClient{ExpectedTest: true, ExpectedIdentity: &authn.Identity{ID: authn.MustParseNamespaceID("user:1")}}, + }, + expectedIdentity: &authn.Identity{ID: authn.MustParseNamespaceID("user:1")}, + }, + { + desc: "should succeed with authentication for configured client for identity with fetch permissions params", + clients: []authn.Client{ + &authntest.FakeClient{ + ExpectedTest: true, + ExpectedIdentity: &authn.Identity{ + ID: authn.MustParseNamespaceID("user:2"), + ClientParams: authn.ClientParams{ + FetchPermissionsParams: authn.FetchPermissionsParams{ + ActionsLookup: []string{ + "datasources:read", + "datasources:query", + }, + Roles: []string{ + "fixed:datasources:reader", + }, + }, + }, + }, + }, + }, + expectedIdentity: &authn.Identity{ + ID: authn.MustParseNamespaceID("user:2"), + ClientParams: authn.ClientParams{ + FetchPermissionsParams: authn.FetchPermissionsParams{ + ActionsLookup: []string{ + "datasources:read", + "datasources:query", + }, + Roles: []string{ + "fixed:datasources:reader", + }, + }, + }, }, - expectedIdentity: &authn.Identity{ID: "user:1"}, }, { desc: "should succeed with authentication for second client when first test fail", clients: []authn.Client{ &authntest.FakeClient{ExpectedName: "1", ExpectedPriority: 1, ExpectedTest: false}, - &authntest.FakeClient{ExpectedName: "2", ExpectedPriority: 2, ExpectedTest: true, ExpectedIdentity: &authn.Identity{ID: "user:2"}}, + &authntest.FakeClient{ + ExpectedName: "2", + ExpectedPriority: 2, + ExpectedTest: true, + ExpectedIdentity: &authn.Identity{ID: authn.MustParseNamespaceID("user:2"), AuthID: "service:some-service", AuthenticatedBy: "service_auth"}, + }, }, - expectedIdentity: &authn.Identity{ID: "user:2"}, + expectedIdentity: &authn.Identity{ID: authn.MustParseNamespaceID("user:2"), AuthID: "service:some-service", AuthenticatedBy: "service_auth"}, }, { desc: "should succeed with authentication for third client when error happened in first", clients: []authn.Client{ &authntest.FakeClient{ExpectedName: "1", ExpectedPriority: 2, ExpectedTest: false}, &authntest.FakeClient{ExpectedName: "2", ExpectedPriority: 1, ExpectedTest: true, ExpectedErr: errors.New("some error")}, - &authntest.FakeClient{ExpectedName: "3", ExpectedPriority: 3, ExpectedTest: true, ExpectedIdentity: &authn.Identity{ID: "user:3"}}, + &authntest.FakeClient{ExpectedName: "3", ExpectedPriority: 3, ExpectedTest: true, ExpectedIdentity: &authn.Identity{ID: authn.MustParseNamespaceID("user:3")}}, }, - expectedIdentity: &authn.Identity{ID: "user:3"}, + expectedIdentity: &authn.Identity{ID: authn.MustParseNamespaceID("user:3")}, }, { desc: "should return error when no client could authenticate the request", @@ -90,16 +135,72 @@ func TestService_Authenticate(t *testing.T) { for _, tt := range tests { t.Run(tt.desc, func(t *testing.T) { + spanRecorder := tracetest.NewSpanRecorder() + tracer := tracing.InitializeTracerForTest(tracing.WithSpanProcessor(spanRecorder)) + svc := setupTests(t, func(svc *Service) { + svc.tracer = tracer + for _, c := range tt.clients { svc.RegisterClient(c) } }) identity, err := svc.Authenticate(context.Background(), &authn.Request{}) + spans := spanRecorder.Ended() if len(tt.expectedErrors) == 0 { assert.NoError(t, err) assert.EqualValues(t, tt.expectedIdentity, identity) + + matchedClients := make([]*authntest.FakeClient, 0) + for _, client := range tt.clients { + fakeClient, _ := client.(*authntest.FakeClient) + if fakeClient.ExpectedTest { + matchedClients = append(matchedClients, fakeClient) + } + } + require.Len(t, spans, 1+len(matchedClients), "must have spans 1+ number of clients tried") + + spansTested := make([]sdktrace.ReadOnlySpan, 0) + for _, span := range spans { + if span.Name() != "authn.Authenticate" { + spansTested = append(spansTested, span) + } + } + + assert.Len(t, spansTested, len(matchedClients), "expected spans with name authn.authenticate to match number of clients tested") + + // since this is a success case, at least one span should have all 3 attributes + passedAuthnIndex := slices.IndexFunc(spansTested, func(span sdktrace.ReadOnlySpan) bool { + return len(span.Attributes()) >= 3 // more than 3 when there are ClientParams in the identity + }) + require.NotEqual(t, -1, passedAuthnIndex, "no spans found all 3 attributes - passed case should have authn attributes set") + passedAuthnSpan := spansTested[passedAuthnIndex] + for _, attr := range passedAuthnSpan.Attributes() { + switch attr.Key { + case "identity.ID": + assert.Equal(t, tt.expectedIdentity.ID.String(), attr.Value.AsString()) + case "identity.AuthID": + assert.Equal(t, tt.expectedIdentity.AuthID, attr.Value.AsString()) + case "identity.AuthenticatedBy": + assert.Equal(t, tt.expectedIdentity.AuthenticatedBy, attr.Value.AsString()) + case "identity.ClientParams.FetchPermissionsParams.ActionsLookup": + if len(tt.expectedIdentity.ClientParams.FetchPermissionsParams.ActionsLookup) > 0 { + assert.Equal(t, tt.expectedIdentity.ClientParams.FetchPermissionsParams.ActionsLookup, attr.Value.AsStringSlice()) + } + case "identity.ClientParams.FetchPermissionsParams.Roles": + if len(tt.expectedIdentity.ClientParams.FetchPermissionsParams.Roles) > 0 { + assert.Equal(t, tt.expectedIdentity.ClientParams.FetchPermissionsParams.Roles, attr.Value.AsStringSlice()) + } + } + } + + if len(matchedClients) > 1 { + failedAuthnIndex := slices.IndexFunc(spansTested, func(span sdktrace.ReadOnlySpan) bool { + return span.Status().Code == codes.Error + }) + assert.NotEqual(t, -1, failedAuthnIndex, "no spans found for the error case - at least one client in multi client test must have failed") + } } else { for _, e := range tt.expectedErrors { assert.ErrorIs(t, err, e) @@ -214,10 +315,10 @@ func TestService_Login(t *testing.T) { client: "fake", expectedClientOK: true, expectedClientIdentity: &authn.Identity{ - ID: "user:1", + ID: authn.MustParseNamespaceID("user:1"), }, expectedIdentity: &authn.Identity{ - ID: "user:1", + ID: authn.MustParseNamespaceID("user:1"), SessionToken: &auth.UserToken{UserId: 1}, }, }, @@ -230,7 +331,7 @@ func TestService_Login(t *testing.T) { desc: "should not login non user identity", client: "fake", expectedClientOK: true, - expectedClientIdentity: &authn.Identity{ID: "apikey:1"}, + expectedClientIdentity: &authn.Identity{ID: authn.MustParseNamespaceID("api-key:1")}, expectedErr: authn.ErrUnsupportedIdentity, }, } @@ -318,31 +419,31 @@ func TestService_Logout(t *testing.T) { tests := []TestCase{ { desc: "should redirect to default redirect url when identity is not a user", - identity: &authn.Identity{ID: authn.NamespacedID(authn.NamespaceServiceAccount, 1)}, + identity: &authn.Identity{ID: authn.MustNewNamespaceID(authn.NamespaceServiceAccount, 1)}, expectedRedirect: &authn.Redirect{URL: "http://localhost:3000/login"}, }, { desc: "should redirect to default redirect url when no external provider was used to authenticate", - identity: &authn.Identity{ID: authn.NamespacedID(authn.NamespaceUser, 1)}, + identity: &authn.Identity{ID: authn.MustNewNamespaceID(authn.NamespaceUser, 1)}, expectedRedirect: &authn.Redirect{URL: "http://localhost:3000/login"}, expectedTokenRevoked: true, }, { desc: "should redirect to default redirect url when client is not found", - identity: &authn.Identity{ID: authn.NamespacedID(authn.NamespaceUser, 1), AuthenticatedBy: "notfound"}, + identity: &authn.Identity{ID: authn.MustNewNamespaceID(authn.NamespaceUser, 1), AuthenticatedBy: "notfound"}, expectedRedirect: &authn.Redirect{URL: "http://localhost:3000/login"}, expectedTokenRevoked: true, }, { desc: "should redirect to default redirect url when client do not implement logout extension", - identity: &authn.Identity{ID: authn.NamespacedID(authn.NamespaceUser, 1), AuthenticatedBy: "azuread"}, + identity: &authn.Identity{ID: authn.MustNewNamespaceID(authn.NamespaceUser, 1), AuthenticatedBy: "azuread"}, expectedRedirect: &authn.Redirect{URL: "http://localhost:3000/login"}, client: &authntest.FakeClient{ExpectedName: "auth.client.azuread"}, expectedTokenRevoked: true, }, { desc: "should redirect to client specific url", - identity: &authn.Identity{ID: authn.NamespacedID(authn.NamespaceUser, 1), AuthenticatedBy: "azuread"}, + identity: &authn.Identity{ID: authn.MustNewNamespaceID(authn.NamespaceUser, 1), AuthenticatedBy: "azuread"}, expectedRedirect: &authn.Redirect{URL: "http://idp.com/logout"}, client: &authntest.MockClient{ NameFunc: func() string { return "auth.client.azuread" }, @@ -387,7 +488,7 @@ func TestService_ResolveIdentity(t *testing.T) { t.Run("should return error for for unknown namespace", func(t *testing.T) { svc := setupTests(t) _, err := svc.ResolveIdentity(context.Background(), 1, "some:1") - assert.ErrorIs(t, err, authn.ErrInvalidNamepsaceID) + assert.ErrorIs(t, err, authn.ErrInvalidNamespaceID) }) t.Run("should return error for for namespace that don't have a resolver", func(t *testing.T) { diff --git a/pkg/services/authn/authnimpl/sync/oauth_token_sync.go b/pkg/services/authn/authnimpl/sync/oauth_token_sync.go index 935b4ec68c9..165613d45df 100644 --- a/pkg/services/authn/authnimpl/sync/oauth_token_sync.go +++ b/pkg/services/authn/authnimpl/sync/oauth_token_sync.go @@ -34,9 +34,8 @@ type OAuthTokenSync struct { } func (s *OAuthTokenSync) SyncOauthTokenHook(ctx context.Context, identity *authn.Identity, _ *authn.Request) error { - namespace, _ := identity.GetNamespacedID() // only perform oauth token check if identity is a user - if namespace != authn.NamespaceUser { + if !identity.ID.IsNamespace(authn.NamespaceUser) { return nil } @@ -50,8 +49,8 @@ func (s *OAuthTokenSync) SyncOauthTokenHook(ctx context.Context, identity *authn return nil } - _, err, _ := s.singleflightGroup.Do(identity.ID, func() (interface{}, error) { - s.log.Debug("Singleflight request for OAuth token sync", "key", identity.ID) + _, err, _ := s.singleflightGroup.Do(identity.ID.String(), func() (interface{}, error) { + s.log.Debug("Singleflight request for OAuth token sync", "key", identity.ID.String()) // FIXME: Consider using context.WithoutCancel instead of context.Background after Go 1.21 update updateCtx, cancel := context.WithTimeout(context.Background(), 15*time.Second) diff --git a/pkg/services/authn/authnimpl/sync/oauth_token_sync_test.go b/pkg/services/authn/authnimpl/sync/oauth_token_sync_test.go index 03db9ff1dd0..56145f55804 100644 --- a/pkg/services/authn/authnimpl/sync/oauth_token_sync_test.go +++ b/pkg/services/authn/authnimpl/sync/oauth_token_sync_test.go @@ -14,7 +14,6 @@ import ( "github.com/grafana/grafana/pkg/login/social/socialtest" "github.com/grafana/grafana/pkg/services/auth" "github.com/grafana/grafana/pkg/services/auth/authtest" - "github.com/grafana/grafana/pkg/services/auth/identity" "github.com/grafana/grafana/pkg/services/authn" "github.com/grafana/grafana/pkg/services/login" "github.com/grafana/grafana/pkg/services/oauthtoken/oauthtokentest" @@ -41,17 +40,17 @@ func TestOAuthTokenSync_SyncOAuthTokenHook(t *testing.T) { tests := []testCase{ { desc: "should skip sync when identity is not a user", - identity: &authn.Identity{ID: "service-account:1"}, + identity: &authn.Identity{ID: authn.MustParseNamespaceID("service-account:1")}, expectTryRefreshTokenCalled: false, }, { desc: "should skip sync when identity is a user but is not authenticated with session token", - identity: &authn.Identity{ID: "user:1"}, + identity: &authn.Identity{ID: authn.MustParseNamespaceID("user:1")}, expectTryRefreshTokenCalled: false, }, { desc: "should invalidate access token and session token if token refresh fails", - identity: &authn.Identity{ID: "user:1", SessionToken: &auth.UserToken{}, AuthenticatedBy: login.AzureADAuthModule}, + identity: &authn.Identity{ID: authn.MustParseNamespaceID("user:1"), SessionToken: &auth.UserToken{}, AuthenticatedBy: login.AzureADAuthModule}, expectHasEntryCalled: true, expectedTryRefreshErr: errors.New("some err"), expectTryRefreshTokenCalled: true, @@ -62,7 +61,7 @@ func TestOAuthTokenSync_SyncOAuthTokenHook(t *testing.T) { }, { desc: "should refresh the token successfully", - identity: &authn.Identity{ID: "user:1", SessionToken: &auth.UserToken{}, AuthenticatedBy: login.AzureADAuthModule}, + identity: &authn.Identity{ID: authn.MustParseNamespaceID("user:1"), SessionToken: &auth.UserToken{}, AuthenticatedBy: login.AzureADAuthModule}, expectHasEntryCalled: false, expectTryRefreshTokenCalled: true, expectInvalidateOauthTokensCalled: false, @@ -70,7 +69,7 @@ func TestOAuthTokenSync_SyncOAuthTokenHook(t *testing.T) { }, { desc: "should not invalidate the token if the token has already been refreshed by another request (singleflight)", - identity: &authn.Identity{ID: "user:1", SessionToken: &auth.UserToken{}, AuthenticatedBy: login.AzureADAuthModule}, + identity: &authn.Identity{ID: authn.MustParseNamespaceID("user:1"), SessionToken: &auth.UserToken{}, AuthenticatedBy: login.AzureADAuthModule}, expectHasEntryCalled: true, expectTryRefreshTokenCalled: true, expectInvalidateOauthTokensCalled: false, @@ -92,7 +91,7 @@ func TestOAuthTokenSync_SyncOAuthTokenHook(t *testing.T) { ) service := &oauthtokentest.MockOauthTokenService{ - HasOAuthEntryFunc: func(ctx context.Context, usr identity.Requester) (*login.UserAuth, bool, error) { + HasOAuthEntryFunc: func(ctx context.Context, usr authn.Requester) (*login.UserAuth, bool, error) { hasEntryCalled = true return tt.expectedHasEntryToken, tt.expectedHasEntryToken != nil, nil }, @@ -100,7 +99,7 @@ func TestOAuthTokenSync_SyncOAuthTokenHook(t *testing.T) { invalidateTokensCalled = true return nil }, - TryTokenRefreshFunc: func(ctx context.Context, usr identity.Requester) error { + TryTokenRefreshFunc: func(ctx context.Context, usr authn.Requester) error { tryRefreshCalled = true return tt.expectedTryRefreshErr }, diff --git a/pkg/services/authn/authnimpl/sync/org_sync.go b/pkg/services/authn/authnimpl/sync/org_sync.go index d8d1146dab4..38429a07704 100644 --- a/pkg/services/authn/authnimpl/sync/org_sync.go +++ b/pkg/services/authn/authnimpl/sync/org_sync.go @@ -8,7 +8,6 @@ import ( "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/services/accesscontrol" - "github.com/grafana/grafana/pkg/services/auth/identity" "github.com/grafana/grafana/pkg/services/authn" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/user" @@ -35,15 +34,14 @@ func (s *OrgSync) SyncOrgRolesHook(ctx context.Context, id *authn.Identity, _ *a ctxLogger := s.log.FromContext(ctx).New("id", id.ID, "login", id.Login) - namespace, identifier := id.GetNamespacedID() - if namespace != authn.NamespaceUser { - ctxLogger.Warn("Failed to sync org role, invalid namespace for identity", "namespace", namespace) + if !id.ID.IsNamespace(authn.NamespaceUser) { + ctxLogger.Warn("Failed to sync org role, invalid namespace for identity", "namespace", id.ID.Namespace()) return nil } - userID, err := identity.IntIdentifier(namespace, identifier) + userID, err := id.ID.ParseInt() if err != nil { - ctxLogger.Warn("Failed to sync org role, invalid ID for identity", "namespace", namespace, "err", err) + ctxLogger.Warn("Failed to sync org role, invalid ID for identity", "namespace", id.ID.Namespace(), "err", err) return nil } @@ -122,9 +120,9 @@ func (s *OrgSync) SyncOrgRolesHook(ctx context.Context, id *authn.Identity, _ *a if _, ok := id.OrgRoles[id.OrgID]; !ok { if len(orgIDs) > 0 { id.OrgID = orgIDs[0] - return s.userService.SetUsingOrg(ctx, &user.SetUsingOrgCommand{ + return s.userService.Update(ctx, &user.UpdateUserCommand{ UserID: userID, - OrgID: id.OrgID, + OrgID: &id.OrgID, }) } } @@ -139,15 +137,14 @@ func (s *OrgSync) SetDefaultOrgHook(ctx context.Context, currentIdentity *authn. ctxLogger := s.log.FromContext(ctx) - namespace, identifier := currentIdentity.GetNamespacedID() - if namespace != identity.NamespaceUser { - ctxLogger.Debug("Skipping default org sync, not a user", "namespace", namespace) + if !currentIdentity.ID.IsNamespace(authn.NamespaceUser) { + ctxLogger.Debug("Skipping default org sync, not a user", "namespace", currentIdentity.ID.Namespace()) return } - userID, err := identity.IntIdentifier(namespace, identifier) + userID, err := currentIdentity.ID.ParseInt() if err != nil { - ctxLogger.Debug("Skipping default org sync, invalid ID for identity", "id", currentIdentity.ID, "namespace", namespace, "err", err) + ctxLogger.Debug("Skipping default org sync, invalid ID for identity", "id", currentIdentity.ID, "namespace", currentIdentity.ID.Namespace(), "err", err) return } @@ -162,8 +159,8 @@ func (s *OrgSync) SetDefaultOrgHook(ctx context.Context, currentIdentity *authn. return } - cmd := user.SetUsingOrgCommand{UserID: userID, OrgID: s.cfg.LoginDefaultOrgId} - if svcErr := s.userService.SetUsingOrg(ctx, &cmd); svcErr != nil { + cmd := user.UpdateUserCommand{UserID: userID, OrgID: &s.cfg.LoginDefaultOrgId} + if svcErr := s.userService.Update(ctx, &cmd); svcErr != nil { ctxLogger.Error("Failed to set default org", "id", currentIdentity.ID, "err", svcErr) } } diff --git a/pkg/services/authn/authnimpl/sync/org_sync_test.go b/pkg/services/authn/authnimpl/sync/org_sync_test.go index 93d46e8f823..f8b774696d9 100644 --- a/pkg/services/authn/authnimpl/sync/org_sync_test.go +++ b/pkg/services/authn/authnimpl/sync/org_sync_test.go @@ -75,7 +75,7 @@ func TestOrgSync_SyncOrgRolesHook(t *testing.T) { args: args{ ctx: context.Background(), id: &authn.Identity{ - ID: "user:1", + ID: authn.MustParseNamespaceID("user:1"), Login: "test", Name: "test", Email: "test", @@ -91,7 +91,7 @@ func TestOrgSync_SyncOrgRolesHook(t *testing.T) { }, }, wantID: &authn.Identity{ - ID: "user:1", + ID: authn.MustParseNamespaceID("user:1"), Login: "test", Name: "test", Email: "test", @@ -137,17 +137,17 @@ func TestOrgSync_SetDefaultOrgHook(t *testing.T) { { name: "should set default org", defaultOrgSetting: 2, - identity: &authn.Identity{ID: "user:1"}, + identity: &authn.Identity{ID: authn.MustParseNamespaceID("user:1")}, setupMock: func(userService *usertest.MockService, orgService *orgtest.FakeOrgService) { - userService.On("SetUsingOrg", mock.Anything, mock.MatchedBy(func(cmd *user.SetUsingOrgCommand) bool { - return cmd.UserID == 1 && cmd.OrgID == 2 + userService.On("Update", mock.Anything, mock.MatchedBy(func(cmd *user.UpdateUserCommand) bool { + return cmd.UserID == 1 && *cmd.OrgID == 2 })).Return(nil) }, }, { name: "should skip setting the default org when default org is not set", defaultOrgSetting: -1, - identity: &authn.Identity{ID: "user:1"}, + identity: &authn.Identity{ID: authn.MustParseNamespaceID("user:1")}, }, { name: "should skip setting the default org when identity is nil", @@ -157,28 +157,28 @@ func TestOrgSync_SetDefaultOrgHook(t *testing.T) { { name: "should skip setting the default org when input err is not nil", defaultOrgSetting: 2, - identity: &authn.Identity{ID: "user:1"}, + identity: &authn.Identity{ID: authn.MustParseNamespaceID("user:1")}, inputErr: fmt.Errorf("error"), }, { name: "should skip setting the default org when identity is not a user", defaultOrgSetting: 2, - identity: &authn.Identity{ID: "service-account:1"}, + identity: &authn.Identity{ID: authn.MustParseNamespaceID("service-account:1")}, }, { name: "should skip setting the default org when user id is not valid", defaultOrgSetting: 2, - identity: &authn.Identity{ID: "user:invalid"}, + identity: &authn.Identity{ID: authn.MustParseNamespaceID("user:invalid")}, }, { name: "should skip setting the default org when user is not allowed to use the configured default org", defaultOrgSetting: 3, - identity: &authn.Identity{ID: "user:1"}, + identity: &authn.Identity{ID: authn.MustParseNamespaceID("user:1")}, }, { name: "should skip setting the default org when validateUsingOrg returns error", defaultOrgSetting: 2, - identity: &authn.Identity{ID: "user:1"}, + identity: &authn.Identity{ID: authn.MustParseNamespaceID("user:1")}, setupMock: func(userService *usertest.MockService, orgService *orgtest.FakeOrgService) { orgService.ExpectedError = fmt.Errorf("error") }, @@ -186,9 +186,9 @@ func TestOrgSync_SetDefaultOrgHook(t *testing.T) { { name: "should skip the hook when the user org update was unsuccessful", defaultOrgSetting: 2, - identity: &authn.Identity{ID: "user:1"}, + identity: &authn.Identity{ID: authn.MustParseNamespaceID("user:1")}, setupMock: func(userService *usertest.MockService, orgService *orgtest.FakeOrgService) { - userService.On("SetUsingOrg", mock.Anything, mock.Anything).Return(fmt.Errorf("error")) + userService.On("Update", mock.Anything, mock.Anything).Return(fmt.Errorf("error")) }, }, } @@ -217,8 +217,6 @@ func TestOrgSync_SetDefaultOrgHook(t *testing.T) { } s.SetDefaultOrgHook(context.Background(), tt.identity, nil, tt.inputErr) - - userService.AssertExpectations(t) }) } } diff --git a/pkg/services/authn/authnimpl/sync/rbac_sync.go b/pkg/services/authn/authnimpl/sync/rbac_sync.go index 121c2dcadbc..df10c2414ae 100644 --- a/pkg/services/authn/authnimpl/sync/rbac_sync.go +++ b/pkg/services/authn/authnimpl/sync/rbac_sync.go @@ -6,7 +6,6 @@ import ( "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/services/accesscontrol" - "github.com/grafana/grafana/pkg/services/auth/identity" "github.com/grafana/grafana/pkg/services/authn" "github.com/grafana/grafana/pkg/services/login" "github.com/grafana/grafana/pkg/services/org" @@ -98,13 +97,12 @@ func (s *RBACSync) SyncCloudRoles(ctx context.Context, ident *authn.Identity, r return nil } - namespace, id := ident.GetNamespacedID() - if namespace != authn.NamespaceUser { + if !ident.ID.IsNamespace(authn.NamespaceUser) { s.log.FromContext(ctx).Debug("Skip syncing cloud role", "id", ident.ID) return nil } - userID, err := identity.IntIdentifier(namespace, id) + userID, err := ident.ID.ParseInt() if err != nil { return err } diff --git a/pkg/services/authn/authnimpl/sync/rbac_sync_test.go b/pkg/services/authn/authnimpl/sync/rbac_sync_test.go index 27436998f4d..a29c4c7c814 100644 --- a/pkg/services/authn/authnimpl/sync/rbac_sync_test.go +++ b/pkg/services/authn/authnimpl/sync/rbac_sync_test.go @@ -7,7 +7,6 @@ import ( "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/services/accesscontrol" acmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock" - "github.com/grafana/grafana/pkg/services/auth/identity" "github.com/grafana/grafana/pkg/services/authn" "github.com/grafana/grafana/pkg/services/login" "github.com/grafana/grafana/pkg/services/org" @@ -24,14 +23,14 @@ func TestRBACSync_SyncPermission(t *testing.T) { testCases := []testCase{ { name: "enriches the identity successfully when SyncPermissions is true", - identity: &authn.Identity{ID: "user:2", OrgID: 1, ClientParams: authn.ClientParams{SyncPermissions: true}}, + identity: &authn.Identity{ID: authn.MustParseNamespaceID("user:2"), OrgID: 1, ClientParams: authn.ClientParams{SyncPermissions: true}}, expectedPermissions: []accesscontrol.Permission{ {Action: accesscontrol.ActionUsersRead}, }, }, { name: "does not load the permissions when SyncPermissions is false", - identity: &authn.Identity{ID: "user:2", OrgID: 1, ClientParams: authn.ClientParams{SyncPermissions: true}}, + identity: &authn.Identity{ID: authn.MustParseNamespaceID("user:2"), OrgID: 1, ClientParams: authn.ClientParams{SyncPermissions: true}}, expectedPermissions: []accesscontrol.Permission{ {Action: accesscontrol.ActionUsersRead}, }, @@ -65,7 +64,7 @@ func TestRBACSync_SyncCloudRoles(t *testing.T) { desc: "should call sync when authenticated with grafana com and has viewer role", module: login.GrafanaComAuthModule, identity: &authn.Identity{ - ID: authn.NamespacedID(authn.NamespaceUser, 1), + ID: authn.MustNewNamespaceID(authn.NamespaceUser, 1), OrgID: 1, OrgRoles: map[int64]org.RoleType{1: org.RoleViewer}, }, @@ -76,7 +75,7 @@ func TestRBACSync_SyncCloudRoles(t *testing.T) { desc: "should call sync when authenticated with grafana com and has editor role", module: login.GrafanaComAuthModule, identity: &authn.Identity{ - ID: authn.NamespacedID(authn.NamespaceUser, 1), + ID: authn.MustNewNamespaceID(authn.NamespaceUser, 1), OrgID: 1, OrgRoles: map[int64]org.RoleType{1: org.RoleEditor}, }, @@ -87,7 +86,7 @@ func TestRBACSync_SyncCloudRoles(t *testing.T) { desc: "should call sync when authenticated with grafana com and has admin role", module: login.GrafanaComAuthModule, identity: &authn.Identity{ - ID: authn.NamespacedID(authn.NamespaceUser, 1), + ID: authn.MustNewNamespaceID(authn.NamespaceUser, 1), OrgID: 1, OrgRoles: map[int64]org.RoleType{1: org.RoleAdmin}, }, @@ -98,7 +97,7 @@ func TestRBACSync_SyncCloudRoles(t *testing.T) { desc: "should not call sync when authenticated with grafana com and has invalid role", module: login.GrafanaComAuthModule, identity: &authn.Identity{ - ID: authn.NamespacedID(authn.NamespaceUser, 1), + ID: authn.MustNewNamespaceID(authn.NamespaceUser, 1), OrgID: 1, OrgRoles: map[int64]org.RoleType{1: org.RoleType("something else")}, }, @@ -109,7 +108,7 @@ func TestRBACSync_SyncCloudRoles(t *testing.T) { desc: "should not call sync when not authenticated with grafana com", module: login.LDAPAuthModule, identity: &authn.Identity{ - ID: authn.NamespacedID(authn.NamespaceUser, 1), + ID: authn.MustNewNamespaceID(authn.NamespaceUser, 1), OrgID: 1, OrgRoles: map[int64]org.RoleType{1: org.RoleAdmin}, }, @@ -143,7 +142,7 @@ func TestRBACSync_SyncCloudRoles(t *testing.T) { func setupTestEnv() *RBACSync { acMock := &acmock.Mock{ - GetUserPermissionsFunc: func(ctx context.Context, siu identity.Requester, o accesscontrol.Options) ([]accesscontrol.Permission, error) { + GetUserPermissionsFunc: func(ctx context.Context, siu authn.Requester, o accesscontrol.Options) ([]accesscontrol.Permission, error) { return []accesscontrol.Permission{ {Action: accesscontrol.ActionUsersRead}, }, nil diff --git a/pkg/services/authn/authnimpl/sync/user_sync.go b/pkg/services/authn/authnimpl/sync/user_sync.go index fb6d4d006e3..864e0d57168 100644 --- a/pkg/services/authn/authnimpl/sync/user_sync.go +++ b/pkg/services/authn/authnimpl/sync/user_sync.go @@ -4,10 +4,8 @@ import ( "context" "errors" "fmt" - "strconv" "github.com/grafana/grafana/pkg/infra/log" - authidentity "github.com/grafana/grafana/pkg/services/auth/identity" "github.com/grafana/grafana/pkg/services/authn" "github.com/grafana/grafana/pkg/services/login" "github.com/grafana/grafana/pkg/services/org" @@ -112,14 +110,13 @@ func (s *UserSync) FetchSyncedUserHook(ctx context.Context, identity *authn.Iden return nil } - namespace, id := identity.GetNamespacedID() - if !authidentity.IsNamespace(namespace, authn.NamespaceUser, authn.NamespaceServiceAccount) { + if !identity.ID.IsNamespace(authn.NamespaceUser, authn.NamespaceServiceAccount) { return nil } - userID, err := strconv.ParseInt(id, 10, 64) + userID, err := identity.ID.ParseInt() if err != nil { - s.log.FromContext(ctx).Warn("got invalid identity ID", "id", id, "err", err) + s.log.FromContext(ctx).Warn("got invalid identity ID", "id", identity.ID, "err", err) return nil } @@ -151,14 +148,13 @@ func (s *UserSync) SyncLastSeenHook(ctx context.Context, identity *authn.Identit return nil } - namespace, id := identity.GetNamespacedID() - if namespace != authn.NamespaceUser && namespace != authn.NamespaceServiceAccount { + if !identity.ID.IsNamespace(authn.NamespaceUser, authn.NamespaceServiceAccount) { return nil } - userID, err := authidentity.IntIdentifier(namespace, id) + userID, err := identity.ID.ParseInt() if err != nil { - s.log.FromContext(ctx).Warn("got invalid identity ID", "id", id, "err", err) + s.log.FromContext(ctx).Warn("got invalid identity ID", "id", identity.ID, "err", err) return nil } @@ -184,14 +180,13 @@ func (s *UserSync) EnableUserHook(ctx context.Context, identity *authn.Identity, return nil } - namespace, id := identity.GetNamespacedID() - if namespace != authn.NamespaceUser { + if !identity.ID.IsNamespace(authn.NamespaceUser) { return nil } - userID, err := authidentity.IntIdentifier(namespace, id) + userID, err := identity.ID.ParseInt() if err != nil { - s.log.FromContext(ctx).Warn("got invalid identity ID", "id", id, "err", err) + s.log.FromContext(ctx).Warn("got invalid identity ID", "id", identity.ID, "err", err) return nil } @@ -394,7 +389,7 @@ func (s *UserSync) lookupByOneOf(ctx context.Context, params login.UserLookupPar // syncUserToIdentity syncs a user to an identity. // This is used to update the identity with the latest user information. func syncUserToIdentity(usr *user.User, id *authn.Identity) { - id.ID = authn.NamespacedID(authn.NamespaceUser, usr.ID) + id.ID = authn.NewNamespaceIDUnchecked(authn.NamespaceUser, usr.ID) id.Login = usr.Login id.Email = usr.Email id.Name = usr.Name diff --git a/pkg/services/authn/authnimpl/sync/user_sync_test.go b/pkg/services/authn/authnimpl/sync/user_sync_test.go index f34b16c168d..4e861a6ad1f 100644 --- a/pkg/services/authn/authnimpl/sync/user_sync_test.go +++ b/pkg/services/authn/authnimpl/sync/user_sync_test.go @@ -110,7 +110,6 @@ func TestUserSync_SyncUserHook(t *testing.T) { args: args{ ctx: context.Background(), id: &authn.Identity{ - ID: "", Login: "test", Name: "test", Email: "test", @@ -124,7 +123,6 @@ func TestUserSync_SyncUserHook(t *testing.T) { }, wantErr: false, wantID: &authn.Identity{ - ID: "", Login: "test", Name: "test", Email: "test", @@ -146,7 +144,6 @@ func TestUserSync_SyncUserHook(t *testing.T) { args: args{ ctx: context.Background(), id: &authn.Identity{ - ID: "", Login: "test", Name: "test", Email: "test", @@ -161,7 +158,7 @@ func TestUserSync_SyncUserHook(t *testing.T) { }, wantErr: false, wantID: &authn.Identity{ - ID: "user:1", + ID: authn.MustParseNamespaceID("user:1"), Login: "test", Name: "test", Email: "test", @@ -185,7 +182,6 @@ func TestUserSync_SyncUserHook(t *testing.T) { args: args{ ctx: context.Background(), id: &authn.Identity{ - ID: "", Login: "test", Name: "test", Email: "test", @@ -200,7 +196,7 @@ func TestUserSync_SyncUserHook(t *testing.T) { }, wantErr: false, wantID: &authn.Identity{ - ID: "user:1", + ID: authn.MustParseNamespaceID("user:1"), Login: "test", Name: "test", Email: "test", @@ -224,7 +220,6 @@ func TestUserSync_SyncUserHook(t *testing.T) { args: args{ ctx: context.Background(), id: &authn.Identity{ - ID: "", AuthID: "2032", AuthenticatedBy: "oauth", Login: "test", @@ -241,7 +236,7 @@ func TestUserSync_SyncUserHook(t *testing.T) { }, wantErr: false, wantID: &authn.Identity{ - ID: "user:1", + ID: authn.MustParseNamespaceID("user:1"), AuthID: "2032", AuthenticatedBy: "oauth", Login: "test", @@ -267,7 +262,6 @@ func TestUserSync_SyncUserHook(t *testing.T) { args: args{ ctx: context.Background(), id: &authn.Identity{ - ID: "", Login: "test", Name: "test", Email: "test", @@ -294,7 +288,6 @@ func TestUserSync_SyncUserHook(t *testing.T) { args: args{ ctx: context.Background(), id: &authn.Identity{ - ID: "", Login: "test_create", Name: "test_create", IsGrafanaAdmin: ptrBool(true), @@ -314,7 +307,7 @@ func TestUserSync_SyncUserHook(t *testing.T) { }, wantErr: false, wantID: &authn.Identity{ - ID: "user:2", + ID: authn.MustParseNamespaceID("user:2"), Login: "test_create", Name: "test_create", Email: "test_create", @@ -342,7 +335,6 @@ func TestUserSync_SyncUserHook(t *testing.T) { args: args{ ctx: context.Background(), id: &authn.Identity{ - ID: "", Login: "test_mod", Name: "test_mod", Email: "test_mod", @@ -360,7 +352,7 @@ func TestUserSync_SyncUserHook(t *testing.T) { }, wantErr: false, wantID: &authn.Identity{ - ID: "user:3", + ID: authn.MustParseNamespaceID("user:3"), Login: "test_mod", Name: "test_mod", Email: "test_mod", @@ -386,7 +378,6 @@ func TestUserSync_SyncUserHook(t *testing.T) { args: args{ ctx: context.Background(), id: &authn.Identity{ - ID: "", Login: "test", Name: "test", Email: "test_mod@test.com", @@ -405,7 +396,7 @@ func TestUserSync_SyncUserHook(t *testing.T) { }, wantErr: false, wantID: &authn.Identity{ - ID: "user:3", + ID: authn.MustParseNamespaceID("user:3"), Login: "test", Name: "test", Email: "test_mod@test.com", @@ -455,7 +446,7 @@ func TestUserSync_FetchSyncedUserHook(t *testing.T) { { desc: "should skip hook when identity is not a user", req: &authn.Request{}, - identity: &authn.Identity{ID: "apikey:1", ClientParams: authn.ClientParams{FetchSyncedUser: true}}, + identity: &authn.Identity{ID: authn.MustParseNamespaceID("api-key:1"), ClientParams: authn.ClientParams{FetchSyncedUser: true}}, }, } @@ -479,7 +470,7 @@ func TestUserSync_EnableDisabledUserHook(t *testing.T) { { desc: "should skip if correct flag is not set", identity: &authn.Identity{ - ID: authn.NamespacedID(authn.NamespaceUser, 1), + ID: authn.MustNewNamespaceID(authn.NamespaceUser, 1), IsDisabled: true, ClientParams: authn.ClientParams{EnableUser: false}, }, @@ -488,7 +479,7 @@ func TestUserSync_EnableDisabledUserHook(t *testing.T) { { desc: "should skip if identity is not a user", identity: &authn.Identity{ - ID: authn.NamespacedID(authn.NamespaceAPIKey, 1), + ID: authn.MustNewNamespaceID(authn.NamespaceAPIKey, 1), IsDisabled: true, ClientParams: authn.ClientParams{EnableUser: true}, }, @@ -497,7 +488,7 @@ func TestUserSync_EnableDisabledUserHook(t *testing.T) { { desc: "should enabled disabled user", identity: &authn.Identity{ - ID: authn.NamespacedID(authn.NamespaceUser, 1), + ID: authn.MustNewNamespaceID(authn.NamespaceUser, 1), IsDisabled: true, ClientParams: authn.ClientParams{EnableUser: true}, }, diff --git a/pkg/services/authn/clients/api_key.go b/pkg/services/authn/clients/api_key.go index c2679470fc7..d01e33e5766 100644 --- a/pkg/services/authn/clients/api_key.go +++ b/pkg/services/authn/clients/api_key.go @@ -10,7 +10,6 @@ import ( "github.com/grafana/grafana/pkg/components/satokengen" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/services/apikey" - authidentity "github.com/grafana/grafana/pkg/services/auth/identity" "github.com/grafana/grafana/pkg/services/authn" "github.com/grafana/grafana/pkg/services/login" "github.com/grafana/grafana/pkg/services/org" @@ -141,7 +140,7 @@ func (s *APIKey) Namespace() string { func (s *APIKey) ResolveIdentity(ctx context.Context, orgID int64, namespaceID authn.NamespaceID) (*authn.Identity, error) { if !namespaceID.IsNamespace(authn.NamespaceAPIKey) { - return nil, authn.ErrInvalidNamepsaceID.Errorf("got unspected namespace: %s", namespaceID.Namespace()) + return nil, authn.ErrInvalidNamespaceID.Errorf("got unspected namespace: %s", namespaceID.Namespace()) } apiKeyID, err := namespaceID.ParseInt() @@ -161,7 +160,7 @@ func (s *APIKey) ResolveIdentity(ctx context.Context, orgID int64, namespaceID a } if key.ServiceAccountId != nil && *key.ServiceAccountId >= 1 { - return nil, authn.ErrInvalidNamepsaceID.Errorf("api key belongs to service account") + return nil, authn.ErrInvalidNamespaceID.Errorf("api key belongs to service account") } return newAPIKeyIdentity(key), nil @@ -189,18 +188,17 @@ func (s *APIKey) Hook(ctx context.Context, identity *authn.Identity, r *authn.Re } func (s *APIKey) getAPIKeyID(ctx context.Context, identity *authn.Identity, r *authn.Request) (apiKeyID int64, exists bool) { - namespace, identifier := identity.GetNamespacedID() - - id, err := authidentity.IntIdentifier(namespace, identifier) + id, err := identity.ID.ParseInt() if err != nil { s.log.Warn("Failed to parse ID from identifier", "err", err) return -1, false } - if namespace == authn.NamespaceAPIKey { + + if identity.ID.IsNamespace(authn.NamespaceAPIKey) { return id, true } - if namespace == authn.NamespaceServiceAccount { + if identity.ID.IsNamespace(authn.NamespaceServiceAccount) { // When the identity is service account, the ID in from the namespace is the service account ID. // We need to fetch the API key in this scenario, as we could use it to uniquely identify a service account token. apiKey, err := s.getAPIKey(ctx, getTokenFromRequest(r)) @@ -211,6 +209,7 @@ func (s *APIKey) getAPIKeyID(ctx context.Context, identity *authn.Identity, r *a return apiKey.ID, true } + return -1, false } @@ -256,7 +255,7 @@ func validateApiKey(orgID int64, key *apikey.APIKey) error { func newAPIKeyIdentity(key *apikey.APIKey) *authn.Identity { return &authn.Identity{ - ID: authn.NamespacedID(authn.NamespaceAPIKey, key.ID), + ID: authn.NewNamespaceIDUnchecked(authn.NamespaceAPIKey, key.ID), OrgID: key.OrgID, OrgRoles: map[int64]org.RoleType{key.OrgID: key.Role}, ClientParams: authn.ClientParams{SyncPermissions: true}, @@ -266,7 +265,7 @@ func newAPIKeyIdentity(key *apikey.APIKey) *authn.Identity { func newServiceAccountIdentity(key *apikey.APIKey) *authn.Identity { return &authn.Identity{ - ID: authn.NamespacedID(authn.NamespaceServiceAccount, *key.ServiceAccountId), + ID: authn.NewNamespaceIDUnchecked(authn.NamespaceServiceAccount, *key.ServiceAccountId), OrgID: key.OrgID, AuthenticatedBy: login.APIKeyAuthModule, ClientParams: authn.ClientParams{FetchSyncedUser: true, SyncPermissions: true}, diff --git a/pkg/services/authn/clients/api_key_test.go b/pkg/services/authn/clients/api_key_test.go index 2905503a633..d52289c643f 100644 --- a/pkg/services/authn/clients/api_key_test.go +++ b/pkg/services/authn/clients/api_key_test.go @@ -47,7 +47,7 @@ func TestAPIKey_Authenticate(t *testing.T) { Role: org.RoleAdmin, }, expectedIdentity: &authn.Identity{ - ID: "api-key:1", + ID: authn.MustParseNamespaceID("api-key:1"), OrgID: 1, OrgRoles: map[int64]org.RoleType{1: org.RoleAdmin}, ClientParams: authn.ClientParams{ @@ -70,7 +70,7 @@ func TestAPIKey_Authenticate(t *testing.T) { ServiceAccountId: intPtr(1), }, expectedIdentity: &authn.Identity{ - ID: "service-account:1", + ID: authn.MustParseNamespaceID("service-account:1"), OrgID: 1, ClientParams: authn.ClientParams{ FetchSyncedUser: true, @@ -205,7 +205,7 @@ func TestAPIKey_GetAPIKeyIDFromIdentity(t *testing.T) { ServiceAccountId: intPtr(1), }, expectedIdentity: &authn.Identity{ - ID: "service-account:1", + ID: authn.MustParseNamespaceID("service-account:1"), OrgID: 1, Name: "test", AuthenticatedBy: login.APIKeyAuthModule, @@ -221,7 +221,7 @@ func TestAPIKey_GetAPIKeyIDFromIdentity(t *testing.T) { Key: hash, }, expectedIdentity: &authn.Identity{ - ID: "api-key:2", + ID: authn.MustParseNamespaceID("api-key:2"), OrgID: 1, Name: "test", AuthenticatedBy: login.APIKeyAuthModule, @@ -237,7 +237,7 @@ func TestAPIKey_GetAPIKeyIDFromIdentity(t *testing.T) { Key: hash, }, expectedIdentity: &authn.Identity{ - ID: "user:2", + ID: authn.MustParseNamespaceID("user:2"), OrgID: 1, Name: "test", AuthenticatedBy: login.APIKeyAuthModule, @@ -253,7 +253,7 @@ func TestAPIKey_GetAPIKeyIDFromIdentity(t *testing.T) { Key: hash, }, expectedIdentity: &authn.Identity{ - ID: "service-account:2", + ID: authn.MustParseNamespaceID("service-account:2"), OrgID: 1, Name: "test", AuthenticatedBy: login.APIKeyAuthModule, @@ -298,7 +298,7 @@ func TestAPIKey_ResolveIdentity(t *testing.T) { { desc: "should return error for invalid namespace", namespaceID: authn.MustParseNamespaceID("user:1"), - expectedErr: authn.ErrInvalidNamepsaceID, + expectedErr: authn.ErrInvalidNamespaceID, }, { desc: "should return error when api key has expired", @@ -328,7 +328,7 @@ func TestAPIKey_ResolveIdentity(t *testing.T) { OrgID: 1, ServiceAccountId: intPtr(1), }, - expectedErr: authn.ErrInvalidNamepsaceID, + expectedErr: authn.ErrInvalidNamespaceID, }, { desc: "should return error when api key is belongs to different org", @@ -351,7 +351,7 @@ func TestAPIKey_ResolveIdentity(t *testing.T) { expectedIdenity: &authn.Identity{ OrgID: 1, OrgRoles: map[int64]org.RoleType{1: org.RoleEditor}, - ID: "api-key:1", + ID: authn.MustParseNamespaceID("api-key:1"), AuthenticatedBy: login.APIKeyAuthModule, ClientParams: authn.ClientParams{SyncPermissions: true}, }, diff --git a/pkg/services/authn/clients/basic_test.go b/pkg/services/authn/clients/basic_test.go index fbf2a96a2d7..f92f29a3144 100644 --- a/pkg/services/authn/clients/basic_test.go +++ b/pkg/services/authn/clients/basic_test.go @@ -24,8 +24,8 @@ func TestBasic_Authenticate(t *testing.T) { { desc: "should success when password client return identity", req: &authn.Request{HTTPRequest: &http.Request{Header: map[string][]string{authorizationHeaderName: {encodeBasicAuth("user", "password")}}}}, - client: authntest.FakePasswordClient{ExpectedIdentity: &authn.Identity{ID: "user:1"}}, - expectedIdentity: &authn.Identity{ID: "user:1"}, + client: authntest.FakePasswordClient{ExpectedIdentity: &authn.Identity{ID: authn.MustParseNamespaceID("user:1")}}, + expectedIdentity: &authn.Identity{ID: authn.MustParseNamespaceID("user:1")}, }, { desc: "should fail when basic auth header could not be decoded", diff --git a/pkg/services/authn/clients/ext_jwt.go b/pkg/services/authn/clients/ext_jwt.go index 303b14d9cad..446a7b0ee05 100644 --- a/pkg/services/authn/clients/ext_jwt.go +++ b/pkg/services/authn/clients/ext_jwt.go @@ -109,8 +109,13 @@ func (s *ExtendedJWT) authenticateAsUser(idTokenClaims, return nil, errJWTInvalid.Errorf("Failed to parse sub: %w", err) } + id, err := authn.ParseNamespaceID(idTokenClaims.Subject) + if err != nil { + return nil, err + } + return &authn.Identity{ - ID: idTokenClaims.Subject, + ID: id, OrgID: s.getDefaultOrgID(), AuthenticatedBy: login.ExtendedJWTModule, AuthID: accessTokenClaims.Subject, @@ -129,8 +134,13 @@ func (s *ExtendedJWT) authenticateAsService(claims *ExtendedJWTClaims) (*authn.I return nil, errJWTInvalid.Errorf("Failed to parse sub: %s", "invalid subject format") } + id, err := authn.ParseNamespaceID(claims.Subject) + if err != nil { + return nil, err + } + return &authn.Identity{ - ID: claims.Subject, + ID: id, OrgID: s.getDefaultOrgID(), AuthenticatedBy: login.ExtendedJWTModule, AuthID: claims.Subject, diff --git a/pkg/services/authn/clients/ext_jwt_test.go b/pkg/services/authn/clients/ext_jwt_test.go index fe8c1135897..9bbba9a684c 100644 --- a/pkg/services/authn/clients/ext_jwt_test.go +++ b/pkg/services/authn/clients/ext_jwt_test.go @@ -166,7 +166,7 @@ func TestExtendedJWT_Authenticate(t *testing.T) { orgID: 1, want: &authn.Identity{OrgID: 1, OrgName: "", OrgRoles: map[int64]roletype.RoleType(nil), - ID: "access-policy:this-uid", Login: "", Name: "", Email: "", + ID: authn.MustParseNamespaceID("access-policy:this-uid"), Login: "", Name: "", Email: "", IsGrafanaAdmin: (*bool)(nil), AuthenticatedBy: "extendedjwt", AuthID: "access-policy:this-uid", IsDisabled: false, HelpFlags1: 0x0, LastSeenAt: time.Date(1, time.January, 1, 0, 0, 0, 0, time.UTC), @@ -196,7 +196,7 @@ func TestExtendedJWT_Authenticate(t *testing.T) { } }, want: &authn.Identity{OrgID: 1, OrgName: "", - OrgRoles: map[int64]roletype.RoleType(nil), ID: "user:2", + OrgRoles: map[int64]roletype.RoleType(nil), ID: authn.MustParseNamespaceID("user:2"), Login: "", Name: "", Email: "", IsGrafanaAdmin: (*bool)(nil), AuthenticatedBy: "extendedjwt", AuthID: "access-policy:this-uid", IsDisabled: false, HelpFlags1: 0x0, diff --git a/pkg/services/authn/clients/grafana.go b/pkg/services/authn/clients/grafana.go index 10e33008600..e95fb2f5828 100644 --- a/pkg/services/authn/clients/grafana.go +++ b/pkg/services/authn/clients/grafana.go @@ -104,12 +104,12 @@ func (c *Grafana) AuthenticatePassword(ctx context.Context, r *authn.Request, us return nil, errInvalidPassword.Errorf("invalid password") } - signedInUser, err := c.userService.GetSignedInUserWithCacheCtx(ctx, &user.GetSignedInUserQuery{OrgID: r.OrgID, UserID: usr.ID}) - if err != nil { - return nil, err - } - - return authn.IdentityFromSignedInUser(authn.NamespacedID(authn.NamespaceUser, signedInUser.UserID), signedInUser, authn.ClientParams{SyncPermissions: true}, login.PasswordAuthModule), nil + return &authn.Identity{ + ID: authn.NewNamespaceIDUnchecked(authn.NamespaceUser, usr.ID), + OrgID: r.OrgID, + ClientParams: authn.ClientParams{FetchSyncedUser: true, SyncPermissions: true}, + AuthenticatedBy: login.PasswordAuthModule, + }, nil } func comparePassword(password, salt, hash string) bool { diff --git a/pkg/services/authn/clients/grafana_test.go b/pkg/services/authn/clients/grafana_test.go index b2cb45154ec..d77afb239a2 100644 --- a/pkg/services/authn/clients/grafana_test.go +++ b/pkg/services/authn/clients/grafana_test.go @@ -125,29 +125,25 @@ func TestGrafana_AuthenticateProxy(t *testing.T) { func TestGrafana_AuthenticatePassword(t *testing.T) { type testCase struct { - desc string - username string - password string - findUser bool - expectedErr error - expectedIdentity *authn.Identity - expectedSignedInUser *user.SignedInUser + desc string + username string + password string + findUser bool + expectedErr error + expectedIdentity *authn.Identity } tests := []testCase{ { - desc: "should successfully authenticate user with correct password", - username: "user", - password: "password", - findUser: true, - expectedSignedInUser: &user.SignedInUser{UserID: 1, OrgID: 1, OrgRole: "Viewer"}, + desc: "should successfully authenticate user with correct password", + username: "user", + password: "password", + findUser: true, expectedIdentity: &authn.Identity{ - ID: "user:1", + ID: authn.MustParseNamespaceID("user:1"), OrgID: 1, - OrgRoles: map[int64]org.RoleType{1: "Viewer"}, - IsGrafanaAdmin: boolPtr(false), - ClientParams: authn.ClientParams{SyncPermissions: true}, AuthenticatedBy: login.PasswordAuthModule, + ClientParams: authn.ClientParams{FetchSyncedUser: true, SyncPermissions: true}, }, }, { @@ -169,8 +165,7 @@ func TestGrafana_AuthenticatePassword(t *testing.T) { t.Run(tt.desc, func(t *testing.T) { hashed, _ := util.EncodePassword("password", "salt") userService := &usertest.FakeUserService{ - ExpectedSignedInUser: tt.expectedSignedInUser, - ExpectedUser: &user.User{Password: user.Password(hashed), Salt: "salt"}, + ExpectedUser: &user.User{ID: 1, Password: user.Password(hashed), Salt: "salt"}, } if !tt.findUser { diff --git a/pkg/services/authn/clients/jwt_test.go b/pkg/services/authn/clients/jwt_test.go index f5d0dbd09ad..538ffb26e65 100644 --- a/pkg/services/authn/clients/jwt_test.go +++ b/pkg/services/authn/clients/jwt_test.go @@ -40,7 +40,6 @@ func TestAuthenticateJWT(t *testing.T) { OrgName: "", OrgRoles: map[int64]roletype.RoleType{1: roletype.RoleAdmin}, Groups: []string{"foo", "bar"}, - ID: "", Login: "eai-doe", Name: "Eai Doe", Email: "eai.doe@cor.po", @@ -92,7 +91,6 @@ func TestAuthenticateJWT(t *testing.T) { OrgID: 0, OrgName: "", OrgRoles: map[int64]roletype.RoleType{1: roletype.RoleAdmin}, - ID: "", Login: "eai-doe", Groups: []string{}, Name: "Eai Doe", diff --git a/pkg/services/authn/clients/oauth.go b/pkg/services/authn/clients/oauth.go index 0a446102780..318e732971f 100644 --- a/pkg/services/authn/clients/oauth.go +++ b/pkg/services/authn/clients/oauth.go @@ -250,7 +250,7 @@ func (c *OAuth) RedirectURL(ctx context.Context, r *authn.Request) (*authn.Redir }, nil } -func (c *OAuth) Logout(ctx context.Context, user identity.Requester) (*authn.Redirect, bool) { +func (c *OAuth) Logout(ctx context.Context, user authn.Requester) (*authn.Redirect, bool) { token := c.oauthService.GetCurrentOAuthToken(ctx, user) namespace, id := user.GetNamespacedID() diff --git a/pkg/services/authn/clients/password_test.go b/pkg/services/authn/clients/password_test.go index dda54325146..76d9d87dc00 100644 --- a/pkg/services/authn/clients/password_test.go +++ b/pkg/services/authn/clients/password_test.go @@ -29,16 +29,16 @@ func TestPassword_AuthenticatePassword(t *testing.T) { username: "test", password: "test", req: &authn.Request{}, - clients: []authn.PasswordClient{authntest.FakePasswordClient{ExpectedIdentity: &authn.Identity{ID: "user:1"}}}, - expectedIdentity: &authn.Identity{ID: "user:1"}, + clients: []authn.PasswordClient{authntest.FakePasswordClient{ExpectedIdentity: &authn.Identity{ID: authn.MustParseNamespaceID("user:1")}}}, + expectedIdentity: &authn.Identity{ID: authn.MustParseNamespaceID("user:1")}, }, { desc: "should success when found in second client", username: "test", password: "test", req: &authn.Request{}, - clients: []authn.PasswordClient{authntest.FakePasswordClient{ExpectedErr: errIdentityNotFound}, authntest.FakePasswordClient{ExpectedIdentity: &authn.Identity{ID: "user:2"}}}, - expectedIdentity: &authn.Identity{ID: "user:2"}, + clients: []authn.PasswordClient{authntest.FakePasswordClient{ExpectedErr: errIdentityNotFound}, authntest.FakePasswordClient{ExpectedIdentity: &authn.Identity{ID: authn.MustParseNamespaceID("user:2")}}}, + expectedIdentity: &authn.Identity{ID: authn.MustParseNamespaceID("user:2")}, }, { desc: "should fail for empty password", diff --git a/pkg/services/authn/clients/proxy.go b/pkg/services/authn/clients/proxy.go index 157af7ee0af..92ebf0c2250 100644 --- a/pkg/services/authn/clients/proxy.go +++ b/pkg/services/authn/clients/proxy.go @@ -14,7 +14,6 @@ import ( "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/remotecache" - authidentity "github.com/grafana/grafana/pkg/services/auth/identity" "github.com/grafana/grafana/pkg/services/authn" "github.com/grafana/grafana/pkg/services/login" "github.com/grafana/grafana/pkg/setting" @@ -125,7 +124,7 @@ func (c *Proxy) retrieveIDFromCache(ctx context.Context, cacheKey string, r *aut } return &authn.Identity{ - ID: authn.NamespacedID(authn.NamespaceUser, uid), + ID: authn.NewNamespaceIDUnchecked(authn.NamespaceUser, uid), OrgID: r.OrgID, // FIXME: This does not match the actual auth module used, but should not have any impact // Maybe caching the auth module used with the user ID would be a good idea @@ -150,14 +149,13 @@ func (c *Proxy) Hook(ctx context.Context, identity *authn.Identity, r *authn.Req return nil } - namespace, identifier := identity.GetNamespacedID() - if namespace != authn.NamespaceUser { + if !identity.ID.IsNamespace(authn.NamespaceUser) { return nil } - id, err := authidentity.IntIdentifier(namespace, identifier) + id, err := identity.ID.ParseInt() if err != nil { - c.log.Warn("Failed to cache proxy user", "error", err, "userId", identifier, "err", err) + c.log.Warn("Failed to cache proxy user", "error", err, "userId", identity.ID.ID(), "err", err) return nil } diff --git a/pkg/services/authn/clients/proxy_test.go b/pkg/services/authn/clients/proxy_test.go index 1f980122476..d55bd68484e 100644 --- a/pkg/services/authn/clients/proxy_test.go +++ b/pkg/services/authn/clients/proxy_test.go @@ -202,8 +202,8 @@ func TestProxy_Hook(t *testing.T) { proxyFieldRole: "X-Role", } cache := &fakeCache{data: make(map[string][]byte)} - userId := 1 - userID := fmt.Sprintf("%s:%d", authn.NamespaceUser, userId) + userId := int64(1) + userID := authn.MustNewNamespaceID(authn.NamespaceUser, userId) // withRole creates a test case for a user with a specific role. withRole := func(role string) func(t *testing.T) { diff --git a/pkg/services/authn/clients/render.go b/pkg/services/authn/clients/render.go index 0a7b692029a..39f6922d258 100644 --- a/pkg/services/authn/clients/render.go +++ b/pkg/services/authn/clients/render.go @@ -42,7 +42,7 @@ func (c *Render) Authenticate(ctx context.Context, r *authn.Request) (*authn.Ide if renderUsr.UserID <= 0 { return &authn.Identity{ - ID: authn.NamespacedID(authn.NamespaceRenderService, 0), + ID: authn.NewNamespaceIDUnchecked(authn.NamespaceRenderService, 0), OrgID: renderUsr.OrgID, OrgRoles: map[int64]org.RoleType{renderUsr.OrgID: org.RoleType(renderUsr.OrgRole)}, ClientParams: authn.ClientParams{SyncPermissions: true}, @@ -52,7 +52,7 @@ func (c *Render) Authenticate(ctx context.Context, r *authn.Request) (*authn.Ide } return &authn.Identity{ - ID: authn.NamespacedID(authn.NamespaceUser, renderUsr.UserID), + ID: authn.NewNamespaceIDUnchecked(authn.NamespaceUser, renderUsr.UserID), LastSeenAt: time.Now(), AuthenticatedBy: login.RenderModule, ClientParams: authn.ClientParams{FetchSyncedUser: true, SyncPermissions: true}, diff --git a/pkg/services/authn/clients/render_test.go b/pkg/services/authn/clients/render_test.go index 24051db326f..8e815de266e 100644 --- a/pkg/services/authn/clients/render_test.go +++ b/pkg/services/authn/clients/render_test.go @@ -35,7 +35,7 @@ func TestRender_Authenticate(t *testing.T) { }, }, expectedIdentity: &authn.Identity{ - ID: "render:0", + ID: authn.MustParseNamespaceID("render:0"), OrgID: 1, OrgRoles: map[int64]org.RoleType{1: org.RoleViewer}, AuthenticatedBy: login.RenderModule, @@ -56,7 +56,7 @@ func TestRender_Authenticate(t *testing.T) { }, }, expectedIdentity: &authn.Identity{ - ID: "user:1", + ID: authn.MustParseNamespaceID("user:1"), AuthenticatedBy: login.RenderModule, ClientParams: authn.ClientParams{FetchSyncedUser: true, SyncPermissions: true}, }, diff --git a/pkg/services/authn/clients/session.go b/pkg/services/authn/clients/session.go index 02ec093b74e..dc928107ba5 100644 --- a/pkg/services/authn/clients/session.go +++ b/pkg/services/authn/clients/session.go @@ -57,7 +57,7 @@ func (s *Session) Authenticate(ctx context.Context, r *authn.Request) (*authn.Id } ident := &authn.Identity{ - ID: authn.NamespacedID(authn.NamespaceUser, token.UserId), + ID: authn.NewNamespaceIDUnchecked(authn.NamespaceUser, token.UserId), SessionToken: token, ClientParams: authn.ClientParams{ FetchSyncedUser: true, diff --git a/pkg/services/authn/clients/session_test.go b/pkg/services/authn/clients/session_test.go index 0315083ea5b..c8fbc329277 100644 --- a/pkg/services/authn/clients/session_test.go +++ b/pkg/services/authn/clients/session_test.go @@ -96,7 +96,7 @@ func TestSession_Authenticate(t *testing.T) { }, args: args{r: &authn.Request{HTTPRequest: validHTTPReq}}, wantID: &authn.Identity{ - ID: "user:1", + ID: authn.MustParseNamespaceID("user:1"), SessionToken: validToken, ClientParams: authn.ClientParams{ SyncPermissions: true, @@ -129,7 +129,7 @@ func TestSession_Authenticate(t *testing.T) { }, args: args{r: &authn.Request{HTTPRequest: validHTTPReq}}, wantID: &authn.Identity{ - ID: "user:1", + ID: authn.MustParseNamespaceID("user:1"), SessionToken: validToken, ClientParams: authn.ClientParams{ SyncPermissions: true, @@ -148,7 +148,7 @@ func TestSession_Authenticate(t *testing.T) { }, args: args{r: &authn.Request{HTTPRequest: validHTTPReq}}, wantID: &authn.Identity{ - ID: "user:1", + ID: authn.MustParseNamespaceID("user:1"), AuthID: "1", AuthenticatedBy: "oauth_azuread", SessionToken: validToken, @@ -170,7 +170,7 @@ func TestSession_Authenticate(t *testing.T) { }, args: args{r: &authn.Request{HTTPRequest: validHTTPReq}}, wantID: &authn.Identity{ - ID: "user:1", + ID: authn.MustParseNamespaceID("user:1"), SessionToken: validToken, ClientParams: authn.ClientParams{ diff --git a/pkg/services/authn/error.go b/pkg/services/authn/error.go index 5420ccd19ad..053ceeacfab 100644 --- a/pkg/services/authn/error.go +++ b/pkg/services/authn/error.go @@ -8,5 +8,4 @@ var ( ErrClientNotConfigured = errutil.BadRequest("auth.client.notConfigured") ErrUnsupportedIdentity = errutil.NotImplemented("auth.identity.unsupported") ErrExpiredAccessToken = errutil.Unauthorized("oauth.expired-token", errutil.WithPublicMessage("OAuth access token expired")) - ErrInvalidNamepsaceID = errutil.BadRequest("auth.identity.invalid-namespace-id") ) diff --git a/pkg/services/authn/identity.go b/pkg/services/authn/identity.go index 59b23faefbb..38d9a51c67b 100644 --- a/pkg/services/authn/identity.go +++ b/pkg/services/authn/identity.go @@ -3,12 +3,10 @@ package authn import ( "fmt" "strconv" - "strings" "time" "golang.org/x/oauth2" - "github.com/grafana/grafana/pkg/models/roletype" "github.com/grafana/grafana/pkg/models/usertoken" "github.com/grafana/grafana/pkg/services/auth/identity" "github.com/grafana/grafana/pkg/services/login" @@ -18,20 +16,22 @@ import ( const GlobalOrgID = int64(0) -var _ identity.Requester = (*Identity)(nil) +type Requester = identity.Requester + +var _ Requester = (*Identity)(nil) type Identity struct { + // ID is the unique identifier for the entity in the Grafana database. + // It is in the format : where namespace is one of the + // Namespace* constants. For example, "user:1" or "api-key:1". + // If the entity is not found in the DB or this entity is non-persistent, this field will be empty. + ID NamespaceID // OrgID is the active organization for the entity. OrgID int64 // OrgName is the name of the active organization. OrgName string // OrgRoles is the list of organizations the entity is a member of and their roles. OrgRoles map[int64]org.RoleType - // ID is the unique identifier for the entity in the Grafana database. - // It is in the format : where namespace is one of the - // Namespace* constants. For example, "user:1" or "api-key:1". - // If the entity is not found in the DB or this entity is non-persistent, this field will be empty. - ID string // Login is the shorthand identifier of the entity. Should be unique. Login string // Name is the display name of the entity. It is not guaranteed to be unique. @@ -74,15 +74,11 @@ type Identity struct { } func (i *Identity) GetID() string { - return i.ID + return i.ID.String() } func (i *Identity) GetNamespacedID() (namespace string, identifier string) { - split := strings.Split(i.GetID(), ":") - if len(split) != 2 { - return "", "" - } - return split[0], split[1] + return i.ID.Namespace(), i.ID.ID() } func (i *Identity) GetAuthID() string { @@ -136,13 +132,13 @@ func (i *Identity) GetOrgName() string { return i.OrgName } -func (i *Identity) GetOrgRole() roletype.RoleType { +func (i *Identity) GetOrgRole() org.RoleType { if i.OrgRoles == nil { - return roletype.RoleNone + return org.RoleNone } if i.OrgRoles[i.GetOrgID()] == "" { - return roletype.RoleNone + return org.RoleNone } return i.OrgRoles[i.GetOrgID()] @@ -177,7 +173,7 @@ func (i *Identity) GetTeams() []int64 { return i.Teams } -func (i *Identity) HasRole(role roletype.RoleType) bool { +func (i *Identity) HasRole(role org.RoleType) bool { if i.GetIsGrafanaAdmin() { return true } @@ -224,7 +220,7 @@ func (i *Identity) SignedInUser() *user.SignedInUser { Teams: i.Teams, Permissions: i.Permissions, IDToken: i.IDToken, - NamespacedID: i.ID, + NamespacedID: i.ID.String(), } if namespace == NamespaceAPIKey { @@ -263,25 +259,3 @@ func (i *Identity) ExternalUserInfo() login.ExternalUserInfo { IsDisabled: i.IsDisabled, } } - -// IdentityFromSignedInUser creates an identity from a SignedInUser. -func IdentityFromSignedInUser(id string, usr *user.SignedInUser, params ClientParams, authenticatedBy string) *Identity { - return &Identity{ - ID: id, - OrgID: usr.OrgID, - OrgName: usr.OrgName, - OrgRoles: map[int64]org.RoleType{usr.OrgID: usr.OrgRole}, - Login: usr.Login, - Name: usr.Name, - Email: usr.Email, - AuthenticatedBy: authenticatedBy, - IsGrafanaAdmin: &usr.IsGrafanaAdmin, - IsDisabled: usr.IsDisabled, - HelpFlags1: usr.HelpFlags1, - LastSeenAt: usr.LastSeenAt, - Teams: usr.Teams, - ClientParams: params, - Permissions: usr.Permissions, - IDToken: usr.IDToken, - } -} diff --git a/pkg/services/authn/namespace.go b/pkg/services/authn/namespace.go index c46da890e86..09d71052cc7 100644 --- a/pkg/services/authn/namespace.go +++ b/pkg/services/authn/namespace.go @@ -1,10 +1,6 @@ package authn import ( - "fmt" - "strconv" - "strings" - "github.com/grafana/grafana/pkg/services/auth/identity" ) @@ -15,75 +11,17 @@ const ( NamespaceAnonymous = identity.NamespaceAnonymous NamespaceRenderService = identity.NamespaceRenderService NamespaceAccessPolicy = identity.NamespaceAccessPolicy - AnonymousNamespaceID = NamespaceAnonymous + ":0" ) -var namespaceLookup = map[string]struct{}{ - NamespaceUser: {}, - NamespaceAPIKey: {}, - NamespaceServiceAccount: {}, - NamespaceAnonymous: {}, - NamespaceRenderService: {}, - NamespaceAccessPolicy: {}, -} +var AnonymousNamespaceID = MustNewNamespaceID(NamespaceAnonymous, 0) -// NamespacedID builds a namespaced ID from a namespace and an ID. -func NamespacedID(namespace string, id int64) string { - return fmt.Sprintf("%s:%d", namespace, id) -} +type NamespaceID = identity.NamespaceID -func ParseNamespaceID(str string) (NamespaceID, error) { - var namespaceID NamespaceID - - parts := strings.Split(str, ":") - if len(parts) != 2 { - return namespaceID, ErrInvalidNamepsaceID.Errorf("expected namespace id to have 2 parts") - } - - namespace, id := parts[0], parts[1] - - if _, ok := namespaceLookup[namespace]; !ok { - return namespaceID, ErrInvalidNamepsaceID.Errorf("got invalid namespace %s", namespace) - } - - namespaceID.id = id - namespaceID.namespace = namespace - - return namespaceID, nil -} - -// MustParseNamespaceID parses namespace id, it will panic it failes to do so. -// Sutable to use in tests or when we can garantuee that we pass a correct format. -func MustParseNamespaceID(str string) NamespaceID { - namespaceID, err := ParseNamespaceID(str) - if err != nil { - panic(err) - } - return namespaceID -} - -// FIXME: use this instead of encoded string through the codebase -type NamespaceID struct { - id string - namespace string -} - -func (ni NamespaceID) ID() string { - return ni.id -} - -func (ni NamespaceID) ParseInt() (int64, error) { - return strconv.ParseInt(ni.id, 10, 64) -} - -func (ni NamespaceID) Namespace() string { - return ni.namespace -} - -func (ni NamespaceID) IsNamespace(expected ...string) bool { - return identity.IsNamespace(ni.namespace, expected...) -} - -func (ni NamespaceID) String() string { - return fmt.Sprintf("%s:%s", ni.namespace, ni.id) -} +var ( + ParseNamespaceID = identity.ParseNamespaceID + MustParseNamespaceID = identity.MustParseNamespaceID + NewNamespaceID = identity.NewNamespaceID + MustNewNamespaceID = identity.MustNewNamespaceID + NewNamespaceIDUnchecked = identity.NewNamespaceIDUnchecked + ErrInvalidNamespaceID = identity.ErrInvalidNamespaceID +) diff --git a/pkg/services/cleanup/cleanup.go b/pkg/services/cleanup/cleanup.go index 3b1a2c5e201..46037db3b4e 100644 --- a/pkg/services/cleanup/cleanup.go +++ b/pkg/services/cleanup/cleanup.go @@ -257,7 +257,7 @@ func (srv *CleanUpService) expireOldVerifications(ctx context.Context) { func (srv *CleanUpService) deleteStaleShortURLs(ctx context.Context) { logger := srv.log.FromContext(ctx) cmd := shorturls.DeleteShortUrlCommand{ - OlderThan: time.Now().Add(-time.Hour * 24 * 7), + OlderThan: time.Now().Add(-time.Duration(srv.Cfg.ShortLinkExpiration*24) * time.Hour), } if err := srv.ShortURLService.DeleteStaleShortURLs(ctx, &cmd); err != nil { logger.Error("Problem deleting stale short urls", "error", err.Error()) diff --git a/pkg/services/contexthandler/contexthandler_test.go b/pkg/services/contexthandler/contexthandler_test.go index f7c46f7208c..3bdede4be3b 100644 --- a/pkg/services/contexthandler/contexthandler_test.go +++ b/pkg/services/contexthandler/contexthandler_test.go @@ -44,7 +44,7 @@ func TestContextHandler(t *testing.T) { }) t.Run("should set identity on successful authentication", func(t *testing.T) { - identity := &authn.Identity{ID: authn.NamespacedID(authn.NamespaceUser, 1), OrgID: 1} + identity := &authn.Identity{ID: authn.MustNewNamespaceID(authn.NamespaceUser, 1), OrgID: 1} handler := contexthandler.ProvideService( setting.NewCfg(), tracing.InitializeTracerForTest(), @@ -150,7 +150,7 @@ func TestContextHandler(t *testing.T) { cfg, tracing.InitializeTracerForTest(), featuremgmt.WithFeatures(), - &authntest.FakeService{ExpectedIdentity: &authn.Identity{ID: id}}, + &authntest.FakeService{ExpectedIdentity: &authn.Identity{ID: authn.MustParseNamespaceID(id)}}, ) server := webtest.NewServer(t, routing.NewRouteRegister()) diff --git a/pkg/services/dashboards/database/database_folder_test.go b/pkg/services/dashboards/database/database_folder_test.go index ae538afbd84..76694da9d26 100644 --- a/pkg/services/dashboards/database/database_folder_test.go +++ b/pkg/services/dashboards/database/database_folder_test.go @@ -24,7 +24,6 @@ import ( "github.com/grafana/grafana/pkg/services/guardian" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/org/orgimpl" - "github.com/grafana/grafana/pkg/services/quota/quotaimpl" "github.com/grafana/grafana/pkg/services/quota/quotatest" "github.com/grafana/grafana/pkg/services/supportbundles/supportbundlestest" "github.com/grafana/grafana/pkg/services/tag/tagimpl" @@ -47,8 +46,7 @@ func TestIntegrationDashboardFolderDataAccess(t *testing.T) { var dashboardStore dashboards.Store setup := func() { - sql := db.InitTestDB(t) - sqlStore, cfg = sql, sql.Cfg + sqlStore, cfg = db.InitTestDBWithCfg(t) quotaService := quotatest.New(false, nil) var err error dashboardStore, err = ProvideDashboardStore(sqlStore, cfg, testFeatureToggles, tagimpl.ProvideService(sqlStore), quotaService) @@ -147,8 +145,7 @@ func TestIntegrationDashboardFolderDataAccess(t *testing.T) { var currentUser *user.SignedInUser setup2 := func() { - sql := db.InitTestDB(t) - sqlStore, cfg = sql, sql.Cfg + sqlStore, cfg = db.InitTestDBWithCfg(t) quotaService := quotatest.New(false, nil) var err error dashboardStore, err = ProvideDashboardStore(sqlStore, cfg, testFeatureToggles, tagimpl.ProvideService(sqlStore), quotaService) @@ -253,8 +250,12 @@ func TestIntegrationDashboardInheritedFolderRBAC(t *testing.T) { var viewer *user.SignedInUser setup := func() { - sql := db.InitTestDB(t) - sqlStore, cfg = sql, sql.Cfg + sqlStore, cfg = db.InitTestDBWithCfg(t) + cfg.AutoAssignOrg = true + cfg.AutoAssignOrgId = 1 + cfg.AutoAssignOrgRole = string(org.RoleViewer) + + tracer := tracing.InitializeTracerForTest() quotaService := quotatest.New(false, nil) // enable nested folders so that the folder table is populated for all the tests @@ -264,18 +265,21 @@ func TestIntegrationDashboardInheritedFolderRBAC(t *testing.T) { dashboardWriteStore, err := ProvideDashboardStore(sqlStore, cfg, features, tagimpl.ProvideService(sqlStore), quotaService) require.NoError(t, err) - usr := createUser(t, sqlStore, cfg, "viewer", "Viewer", false) + orgService, err := orgimpl.ProvideService(sqlStore, cfg, quotaService) + require.NoError(t, err) + usrSvc, err := userimpl.ProvideService( + sqlStore, orgService, cfg, nil, nil, tracer, + quotaService, supportbundlestest.NewFakeBundleService(), + ) + require.NoError(t, err) + + usr := createUser(t, usrSvc, orgService, "viewer", false) viewer = &user.SignedInUser{ UserID: usr.ID, OrgID: usr.OrgID, OrgRole: org.RoleViewer, } - orgService, err := orgimpl.ProvideService(sqlStore, cfg, quotaService) - require.NoError(t, err) - usrSvc, err := userimpl.ProvideService(sqlStore, orgService, cfg, nil, nil, quotaService, supportbundlestest.NewFakeBundleService()) - require.NoError(t, err) - // create admin user in the same org currentUserCmd := user.CreateUserCommand{Login: "admin", Email: "admin@test.com", Name: "an admin", IsAdmin: false, OrgID: viewer.OrgID} u, err := usrSvc.Create(context.Background(), ¤tUserCmd) @@ -301,7 +305,7 @@ func TestIntegrationDashboardInheritedFolderRBAC(t *testing.T) { guardian.New = origNewGuardian }) - folderSvc := folderimpl.ProvideService(mock.New(), bus.ProvideBus(tracing.InitializeTracerForTest()), cfg, dashboardWriteStore, folderimpl.ProvideDashboardFolderStore(sqlStore), sqlStore, features, supportbundlestest.NewFakeBundleService(), nil) + folderSvc := folderimpl.ProvideService(mock.New(), bus.ProvideBus(tracer), cfg, dashboardWriteStore, folderimpl.ProvideDashboardFolderStore(sqlStore), sqlStore, features, supportbundlestest.NewFakeBundleService(), nil) parentUID := "" for i := 0; ; i++ { @@ -442,27 +446,14 @@ func moveDashboard(t *testing.T, dashboardStore dashboards.Store, orgId int64, d return dash } -func createUser(t *testing.T, sqlStore db.DB, cfg *setting.Cfg, name string, role string, isAdmin bool) user.User { +func createUser(t *testing.T, userSrv user.Service, orgSrv org.Service, name string, isAdmin bool) user.User { t.Helper() - cfg.AutoAssignOrg = true - cfg.AutoAssignOrgId = 1 - cfg.AutoAssignOrgRole = role - qs := quotaimpl.ProvideService(sqlStore, cfg) - orgService, err := orgimpl.ProvideService(sqlStore, cfg, qs) - require.NoError(t, err) - usrSvc, err := userimpl.ProvideService(sqlStore, orgService, cfg, nil, nil, qs, supportbundlestest.NewFakeBundleService()) - require.NoError(t, err) - - o, err := orgService.CreateWithMember(context.Background(), &org.CreateOrgCommand{Name: fmt.Sprintf("test org %d", time.Now().UnixNano())}) + o, err := orgSrv.CreateWithMember(context.Background(), &org.CreateOrgCommand{Name: fmt.Sprintf("test org %d", time.Now().UnixNano())}) require.NoError(t, err) currentUserCmd := user.CreateUserCommand{Login: name, Email: name + "@test.com", Name: "a " + name, IsAdmin: isAdmin, OrgID: o.ID} - currentUser, err := usrSvc.Create(context.Background(), ¤tUserCmd) + currentUser, err := userSrv.Create(context.Background(), ¤tUserCmd) require.NoError(t, err) - orgs, err := orgService.GetUserOrgList(context.Background(), &org.GetUserOrgListQuery{UserID: currentUser.ID}) - require.NoError(t, err) - require.Equal(t, org.RoleType(role), orgs[0].Role) - require.Equal(t, o.ID, orgs[0].OrgID) return *currentUser } diff --git a/pkg/services/dashboards/database/database_provisioning_test.go b/pkg/services/dashboards/database/database_provisioning_test.go index 4415cd77bec..798368370e9 100644 --- a/pkg/services/dashboards/database/database_provisioning_test.go +++ b/pkg/services/dashboards/database/database_provisioning_test.go @@ -18,9 +18,9 @@ func TestIntegrationDashboardProvisioningTest(t *testing.T) { if testing.Short() { t.Skip("skipping integration test") } - sqlStore := db.InitTestDB(t) + sqlStore, cfg := db.InitTestDBWithCfg(t) quotaService := quotatest.New(false, nil) - dashboardStore, err := ProvideDashboardStore(sqlStore, sqlStore.Cfg, testFeatureToggles, tagimpl.ProvideService(sqlStore), quotaService) + dashboardStore, err := ProvideDashboardStore(sqlStore, cfg, testFeatureToggles, tagimpl.ProvideService(sqlStore), quotaService) require.NoError(t, err) folderCmd := dashboards.SaveDashboardCommand{ diff --git a/pkg/services/dashboards/database/database_test.go b/pkg/services/dashboards/database/database_test.go index 8a72e4374d5..2e76c05c1e6 100644 --- a/pkg/services/dashboards/database/database_test.go +++ b/pkg/services/dashboards/database/database_test.go @@ -49,7 +49,7 @@ func TestIntegrationDashboardDataAccess(t *testing.T) { var dashboardStore dashboards.Store setup := func() { - sqlStore, cfg = db.InitTestDBwithCfg(t) + sqlStore, cfg = db.InitTestDBWithCfg(t) quotaService := quotatest.New(false, nil) var err error dashboardStore, err = ProvideDashboardStore(sqlStore, cfg, testFeatureToggles, tagimpl.ProvideService(sqlStore), quotaService) @@ -651,7 +651,7 @@ func TestIntegrationDashboard_Filter(t *testing.T) { }, Filters: []interface{}{ searchstore.TitleFilter{ - Dialect: sqlStore.Dialect, + Dialect: sqlStore.GetDialect(), Title: "Beta", }, }, @@ -711,9 +711,9 @@ func TestIntegrationFindDashboardsByTitle(t *testing.T) { orgID := int64(1) insertTestDashboard(t, dashboardStore, "dashboard under general", orgID, 0, "", false) - ac := acimpl.ProvideAccessControl(sqlStore.Cfg) + ac := acimpl.ProvideAccessControl(cfg) folderStore := folderimpl.ProvideDashboardFolderStore(sqlStore) - folderServiceWithFlagOn := folderimpl.ProvideService(ac, bus.ProvideBus(tracing.InitializeTracerForTest()), sqlStore.Cfg, dashboardStore, folderStore, sqlStore, features, supportbundlestest.NewFakeBundleService(), nil) + folderServiceWithFlagOn := folderimpl.ProvideService(ac, bus.ProvideBus(tracing.InitializeTracerForTest()), cfg, dashboardStore, folderStore, sqlStore, features, supportbundlestest.NewFakeBundleService(), nil) user := &user.SignedInUser{ OrgID: 1, @@ -828,9 +828,9 @@ func TestIntegrationFindDashboardsByFolder(t *testing.T) { orgID := int64(1) insertTestDashboard(t, dashboardStore, "dashboard under general", orgID, 0, "", false) - ac := acimpl.ProvideAccessControl(sqlStore.Cfg) + ac := acimpl.ProvideAccessControl(cfg) folderStore := folderimpl.ProvideDashboardFolderStore(sqlStore) - folderServiceWithFlagOn := folderimpl.ProvideService(ac, bus.ProvideBus(tracing.InitializeTracerForTest()), sqlStore.Cfg, dashboardStore, folderStore, sqlStore, features, supportbundlestest.NewFakeBundleService(), nil) + folderServiceWithFlagOn := folderimpl.ProvideService(ac, bus.ProvideBus(tracing.InitializeTracerForTest()), cfg, dashboardStore, folderStore, sqlStore, features, supportbundlestest.NewFakeBundleService(), nil) user := &user.SignedInUser{ OrgID: 1, diff --git a/pkg/services/dashboards/database/migrations/folder_uid_migrator.go b/pkg/services/dashboards/database/migrations/folder_uid_migrator.go index 4db022cfd81..fd1a075c82f 100644 --- a/pkg/services/dashboards/database/migrations/folder_uid_migrator.go +++ b/pkg/services/dashboards/database/migrations/folder_uid_migrator.go @@ -5,6 +5,18 @@ import ( "xorm.io/xorm" ) +type DummyMigration struct { + migrator.MigrationBase +} + +func (m *DummyMigration) SQL(dialect migrator.Dialect) string { + return "code migration" +} + +func (m *DummyMigration) Exec(sess *xorm.Session, mgrtr *migrator.Migrator) error { + return nil +} + // FolderUIDMigration is a code migration that populates folder_uid column type FolderUIDMigration struct { migrator.MigrationBase @@ -78,17 +90,13 @@ func AddDashboardFolderMigrations(mg *migrator.Migrator) { mg.AddMigration("Populate dashboard folder_uid column", &FolderUIDMigration{}) - mg.AddMigration("Add unique index for dashboard_org_id_folder_uid_title", migrator.NewAddIndexMigration(migrator.Table{Name: "dashboard"}, &migrator.Index{ - Cols: []string{"org_id", "folder_uid", "title"}, Type: migrator.UniqueIndex, - })) + mg.AddMigration("Add unique index for dashboard_org_id_folder_uid_title", &DummyMigration{}) mg.AddMigration("Delete unique index for dashboard_org_id_folder_id_title", migrator.NewDropIndexMigration(migrator.Table{Name: "dashboard"}, &migrator.Index{ Cols: []string{"org_id", "folder_id", "title"}, Type: migrator.UniqueIndex, })) - mg.AddMigration("Delete unique index for dashboard_org_id_folder_uid_title", migrator.NewDropIndexMigration(migrator.Table{Name: "dashboard"}, &migrator.Index{ - Cols: []string{"org_id", "folder_uid", "title"}, Type: migrator.UniqueIndex, - })) + mg.AddMigration("Delete unique index for dashboard_org_id_folder_uid_title", &DummyMigration{}) mg.AddMigration("Add unique index for dashboard_org_id_folder_uid_title_is_folder", migrator.NewAddIndexMigration(migrator.Table{Name: "dashboard"}, &migrator.Index{ Cols: []string{"org_id", "folder_uid", "title", "is_folder"}, Type: migrator.UniqueIndex, diff --git a/pkg/services/datasources/service/datasource.go b/pkg/services/datasources/service/datasource.go index 37c493e3b1a..ac2cb09a6f6 100644 --- a/pkg/services/datasources/service/datasource.go +++ b/pkg/services/datasources/service/datasource.go @@ -595,7 +595,6 @@ func (s *Service) dsTLSOptions(ctx context.Context, ds *datasources.DataSource) if tlsClientAuth { if val, exists, err := s.DecryptedValue(ctx, ds, "tlsClientCert"); err == nil { - fmt.Print("\n\n\n\n", val, exists, err, "\n\n\n\n") if exists && len(val) > 0 { opts.ClientCertificate = val } diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index 0e6575e57c3..8f9af951279 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -208,8 +208,9 @@ var ( { Name: "returnToPrevious", Description: "Enables the return to previous context functionality", - Stage: FeatureStagePublicPreview, + Stage: FeatureStageGeneralAvailability, FrontendOnly: true, + Expression: "true", // enabled by default Owner: grafanaFrontendPlatformSquad, }, { @@ -1201,6 +1202,23 @@ var ( Owner: grafanaObservabilityMetricsSquad, RequiresRestart: true, }, + { + Name: "grafanaManagedRecordingRules", + Description: "Enables Grafana-managed recording rules.", + Stage: FeatureStageExperimental, + Owner: grafanaAlertingSquad, + AllowSelfServe: false, + HideFromDocs: true, + HideFromAdminPage: true, + }, + { + Name: "queryLibrary", + Description: "Enables Query Library feature in Explore", + Stage: FeatureStageExperimental, + Owner: grafanaExploreSquad, + FrontendOnly: false, + AllowSelfServe: false, + }, } ) diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv index b6781497f9d..fe1848088b8 100644 --- a/pkg/services/featuremgmt/toggles_gen.csv +++ b/pkg/services/featuremgmt/toggles_gen.csv @@ -25,7 +25,7 @@ scenes,experimental,@grafana/dashboards-squad,false,false,true disableSecretsCompatibility,experimental,@grafana/hosted-grafana-team,false,true,false logRequestsInstrumentedAsUnknown,experimental,@grafana/hosted-grafana-team,false,false,false topnav,deprecated,@grafana/grafana-frontend-platform,false,false,false -returnToPrevious,preview,@grafana/grafana-frontend-platform,false,false,true +returnToPrevious,GA,@grafana/grafana-frontend-platform,false,false,true grpcServer,preview,@grafana/grafana-app-platform-squad,false,false,false unifiedStorage,experimental,@grafana/grafana-app-platform-squad,true,true,false cloudWatchCrossAccountQuerying,GA,@grafana/aws-datasources,false,false,false @@ -160,3 +160,5 @@ newDashboardWithFiltersAndGroupBy,experimental,@grafana/dashboards-squad,false,f cloudWatchNewLabelParsing,GA,@grafana/aws-datasources,false,false,false accessActionSets,experimental,@grafana/identity-access-team,false,false,false disableNumericMetricsSortingInExpressions,experimental,@grafana/observability-metrics,false,true,false +grafanaManagedRecordingRules,experimental,@grafana/alerting-squad,false,false,false +queryLibrary,experimental,@grafana/explore-squad,false,false,false diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go index e63bf491bc6..658e9d58dbd 100644 --- a/pkg/services/featuremgmt/toggles_gen.go +++ b/pkg/services/featuremgmt/toggles_gen.go @@ -650,4 +650,12 @@ const ( // FlagDisableNumericMetricsSortingInExpressions // In server-side expressions, disable the sorting of numeric-kind metrics by their metric name or labels. FlagDisableNumericMetricsSortingInExpressions = "disableNumericMetricsSortingInExpressions" + + // FlagGrafanaManagedRecordingRules + // Enables Grafana-managed recording rules. + FlagGrafanaManagedRecordingRules = "grafanaManagedRecordingRules" + + // FlagQueryLibrary + // Enables Query Library feature in Explore + FlagQueryLibrary = "queryLibrary" ) diff --git a/pkg/services/featuremgmt/toggles_gen.json b/pkg/services/featuremgmt/toggles_gen.json index ee818f42a22..d72a6888c07 100644 --- a/pkg/services/featuremgmt/toggles_gen.json +++ b/pkg/services/featuremgmt/toggles_gen.json @@ -201,12 +201,15 @@ { "metadata": { "name": "returnToPrevious", - "resourceVersion": "1713545444177", - "creationTimestamp": "2024-04-19T16:50:44Z" + "resourceVersion": "1713870623848", + "creationTimestamp": "2024-04-19T16:50:44Z", + "annotations": { + "grafana.app/updatedTimestamp": "2024-04-23 11:10:23.848446 +0000 UTC" + } }, "spec": { "description": "Enables the return to previous context functionality", - "stage": "preview", + "stage": "GA", "codeowner": "@grafana/grafana-frontend-platform", "frontend": true } @@ -2071,6 +2074,35 @@ "codeowner": "@grafana/dataviz-squad", "frontend": true } + }, + { + "metadata": { + "name": "grafanaManagedRecordingRules", + "resourceVersion": "1713795659477", + "creationTimestamp": "2024-04-22T14:20:59Z" + }, + "spec": { + "description": "Enables Grafana-managed recording rules.", + "stage": "experimental", + "codeowner": "@grafana/alerting-squad", + "hideFromAdminPage": true, + "hideFromDocs": true + } + }, + { + "metadata": { + "name": "queryLibrary", + "resourceVersion": "1713260947272", + "creationTimestamp": "2024-04-16T07:18:28Z", + "annotations": { + "grafana.app/updatedTimestamp": "2024-04-16 09:49:07.272595 +0000 UTC" + } + }, + "spec": { + "description": "Enables Query Library feature in Explore", + "stage": "experimental", + "codeowner": "@grafana/explore-squad" + } } ] -} \ No newline at end of file +} diff --git a/pkg/services/folder/folderimpl/dashboard_folder_store_test.go b/pkg/services/folder/folderimpl/dashboard_folder_store_test.go index bfa481039f5..8345f6ce32a 100644 --- a/pkg/services/folder/folderimpl/dashboard_folder_store_test.go +++ b/pkg/services/folder/folderimpl/dashboard_folder_store_test.go @@ -28,7 +28,7 @@ func TestIntegrationDashboardFolderStore(t *testing.T) { var dashboardStore dashboards.Store setup := func() { - sqlStore, cfg = db.InitTestDBwithCfg(t) + sqlStore, cfg = db.InitTestDBWithCfg(t) quotaService := quotatest.New(false, nil) var err error dashboardStore, err = database.ProvideDashboardStore(sqlStore, cfg, featuremgmt.WithFeatures(featuremgmt.FlagPanelTitleSearch), tagimpl.ProvideService(sqlStore), quotaService) @@ -40,7 +40,7 @@ func TestIntegrationDashboardFolderStore(t *testing.T) { title := "Very Unique Name" var sqlStore db.DB var folder1, folder2 *dashboards.Dashboard - sqlStore = db.InitTestDB(t) + sqlStore, cfg = db.InitTestDBWithCfg(t) folderStore := ProvideDashboardFolderStore(sqlStore) folder2 = insertTestFolder(t, dashboardStore, "TEST", orgId, "", "prod") _ = insertTestDashboard(t, dashboardStore, title, orgId, folder2.ID, folder2.UID, "prod") diff --git a/pkg/services/folder/folderimpl/folder_test.go b/pkg/services/folder/folderimpl/folder_test.go index 56ab7e128a3..5abe492742c 100644 --- a/pkg/services/folder/folderimpl/folder_test.go +++ b/pkg/services/folder/folderimpl/folder_test.go @@ -58,7 +58,7 @@ func TestIntegrationProvideFolderService(t *testing.T) { t.Run("should register scope resolvers", func(t *testing.T) { cfg := setting.NewCfg() ac := acmock.New() - db := sqlstore.InitTestDB(t) + db := db.InitTestDB(t) ProvideService(ac, bus.ProvideBus(tracing.InitializeTracerForTest()), cfg, nil, nil, db, featuremgmt.WithFeatures(), supportbundlestest.NewFakeBundleService(), nil) require.Len(t, ac.Calls.RegisterAttributeScopeResolver, 3) @@ -71,12 +71,11 @@ func TestIntegrationFolderService(t *testing.T) { } t.Run("Folder service tests", func(t *testing.T) { dashStore := &dashboards.FakeDashboardStore{} - db := sqlstore.InitTestDB(t) - nestedFolderStore := ProvideStore(db, db.Cfg) + db, cfg := sqlstore.InitTestDB(t) + nestedFolderStore := ProvideStore(db, cfg) folderStore := foldertest.NewFakeFolderStore(t) - cfg := setting.NewCfg() features := featuremgmt.WithFeatures() ac := acmock.New().WithPermissions([]accesscontrol.Permission{ @@ -354,16 +353,14 @@ func TestIntegrationNestedFolderService(t *testing.T) { if testing.Short() { t.Skip("skipping integration test") } - db := sqlstore.InitTestDB(t) + db, cfg := sqlstore.InitTestDB(t) quotaService := quotatest.New(false, nil) folderStore := ProvideDashboardFolderStore(db) - cfg := setting.NewCfg() - featuresFlagOn := featuremgmt.WithFeatures("nestedFolders") - dashStore, err := database.ProvideDashboardStore(db, db.Cfg, featuresFlagOn, tagimpl.ProvideService(db), quotaService) + dashStore, err := database.ProvideDashboardStore(db, cfg, featuresFlagOn, tagimpl.ProvideService(db), quotaService) require.NoError(t, err) - nestedFolderStore := ProvideStore(db, db.Cfg) + nestedFolderStore := ProvideStore(db, cfg) b := bus.ProvideBus(tracing.InitializeTracerForTest()) ac := acimpl.ProvideAccessControl(cfg) @@ -479,9 +476,9 @@ func TestIntegrationNestedFolderService(t *testing.T) { }) t.Run("With nested folder feature flag off", func(t *testing.T) { featuresFlagOff := featuremgmt.WithFeatures() - dashStore, err := database.ProvideDashboardStore(db, db.Cfg, featuresFlagOff, tagimpl.ProvideService(db), quotaService) + dashStore, err := database.ProvideDashboardStore(db, cfg, featuresFlagOff, tagimpl.ProvideService(db), quotaService) require.NoError(t, err) - nestedFolderStore := ProvideStore(db, db.Cfg) + nestedFolderStore := ProvideStore(db, cfg) serviceWithFlagOff := &Service{ cfg: cfg, @@ -644,9 +641,9 @@ func TestIntegrationNestedFolderService(t *testing.T) { lps, err := librarypanels.ProvideService(cfg, db, routeRegister, elementService, tc.service) require.NoError(t, err) - dashStore, err := database.ProvideDashboardStore(db, db.Cfg, tc.featuresFlag, tagimpl.ProvideService(db), quotaService) + dashStore, err := database.ProvideDashboardStore(db, cfg, tc.featuresFlag, tagimpl.ProvideService(db), quotaService) require.NoError(t, err) - nestedFolderStore := ProvideStore(db, db.Cfg) + nestedFolderStore := ProvideStore(db, cfg) tc.service.dashboardStore = dashStore tc.service.store = nestedFolderStore @@ -737,11 +734,11 @@ func TestNestedFolderServiceFeatureToggle(t *testing.T) { dashboardFolderStore := foldertest.NewFakeFolderStore(t) - cfg := setting.NewCfg() + db, cfg := sqlstore.InitTestDB(t) folderService := &Service{ cfg: cfg, store: nestedFolderStore, - db: sqlstore.InitTestDB(t), + db: db, dashboardStore: &dashStore, dashboardFolderStore: dashboardFolderStore, features: featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders), @@ -765,7 +762,7 @@ func TestFolderServiceDualWrite(t *testing.T) { guardian.New = g }) - db := sqlstore.InitTestDB(t) + db, _ := sqlstore.InitTestDB(t) cfg := setting.NewCfg() features := featuremgmt.WithFeatures() nestedFolderStore := ProvideStore(db, cfg) @@ -778,7 +775,7 @@ func TestFolderServiceDualWrite(t *testing.T) { folderService := &Service{ cfg: setting.NewCfg(), store: nestedFolderStore, - db: sqlstore.InitTestDB(t), + db: db, dashboardStore: dashStore, dashboardFolderStore: dashboardFolderStore, features: featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders), @@ -845,7 +842,8 @@ func TestNestedFolderService(t *testing.T) { nestedFolderStore := NewFakeStore() - folderSvc := setup(t, dashStore, dashboardFolderStore, nestedFolderStore, featuremgmt.WithFeatures(), acimpl.ProvideAccessControl(setting.NewCfg()), sqlstore.InitTestDB(t)) + db, _ := sqlstore.InitTestDB(t) + folderSvc := setup(t, dashStore, dashboardFolderStore, nestedFolderStore, featuremgmt.WithFeatures(), acimpl.ProvideAccessControl(setting.NewCfg()), db) _, err := folderSvc.Create(context.Background(), &folder.CreateFolderCommand{ OrgID: orgID, Title: dash.Title, @@ -877,7 +875,8 @@ func TestNestedFolderService(t *testing.T) { dashboardFolderStore := foldertest.NewFakeFolderStore(t) nestedFolderStore := NewFakeStore() - folderSvc := setup(t, dashStore, dashboardFolderStore, nestedFolderStore, featuremgmt.WithFeatures("nestedFolders"), acimpl.ProvideAccessControl(setting.NewCfg()), sqlstore.InitTestDB(t)) + db, _ := sqlstore.InitTestDB(t) + folderSvc := setup(t, dashStore, dashboardFolderStore, nestedFolderStore, featuremgmt.WithFeatures("nestedFolders"), acimpl.ProvideAccessControl(setting.NewCfg()), db) _, err := folderSvc.Create(context.Background(), &folder.CreateFolderCommand{ OrgID: orgID, Title: dash.Title, @@ -942,7 +941,8 @@ func TestNestedFolderService(t *testing.T) { nestedFolderUser.Permissions[orgID] = map[string][]string{dashboards.ActionFoldersWrite: {dashboards.ScopeFoldersProvider.GetResourceScopeUID("some_parent")}} nestedFolderStore := NewFakeStore() - folderSvc := setup(t, dashStore, dashboardFolderStore, nestedFolderStore, featuremgmt.WithFeatures("nestedFolders"), acimpl.ProvideAccessControl(setting.NewCfg()), sqlstore.InitTestDB(t)) + db, _ := sqlstore.InitTestDB(t) + folderSvc := setup(t, dashStore, dashboardFolderStore, nestedFolderStore, featuremgmt.WithFeatures("nestedFolders"), acimpl.ProvideAccessControl(setting.NewCfg()), db) _, err := folderSvc.Create(context.Background(), &folder.CreateFolderCommand{ OrgID: orgID, Title: dash.Title, @@ -969,9 +969,10 @@ func TestNestedFolderService(t *testing.T) { dashboardFolderStore := foldertest.NewFakeFolderStore(t) nestedFolderStore := NewFakeStore() + db, _ := sqlstore.InitTestDB(t) folderSvc := setup(t, dashStore, dashboardFolderStore, nestedFolderStore, featuremgmt.WithFeatures("nestedFolders"), actest.FakeAccessControl{ ExpectedEvaluate: true, - }, sqlstore.InitTestDB(t)) + }, db) f, err := folderSvc.Create(context.Background(), &folder.CreateFolderCommand{ OrgID: orgID, Title: "myFolder", @@ -1019,9 +1020,10 @@ func TestNestedFolderService(t *testing.T) { SignedInUser: usr, } + db, _ := sqlstore.InitTestDB(t) folderSvc := setup(t, dashStore, dashboardFolderStore, nestedFolderStore, featuremgmt.WithFeatures("nestedFolders"), actest.FakeAccessControl{ ExpectedEvaluate: true, - }, sqlstore.InitTestDB(t)) + }, db) _, err := folderSvc.Create(context.Background(), &cmd) require.Error(t, err, folder.ErrCircularReference) // CreateFolder should not call the folder store's create method. @@ -1048,9 +1050,10 @@ func TestNestedFolderService(t *testing.T) { nestedFolderStore.ExpectedError = errors.New("FAILED") // the service return success as long as the legacy create succeeds + db, _ := sqlstore.InitTestDB(t) folderSvc := setup(t, dashStore, dashboardFolderStore, nestedFolderStore, featuremgmt.WithFeatures("nestedFolders"), actest.FakeAccessControl{ ExpectedEvaluate: true, - }, sqlstore.InitTestDB(t)) + }, db) _, err := folderSvc.Create(context.Background(), &folder.CreateFolderCommand{ OrgID: orgID, Title: "myFolder", @@ -1232,9 +1235,10 @@ func TestNestedFolderService(t *testing.T) { //nestedFolderStore.ExpectedFolder = &folder.Folder{UID: "myFolder", ParentUID: "newFolder"} nestedFolderStore.ExpectedParentFolders = parents + db, _ := sqlstore.InitTestDB(t) folderSvc := setup(t, dashStore, dashboardFolderStore, nestedFolderStore, featuremgmt.WithFeatures("nestedFolders"), actest.FakeAccessControl{ ExpectedEvaluate: true, - }, sqlstore.InitTestDB(t)) + }, db) _, err := folderSvc.Create(context.Background(), &folder.CreateFolderCommand{ Title: "folder", OrgID: orgID, @@ -1277,16 +1281,14 @@ func TestIntegrationNestedFolderSharedWithMe(t *testing.T) { if testing.Short() { t.Skip("skipping integration test") } - db := sqlstore.InitTestDB(t) + db, cfg := sqlstore.InitTestDB(t) quotaService := quotatest.New(false, nil) folderStore := ProvideDashboardFolderStore(db) - cfg := setting.NewCfg() - featuresFlagOn := featuremgmt.WithFeatures("nestedFolders") - dashStore, err := database.ProvideDashboardStore(db, db.Cfg, featuresFlagOn, tagimpl.ProvideService(db), quotaService) + dashStore, err := database.ProvideDashboardStore(db, cfg, featuresFlagOn, tagimpl.ProvideService(db), quotaService) require.NoError(t, err) - nestedFolderStore := ProvideStore(db, db.Cfg) + nestedFolderStore := ProvideStore(db, cfg) b := bus.ProvideBus(tracing.InitializeTracerForTest()) ac := acimpl.ProvideAccessControl(cfg) @@ -1631,7 +1633,7 @@ func TestIntegrationNestedFolderSharedWithMe(t *testing.T) { } func TestFolderServiceGetFolder(t *testing.T) { - db := sqlstore.InitTestDB(t) + db, _ := sqlstore.InitTestDB(t) signedInAdminUser := user.SignedInUser{UserID: 1, OrgID: orgID, Permissions: map[int64]map[string][]string{ orgID: { @@ -1653,9 +1655,9 @@ func TestFolderServiceGetFolder(t *testing.T) { cfg := setting.NewCfg() featuresFlagOff := featuremgmt.WithFeatures() - dashStore, err := database.ProvideDashboardStore(db, db.Cfg, featuresFlagOff, tagimpl.ProvideService(db), quotaService) + dashStore, err := database.ProvideDashboardStore(db, cfg, featuresFlagOff, tagimpl.ProvideService(db), quotaService) require.NoError(t, err) - nestedFolderStore := ProvideStore(db, db.Cfg) + nestedFolderStore := ProvideStore(db, cfg) b := bus.ProvideBus(tracing.InitializeTracerForTest()) ac := acimpl.ProvideAccessControl(cfg) @@ -1731,16 +1733,14 @@ func TestFolderServiceGetFolder(t *testing.T) { } func TestFolderServiceGetFolders(t *testing.T) { - db := sqlstore.InitTestDB(t) + db, cfg := sqlstore.InitTestDB(t) quotaService := quotatest.New(false, nil) folderStore := ProvideDashboardFolderStore(db) - cfg := setting.NewCfg() - featuresFlagOff := featuremgmt.WithFeatures() - dashStore, err := database.ProvideDashboardStore(db, db.Cfg, featuresFlagOff, tagimpl.ProvideService(db), quotaService) + dashStore, err := database.ProvideDashboardStore(db, cfg, featuresFlagOff, tagimpl.ProvideService(db), quotaService) require.NoError(t, err) - nestedFolderStore := ProvideStore(db, db.Cfg) + nestedFolderStore := ProvideStore(db, cfg) b := bus.ProvideBus(tracing.InitializeTracerForTest()) ac := acimpl.ProvideAccessControl(cfg) @@ -1809,7 +1809,7 @@ func TestFolderServiceGetFolders(t *testing.T) { // TODO replace it with an API test under /pkg/tests/api/folders // whenever the golang client with get updated to allow filtering child folders by permission func TestGetChildrenFilterByPermission(t *testing.T) { - db := sqlstore.InitTestDB(t) + db, cfg := sqlstore.InitTestDB(t) signedInAdminUser := user.SignedInUser{UserID: 1, OrgID: orgID, Permissions: map[int64]map[string][]string{ orgID: { @@ -1822,12 +1822,10 @@ func TestGetChildrenFilterByPermission(t *testing.T) { quotaService := quotatest.New(false, nil) folderStore := ProvideDashboardFolderStore(db) - cfg := setting.NewCfg() - featuresFlagOff := featuremgmt.WithFeatures() - dashStore, err := database.ProvideDashboardStore(db, db.Cfg, featuresFlagOff, tagimpl.ProvideService(db), quotaService) + dashStore, err := database.ProvideDashboardStore(db, cfg, featuresFlagOff, tagimpl.ProvideService(db), quotaService) require.NoError(t, err) - nestedFolderStore := ProvideStore(db, db.Cfg) + nestedFolderStore := ProvideStore(db, cfg) b := bus.ProvideBus(tracing.InitializeTracerForTest()) ac := acimpl.ProvideAccessControl(cfg) diff --git a/pkg/services/folder/folderimpl/sqlstore_test.go b/pkg/services/folder/folderimpl/sqlstore_test.go index 9f54c152653..5c33c3d935b 100644 --- a/pkg/services/folder/folderimpl/sqlstore_test.go +++ b/pkg/services/folder/folderimpl/sqlstore_test.go @@ -31,10 +31,10 @@ func TestIntegrationCreate(t *testing.T) { t.Skip("skipping integration test") } - db := sqlstore.InitTestDB(t) - folderStore := ProvideStore(db, db.Cfg) + db, cfg := sqlstore.InitTestDB(t) + folderStore := ProvideStore(db, cfg) - orgID := CreateOrg(t, db, db.Cfg) + orgID := CreateOrg(t, db, cfg) t.Run("creating a folder without providing a UID should fail", func(t *testing.T) { _, err := folderStore.Create(context.Background(), folder.CreateFolderCommand{ @@ -151,10 +151,10 @@ func TestIntegrationDelete(t *testing.T) { t.Skip("skipping integration test") } - db := sqlstore.InitTestDB(t) - folderStore := ProvideStore(db, db.Cfg) + db, cfg := sqlstore.InitTestDB(t) + folderStore := ProvideStore(db, cfg) - orgID := CreateOrg(t, db, db.Cfg) + orgID := CreateOrg(t, db, cfg) /* t.Run("attempt to delete unknown folder should fail", func(t *testing.T) { @@ -198,10 +198,10 @@ func TestIntegrationUpdate(t *testing.T) { t.Skip("skipping integration test") } - db := sqlstore.InitTestDB(t) - folderStore := ProvideStore(db, db.Cfg) + db, cfg := sqlstore.InitTestDB(t) + folderStore := ProvideStore(db, cfg) - orgID := CreateOrg(t, db, db.Cfg) + orgID := CreateOrg(t, db, cfg) // create parent folder parent, err := folderStore.Create(context.Background(), folder.CreateFolderCommand{ @@ -373,10 +373,10 @@ func TestIntegrationGet(t *testing.T) { t.Skip("skipping integration test") } - db := sqlstore.InitTestDB(t) - folderStore := ProvideStore(db, db.Cfg) + db, cfg := sqlstore.InitTestDB(t) + folderStore := ProvideStore(db, cfg) - orgID := CreateOrg(t, db, db.Cfg) + orgID := CreateOrg(t, db, cfg) // create folder uid1 := util.GenerateShortUID() @@ -490,10 +490,10 @@ func TestIntegrationGetParents(t *testing.T) { t.Skip("skipping integration test") } - db := sqlstore.InitTestDB(t) - folderStore := ProvideStore(db, db.Cfg) + db, cfg := sqlstore.InitTestDB(t) + folderStore := ProvideStore(db, cfg) - orgID := CreateOrg(t, db, db.Cfg) + orgID := CreateOrg(t, db, cfg) // create folder uid1 := util.GenerateShortUID() @@ -558,10 +558,10 @@ func TestIntegrationGetChildren(t *testing.T) { t.Skip("skipping integration test") } - db := sqlstore.InitTestDB(t) - folderStore := ProvideStore(db, db.Cfg) + db, cfg := sqlstore.InitTestDB(t) + folderStore := ProvideStore(db, cfg) - orgID := CreateOrg(t, db, db.Cfg) + orgID := CreateOrg(t, db, cfg) // create folder uid1 := util.GenerateShortUID() @@ -738,10 +738,10 @@ func TestIntegrationGetHeight(t *testing.T) { t.Skip("skipping integration test") } - db := sqlstore.InitTestDB(t) - folderStore := ProvideStore(db, db.Cfg) + db, cfg := sqlstore.InitTestDB(t) + folderStore := ProvideStore(db, cfg) - orgID := CreateOrg(t, db, db.Cfg) + orgID := CreateOrg(t, db, cfg) // create folder uid1 := util.GenerateShortUID() @@ -771,10 +771,10 @@ func TestIntegrationGetFolders(t *testing.T) { } foldersNum := 10 - db := sqlstore.InitTestDB(t) - folderStore := ProvideStore(db, db.Cfg) + db, cfg := sqlstore.InitTestDB(t) + folderStore := ProvideStore(db, cfg) - orgID := CreateOrg(t, db, db.Cfg) + orgID := CreateOrg(t, db, cfg) // create folders uids := make([]string, 0) diff --git a/pkg/services/grpcserver/service.go b/pkg/services/grpcserver/service.go index 0eaf246e04d..ae2f9650a46 100644 --- a/pkg/services/grpcserver/service.go +++ b/pkg/services/grpcserver/service.go @@ -8,8 +8,7 @@ import ( "github.com/grafana/dskit/instrument" "github.com/grafana/dskit/middleware" "github.com/grafana/grafana-plugin-sdk-go/backend" - grpc_middleware "github.com/grpc-ecosystem/go-grpc-middleware" - grpcAuth "github.com/grpc-ecosystem/go-grpc-middleware/auth" + grpcAuth "github.com/grpc-ecosystem/go-grpc-middleware/v2/interceptors/auth" "github.com/prometheus/client_golang/prometheus" "google.golang.org/grpc" "google.golang.org/grpc/credentials" @@ -67,22 +66,18 @@ func ProvideService(cfg *setting.Cfg, features featuremgmt.FeatureToggles, authe // Default auth is admin token check, but this can be overridden by // services which implement ServiceAuthFuncOverride interface. - // See https://github.com/grpc-ecosystem/go-grpc-middleware/blob/master/auth/auth.go#L30. + // See https://github.com/grpc-ecosystem/go-grpc-middleware/blob/main/interceptors/auth/auth.go#L30. opts = append(opts, []grpc.ServerOption{ - grpc.UnaryInterceptor( - grpc_middleware.ChainUnaryServer( - grpcAuth.UnaryServerInterceptor(authenticator.Authenticate), - interceptors.TracingUnaryInterceptor(tracer), - interceptors.LoggingUnaryInterceptor(s.cfg, s.logger), // needs to be registered after tracing interceptor to get trace id - middleware.UnaryServerInstrumentInterceptor(grpcRequestDuration), - ), + grpc.ChainUnaryInterceptor( + grpcAuth.UnaryServerInterceptor(authenticator.Authenticate), + interceptors.TracingUnaryInterceptor(tracer), + interceptors.LoggingUnaryInterceptor(s.cfg, s.logger), // needs to be registered after tracing interceptor to get trace id + middleware.UnaryServerInstrumentInterceptor(grpcRequestDuration), ), - grpc.StreamInterceptor( - grpc_middleware.ChainStreamServer( - interceptors.TracingStreamInterceptor(tracer), - grpcAuth.StreamServerInterceptor(authenticator.Authenticate), - middleware.StreamServerInstrumentInterceptor(grpcRequestDuration), - ), + grpc.ChainStreamInterceptor( + interceptors.TracingStreamInterceptor(tracer), + grpcAuth.StreamServerInterceptor(authenticator.Authenticate), + middleware.StreamServerInstrumentInterceptor(grpcRequestDuration), ), }...) @@ -90,12 +85,20 @@ func ProvideService(cfg *setting.Cfg, features featuremgmt.FeatureToggles, authe opts = append(opts, grpc.Creds(credentials.NewTLS(cfg.GRPCServerTLSConfig))) } + if s.cfg.GRPCServerMaxRecvMsgSize > 0 { + opts = append(opts, grpc.MaxRecvMsgSize(s.cfg.GRPCServerMaxRecvMsgSize)) + } + + if s.cfg.GRPCServerMaxSendMsgSize > 0 { + opts = append(opts, grpc.MaxSendMsgSize(s.cfg.GRPCServerMaxSendMsgSize)) + } + s.server = grpc.NewServer(opts...) return s, nil } func (s *gPRCServerService) Run(ctx context.Context) error { - s.logger.Info("Running GRPC server", "address", s.cfg.GRPCServerAddress, "network", s.cfg.GRPCServerNetwork, "tls", s.cfg.GRPCServerTLSConfig != nil) + s.logger.Info("Running GRPC server", "address", s.cfg.GRPCServerAddress, "network", s.cfg.GRPCServerNetwork, "tls", s.cfg.GRPCServerTLSConfig != nil, "max_recv_msg_size", s.cfg.GRPCServerMaxRecvMsgSize, "max_send_msg_size", s.cfg.GRPCServerMaxSendMsgSize) listener, err := net.Listen(s.cfg.GRPCServerNetwork, s.cfg.GRPCServerAddress) if err != nil { diff --git a/pkg/services/libraryelements/libraryelements_test.go b/pkg/services/libraryelements/libraryelements_test.go index 189ccfac350..7dda316dc36 100644 --- a/pkg/services/libraryelements/libraryelements_test.go +++ b/pkg/services/libraryelements/libraryelements_test.go @@ -379,22 +379,22 @@ func scenarioWithPanel(t *testing.T, desc string, fn func(t *testing.T, sc scena t.Helper() features := featuremgmt.WithFeatures() - sqlStore := db.InitTestDB(t) + sqlStore, cfg := db.InitTestDBWithCfg(t) ac := actest.FakeAccessControl{} quotaService := quotatest.New(false, nil) - dashboardStore, err := database.ProvideDashboardStore(sqlStore, sqlStore.Cfg, features, tagimpl.ProvideService(sqlStore), quotaService) + dashboardStore, err := database.ProvideDashboardStore(sqlStore, cfg, features, tagimpl.ProvideService(sqlStore), quotaService) require.NoError(t, err) folderPermissions := acmock.NewMockedPermissionsService() dashboardPermissions := acmock.NewMockedPermissionsService() folderStore := folderimpl.ProvideDashboardFolderStore(sqlStore) dashboardService, svcErr := dashboardservice.ProvideDashboardServiceImpl( - sqlStore.Cfg, dashboardStore, folderStore, + cfg, dashboardStore, folderStore, features, folderPermissions, dashboardPermissions, ac, foldertest.NewFakeService(), nil, ) require.NoError(t, svcErr) - guardian.InitAccessControlGuardian(sqlStore.Cfg, ac, dashboardService) + guardian.InitAccessControlGuardian(cfg, ac, dashboardService) testScenario(t, desc, func(t *testing.T, sc scenarioContext) { // nolint:staticcheck @@ -439,28 +439,29 @@ func testScenario(t *testing.T, desc string, fn func(t *testing.T, sc scenarioCo webCtx := web.Context{Req: req} features := featuremgmt.WithFeatures() - sqlStore := db.InitTestDB(t) + tracer := tracing.InitializeTracerForTest() + sqlStore, cfg := db.InitTestDBWithCfg(t) quotaService := quotatest.New(false, nil) - dashboardStore, err := database.ProvideDashboardStore(sqlStore, sqlStore.Cfg, features, tagimpl.ProvideService(sqlStore), quotaService) + dashboardStore, err := database.ProvideDashboardStore(sqlStore, cfg, features, tagimpl.ProvideService(sqlStore), quotaService) require.NoError(t, err) - ac := acimpl.ProvideAccessControl(sqlStore.Cfg) + ac := acimpl.ProvideAccessControl(cfg) folderPermissions := acmock.NewMockedPermissionsService() folderPermissions.On("SetPermissions", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return([]accesscontrol.ResourcePermission{}, nil) dashboardPermissions := acmock.NewMockedPermissionsService() folderStore := folderimpl.ProvideDashboardFolderStore(sqlStore) dashService, dashSvcErr := dashboardservice.ProvideDashboardServiceImpl( - sqlStore.Cfg, dashboardStore, folderStore, + cfg, dashboardStore, folderStore, features, folderPermissions, dashboardPermissions, ac, foldertest.NewFakeService(), nil, ) require.NoError(t, dashSvcErr) - guardian.InitAccessControlGuardian(sqlStore.Cfg, ac, dashService) + guardian.InitAccessControlGuardian(cfg, ac, dashService) service := LibraryElementService{ - Cfg: sqlStore.Cfg, + Cfg: cfg, features: featuremgmt.WithFeatures(), SQLStore: sqlStore, - folderService: folderimpl.ProvideService(ac, bus.ProvideBus(tracing.InitializeTracerForTest()), sqlStore.Cfg, dashboardStore, folderStore, sqlStore, features, supportbundlestest.NewFakeBundleService(), nil), + folderService: folderimpl.ProvideService(ac, bus.ProvideBus(tracer), cfg, dashboardStore, folderStore, sqlStore, features, supportbundlestest.NewFakeBundleService(), nil), } // deliberate difference between signed in user and user in db to make it crystal clear @@ -471,9 +472,12 @@ func testScenario(t *testing.T, desc string, fn func(t *testing.T, sc scenarioCo Name: "User In DB", Login: userInDbName, } - orgSvc, err := orgimpl.ProvideService(sqlStore, sqlStore.Cfg, quotaService) + orgSvc, err := orgimpl.ProvideService(sqlStore, cfg, quotaService) require.NoError(t, err) - usrSvc, err := userimpl.ProvideService(sqlStore, orgSvc, sqlStore.Cfg, nil, nil, quotaService, supportbundlestest.NewFakeBundleService()) + usrSvc, err := userimpl.ProvideService( + sqlStore, orgSvc, cfg, nil, nil, tracer, + quotaService, supportbundlestest.NewFakeBundleService(), + ) require.NoError(t, err) _, err = usrSvc.Create(context.Background(), &cmd) require.NoError(t, err) diff --git a/pkg/services/librarypanels/librarypanels_test.go b/pkg/services/librarypanels/librarypanels_test.go index ab36bda9fa1..771f673b078 100644 --- a/pkg/services/librarypanels/librarypanels_test.go +++ b/pkg/services/librarypanels/librarypanels_test.go @@ -815,7 +815,7 @@ func testScenario(t *testing.T, desc string, fn func(t *testing.T, sc scenarioCo t.Run(desc, func(t *testing.T) { orgID := int64(1) role := org.RoleAdmin - sqlStore, cfg := db.InitTestDBwithCfg(t) + sqlStore, cfg := db.InitTestDBWithCfg(t) quotaService := quotatest.New(false, nil) ac := actest.FakeAccessControl{ExpectedEvaluate: true} @@ -870,9 +870,12 @@ func testScenario(t *testing.T, desc string, fn func(t *testing.T, sc scenarioCo Login: userInDbName, } ctx := appcontext.WithUser(context.Background(), usr) - orgSvc, err := orgimpl.ProvideService(sqlStore, sqlStore.Cfg, quotaService) + orgSvc, err := orgimpl.ProvideService(sqlStore, cfg, quotaService) require.NoError(t, err) - usrSvc, err := userimpl.ProvideService(sqlStore, orgSvc, sqlStore.Cfg, nil, nil, quotaService, supportbundlestest.NewFakeBundleService()) + usrSvc, err := userimpl.ProvideService( + sqlStore, orgSvc, cfg, nil, nil, tracing.InitializeTracerForTest(), + quotaService, supportbundlestest.NewFakeBundleService(), + ) require.NoError(t, err) _, err = usrSvc.Create(context.Background(), &cmd) require.NoError(t, err) diff --git a/pkg/services/ngalert/accesscontrol/rules_test.go b/pkg/services/ngalert/accesscontrol/rules_test.go index 05301b1c2ae..d426d0ddd90 100644 --- a/pkg/services/ngalert/accesscontrol/rules_test.go +++ b/pkg/services/ngalert/accesscontrol/rules_test.go @@ -92,6 +92,8 @@ func createUserWithPermissions(permissions map[string][]string) identity.Request func TestAuthorizeRuleChanges(t *testing.T) { groupKey := models.GenerateGroupKey(rand.Int63()) namespaceIdScope := dashboards.ScopeFoldersProvider.GetResourceScopeUID(groupKey.NamespaceUID) + gen := models.RuleGen + genWithGroupKey := gen.With(gen.WithGroupKey(groupKey)) testCases := []struct { name string @@ -103,7 +105,7 @@ func TestAuthorizeRuleChanges(t *testing.T) { changes: func() *store.GroupDelta { return &store.GroupDelta{ GroupKey: groupKey, - New: models.GenerateAlertRules(rand.Intn(4)+1, models.AlertRuleGen(models.WithGroupKey(groupKey))), + New: genWithGroupKey.GenerateManyRef(1, 5), Update: nil, Delete: nil, } @@ -132,8 +134,8 @@ func TestAuthorizeRuleChanges(t *testing.T) { { name: "if there are rules to delete it should check delete action and query for datasource", changes: func() *store.GroupDelta { - rules := models.GenerateAlertRules(rand.Intn(4)+1, models.AlertRuleGen(models.WithGroupKey(groupKey))) - rules2 := models.GenerateAlertRules(rand.Intn(4)+1, models.AlertRuleGen(models.WithGroupKey(groupKey))) + rules := genWithGroupKey.GenerateManyRef(1, 5) + rules2 := genWithGroupKey.GenerateManyRef(1, 5) return &store.GroupDelta{ GroupKey: groupKey, AffectedGroups: map[models.AlertRuleGroupKey]models.RulesGroup{ @@ -162,8 +164,8 @@ func TestAuthorizeRuleChanges(t *testing.T) { { name: "if there are rules to update within the same namespace it should check update action and access to datasource", changes: func() *store.GroupDelta { - rules1 := models.GenerateAlertRules(rand.Intn(4)+1, models.AlertRuleGen(models.WithGroupKey(groupKey))) - rules := models.GenerateAlertRules(rand.Intn(4)+1, models.AlertRuleGen(models.WithGroupKey(groupKey))) + rules1 := genWithGroupKey.GenerateManyRef(1, 5) + rules := genWithGroupKey.GenerateManyRef(1, 5) updates := make([]store.RuleDelta, 0, len(rules)) for _, rule := range rules { @@ -207,18 +209,14 @@ func TestAuthorizeRuleChanges(t *testing.T) { { name: "if there are rules that are moved between namespaces it should check delete+add action and access to group where rules come from", changes: func() *store.GroupDelta { - rules1 := models.GenerateAlertRules(rand.Intn(4)+1, models.AlertRuleGen(models.WithGroupKey(groupKey))) - rules := models.GenerateAlertRules(rand.Intn(4)+1, models.AlertRuleGen(models.WithGroupKey(groupKey))) + rules1 := genWithGroupKey.GenerateManyRef(1, 5) + rules := genWithGroupKey.GenerateManyRef(1, 5) targetGroupKey := models.GenerateGroupKey(groupKey.OrgID) updates := make([]store.RuleDelta, 0, len(rules)) for _, rule := range rules { - cp := models.CopyRule(rule) - models.WithGroupKey(targetGroupKey)(cp) - cp.Data = []models.AlertQuery{ - models.GenerateAlertQuery(), - } + cp := models.CopyRule(rule, gen.WithGroupKey(targetGroupKey), gen.WithQuery(gen.GenerateQuery())) updates = append(updates, store.RuleDelta{ Existing: rule, @@ -269,8 +267,8 @@ func TestAuthorizeRuleChanges(t *testing.T) { NamespaceUID: groupKey.NamespaceUID, RuleGroup: util.GenerateShortUID(), } - sourceGroup := models.GenerateAlertRules(rand.Intn(4)+1, models.AlertRuleGen(models.WithGroupKey(groupKey))) - targetGroup := models.GenerateAlertRules(rand.Intn(4)+1, models.AlertRuleGen(models.WithGroupKey(targetGroupKey))) + sourceGroup := genWithGroupKey.GenerateManyRef(1, 5) + targetGroup := gen.With(gen.WithGroupKey(targetGroupKey)).GenerateManyRef(1, 5) updates := make([]store.RuleDelta, 0, len(sourceGroup)) toCopy := len(sourceGroup) @@ -279,11 +277,7 @@ func TestAuthorizeRuleChanges(t *testing.T) { } for i := 0; i < toCopy; i++ { rule := sourceGroup[0] - cp := models.CopyRule(rule) - models.WithGroupKey(targetGroupKey)(cp) - cp.Data = []models.AlertQuery{ - models.GenerateAlertQuery(), - } + cp := models.CopyRule(rule, gen.WithGroupKey(targetGroupKey), gen.WithQuery(models.GenerateAlertQuery())) updates = append(updates, store.RuleDelta{ Existing: rule, @@ -379,7 +373,7 @@ func TestAuthorizeRuleChanges(t *testing.T) { } func TestCheckDatasourcePermissionsForRule(t *testing.T) { - rule := models.AlertRuleGen()() + rule := models.RuleGen.GenerateRef() expressionByType := models.GenerateAlertQuery() expressionByType.QueryType = expr.DatasourceType @@ -442,7 +436,7 @@ func TestCheckDatasourcePermissionsForRule(t *testing.T) { func Test_authorizeAccessToRuleGroup(t *testing.T) { t.Run("should return true if user has access to all datasources of all rules in group", func(t *testing.T) { - rules := models.GenerateAlertRules(rand.Intn(4)+1, models.AlertRuleGen()) + rules := models.RuleGen.GenerateManyRef(1, 5) var scopes []string for _, rule := range rules { for _, query := range rule.Data { @@ -470,7 +464,9 @@ func Test_authorizeAccessToRuleGroup(t *testing.T) { }) t.Run("should return false if user does not have access to at least one rule in group", func(t *testing.T) { f := &folder.Folder{UID: "test-folder"} - rules := models.GenerateAlertRules(rand.Intn(4)+1, models.AlertRuleGen(models.WithNamespace(f))) + gen := models.RuleGen + genWithFolder := gen.With(gen.WithNamespace(f)) + rules := genWithFolder.GenerateManyRef(1, 5) var scopes []string for _, rule := range rules { for _, query := range rule.Data { @@ -487,7 +483,7 @@ func Test_authorizeAccessToRuleGroup(t *testing.T) { datasources.ActionQuery: scopes, } - rule := models.AlertRuleGen(models.WithNamespace(f))() + rule := genWithFolder.GenerateRef() rules = append(rules, rule) ac := &recordingAccessControlFake{} diff --git a/pkg/services/ngalert/accesscontrol/silences.go b/pkg/services/ngalert/accesscontrol/silences.go index 178e5ffa4d0..9d9666b674a 100644 --- a/pkg/services/ngalert/accesscontrol/silences.go +++ b/pkg/services/ngalert/accesscontrol/silences.go @@ -9,6 +9,7 @@ import ( ac "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/auth/identity" "github.com/grafana/grafana/pkg/services/dashboards" + "github.com/grafana/grafana/pkg/services/ngalert/models" ) const ( @@ -78,10 +79,6 @@ var ( } ) -type Silence interface { - GetRuleUID() *string -} - type RuleUIDToNamespaceStore interface { GetNamespacesByRuleUID(ctx context.Context, orgID int64, uids ...string) (map[string]string, error) } @@ -104,7 +101,7 @@ func NewSilenceService(ac ac.AccessControl, store RuleUIDToNamespaceStore) *Sile // Global silence (one that is not attached to a particular rule) is considered available to all users. // For silences that are not attached to a rule, are checked against authorization. // This method is more preferred when many silences need to be checked. -func (s SilenceService) FilterByAccess(ctx context.Context, user identity.Requester, silences ...Silence) ([]Silence, error) { +func (s SilenceService) FilterByAccess(ctx context.Context, user identity.Requester, silences ...*models.Silence) ([]*models.Silence, error) { canAll, err := s.HasAccess(ctx, user, readAllSilencesEvaluator) if err != nil || canAll { // return early if user can either read all silences or there is an error return silences, err @@ -113,8 +110,8 @@ func (s SilenceService) FilterByAccess(ctx context.Context, user identity.Reques if err != nil || !canSome { return nil, err } - result := make([]Silence, 0, len(silences)) - silencesByRuleUID := make(map[string][]Silence, len(silences)) + result := make([]*models.Silence, 0, len(silences)) + silencesByRuleUID := make(map[string][]*models.Silence, len(silences)) for _, silence := range silences { ruleUID := silence.GetRuleUID() if ruleUID == nil { // if this is a general silence @@ -154,7 +151,7 @@ func (s SilenceService) FilterByAccess(ctx context.Context, user identity.Reques } // AuthorizeReadSilence checks if user has access to read a silence -func (s SilenceService) AuthorizeReadSilence(ctx context.Context, user identity.Requester, silence Silence) error { +func (s SilenceService) AuthorizeReadSilence(ctx context.Context, user identity.Requester, silence *models.Silence) error { canAll, err := s.HasAccess(ctx, user, readAllSilencesEvaluator) if canAll || err != nil { // return early if user can either read all silences or there is error return err @@ -186,7 +183,7 @@ func (s SilenceService) AuthorizeReadSilence(ctx context.Context, user identity. } // AuthorizeCreateSilence checks if user has access to create a silence. Returns ErrAuthorizationBase if user is not authorized -func (s SilenceService) AuthorizeCreateSilence(ctx context.Context, user identity.Requester, silence Silence) error { +func (s SilenceService) AuthorizeCreateSilence(ctx context.Context, user identity.Requester, silence *models.Silence) error { canAny, err := s.HasAccess(ctx, user, createAnySilenceEvaluator) if err != nil || canAny { // return early if user can either create any silence or there is an error @@ -215,7 +212,7 @@ func (s SilenceService) AuthorizeCreateSilence(ctx context.Context, user identit } // AuthorizeUpdateSilence checks if user has access to update\expire a silence. Returns ErrAuthorizationBase if user is not authorized -func (s SilenceService) AuthorizeUpdateSilence(ctx context.Context, user identity.Requester, silence Silence) error { +func (s SilenceService) AuthorizeUpdateSilence(ctx context.Context, user identity.Requester, silence *models.Silence) error { canAny, err := s.HasAccess(ctx, user, updateAnySilenceEvaluator) if err != nil || canAny { // return early if user can either update any silence or there is an error diff --git a/pkg/services/ngalert/accesscontrol/silences_test.go b/pkg/services/ngalert/accesscontrol/silences_test.go index 1d85886032b..570c51f4773 100644 --- a/pkg/services/ngalert/accesscontrol/silences_test.go +++ b/pkg/services/ngalert/accesscontrol/silences_test.go @@ -11,6 +11,7 @@ import ( ac "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/auth/identity" "github.com/grafana/grafana/pkg/services/dashboards" + "github.com/grafana/grafana/pkg/services/ngalert/models" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/tsdb/cloudwatch/utils" ) @@ -18,15 +19,15 @@ import ( var orgID = rand.Int63() func TestFilterByAccess(t *testing.T) { - global := testSilence{ID: "global", RuleUID: nil} - ruleSilence1 := testSilence{ID: "rule-1", RuleUID: utils.Pointer("rule-1-uid")} + global := testSilence("global", nil) + ruleSilence1 := testSilence("rule-1", utils.Pointer("rule-1-uid")) folder1 := "rule-1-folder-uid" folder1Scope := dashboards.ScopeFoldersProvider.GetResourceScopeUID(folder1) - ruleSilence2 := testSilence{ID: "rule-2", RuleUID: utils.Pointer("rule-2-uid")} + ruleSilence2 := testSilence("rule-2", utils.Pointer("rule-2-uid")) folder2 := "rule-2-folder-uid" - notFoundRule := testSilence{ID: "unknown-rule", RuleUID: utils.Pointer("unknown-rule-uid")} + notFoundRule := testSilence("unknown-rule", utils.Pointer("unknown-rule-uid")) - silences := []Silence{ + silences := []*models.Silence{ global, ruleSilence1, ruleSilence2, @@ -36,19 +37,19 @@ func TestFilterByAccess(t *testing.T) { testCases := []struct { name string user identity.Requester - expected []Silence + expected []*models.Silence expectedDbAccess bool }{ { name: "no silence access, empty list", user: newUser(), - expected: []Silence{}, + expected: []*models.Silence{}, expectedDbAccess: false, }, { name: "instance reader should get all", user: newUser(ac.Permission{Action: instancesRead}), - expected: []Silence{ + expected: []*models.Silence{ global, ruleSilence1, ruleSilence2, @@ -59,7 +60,7 @@ func TestFilterByAccess(t *testing.T) { { name: "silence reader should get global + folder", user: newUser(ac.Permission{Action: silenceRead, Scope: folder1Scope}), - expected: []Silence{ + expected: []*models.Silence{ global, ruleSilence1, }, @@ -71,8 +72,8 @@ func TestFilterByAccess(t *testing.T) { ac := &recordingAccessControlFake{} store := &fakeRuleUIDToNamespaceStore{ Response: map[string]string{ - *ruleSilence1.RuleUID: folder1, - *ruleSilence2.RuleUID: folder2, + *ruleSilence1.GetRuleUID(): folder1, + *ruleSilence2.GetRuleUID(): folder2, }, } svc := NewSilenceService(ac, store) @@ -93,60 +94,60 @@ func TestFilterByAccess(t *testing.T) { } func TestAuthorizeReadSilence(t *testing.T) { - global := testSilence{ID: "global", RuleUID: nil} - ruleSilence1 := testSilence{ID: "rule-1", RuleUID: utils.Pointer("rule-1-uid")} + global := testSilence("global", nil) + ruleSilence1 := testSilence("rule-1", utils.Pointer("rule-1-uid")) folder1 := "rule-1-folder-uid" folder1Scope := dashboards.ScopeFoldersProvider.GetResourceScopeUID(folder1) - ruleSilence2 := testSilence{ID: "rule-2", RuleUID: utils.Pointer("rule-2-uid")} + ruleSilence2 := testSilence("rule-2", utils.Pointer("rule-2-uid")) folder2 := "rule-2-folder-uid" - notFoundRule := testSilence{ID: "unknown-rule", RuleUID: utils.Pointer("unknown-rule-uid")} + notFoundRule := testSilence("unknown-rule", utils.Pointer("unknown-rule-uid")) testCases := []struct { name string user identity.Requester - silence []testSilence + silence []*models.Silence expectedErr error expectedDbAccess bool }{ { name: "not authorized without permissions", user: newUser(), - silence: []testSilence{global, ruleSilence1, notFoundRule}, + silence: []*models.Silence{global, ruleSilence1, notFoundRule}, expectedErr: ErrAuthorizationBase, expectedDbAccess: false, }, { name: "instance reader can read any silence", user: newUser(ac.Permission{Action: instancesRead}), - silence: []testSilence{global, ruleSilence1, notFoundRule}, + silence: []*models.Silence{global, ruleSilence1, notFoundRule}, expectedErr: nil, expectedDbAccess: false, }, { name: "silence reader can read global", user: newUser(ac.Permission{Action: silenceRead, Scope: folder1Scope}), - silence: []testSilence{global}, + silence: []*models.Silence{global}, expectedErr: nil, expectedDbAccess: false, }, { name: "silence reader can read from allowed folder", user: newUser(ac.Permission{Action: silenceRead, Scope: folder1Scope}), - silence: []testSilence{ruleSilence1}, + silence: []*models.Silence{ruleSilence1}, expectedErr: nil, expectedDbAccess: true, }, { name: "silence reader cannot read from other folders", user: newUser(ac.Permission{Action: silenceRead, Scope: folder1Scope}), - silence: []testSilence{ruleSilence2}, + silence: []*models.Silence{ruleSilence2}, expectedErr: ErrAuthorizationBase, expectedDbAccess: true, }, { name: "silence reader cannot read unknown rule", user: newUser(ac.Permission{Action: silenceRead, Scope: folder1Scope}), - silence: []testSilence{notFoundRule}, + silence: []*models.Silence{notFoundRule}, expectedErr: ErrAuthorizationBase, expectedDbAccess: true, }, @@ -155,12 +156,12 @@ func TestAuthorizeReadSilence(t *testing.T) { for _, testCase := range testCases { t.Run(testCase.name, func(t *testing.T) { for _, silence := range testCase.silence { - t.Run(silence.ID, func(t *testing.T) { + t.Run(*silence.ID, func(t *testing.T) { ac := &recordingAccessControlFake{} store := &fakeRuleUIDToNamespaceStore{ Response: map[string]string{ - *ruleSilence1.RuleUID: folder1, - *ruleSilence2.RuleUID: folder2, + *ruleSilence1.GetRuleUID(): folder1, + *ruleSilence2.GetRuleUID(): folder2, }, } svc := NewSilenceService(ac, store) @@ -183,16 +184,16 @@ func TestAuthorizeReadSilence(t *testing.T) { } func TestAuthorizeCreateSilence(t *testing.T) { - global := testSilence{ID: "global", RuleUID: nil} - ruleSilence1 := testSilence{ID: "rule-1", RuleUID: utils.Pointer("rule-1-uid")} + global := testSilence("global", nil) + ruleSilence1 := testSilence("rule-1", utils.Pointer("rule-1-uid")) folder1 := "rule-1-folder-uid" folder1Scope := dashboards.ScopeFoldersProvider.GetResourceScopeUID(folder1) - ruleSilence2 := testSilence{ID: "rule-2", RuleUID: utils.Pointer("rule-2-uid")} + ruleSilence2 := testSilence("rule-2", utils.Pointer("rule-2-uid")) folder2 := "rule-2-folder-uid" folder2Scope := dashboards.ScopeFoldersProvider.GetResourceScopeUID(folder2) - notFoundRule := testSilence{ID: "unknown-rule", RuleUID: utils.Pointer("unknown-rule-uid")} + notFoundRule := testSilence("unknown-rule", utils.Pointer("unknown-rule-uid")) - silences := []testSilence{ + silences := []*models.Silence{ global, ruleSilence1, ruleSilence2, @@ -208,7 +209,7 @@ func TestAuthorizeCreateSilence(t *testing.T) { user identity.Requester expectedErr error expectedDbAccess bool - overrides map[testSilence]override + overrides map[*models.Silence]override }{ { name: "not authorized without permissions", @@ -243,7 +244,7 @@ func TestAuthorizeCreateSilence(t *testing.T) { { name: "instance read + silence create", user: newUser(ac.Permission{Action: silenceCreate, Scope: folder1Scope}, ac.Permission{Action: instancesRead}), - overrides: map[testSilence]override{ + overrides: map[*models.Silence]override{ global: { expectedErr: ErrAuthorizationBase, expectedDbAccess: false, @@ -259,7 +260,7 @@ func TestAuthorizeCreateSilence(t *testing.T) { { name: "silence read + instance create", user: newUser(ac.Permission{Action: silenceRead, Scope: folder1Scope}, ac.Permission{Action: instancesCreate}), - overrides: map[testSilence]override{ + overrides: map[*models.Silence]override{ global: { expectedErr: nil, expectedDbAccess: false, @@ -275,7 +276,7 @@ func TestAuthorizeCreateSilence(t *testing.T) { { name: "silence read + create", user: newUser(ac.Permission{Action: silenceRead, Scope: folder1Scope}, ac.Permission{Action: silenceCreate, Scope: folder1Scope}), - overrides: map[testSilence]override{ + overrides: map[*models.Silence]override{ global: { expectedErr: ErrAuthorizationBase, expectedDbAccess: false, @@ -299,12 +300,12 @@ func TestAuthorizeCreateSilence(t *testing.T) { expectedErr = s.expectedErr expectedDbAccess = s.expectedDbAccess } - t.Run(silence.ID, func(t *testing.T) { + t.Run(*silence.ID, func(t *testing.T) { ac := &recordingAccessControlFake{} store := &fakeRuleUIDToNamespaceStore{ Response: map[string]string{ - *ruleSilence1.RuleUID: folder1, - *ruleSilence2.RuleUID: folder2, + *ruleSilence1.GetRuleUID(): folder1, + *ruleSilence2.GetRuleUID(): folder2, }, } svc := NewSilenceService(ac, store) @@ -328,16 +329,16 @@ func TestAuthorizeCreateSilence(t *testing.T) { } func TestAuthorizeUpdateSilence(t *testing.T) { - global := testSilence{ID: "global", RuleUID: nil} - ruleSilence1 := testSilence{ID: "rule-1", RuleUID: utils.Pointer("rule-1-uid")} + global := testSilence("global", nil) + ruleSilence1 := testSilence("rule-1", utils.Pointer("rule-1-uid")) folder1 := "rule-1-folder-uid" folder1Scope := dashboards.ScopeFoldersProvider.GetResourceScopeUID(folder1) - ruleSilence2 := testSilence{ID: "rule-2", RuleUID: utils.Pointer("rule-2-uid")} + ruleSilence2 := testSilence("rule-2", utils.Pointer("rule-2-uid")) folder2 := "rule-2-folder-uid" folder2Scope := dashboards.ScopeFoldersProvider.GetResourceScopeUID(folder2) - notFoundRule := testSilence{ID: "unknown-rule", RuleUID: utils.Pointer("unknown-rule-uid")} + notFoundRule := testSilence("unknown-rule", utils.Pointer("unknown-rule-uid")) - silences := []testSilence{ + silences := []*models.Silence{ global, ruleSilence1, ruleSilence2, @@ -353,7 +354,7 @@ func TestAuthorizeUpdateSilence(t *testing.T) { user identity.Requester expectedErr error expectedDbAccess bool - overrides map[testSilence]override + overrides map[*models.Silence]override }{ { name: "not authorized without permissions", @@ -388,7 +389,7 @@ func TestAuthorizeUpdateSilence(t *testing.T) { { name: "instance read + silence write", user: newUser(ac.Permission{Action: silenceWrite, Scope: folder1Scope}, ac.Permission{Action: instancesRead}), - overrides: map[testSilence]override{ + overrides: map[*models.Silence]override{ global: { expectedErr: ErrAuthorizationBase, expectedDbAccess: false, @@ -404,7 +405,7 @@ func TestAuthorizeUpdateSilence(t *testing.T) { { name: "silence read + instance write", user: newUser(ac.Permission{Action: silenceRead, Scope: folder1Scope}, ac.Permission{Action: instancesWrite}), - overrides: map[testSilence]override{ + overrides: map[*models.Silence]override{ global: { expectedErr: nil, expectedDbAccess: false, @@ -420,7 +421,7 @@ func TestAuthorizeUpdateSilence(t *testing.T) { { name: "silence read + write", user: newUser(ac.Permission{Action: silenceRead, Scope: folder1Scope}, ac.Permission{Action: silenceWrite, Scope: folder1Scope}), - overrides: map[testSilence]override{ + overrides: map[*models.Silence]override{ global: { expectedErr: ErrAuthorizationBase, expectedDbAccess: false, @@ -444,12 +445,12 @@ func TestAuthorizeUpdateSilence(t *testing.T) { expectedErr = s.expectedErr expectedDbAccess = s.expectedDbAccess } - t.Run(silence.ID, func(t *testing.T) { + t.Run(*silence.ID, func(t *testing.T) { ac := &recordingAccessControlFake{} store := &fakeRuleUIDToNamespaceStore{ Response: map[string]string{ - *ruleSilence1.RuleUID: folder1, - *ruleSilence2.RuleUID: folder2, + *ruleSilence1.GetRuleUID(): folder1, + *ruleSilence2.GetRuleUID(): folder2, }, } svc := NewSilenceService(ac, store) @@ -472,13 +473,8 @@ func TestAuthorizeUpdateSilence(t *testing.T) { } } -type testSilence struct { - ID string - RuleUID *string -} - -func (t testSilence) GetRuleUID() *string { - return t.RuleUID +func testSilence(id string, ruleUID *string) *models.Silence { + return &models.Silence{ID: &id, RuleUID: ruleUID} } type fakeRuleUIDToNamespaceStore struct { diff --git a/pkg/services/ngalert/api/api_alertmanager.go b/pkg/services/ngalert/api/api_alertmanager.go index bdb3756de43..4e9debc356a 100644 --- a/pkg/services/ngalert/api/api_alertmanager.go +++ b/pkg/services/ngalert/api/api_alertmanager.go @@ -9,14 +9,12 @@ import ( "strings" "time" - "github.com/go-openapi/strfmt" alertingNotify "github.com/grafana/alerting/notify" "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/services/accesscontrol" contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" - authz "github.com/grafana/grafana/pkg/services/ngalert/accesscontrol" apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" "github.com/grafana/grafana/pkg/services/ngalert/notifier" "github.com/grafana/grafana/pkg/services/ngalert/store" @@ -58,50 +56,6 @@ func (srv AlertmanagerSrv) RouteGetAMStatus(c *contextmodel.ReqContext) response return response.JSON(http.StatusOK, status) } -func (srv AlertmanagerSrv) RouteCreateSilence(c *contextmodel.ReqContext, postableSilence apimodels.PostableSilence) response.Response { - err := postableSilence.Validate(strfmt.Default) - if err != nil { - srv.log.Error("Silence failed validation", "error", err) - return ErrResp(http.StatusBadRequest, err, "silence failed validation") - } - - action := accesscontrol.ActionAlertingInstanceUpdate - if postableSilence.ID == "" { - action = accesscontrol.ActionAlertingInstanceCreate - } - evaluator := accesscontrol.EvalPermission(action) - if !accesscontrol.HasAccess(srv.ac, c)(evaluator) { - errAction := "update" - if postableSilence.ID == "" { - errAction = "create" - } - return response.Err(authz.NewAuthorizationErrorWithPermissions(fmt.Sprintf("%s silences", errAction), evaluator)) - } - - silenceID, err := srv.mam.CreateSilence(c.Req.Context(), c.SignedInUser.GetOrgID(), &postableSilence) - if err != nil { - if errors.Is(err, notifier.ErrNoAlertmanagerForOrg) { - return ErrResp(http.StatusNotFound, err, "") - } - if errors.Is(err, notifier.ErrAlertmanagerNotReady) { - return ErrResp(http.StatusConflict, err, "") - } - - if errors.Is(err, alertingNotify.ErrSilenceNotFound) { - return ErrResp(http.StatusNotFound, err, "") - } - - if errors.Is(err, alertingNotify.ErrCreateSilenceBadPayload) { - return ErrResp(http.StatusBadRequest, err, "") - } - - return ErrResp(http.StatusInternalServerError, err, "failed to create silence") - } - return response.JSON(http.StatusAccepted, apimodels.PostSilencesOKBody{ - SilenceID: silenceID, - }) -} - func (srv AlertmanagerSrv) RouteDeleteAlertingConfig(c *contextmodel.ReqContext) response.Response { am, errResp := srv.AlertmanagerFor(c.SignedInUser.GetOrgID()) if errResp != nil { @@ -116,22 +70,6 @@ func (srv AlertmanagerSrv) RouteDeleteAlertingConfig(c *contextmodel.ReqContext) return response.JSON(http.StatusAccepted, util.DynMap{"message": "configuration deleted; the default is applied"}) } -func (srv AlertmanagerSrv) RouteDeleteSilence(c *contextmodel.ReqContext, silenceID string) response.Response { - if err := srv.mam.DeleteSilence(c.Req.Context(), c.SignedInUser.GetOrgID(), silenceID); err != nil { - if errors.Is(err, notifier.ErrNoAlertmanagerForOrg) { - return ErrResp(http.StatusNotFound, err, "") - } - if errors.Is(err, notifier.ErrAlertmanagerNotReady) { - return ErrResp(http.StatusConflict, err, "") - } - if errors.Is(err, alertingNotify.ErrSilenceNotFound) { - return ErrResp(http.StatusNotFound, err, "") - } - return ErrResp(http.StatusInternalServerError, err, "") - } - return response.JSON(http.StatusOK, util.DynMap{"message": "silence deleted"}) -} - func (srv AlertmanagerSrv) RouteGetAlertingConfig(c *contextmodel.ReqContext) response.Response { canSeeAutogen := c.SignedInUser.HasRole(org.RoleAdmin) config, err := srv.mam.GetAlertmanagerConfiguration(c.Req.Context(), c.SignedInUser.GetOrgID(), canSeeAutogen) @@ -207,40 +145,6 @@ func (srv AlertmanagerSrv) RouteGetAMAlerts(c *contextmodel.ReqContext) response return response.JSON(http.StatusOK, alerts) } -func (srv AlertmanagerSrv) RouteGetSilence(c *contextmodel.ReqContext, silenceID string) response.Response { - am, errResp := srv.AlertmanagerFor(c.SignedInUser.GetOrgID()) - if errResp != nil { - return errResp - } - - gettableSilence, err := am.GetSilence(c.Req.Context(), silenceID) - if err != nil { - if errors.Is(err, alertingNotify.ErrSilenceNotFound) { - return ErrResp(http.StatusNotFound, err, "") - } - // any other error here should be an unexpected failure and thus an internal error - return ErrResp(http.StatusInternalServerError, err, "") - } - return response.JSON(http.StatusOK, gettableSilence) -} - -func (srv AlertmanagerSrv) RouteGetSilences(c *contextmodel.ReqContext) response.Response { - am, errResp := srv.AlertmanagerFor(c.SignedInUser.GetOrgID()) - if errResp != nil { - return errResp - } - - gettableSilences, err := am.ListSilences(c.Req.Context(), c.QueryStrings("filter")) - if err != nil { - if errors.Is(err, alertingNotify.ErrListSilencesBadPayload) { - return ErrResp(http.StatusBadRequest, err, "") - } - // any other error here should be an unexpected failure and thus an internal error - return ErrResp(http.StatusInternalServerError, err, "") - } - return response.JSON(http.StatusOK, gettableSilences) -} - func (srv AlertmanagerSrv) RoutePostGrafanaAlertingConfigHistoryActivate(c *contextmodel.ReqContext, id string) response.Response { confId, err := strconv.ParseInt(id, 10, 64) if err != nil { @@ -304,7 +208,7 @@ func (srv AlertmanagerSrv) RoutePostAlertingConfig(c *contextmodel.ReqContext, b return response.Error(http.StatusConflict, err.Error(), err) } - return ErrResp(http.StatusInternalServerError, err, "") + return response.ErrOrFallback(http.StatusInternalServerError, err.Error(), err) } func (srv AlertmanagerSrv) RouteGetReceivers(c *contextmodel.ReqContext) response.Response { diff --git a/pkg/services/ngalert/api/api_alertmanager_silences.go b/pkg/services/ngalert/api/api_alertmanager_silences.go new file mode 100644 index 00000000000..6226e73a528 --- /dev/null +++ b/pkg/services/ngalert/api/api_alertmanager_silences.go @@ -0,0 +1,112 @@ +package api + +import ( + "errors" + "fmt" + "net/http" + + "github.com/go-openapi/strfmt" + + alertingNotify "github.com/grafana/alerting/notify" + "github.com/grafana/grafana/pkg/api/response" + "github.com/grafana/grafana/pkg/services/accesscontrol" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" + authz "github.com/grafana/grafana/pkg/services/ngalert/accesscontrol" + apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" + "github.com/grafana/grafana/pkg/services/ngalert/notifier" + "github.com/grafana/grafana/pkg/util" +) + +func (srv AlertmanagerSrv) RouteGetSilence(c *contextmodel.ReqContext, silenceID string) response.Response { + am, errResp := srv.AlertmanagerFor(c.SignedInUser.GetOrgID()) + if errResp != nil { + return errResp + } + + gettableSilence, err := am.GetSilence(c.Req.Context(), silenceID) + if err != nil { + if errors.Is(err, alertingNotify.ErrSilenceNotFound) { + return ErrResp(http.StatusNotFound, err, "") + } + // any other error here should be an unexpected failure and thus an internal error + return ErrResp(http.StatusInternalServerError, err, "") + } + return response.JSON(http.StatusOK, gettableSilence) +} + +func (srv AlertmanagerSrv) RouteGetSilences(c *contextmodel.ReqContext) response.Response { + am, errResp := srv.AlertmanagerFor(c.SignedInUser.GetOrgID()) + if errResp != nil { + return errResp + } + + gettableSilences, err := am.ListSilences(c.Req.Context(), c.QueryStrings("filter")) + if err != nil { + if errors.Is(err, alertingNotify.ErrListSilencesBadPayload) { + return ErrResp(http.StatusBadRequest, err, "") + } + // any other error here should be an unexpected failure and thus an internal error + return ErrResp(http.StatusInternalServerError, err, "") + } + return response.JSON(http.StatusOK, gettableSilences) +} + +func (srv AlertmanagerSrv) RouteCreateSilence(c *contextmodel.ReqContext, postableSilence apimodels.PostableSilence) response.Response { + err := postableSilence.Validate(strfmt.Default) + if err != nil { + srv.log.Error("Silence failed validation", "error", err) + return ErrResp(http.StatusBadRequest, err, "silence failed validation") + } + + action := accesscontrol.ActionAlertingInstanceUpdate + if postableSilence.ID == "" { + action = accesscontrol.ActionAlertingInstanceCreate + } + evaluator := accesscontrol.EvalPermission(action) + if !accesscontrol.HasAccess(srv.ac, c)(evaluator) { + errAction := "update" + if postableSilence.ID == "" { + errAction = "create" + } + return response.Err(authz.NewAuthorizationErrorWithPermissions(fmt.Sprintf("%s silences", errAction), evaluator)) + } + + silenceID, err := srv.mam.CreateSilence(c.Req.Context(), c.SignedInUser.GetOrgID(), &postableSilence) + if err != nil { + if errors.Is(err, notifier.ErrNoAlertmanagerForOrg) { + return ErrResp(http.StatusNotFound, err, "") + } + if errors.Is(err, notifier.ErrAlertmanagerNotReady) { + return ErrResp(http.StatusConflict, err, "") + } + + if errors.Is(err, alertingNotify.ErrSilenceNotFound) { + return ErrResp(http.StatusNotFound, err, "") + } + + if errors.Is(err, alertingNotify.ErrCreateSilenceBadPayload) { + return ErrResp(http.StatusBadRequest, err, "") + } + + return ErrResp(http.StatusInternalServerError, err, "failed to create silence") + } + return response.JSON(http.StatusAccepted, apimodels.PostSilencesOKBody{ + SilenceID: silenceID, + }) +} + +func (srv AlertmanagerSrv) RouteDeleteSilence(c *contextmodel.ReqContext, silenceID string) response.Response { + if err := srv.mam.DeleteSilence(c.Req.Context(), c.SignedInUser.GetOrgID(), silenceID); err != nil { + if errors.Is(err, notifier.ErrNoAlertmanagerForOrg) { + return ErrResp(http.StatusNotFound, err, "") + } + if errors.Is(err, notifier.ErrAlertmanagerNotReady) { + return ErrResp(http.StatusConflict, err, "") + } + if errors.Is(err, alertingNotify.ErrSilenceNotFound) { + return ErrResp(http.StatusNotFound, err, "") + } + return ErrResp(http.StatusInternalServerError, err, "") + } + return response.JSON(http.StatusOK, util.DynMap{"message": "silence deleted"}) +} diff --git a/pkg/services/ngalert/api/api_alertmanager_silences_test.go b/pkg/services/ngalert/api/api_alertmanager_silences_test.go new file mode 100644 index 00000000000..a2279dd0603 --- /dev/null +++ b/pkg/services/ngalert/api/api_alertmanager_silences_test.go @@ -0,0 +1,194 @@ +package api + +import ( + "context" + "math/rand" + "net/http" + "testing" + "time" + + "github.com/go-openapi/strfmt" + amv2 "github.com/prometheus/alertmanager/api/v2/models" + "github.com/stretchr/testify/require" + + "github.com/grafana/grafana/pkg/services/accesscontrol" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" + apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" + "github.com/grafana/grafana/pkg/services/org" + "github.com/grafana/grafana/pkg/services/user" + "github.com/grafana/grafana/pkg/util" + "github.com/grafana/grafana/pkg/web" +) + +func TestSilenceCreate(t *testing.T) { + makeSilence := func(comment string, createdBy string, + startsAt, endsAt strfmt.DateTime, matchers amv2.Matchers) amv2.Silence { + return amv2.Silence{ + Comment: &comment, + CreatedBy: &createdBy, + StartsAt: &startsAt, + EndsAt: &endsAt, + Matchers: matchers, + } + } + + now := time.Now() + dt := func(t time.Time) strfmt.DateTime { return strfmt.DateTime(t) } + tru := true + testString := "testName" + matchers := amv2.Matchers{&amv2.Matcher{Name: &testString, IsEqual: &tru, IsRegex: &tru, Value: &testString}} + + cases := []struct { + name string + silence amv2.Silence + status int + }{ + {"Valid Silence", + makeSilence("", "tests", dt(now), dt(now.Add(1*time.Second)), matchers), + http.StatusAccepted, + }, + {"No Comment Silence", + func() amv2.Silence { + s := makeSilence("", "tests", dt(now), dt(now.Add(1*time.Second)), matchers) + s.Comment = nil + return s + }(), + http.StatusBadRequest, + }, + } + + for _, cas := range cases { + t.Run(cas.name, func(t *testing.T) { + rc := contextmodel.ReqContext{ + Context: &web.Context{ + Req: &http.Request{}, + }, + SignedInUser: &user.SignedInUser{ + OrgRole: org.RoleEditor, + OrgID: 1, + Permissions: map[int64]map[string][]string{ + 1: {accesscontrol.ActionAlertingInstanceCreate: {}}, + }, + }, + } + + srv := createSut(t) + + resp := srv.RouteCreateSilence(&rc, amv2.PostableSilence{ + ID: "", + Silence: cas.silence, + }) + require.Equal(t, cas.status, resp.Status()) + }) + } +} + +func TestRouteCreateSilence(t *testing.T) { + tesCases := []struct { + name string + silence func() apimodels.PostableSilence + permissions map[int64]map[string][]string + expectedStatus int + }{ + { + name: "new silence, role-based access control is enabled, not authorized", + silence: silenceGen(withEmptyID), + permissions: map[int64]map[string][]string{ + 1: {}, + }, + expectedStatus: http.StatusForbidden, + }, + { + name: "new silence, role-based access control is enabled, authorized", + silence: silenceGen(withEmptyID), + permissions: map[int64]map[string][]string{ + 1: {accesscontrol.ActionAlertingInstanceCreate: {}}, + }, + expectedStatus: http.StatusAccepted, + }, + { + name: "update silence, role-based access control is enabled, not authorized", + silence: silenceGen(), + permissions: map[int64]map[string][]string{ + 1: {accesscontrol.ActionAlertingInstanceCreate: {}}, + }, + expectedStatus: http.StatusForbidden, + }, + { + name: "update silence, role-based access control is enabled, authorized", + silence: silenceGen(), + permissions: map[int64]map[string][]string{ + 1: {accesscontrol.ActionAlertingInstanceUpdate: {}}, + }, + expectedStatus: http.StatusAccepted, + }, + } + + for _, tesCase := range tesCases { + t.Run(tesCase.name, func(t *testing.T) { + sut := createSut(t) + + rc := contextmodel.ReqContext{ + Context: &web.Context{ + Req: &http.Request{}, + }, + SignedInUser: &user.SignedInUser{ + Permissions: tesCase.permissions, + OrgID: 1, + }, + } + + silence := tesCase.silence() + + if silence.ID != "" { + alertmanagerFor, err := sut.mam.AlertmanagerFor(1) + require.NoError(t, err) + silence.ID = "" + newID, err := alertmanagerFor.CreateSilence(context.Background(), &silence) + require.NoError(t, err) + silence.ID = newID + } + + response := sut.RouteCreateSilence(&rc, silence) + require.Equal(t, tesCase.expectedStatus, response.Status()) + }) + } +} + +func silenceGen(mutatorFuncs ...func(*apimodels.PostableSilence)) func() apimodels.PostableSilence { + return func() apimodels.PostableSilence { + testString := util.GenerateShortUID() + isEqual := rand.Int()%2 == 0 + isRegex := rand.Int()%2 == 0 + value := util.GenerateShortUID() + if isRegex { + value = ".*" + util.GenerateShortUID() + } + + matchers := amv2.Matchers{&amv2.Matcher{Name: &testString, IsEqual: &isEqual, IsRegex: &isRegex, Value: &value}} + comment := util.GenerateShortUID() + starts := strfmt.DateTime(timeNow().Add(-time.Duration(rand.Int63n(9)+1) * time.Second)) + ends := strfmt.DateTime(timeNow().Add(time.Duration(rand.Int63n(9)+1) * time.Second)) + createdBy := "User-" + util.GenerateShortUID() + s := apimodels.PostableSilence{ + ID: util.GenerateShortUID(), + Silence: amv2.Silence{ + Comment: &comment, + CreatedBy: &createdBy, + EndsAt: &ends, + Matchers: matchers, + StartsAt: &starts, + }, + } + + for _, mutator := range mutatorFuncs { + mutator(&s) + } + + return s + } +} + +func withEmptyID(silence *apimodels.PostableSilence) { + silence.ID = "" +} diff --git a/pkg/services/ngalert/api/api_alertmanager_test.go b/pkg/services/ngalert/api/api_alertmanager_test.go index da6c5c92d11..a9b04aa3934 100644 --- a/pkg/services/ngalert/api/api_alertmanager_test.go +++ b/pkg/services/ngalert/api/api_alertmanager_test.go @@ -4,23 +4,20 @@ import ( "context" "crypto/md5" "encoding/json" - "math/rand" "net/http" "testing" "time" - "github.com/go-openapi/strfmt" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" - alertingNotify "github.com/grafana/alerting/notify" - amv2 "github.com/prometheus/alertmanager/api/v2/models" "github.com/prometheus/alertmanager/pkg/labels" "github.com/prometheus/client_golang/prometheus" "github.com/stretchr/testify/require" + alertingNotify "github.com/grafana/alerting/notify" + "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/infra/log" - "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/accesscontrol/acimpl" contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/featuremgmt" @@ -35,7 +32,6 @@ import ( secretsManager "github.com/grafana/grafana/pkg/services/secrets/manager" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/setting" - "github.com/grafana/grafana/pkg/util" "github.com/grafana/grafana/pkg/web" ) @@ -624,141 +620,6 @@ func TestRoutePostTestTemplates(t *testing.T) { }) } -func TestSilenceCreate(t *testing.T) { - makeSilence := func(comment string, createdBy string, - startsAt, endsAt strfmt.DateTime, matchers amv2.Matchers) amv2.Silence { - return amv2.Silence{ - Comment: &comment, - CreatedBy: &createdBy, - StartsAt: &startsAt, - EndsAt: &endsAt, - Matchers: matchers, - } - } - - now := time.Now() - dt := func(t time.Time) strfmt.DateTime { return strfmt.DateTime(t) } - tru := true - testString := "testName" - matchers := amv2.Matchers{&amv2.Matcher{Name: &testString, IsEqual: &tru, IsRegex: &tru, Value: &testString}} - - cases := []struct { - name string - silence amv2.Silence - status int - }{ - {"Valid Silence", - makeSilence("", "tests", dt(now), dt(now.Add(1*time.Second)), matchers), - http.StatusAccepted, - }, - {"No Comment Silence", - func() amv2.Silence { - s := makeSilence("", "tests", dt(now), dt(now.Add(1*time.Second)), matchers) - s.Comment = nil - return s - }(), - http.StatusBadRequest, - }, - } - - for _, cas := range cases { - t.Run(cas.name, func(t *testing.T) { - rc := contextmodel.ReqContext{ - Context: &web.Context{ - Req: &http.Request{}, - }, - SignedInUser: &user.SignedInUser{ - OrgRole: org.RoleEditor, - OrgID: 1, - Permissions: map[int64]map[string][]string{ - 1: {accesscontrol.ActionAlertingInstanceCreate: {}}, - }, - }, - } - - srv := createSut(t) - - resp := srv.RouteCreateSilence(&rc, amv2.PostableSilence{ - ID: "", - Silence: cas.silence, - }) - require.Equal(t, cas.status, resp.Status()) - }) - } -} - -func TestRouteCreateSilence(t *testing.T) { - tesCases := []struct { - name string - silence func() apimodels.PostableSilence - permissions map[int64]map[string][]string - expectedStatus int - }{ - { - name: "new silence, role-based access control is enabled, not authorized", - silence: silenceGen(withEmptyID), - permissions: map[int64]map[string][]string{ - 1: {}, - }, - expectedStatus: http.StatusForbidden, - }, - { - name: "new silence, role-based access control is enabled, authorized", - silence: silenceGen(withEmptyID), - permissions: map[int64]map[string][]string{ - 1: {accesscontrol.ActionAlertingInstanceCreate: {}}, - }, - expectedStatus: http.StatusAccepted, - }, - { - name: "update silence, role-based access control is enabled, not authorized", - silence: silenceGen(), - permissions: map[int64]map[string][]string{ - 1: {accesscontrol.ActionAlertingInstanceCreate: {}}, - }, - expectedStatus: http.StatusForbidden, - }, - { - name: "update silence, role-based access control is enabled, authorized", - silence: silenceGen(), - permissions: map[int64]map[string][]string{ - 1: {accesscontrol.ActionAlertingInstanceUpdate: {}}, - }, - expectedStatus: http.StatusAccepted, - }, - } - - for _, tesCase := range tesCases { - t.Run(tesCase.name, func(t *testing.T) { - sut := createSut(t) - - rc := contextmodel.ReqContext{ - Context: &web.Context{ - Req: &http.Request{}, - }, - SignedInUser: &user.SignedInUser{ - Permissions: tesCase.permissions, - OrgID: 1, - }, - } - - silence := tesCase.silence() - - if silence.ID != "" { - alertmanagerFor, err := sut.mam.AlertmanagerFor(1) - require.NoError(t, err) - silence.ID = "" - newID, err := alertmanagerFor.CreateSilence(context.Background(), &silence) - require.NoError(t, err) - silence.ID = newID - } - - response := sut.RouteCreateSilence(&rc, silence) - require.Equal(t, tesCase.expectedStatus, response.Status()) - }) - } -} - func createSut(t *testing.T) AlertmanagerSrv { t.Helper() @@ -973,44 +834,6 @@ var brokenConfig = ` } }` -func silenceGen(mutatorFuncs ...func(*apimodels.PostableSilence)) func() apimodels.PostableSilence { - return func() apimodels.PostableSilence { - testString := util.GenerateShortUID() - isEqual := rand.Int()%2 == 0 - isRegex := rand.Int()%2 == 0 - value := util.GenerateShortUID() - if isRegex { - value = ".*" + util.GenerateShortUID() - } - - matchers := amv2.Matchers{&amv2.Matcher{Name: &testString, IsEqual: &isEqual, IsRegex: &isRegex, Value: &value}} - comment := util.GenerateShortUID() - starts := strfmt.DateTime(timeNow().Add(-time.Duration(rand.Int63n(9)+1) * time.Second)) - ends := strfmt.DateTime(timeNow().Add(time.Duration(rand.Int63n(9)+1) * time.Second)) - createdBy := "User-" + util.GenerateShortUID() - s := apimodels.PostableSilence{ - ID: util.GenerateShortUID(), - Silence: amv2.Silence{ - Comment: &comment, - CreatedBy: &createdBy, - EndsAt: &ends, - Matchers: matchers, - StartsAt: &starts, - }, - } - - for _, mutator := range mutatorFuncs { - mutator(&s) - } - - return s - } -} - -func withEmptyID(silence *apimodels.PostableSilence) { - silence.ID = "" -} - func createRequestCtxInOrg(org int64) *contextmodel.ReqContext { return &contextmodel.ReqContext{ Context: &web.Context{ diff --git a/pkg/services/ngalert/api/api_prometheus.go b/pkg/services/ngalert/api/api_prometheus.go index dae93551bea..d2601fa7e96 100644 --- a/pkg/services/ngalert/api/api_prometheus.go +++ b/pkg/services/ngalert/api/api_prometheus.go @@ -1,10 +1,10 @@ package api import ( + "context" "encoding/json" "errors" "fmt" - "net/http" "net/url" "sort" "strconv" @@ -17,7 +17,6 @@ import ( "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/infra/log" contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" - "github.com/grafana/grafana/pkg/services/folder" apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" "github.com/grafana/grafana/pkg/services/ngalert/eval" ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models" @@ -61,6 +60,20 @@ func (srv PrometheusSrv) RouteGetAlertStatuses(c *contextmodel.ReqContext) respo // As we are using req.Form directly, this triggers a call to ParseForm() if needed. c.Query("") + resp := PrepareAlertStatuses(srv.manager, AlertStatusesOptions{ + OrgID: c.SignedInUser.GetOrgID(), + Query: c.Req.Form, + }) + + return response.JSON(resp.HTTPStatusCode(), resp) +} + +type AlertStatusesOptions struct { + OrgID int64 + Query url.Values +} + +func PrepareAlertStatuses(manager state.AlertInstanceManager, opts AlertStatusesOptions) apimodels.AlertResponse { alertResponse := apimodels.AlertResponse{ DiscoveryBase: apimodels.DiscoveryBase{ Status: "success", @@ -71,11 +84,11 @@ func (srv PrometheusSrv) RouteGetAlertStatuses(c *contextmodel.ReqContext) respo } var labelOptions []ngmodels.LabelOption - if !getBoolWithDefault(c.Req.Form, queryIncludeInternalLabels, false) { + if !getBoolWithDefault(opts.Query, queryIncludeInternalLabels, false) { labelOptions = append(labelOptions, ngmodels.WithoutInternalLabels()) } - for _, alertState := range srv.manager.GetAll(c.SignedInUser.GetOrgID()) { + for _, alertState := range manager.GetAll(opts.OrgID) { startsAt := alertState.StartsAt valString := "" @@ -95,7 +108,7 @@ func (srv PrometheusSrv) RouteGetAlertStatuses(c *contextmodel.ReqContext) respo }) } - return response.JSON(alertResponse.HTTPStatusCode(), alertResponse) + return alertResponse } func formatValues(alertState *state.State) string { @@ -126,16 +139,16 @@ func formatValues(alertState *state.State) string { return fv } -func getPanelIDFromRequest(r *http.Request) (int64, error) { - if s := strings.TrimSpace(r.URL.Query().Get("panel_id")); s != "" { +func getPanelIDFromQuery(v url.Values) (int64, error) { + if s := strings.TrimSpace(v.Get("panel_id")); s != "" { return strconv.ParseInt(s, 10, 64) } return 0, nil } -func getMatchersFromRequest(r *http.Request) (labels.Matchers, error) { +func getMatchersFromQuery(v url.Values) (labels.Matchers, error) { var matchers labels.Matchers - for _, s := range r.URL.Query()["matcher"] { + for _, s := range v["matcher"] { var m labels.Matcher if err := json.Unmarshal([]byte(s), &m); err != nil { return nil, err @@ -148,9 +161,9 @@ func getMatchersFromRequest(r *http.Request) (labels.Matchers, error) { return matchers, nil } -func getStatesFromRequest(r *http.Request) ([]eval.State, error) { +func getStatesFromQuery(v url.Values) ([]eval.State, error) { var states []eval.State - for _, s := range r.URL.Query()["state"] { + for _, s := range v["state"] { s = strings.ToLower(s) switch s { case "normal", "inactive": @@ -171,6 +184,18 @@ func getStatesFromRequest(r *http.Request) ([]eval.State, error) { return states, nil } +type RuleGroupStatusesOptions struct { + Ctx context.Context + OrgID int64 + Query url.Values + Namespaces map[string]string + AuthorizeRuleGroup func(rules []*ngmodels.AlertRule) (bool, error) +} + +type ListAlertRulesStore interface { + ListAlertRules(ctx context.Context, query *ngmodels.ListAlertRulesQuery) (ngmodels.RulesGroup, error) +} + func (srv PrometheusSrv) RouteGetRuleStatuses(c *contextmodel.ReqContext) response.Response { // As we are using req.Form directly, this triggers a call to ParseForm() if needed. c.Query("") @@ -184,48 +209,6 @@ func (srv PrometheusSrv) RouteGetRuleStatuses(c *contextmodel.ReqContext) respon }, } - dashboardUID := c.Query("dashboard_uid") - panelID, err := getPanelIDFromRequest(c.Req) - if err != nil { - ruleResponse.DiscoveryBase.Status = "error" - ruleResponse.DiscoveryBase.Error = fmt.Sprintf("invalid panel_id: %s", err.Error()) - ruleResponse.DiscoveryBase.ErrorType = apiv1.ErrBadData - return response.JSON(ruleResponse.HTTPStatusCode(), ruleResponse) - } - if dashboardUID == "" && panelID != 0 { - ruleResponse.DiscoveryBase.Status = "error" - ruleResponse.DiscoveryBase.Error = "panel_id must be set with dashboard_uid" - ruleResponse.DiscoveryBase.ErrorType = apiv1.ErrBadData - return response.JSON(ruleResponse.HTTPStatusCode(), ruleResponse) - } - - limitGroups := getInt64WithDefault(c.Req.Form, "limit", -1) - limitRulesPerGroup := getInt64WithDefault(c.Req.Form, "limit_rules", -1) - limitAlertsPerRule := getInt64WithDefault(c.Req.Form, "limit_alerts", -1) - matchers, err := getMatchersFromRequest(c.Req) - if err != nil { - ruleResponse.DiscoveryBase.Status = "error" - ruleResponse.DiscoveryBase.Error = err.Error() - ruleResponse.DiscoveryBase.ErrorType = apiv1.ErrBadData - return response.JSON(ruleResponse.HTTPStatusCode(), ruleResponse) - } - withStates, err := getStatesFromRequest(c.Req) - if err != nil { - ruleResponse.DiscoveryBase.Status = "error" - ruleResponse.DiscoveryBase.Error = err.Error() - ruleResponse.DiscoveryBase.ErrorType = apiv1.ErrBadData - return response.JSON(ruleResponse.HTTPStatusCode(), ruleResponse) - } - withStatesFast := make(map[eval.State]struct{}) - for _, state := range withStates { - withStatesFast[state] = struct{}{} - } - - var labelOptions []ngmodels.LabelOption - if !getBoolWithDefault(c.Req.Form, queryIncludeInternalLabels, false) { - labelOptions = append(labelOptions, ngmodels.WithoutInternalLabels()) - } - namespaceMap, err := srv.store.GetUserVisibleNamespaces(c.Req.Context(), c.SignedInUser.GetOrgID(), c.SignedInUser) if err != nil { ruleResponse.DiscoveryBase.Status = "error" @@ -234,28 +217,98 @@ func (srv PrometheusSrv) RouteGetRuleStatuses(c *contextmodel.ReqContext) respon return response.JSON(ruleResponse.HTTPStatusCode(), ruleResponse) } - if len(namespaceMap) == 0 { - srv.log.Debug("User does not have access to any namespaces") - return response.JSON(ruleResponse.HTTPStatusCode(), ruleResponse) + namespaces := map[string]string{} + for namespaceUID, folder := range namespaceMap { + namespaces[namespaceUID] = folder.Fullpath } - namespaceUIDs := make([]string, len(namespaceMap)) - for k := range namespaceMap { + ruleResponse = PrepareRuleGroupStatuses(srv.log, srv.manager, srv.store, RuleGroupStatusesOptions{ + Ctx: c.Req.Context(), + OrgID: c.OrgID, + Query: c.Req.Form, + Namespaces: namespaces, + AuthorizeRuleGroup: func(rules []*ngmodels.AlertRule) (bool, error) { + return srv.authz.HasAccessToRuleGroup(c.Req.Context(), c.SignedInUser, rules) + }, + }) + + return response.JSON(ruleResponse.HTTPStatusCode(), ruleResponse) +} + +func PrepareRuleGroupStatuses(log log.Logger, manager state.AlertInstanceManager, store ListAlertRulesStore, opts RuleGroupStatusesOptions) apimodels.RuleResponse { + ruleResponse := apimodels.RuleResponse{ + DiscoveryBase: apimodels.DiscoveryBase{ + Status: "success", + }, + Data: apimodels.RuleDiscovery{ + RuleGroups: []apimodels.RuleGroup{}, + }, + } + + dashboardUID := opts.Query.Get("dashboard_uid") + panelID, err := getPanelIDFromQuery(opts.Query) + if err != nil { + ruleResponse.DiscoveryBase.Status = "error" + ruleResponse.DiscoveryBase.Error = fmt.Sprintf("invalid panel_id: %s", err.Error()) + ruleResponse.DiscoveryBase.ErrorType = apiv1.ErrBadData + return ruleResponse + } + if dashboardUID == "" && panelID != 0 { + ruleResponse.DiscoveryBase.Status = "error" + ruleResponse.DiscoveryBase.Error = "panel_id must be set with dashboard_uid" + ruleResponse.DiscoveryBase.ErrorType = apiv1.ErrBadData + return ruleResponse + } + + limitGroups := getInt64WithDefault(opts.Query, "limit", -1) + limitRulesPerGroup := getInt64WithDefault(opts.Query, "limit_rules", -1) + limitAlertsPerRule := getInt64WithDefault(opts.Query, "limit_alerts", -1) + matchers, err := getMatchersFromQuery(opts.Query) + if err != nil { + ruleResponse.DiscoveryBase.Status = "error" + ruleResponse.DiscoveryBase.Error = err.Error() + ruleResponse.DiscoveryBase.ErrorType = apiv1.ErrBadData + return ruleResponse + } + withStates, err := getStatesFromQuery(opts.Query) + if err != nil { + ruleResponse.DiscoveryBase.Status = "error" + ruleResponse.DiscoveryBase.Error = err.Error() + ruleResponse.DiscoveryBase.ErrorType = apiv1.ErrBadData + return ruleResponse + } + withStatesFast := make(map[eval.State]struct{}) + for _, state := range withStates { + withStatesFast[state] = struct{}{} + } + + var labelOptions []ngmodels.LabelOption + if !getBoolWithDefault(opts.Query, queryIncludeInternalLabels, false) { + labelOptions = append(labelOptions, ngmodels.WithoutInternalLabels()) + } + + if len(opts.Namespaces) == 0 { + log.Debug("User does not have access to any namespaces") + return ruleResponse + } + + namespaceUIDs := make([]string, len(opts.Namespaces)) + for k := range opts.Namespaces { namespaceUIDs = append(namespaceUIDs, k) } alertRuleQuery := ngmodels.ListAlertRulesQuery{ - OrgID: c.SignedInUser.GetOrgID(), + OrgID: opts.OrgID, NamespaceUIDs: namespaceUIDs, DashboardUID: dashboardUID, PanelID: panelID, } - ruleList, err := srv.store.ListAlertRules(c.Req.Context(), &alertRuleQuery) + ruleList, err := store.ListAlertRules(opts.Ctx, &alertRuleQuery) if err != nil { ruleResponse.DiscoveryBase.Status = "error" ruleResponse.DiscoveryBase.Error = fmt.Sprintf("failure getting rules: %s", err.Error()) ruleResponse.DiscoveryBase.ErrorType = apiv1.ErrServer - return response.JSON(ruleResponse.HTTPStatusCode(), ruleResponse) + return ruleResponse } // Group rules together by Namespace and Rule Group. Rules are also grouped by Org ID, @@ -275,22 +328,22 @@ func (srv PrometheusSrv) RouteGetRuleStatuses(c *contextmodel.ReqContext) respon rulesTotals := make(map[string]int64, len(groupedRules)) for groupKey, rules := range groupedRules { - folder := namespaceMap[groupKey.NamespaceUID] - if folder == nil { - srv.log.Warn("Query returned rules that belong to folder the user does not have access to. All rules that belong to that namespace will not be added to the response", "folder_uid", groupKey.NamespaceUID) + folder, ok := opts.Namespaces[groupKey.NamespaceUID] + if !ok { + log.Warn("Query returned rules that belong to folder the user does not have access to. All rules that belong to that namespace will not be added to the response", "folder_uid", groupKey.NamespaceUID) continue } - ok, err := srv.authz.HasAccessToRuleGroup(c.Req.Context(), c.SignedInUser, rules) + ok, err := opts.AuthorizeRuleGroup(rules) if err != nil { ruleResponse.DiscoveryBase.Status = "error" ruleResponse.DiscoveryBase.Error = fmt.Sprintf("cannot authorize access to rule group: %s", err.Error()) ruleResponse.DiscoveryBase.ErrorType = apiv1.ErrServer - return response.JSON(ruleResponse.HTTPStatusCode(), ruleResponse) + return ruleResponse } if !ok { continue } - ruleGroup, totals := srv.toRuleGroup(groupKey, folder, rules, limitAlertsPerRule, withStatesFast, matchers, labelOptions) + ruleGroup, totals := toRuleGroup(log, manager, groupKey, folder, rules, limitAlertsPerRule, withStatesFast, matchers, labelOptions) ruleGroup.Totals = totals for k, v := range totals { rulesTotals[k] += v @@ -335,7 +388,7 @@ func (srv PrometheusSrv) RouteGetRuleStatuses(c *contextmodel.ReqContext) respon ruleResponse.Data.RuleGroups = ruleResponse.Data.RuleGroups[0:limitGroups] } - return response.JSON(ruleResponse.HTTPStatusCode(), ruleResponse) + return ruleResponse } // This is the same as matchers.Matches but avoids the need to create a LabelSet @@ -348,11 +401,11 @@ func matchersMatch(matchers []*labels.Matcher, labels map[string]string) bool { return true } -func (srv PrometheusSrv) toRuleGroup(groupKey ngmodels.AlertRuleGroupKey, folder *folder.Folder, rules []*ngmodels.AlertRule, limitAlerts int64, withStates map[eval.State]struct{}, matchers labels.Matchers, labelOptions []ngmodels.LabelOption) (*apimodels.RuleGroup, map[string]int64) { +func toRuleGroup(log log.Logger, manager state.AlertInstanceManager, groupKey ngmodels.AlertRuleGroupKey, folderFullPath string, rules []*ngmodels.AlertRule, limitAlerts int64, withStates map[eval.State]struct{}, matchers labels.Matchers, labelOptions []ngmodels.LabelOption) (*apimodels.RuleGroup, map[string]int64) { newGroup := &apimodels.RuleGroup{ Name: groupKey.RuleGroup, // file is what Prometheus uses for provisioning, we replace it with namespace which is the folder in Grafana. - File: folder.Fullpath, + File: folderFullPath, } rulesTotals := make(map[string]int64, len(rules)) @@ -362,7 +415,7 @@ func (srv PrometheusSrv) toRuleGroup(groupKey ngmodels.AlertRuleGroupKey, folder alertingRule := apimodels.AlertingRule{ State: "inactive", Name: rule.Title, - Query: ruleToQuery(srv.log, rule), + Query: ruleToQuery(log, rule), Duration: rule.For.Seconds(), Annotations: rule.Annotations, } @@ -375,7 +428,7 @@ func (srv PrometheusSrv) toRuleGroup(groupKey ngmodels.AlertRuleGroupKey, folder LastEvaluation: time.Time{}, } - states := srv.manager.GetStatesForRuleUID(rule.OrgID, rule.UID) + states := manager.GetStatesForRuleUID(rule.OrgID, rule.UID) totals := make(map[string]int64) totalsFiltered := make(map[string]int64) for _, alertState := range states { diff --git a/pkg/services/ngalert/api/api_prometheus_test.go b/pkg/services/ngalert/api/api_prometheus_test.go index e08152d7441..905bc81e99b 100644 --- a/pkg/services/ngalert/api/api_prometheus_test.go +++ b/pkg/services/ngalert/api/api_prometheus_test.go @@ -5,7 +5,6 @@ import ( "encoding/json" "errors" "fmt" - "math/rand" "net/http" "testing" "time" @@ -21,7 +20,6 @@ import ( "github.com/grafana/grafana/pkg/services/accesscontrol/acimpl" contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/datasources" - "github.com/grafana/grafana/pkg/services/folder" "github.com/grafana/grafana/pkg/services/ngalert/accesscontrol" apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" "github.com/grafana/grafana/pkg/services/ngalert/eval" @@ -283,10 +281,12 @@ func withLabels(labels data.Labels) forEachState { } func TestRouteGetRuleStatuses(t *testing.T) { - t.Skip() // TODO: Flaky test: https://github.com/grafana/grafana/issues/69146 + // t.Skip() // TODO: Flaky test: https://github.com/grafana/grafana/issues/69146 timeNow = func() time.Time { return time.Date(2022, 3, 10, 14, 0, 0, 0, time.UTC) } orgID := int64(1) + gen := ngmodels.RuleGen + gen = gen.With(gen.WithOrgID(orgID)) queryPermissions := map[int64]map[string][]string{1: {datasources.ActionQuery: {datasources.ScopeAll}}} req, err := http.NewRequest("GET", "/api/v1/rules", nil) @@ -363,7 +363,7 @@ func TestRouteGetRuleStatuses(t *testing.T) { } } } -`, folder.Title), string(r.Body())) +`, folder.Fullpath), string(r.Body())) }) t.Run("with the inclusion of internal Labels", func(t *testing.T) { @@ -429,7 +429,7 @@ func TestRouteGetRuleStatuses(t *testing.T) { } } } -`, folder.Title), string(r.Body())) +`, folder.Fullpath), string(r.Body())) }) t.Run("with a rule that has multiple queries", func(t *testing.T) { @@ -488,7 +488,7 @@ func TestRouteGetRuleStatuses(t *testing.T) { } } } -`, folder.Title), string(r.Body())) +`, folder.Fullpath), string(r.Body())) }) t.Run("with many rules in a group", func(t *testing.T) { @@ -496,7 +496,8 @@ func TestRouteGetRuleStatuses(t *testing.T) { ruleStore := fakes.NewRuleStore(t) fakeAIM := NewFakeAlertInstanceManager(t) groupKey := ngmodels.GenerateGroupKey(orgID) - _, rules := ngmodels.GenerateUniqueAlertRules(rand.Intn(5)+5, ngmodels.AlertRuleGen(withGroupKey(groupKey), ngmodels.WithUniqueGroupIndex())) + gen := ngmodels.RuleGen + rules := gen.With(gen.WithGroupKey(groupKey), gen.WithUniqueGroupIndex()).GenerateManyRef(5, 10) ruleStore.PutRule(context.Background(), rules...) api := PrometheusSrv{ @@ -539,20 +540,19 @@ func TestRouteGetRuleStatuses(t *testing.T) { ruleStore := fakes.NewRuleStore(t) fakeAIM := NewFakeAlertInstanceManager(t) - rules := ngmodels.GenerateAlertRules(rand.Intn(4)+2, ngmodels.AlertRuleGen(withOrgID(orgID))) + rules := gen.GenerateManyRef(2, 6) ruleStore.PutRule(context.Background(), rules...) - ruleStore.PutRule(context.Background(), ngmodels.GenerateAlertRules(rand.Intn(4)+2, ngmodels.AlertRuleGen(withOrgID(orgID)))...) + ruleStore.PutRule(context.Background(), gen.GenerateManyRef(2, 6)...) api := PrometheusSrv{ log: log.NewNopLogger(), manager: fakeAIM, store: ruleStore, - authz: &fakeRuleAccessControlService{}, + authz: accesscontrol.NewRuleService(acimpl.ProvideAccessControl(setting.NewCfg())), } c := &contextmodel.ReqContext{Context: &web.Context{Req: req}, SignedInUser: &user.SignedInUser{OrgID: orgID, Permissions: createPermissionsForRules(rules, orgID)}} - //c.SignedInUser.Permissions[1] = createPermissionsForRules(rules) response := api.RouteGetRuleStatuses(c) require.Equal(t, http.StatusOK, response.Status()) result := &apimodels.RuleResponse{} @@ -576,9 +576,11 @@ func TestRouteGetRuleStatuses(t *testing.T) { t.Run("test totals are expected", func(t *testing.T) { fakeStore, fakeAIM, api := setupAPI(t) // Create rules in the same Rule Group to keep assertions simple - rules := ngmodels.GenerateAlertRules(3, ngmodels.AlertRuleGen(withOrgID(orgID), withGroup("Rule-Group-1"), withNamespace(&folder.Folder{ - Title: "Folder-1", - }))) + rules := gen.With(gen.WithGroupKey(ngmodels.AlertRuleGroupKey{ + RuleGroup: "Rule-Group-1", + NamespaceUID: "Folder-1", + OrgID: orgID, + })).GenerateManyRef(3) // Need to sort these so we add alerts to the rules as ordered in the response ngmodels.AlertRulesBy(ngmodels.AlertRulesByIndex).Sort(rules) // The last two rules will have errors, however the first will be alerting @@ -636,7 +638,7 @@ func TestRouteGetRuleStatuses(t *testing.T) { t.Run("test time of first firing alert", func(t *testing.T) { fakeStore, fakeAIM, api := setupAPI(t) // Create rules in the same Rule Group to keep assertions simple - rules := ngmodels.GenerateAlertRules(1, ngmodels.AlertRuleGen(withOrgID(orgID))) + rules := gen.GenerateManyRef(1) fakeStore.PutRule(context.Background(), rules...) getRuleResponse := func() apimodels.RuleResponse { @@ -692,7 +694,7 @@ func TestRouteGetRuleStatuses(t *testing.T) { t.Run("test with limit on Rule Groups", func(t *testing.T) { fakeStore, _, api := setupAPI(t) - rules := ngmodels.GenerateAlertRules(2, ngmodels.AlertRuleGen(withOrgID(orgID))) + rules := gen.GenerateManyRef(2) fakeStore.PutRule(context.Background(), rules...) t.Run("first without limit", func(t *testing.T) { @@ -764,7 +766,7 @@ func TestRouteGetRuleStatuses(t *testing.T) { t.Run("test with limit rules", func(t *testing.T) { fakeStore, _, api := setupAPI(t) - rules := ngmodels.GenerateAlertRules(2, ngmodels.AlertRuleGen(withOrgID(orgID), withGroup("Rule-Group-1"))) + rules := gen.With(gen.WithGroupName("Rule-Group-1")).GenerateManyRef(2) fakeStore.PutRule(context.Background(), rules...) t.Run("first without limit", func(t *testing.T) { @@ -837,7 +839,7 @@ func TestRouteGetRuleStatuses(t *testing.T) { t.Run("test with limit alerts", func(t *testing.T) { fakeStore, fakeAIM, api := setupAPI(t) - rules := ngmodels.GenerateAlertRules(2, ngmodels.AlertRuleGen(withOrgID(orgID), withGroup("Rule-Group-1"))) + rules := gen.With(gen.WithGroupName("Rule-Group-1")).GenerateManyRef(2) fakeStore.PutRule(context.Background(), rules...) // create a normal and firing alert for each rule for _, r := range rules { @@ -924,11 +926,15 @@ func TestRouteGetRuleStatuses(t *testing.T) { }) t.Run("test with filters on state", func(t *testing.T) { + t.Skip() // TODO: Flaky test: https://github.com/grafana/grafana/issues/69146 + fakeStore, fakeAIM, api := setupAPI(t) // create two rules in the same Rule Group to keep assertions simple - rules := ngmodels.GenerateAlertRules(3, ngmodels.AlertRuleGen(withOrgID(orgID), withGroup("Rule-Group-1"), withNamespace(&folder.Folder{ - Title: "Folder-1", - }))) + rules := gen.With(gen.WithGroupKey(ngmodels.AlertRuleGroupKey{ + NamespaceUID: "Folder-1", + RuleGroup: "Rule-Group-1", + OrgID: orgID, + })).GenerateManyRef(2) // Need to sort these so we add alerts to the rules as ordered in the response ngmodels.AlertRulesBy(ngmodels.AlertRulesByIndex).Sort(rules) // The last two rules will have errors, however the first will be alerting @@ -1083,9 +1089,11 @@ func TestRouteGetRuleStatuses(t *testing.T) { t.Run("test with matcher on labels", func(t *testing.T) { fakeStore, fakeAIM, api := setupAPI(t) // create two rules in the same Rule Group to keep assertions simple - rules := ngmodels.GenerateAlertRules(1, ngmodels.AlertRuleGen(withOrgID(orgID), withGroup("Rule-Group-1"), withNamespace(&folder.Folder{ - Title: "Folder-1", - }))) + rules := gen.With(gen.WithGroupKey(ngmodels.AlertRuleGroupKey{ + NamespaceUID: "Folder-1", + RuleGroup: "Rule-Group-1", + OrgID: orgID, + })).GenerateManyRef(1) fakeStore.PutRule(context.Background(), rules...) // create a normal and alerting state for each rule @@ -1255,23 +1263,25 @@ func TestRouteGetRuleStatuses(t *testing.T) { func setupAPI(t *testing.T) (*fakes.RuleStore, *fakeAlertInstanceManager, PrometheusSrv) { fakeStore := fakes.NewRuleStore(t) fakeAIM := NewFakeAlertInstanceManager(t) + fakeAuthz := &fakeRuleAccessControlService{} api := PrometheusSrv{ log: log.NewNopLogger(), manager: fakeAIM, store: fakeStore, - authz: accesscontrol.NewRuleService(acimpl.ProvideAccessControl(setting.NewCfg())), + authz: fakeAuthz, } return fakeStore, fakeAIM, api } -func generateRuleAndInstanceWithQuery(t *testing.T, orgID int64, fakeAIM *fakeAlertInstanceManager, fakeStore *fakes.RuleStore, query func(r *ngmodels.AlertRule)) { +func generateRuleAndInstanceWithQuery(t *testing.T, orgID int64, fakeAIM *fakeAlertInstanceManager, fakeStore *fakes.RuleStore, query ngmodels.AlertRuleMutator) { t.Helper() - rules := ngmodels.GenerateAlertRules(1, ngmodels.AlertRuleGen(withOrgID(orgID), asFixture(), query)) + gen := ngmodels.RuleGen + r := gen.With(gen.WithOrgID(orgID), asFixture(), query).GenerateRef() - fakeAIM.GenerateAlertInstances(orgID, rules[0].UID, 1, func(s *state.State) *state.State { + fakeAIM.GenerateAlertInstances(orgID, r.UID, 1, func(s *state.State) *state.State { s.Labels = data.Labels{ "job": "prometheus", alertingModels.NamespaceUIDLabel: "test_namespace_uid", @@ -1281,14 +1291,12 @@ func generateRuleAndInstanceWithQuery(t *testing.T, orgID int64, fakeAIM *fakeAl return s }) - for _, r := range rules { - fakeStore.PutRule(context.Background(), r) - } + fakeStore.PutRule(context.Background(), r) } // asFixture removes variable values of the alert rule. // we're not too interested in variability of the rule in this scenario. -func asFixture() func(r *ngmodels.AlertRule) { +func asFixture() ngmodels.AlertRuleMutator { return func(r *ngmodels.AlertRule) { r.Title = "AlwaysFiring" r.NamespaceUID = "namespaceUID" @@ -1304,7 +1312,7 @@ func asFixture() func(r *ngmodels.AlertRule) { } } -func withClassicConditionSingleQuery() func(r *ngmodels.AlertRule) { +func withClassicConditionSingleQuery() ngmodels.AlertRuleMutator { return func(r *ngmodels.AlertRule) { queries := []ngmodels.AlertQuery{ { @@ -1326,7 +1334,7 @@ func withClassicConditionSingleQuery() func(r *ngmodels.AlertRule) { } } -func withExpressionsMultiQuery() func(r *ngmodels.AlertRule) { +func withExpressionsMultiQuery() ngmodels.AlertRuleMutator { return func(r *ngmodels.AlertRule) { queries := []ngmodels.AlertQuery{ { diff --git a/pkg/services/ngalert/api/api_provisioning_test.go b/pkg/services/ngalert/api/api_provisioning_test.go index e2db47386db..27eeb9e9da9 100644 --- a/pkg/services/ngalert/api/api_provisioning_test.go +++ b/pkg/services/ngalert/api/api_provisioning_test.go @@ -694,12 +694,12 @@ func TestProvisioningApi(t *testing.T) { is_paused = false notification_settings { - receiver = "Test-Receiver" - group_by = ["alertname", "grafana_folder", "test"] - group_wait = "1s" - group_interval = "5s" - repeat_interval = "5m" - mute_time_intervals = ["test-mute"] + contact_point = "Test-Receiver" + group_by = ["alertname", "grafana_folder", "test"] + group_wait = "1s" + group_interval = "5s" + repeat_interval = "5m" + mute_timings = ["test-mute"] } } } diff --git a/pkg/services/ngalert/api/api_ruler.go b/pkg/services/ngalert/api/api_ruler.go index 8a77b72a9cf..53f1e1464cb 100644 --- a/pkg/services/ngalert/api/api_ruler.go +++ b/pkg/services/ngalert/api/api_ruler.go @@ -234,7 +234,7 @@ func (srv RulerSrv) RouteGetRulesConfig(c *contextmodel.ReqContext) response.Res } dashboardUID := c.Query("dashboard_uid") - panelID, err := getPanelIDFromRequest(c.Req) + panelID, err := getPanelIDFromQuery(c.Req.URL.Query()) if err != nil { return ErrResp(http.StatusBadRequest, err, "invalid panel_id") } diff --git a/pkg/services/ngalert/api/api_ruler_export_test.go b/pkg/services/ngalert/api/api_ruler_export_test.go index ae91fb0389a..b1281045a00 100644 --- a/pkg/services/ngalert/api/api_ruler_export_test.go +++ b/pkg/services/ngalert/api/api_ruler_export_test.go @@ -9,7 +9,6 @@ import ( "path" "sort" "strings" - "sync" "testing" "github.com/stretchr/testify/assert" @@ -198,7 +197,6 @@ func TestExportFromPayload(t *testing.T) { } func TestExportRules(t *testing.T) { - uids := sync.Map{} orgID := int64(1) f1 := randFolder() f2 := randFolder() @@ -210,33 +208,20 @@ func TestExportRules(t *testing.T) { NamespaceUID: f1.UID, RuleGroup: "HAS-ACCESS-1", } - accessQuery := ngmodels.GenerateAlertQuery() - noAccessQuery := ngmodels.GenerateAlertQuery() - _, hasAccess1 := ngmodels.GenerateUniqueAlertRules(5, - ngmodels.AlertRuleGen( - ngmodels.WithUniqueUID(&uids), - withGroupKey(hasAccessKey1), - ngmodels.WithQuery(accessQuery), - ngmodels.WithUniqueGroupIndex(), - )) + gen := ngmodels.RuleGen + accessQuery := gen.GenerateQuery() + noAccessQuery := gen.GenerateQuery() + + hasAccess1 := gen.With(gen.WithGroupKey(hasAccessKey1), gen.WithQuery(accessQuery), gen.WithUniqueGroupIndex()).GenerateManyRef(5) ruleStore.PutRule(context.Background(), hasAccess1...) noAccessKey1 := ngmodels.AlertRuleGroupKey{ OrgID: orgID, NamespaceUID: f1.UID, RuleGroup: "NO-ACCESS", } - _, noAccess1 := ngmodels.GenerateUniqueAlertRules(5, - ngmodels.AlertRuleGen( - ngmodels.WithUniqueUID(&uids), - withGroupKey(noAccessKey1), - ngmodels.WithQuery(noAccessQuery), - )) - noAccessRule := ngmodels.AlertRuleGen( - ngmodels.WithUniqueUID(&uids), - withGroupKey(noAccessKey1), - ngmodels.WithQuery(accessQuery), - )() + noAccess1 := gen.With(gen.WithGroupKey(noAccessKey1), gen.WithQuery(noAccessQuery)).GenerateManyRef(5) + noAccessRule := gen.With(gen.WithGroupKey(noAccessKey1), gen.WithQuery(accessQuery)).GenerateRef() noAccess1 = append(noAccess1, noAccessRule) ruleStore.PutRule(context.Background(), noAccess1...) @@ -245,21 +230,10 @@ func TestExportRules(t *testing.T) { NamespaceUID: f2.UID, RuleGroup: "HAS-ACCESS-2", } - _, hasAccess2 := ngmodels.GenerateUniqueAlertRules(5, - ngmodels.AlertRuleGen( - ngmodels.WithUniqueUID(&uids), - withGroupKey(hasAccessKey2), - ngmodels.WithQuery(accessQuery), - ngmodels.WithUniqueGroupIndex(), - )) + hasAccess2 := gen.With(gen.WithGroupKey(hasAccessKey2), gen.WithQuery(accessQuery), gen.WithUniqueGroupIndex()).GenerateManyRef(5) ruleStore.PutRule(context.Background(), hasAccess2...) - _, noAccessByFolder := ngmodels.GenerateUniqueAlertRules(10, - ngmodels.AlertRuleGen( - ngmodels.WithUniqueUID(&uids), - ngmodels.WithQuery(accessQuery), // no access because of folder - ngmodels.WithNamespaceUIDNotIn(f1.UID, f2.UID), - )) + noAccessByFolder := gen.With(gen.WithQuery(accessQuery), gen.WithNamespaceUIDNotIn(f1.UID, f2.UID)).GenerateManyRef(10) ruleStore.PutRule(context.Background(), noAccessByFolder...) // overwrite the folders visible to user because PutRule automatically creates folders in the fake store. diff --git a/pkg/services/ngalert/api/api_ruler_test.go b/pkg/services/ngalert/api/api_ruler_test.go index 7856e858088..99c109c00ca 100644 --- a/pkg/services/ngalert/api/api_ruler_test.go +++ b/pkg/services/ngalert/api/api_ruler_test.go @@ -33,7 +33,6 @@ import ( "github.com/grafana/grafana/pkg/services/ngalert/tests/fakes" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/setting" - "github.com/grafana/grafana/pkg/util" "github.com/grafana/grafana/pkg/util/cmputil" "github.com/grafana/grafana/pkg/web" ) @@ -67,12 +66,13 @@ func TestRouteDeleteAlertRules(t *testing.T) { orgID := rand.Int63() folder := randFolder() + gen := models.RuleGen.With(models.RuleGen.WithOrgID(orgID)) initFakeRuleStore := func(t *testing.T) *fakes.RuleStore { ruleStore := fakes.NewRuleStore(t) ruleStore.Folders[orgID] = append(ruleStore.Folders[orgID], folder) // add random data - ruleStore.PutRule(context.Background(), models.GenerateAlertRulesSmallNonEmpty(models.AlertRuleGen(withOrgID(orgID)))...) + ruleStore.PutRule(context.Background(), gen.GenerateManyRef(1, 5)...) return ruleStore } @@ -80,7 +80,7 @@ func TestRouteDeleteAlertRules(t *testing.T) { t.Run("and group argument is empty", func(t *testing.T) { t.Run("return Forbidden if user is not authorized to access any group in the folder", func(t *testing.T) { ruleStore := initFakeRuleStore(t) - ruleStore.PutRule(context.Background(), models.GenerateAlertRulesSmallNonEmpty(models.AlertRuleGen(withOrgID(orgID), withNamespace(folder)))...) + ruleStore.PutRule(context.Background(), gen.With(gen.WithNamespace(folder)).GenerateManyRef(1, 5)...) request := createRequestContextWithPerms(orgID, map[int64]map[string][]string{}, nil) @@ -93,16 +93,20 @@ func TestRouteDeleteAlertRules(t *testing.T) { ruleStore := initFakeRuleStore(t) provisioningStore := fakes.NewFakeProvisioningStore() - authorizedRulesInFolder := models.GenerateAlertRulesSmallNonEmpty(models.AlertRuleGen(withOrgID(orgID), withNamespace(folder), withGroup("authz_"+util.GenerateShortUID()))) + folderGen := gen.With(gen.WithNamespace(folder)) - provisionedRulesInFolder := models.GenerateAlertRulesSmallNonEmpty(models.AlertRuleGen(withOrgID(orgID), withNamespace(folder), withGroup("provisioned_"+util.GenerateShortUID()))) - err := provisioningStore.SetProvenance(context.Background(), provisionedRulesInFolder[0], orgID, models.ProvenanceAPI) - require.NoError(t, err) + authorizedRulesInFolder := folderGen.With(gen.WithGroupPrefix("authz-")).GenerateManyRef(1, 5) + + provisionedRulesInFolder := folderGen.With(gen.WithGroupPrefix("provisioned-")).GenerateManyRef(1, 5) + for _, rule := range provisionedRulesInFolder { + err := provisioningStore.SetProvenance(context.Background(), rule, orgID, models.ProvenanceAPI) + require.NoError(t, err) + } ruleStore.PutRule(context.Background(), authorizedRulesInFolder...) ruleStore.PutRule(context.Background(), provisionedRulesInFolder...) // more rules in the same namespace but user does not have access to them - ruleStore.PutRule(context.Background(), models.GenerateAlertRulesSmallNonEmpty(models.AlertRuleGen(withOrgID(orgID), withNamespace(folder), withGroup("unauthz"+util.GenerateShortUID())))...) + ruleStore.PutRule(context.Background(), folderGen.With(gen.WithGroupPrefix("unauthz")).GenerateManyRef(1, 5)...) permissions := createPermissionsForRules(append(authorizedRulesInFolder, provisionedRulesInFolder...), orgID) requestCtx := createRequestContextWithPerms(orgID, permissions, nil) @@ -116,13 +120,15 @@ func TestRouteDeleteAlertRules(t *testing.T) { ruleStore := initFakeRuleStore(t) provisioningStore := fakes.NewFakeProvisioningStore() - provisionedRulesInFolder := models.GenerateAlertRulesSmallNonEmpty(models.AlertRuleGen(withOrgID(orgID), withNamespace(folder), withGroup(util.GenerateShortUID()))) + folderGen := gen.With(gen.WithNamespace(folder)) + + provisionedRulesInFolder := folderGen.With(gen.WithSameGroup()).GenerateManyRef(1, 5) err := provisioningStore.SetProvenance(context.Background(), provisionedRulesInFolder[0], orgID, models.ProvenanceAPI) require.NoError(t, err) ruleStore.PutRule(context.Background(), provisionedRulesInFolder...) // more rules in the same namespace but user does not have access to them - ruleStore.PutRule(context.Background(), models.GenerateAlertRulesSmallNonEmpty(models.AlertRuleGen(withOrgID(orgID), withNamespace(folder), withGroup(util.GenerateShortUID())))...) + ruleStore.PutRule(context.Background(), folderGen.With(gen.WithSameGroup()).GenerateManyRef(1, 5)...) permissions := createPermissionsForRules(provisionedRulesInFolder, orgID) requestCtx := createRequestContextWithPerms(orgID, permissions, nil) @@ -143,19 +149,20 @@ func TestRouteDeleteAlertRules(t *testing.T) { }) }) t.Run("and group argument is not empty", func(t *testing.T) { - groupName := util.GenerateShortUID() t.Run("return Forbidden if user is not authorized to access the group", func(t *testing.T) { ruleStore := initFakeRuleStore(t) - authorizedRulesInGroup := models.GenerateAlertRulesSmallNonEmpty(models.AlertRuleGen(withOrgID(orgID), withNamespace(folder), withGroup(groupName))) + groupGen := gen.With(gen.WithNamespace(folder), gen.WithSameGroup()) + + authorizedRulesInGroup := groupGen.GenerateManyRef(1, 5) ruleStore.PutRule(context.Background(), authorizedRulesInGroup...) // more rules in the same group but user is not authorized to access them - ruleStore.PutRule(context.Background(), models.GenerateAlertRulesSmallNonEmpty(models.AlertRuleGen(withOrgID(orgID), withNamespace(folder), withGroup(groupName)))...) + ruleStore.PutRule(context.Background(), groupGen.GenerateManyRef(1, 5)...) permissions := createPermissionsForRules(authorizedRulesInGroup, orgID) requestCtx := createRequestContextWithPerms(orgID, permissions, nil) - response := createService(ruleStore).RouteDeleteAlertRules(requestCtx, folder.UID, groupName) + response := createService(ruleStore).RouteDeleteAlertRules(requestCtx, folder.UID, authorizedRulesInGroup[0].RuleGroup) require.Equalf(t, http.StatusForbidden, response.Status(), "Expected 403 but got %d: %v", response.Status(), string(response.Body())) deleteCommands := getRecordedCommand(ruleStore) @@ -165,7 +172,9 @@ func TestRouteDeleteAlertRules(t *testing.T) { ruleStore := initFakeRuleStore(t) provisioningStore := fakes.NewFakeProvisioningStore() - provisionedRulesInFolder := models.GenerateAlertRulesSmallNonEmpty(models.AlertRuleGen(withOrgID(orgID), withNamespace(folder), withGroup(groupName))) + groupGen := gen.With(gen.WithNamespace(folder), gen.WithSameGroup()) + + provisionedRulesInFolder := groupGen.GenerateManyRef(1, 5) err := provisioningStore.SetProvenance(context.Background(), provisionedRulesInFolder[0], orgID, models.ProvenanceAPI) require.NoError(t, err) @@ -174,7 +183,7 @@ func TestRouteDeleteAlertRules(t *testing.T) { permissions := createPermissionsForRules(provisionedRulesInFolder, orgID) requestCtx := createRequestContextWithPerms(orgID, permissions, nil) - response := createServiceWithProvenanceStore(ruleStore, provisioningStore).RouteDeleteAlertRules(requestCtx, folder.UID, groupName) + response := createServiceWithProvenanceStore(ruleStore, provisioningStore).RouteDeleteAlertRules(requestCtx, folder.UID, provisionedRulesInFolder[0].RuleGroup) require.Equalf(t, 400, response.Status(), "Expected 400 but got %d: %v", response.Status(), string(response.Body())) deleteCommands := getRecordedCommand(ruleStore) @@ -185,15 +194,17 @@ func TestRouteDeleteAlertRules(t *testing.T) { } func TestRouteGetNamespaceRulesConfig(t *testing.T) { + gen := models.RuleGen t.Run("fine-grained access is enabled", func(t *testing.T) { t.Run("should return rules for which user has access to data source", func(t *testing.T) { orgID := rand.Int63() folder := randFolder() ruleStore := fakes.NewRuleStore(t) ruleStore.Folders[orgID] = append(ruleStore.Folders[orgID], folder) - expectedRules := models.GenerateAlertRules(rand.Intn(4)+2, models.AlertRuleGen(withOrgID(orgID), withNamespace(folder))) + folderGen := gen.With(gen.WithOrgID(orgID), gen.WithNamespace(folder)) + expectedRules := folderGen.GenerateManyRef(2, 6) ruleStore.PutRule(context.Background(), expectedRules...) - ruleStore.PutRule(context.Background(), models.GenerateAlertRules(rand.Intn(4)+2, models.AlertRuleGen(withOrgID(orgID), withNamespace(folder)))...) + ruleStore.PutRule(context.Background(), folderGen.GenerateManyRef(2, 6)...) permissions := createPermissionsForRules(expectedRules, orgID) req := createRequestContextWithPerms(orgID, permissions, nil) @@ -227,7 +238,7 @@ func TestRouteGetNamespaceRulesConfig(t *testing.T) { folder := randFolder() ruleStore := fakes.NewRuleStore(t) ruleStore.Folders[orgID] = append(ruleStore.Folders[orgID], folder) - expectedRules := models.GenerateAlertRules(rand.Intn(4)+2, models.AlertRuleGen(withOrgID(orgID), withNamespace(folder))) + expectedRules := gen.With(gen.WithOrgID(orgID), gen.WithNamespace(folder)).GenerateManyRef(2, 6) ruleStore.PutRule(context.Background(), expectedRules...) svc := createService(ruleStore) @@ -271,7 +282,7 @@ func TestRouteGetNamespaceRulesConfig(t *testing.T) { groupKey := models.GenerateGroupKey(orgID) groupKey.NamespaceUID = folder.UID - expectedRules := models.GenerateAlertRules(rand.Intn(5)+5, models.AlertRuleGen(withGroupKey(groupKey), models.WithUniqueGroupIndex())) + expectedRules := gen.With(gen.WithGroupKey(groupKey), gen.WithUniqueGroupIndex()).GenerateManyRef(5, 10) ruleStore.PutRule(context.Background(), expectedRules...) perms := createPermissionsForRules(expectedRules, orgID) @@ -308,6 +319,7 @@ func TestRouteGetNamespaceRulesConfig(t *testing.T) { } func TestRouteGetRulesConfig(t *testing.T) { + gen := models.RuleGen t.Run("fine-grained access is enabled", func(t *testing.T) { t.Run("should check access to data source", func(t *testing.T) { orgID := rand.Int63() @@ -321,8 +333,8 @@ func TestRouteGetRulesConfig(t *testing.T) { group2Key := models.GenerateGroupKey(orgID) group2Key.NamespaceUID = folder2.UID - group1 := models.GenerateAlertRules(rand.Intn(4)+2, models.AlertRuleGen(withGroupKey(group1Key))) - group2 := models.GenerateAlertRules(rand.Intn(4)+2, models.AlertRuleGen(withGroupKey(group2Key))) + group1 := gen.With(gen.WithGroupKey(group1Key)).GenerateManyRef(2, 6) + group2 := gen.With(gen.WithGroupKey(group2Key)).GenerateManyRef(2, 6) ruleStore.PutRule(context.Background(), append(group1, group2...)...) t.Run("and do not return group if user does not have access to one of rules", func(t *testing.T) { @@ -355,7 +367,7 @@ func TestRouteGetRulesConfig(t *testing.T) { groupKey := models.GenerateGroupKey(orgID) groupKey.NamespaceUID = folder.UID - expectedRules := models.GenerateAlertRules(rand.Intn(5)+5, models.AlertRuleGen(withGroupKey(groupKey), models.WithUniqueGroupIndex())) + expectedRules := gen.With(gen.WithGroupKey(groupKey), gen.WithUniqueGroupIndex()).GenerateManyRef(5, 10) ruleStore.PutRule(context.Background(), expectedRules...) perms := createPermissionsForRules(expectedRules, orgID) @@ -392,6 +404,7 @@ func TestRouteGetRulesConfig(t *testing.T) { } func TestRouteGetRulesGroupConfig(t *testing.T) { + gen := models.RuleGen t.Run("fine-grained access is enabled", func(t *testing.T) { t.Run("should check access to data source", func(t *testing.T) { orgID := rand.Int63() @@ -401,7 +414,7 @@ func TestRouteGetRulesGroupConfig(t *testing.T) { groupKey := models.GenerateGroupKey(orgID) groupKey.NamespaceUID = folder.UID - expectedRules := models.GenerateAlertRules(rand.Intn(4)+2, models.AlertRuleGen(withGroupKey(groupKey))) + expectedRules := gen.With(gen.WithGroupKey(groupKey)).GenerateManyRef(2, 6) ruleStore.PutRule(context.Background(), expectedRules...) t.Run("and return Forbidden if user does not have access one of rules", func(t *testing.T) { @@ -439,7 +452,7 @@ func TestRouteGetRulesGroupConfig(t *testing.T) { groupKey := models.GenerateGroupKey(orgID) groupKey.NamespaceUID = folder.UID - expectedRules := models.GenerateAlertRules(rand.Intn(5)+5, models.AlertRuleGen(withGroupKey(groupKey), models.WithUniqueGroupIndex())) + expectedRules := gen.With(gen.WithGroupKey(groupKey), gen.WithUniqueGroupIndex()).GenerateManyRef(5, 10) ruleStore.PutRule(context.Background(), expectedRules...) perms := createPermissionsForRules(expectedRules, orgID) @@ -475,14 +488,15 @@ func TestVerifyProvisionedRulesNotAffected(t *testing.T) { orgID := rand.Int63() group := models.GenerateGroupKey(orgID) affectedGroups := make(map[models.AlertRuleGroupKey]models.RulesGroup) + gen := models.RuleGen var allRules []*models.AlertRule { - rules := models.GenerateAlertRules(rand.Intn(3)+1, models.AlertRuleGen(withGroupKey(group))) + rules := gen.With(gen.WithGroupKey(group)).GenerateManyRef(1, 4) allRules = append(allRules, rules...) affectedGroups[group] = rules for i := 0; i < rand.Intn(3)+1; i++ { g := models.GenerateGroupKey(orgID) - rules := models.GenerateAlertRules(rand.Intn(3)+1, models.AlertRuleGen(withGroupKey(g))) + rules := gen.With(gen.WithGroupKey(g)).GenerateManyRef(1, 4) allRules = append(allRules, rules...) affectedGroups[g] = rules } @@ -533,20 +547,15 @@ func TestVerifyProvisionedRulesNotAffected(t *testing.T) { } func TestValidateQueries(t *testing.T) { + gen := models.RuleGen delta := store.GroupDelta{ New: []*models.AlertRule{ - models.AlertRuleGen(func(rule *models.AlertRule) { - rule.Condition = "New" - })(), + gen.With(gen.WithCondition("New")).GenerateRef(), }, Update: []store.RuleDelta{ { - Existing: models.AlertRuleGen(func(rule *models.AlertRule) { - rule.Condition = "Update_Existing" - })(), - New: models.AlertRuleGen(func(rule *models.AlertRule) { - rule.Condition = "Update_New" - })(), + Existing: gen.With(gen.WithCondition("New")).GenerateRef(), + New: gen.With(gen.WithCondition("Update_New")).GenerateRef(), Diff: cmputil.DiffReport{ cmputil.Diff{ Path: "SomeField", @@ -554,12 +563,8 @@ func TestValidateQueries(t *testing.T) { }, }, { - Existing: models.AlertRuleGen(func(rule *models.AlertRule) { - rule.Condition = "Update_Index_Existing" - })(), - New: models.AlertRuleGen(func(rule *models.AlertRule) { - rule.Condition = "Update_Index_New" - })(), + Existing: gen.With(gen.WithCondition("Update_Index_Existing")).GenerateRef(), + New: gen.With(gen.WithCondition("Update_Index_New")).GenerateRef(), Diff: cmputil.DiffReport{ cmputil.Diff{ Path: "RuleGroupIndex", @@ -567,11 +572,7 @@ func TestValidateQueries(t *testing.T) { }, }, }, - Delete: []*models.AlertRule{ - models.AlertRuleGen(func(rule *models.AlertRule) { - rule.Condition = "Deleted" - })(), - }, + Delete: gen.With(gen.WithCondition("Deleted")).GenerateManyRef(1), } t.Run("should not validate deleted rules or updated rules with ignored fields", func(t *testing.T) { @@ -694,29 +695,3 @@ func createPermissionsForRules(rules []*models.AlertRule, orgID int64) map[int64 } return map[int64]map[string][]string{orgID: permissions} } - -func withOrgID(orgId int64) func(rule *models.AlertRule) { - return func(rule *models.AlertRule) { - rule.OrgID = orgId - } -} - -func withGroup(groupName string) func(rule *models.AlertRule) { - return func(rule *models.AlertRule) { - rule.RuleGroup = groupName - } -} - -func withNamespace(namespace *folder.Folder) func(rule *models.AlertRule) { - return func(rule *models.AlertRule) { - rule.NamespaceUID = namespace.UID - } -} - -func withGroupKey(groupKey models.AlertRuleGroupKey) func(rule *models.AlertRule) { - return func(rule *models.AlertRule) { - rule.RuleGroup = groupKey.RuleGroup - rule.OrgID = groupKey.OrgID - rule.NamespaceUID = groupKey.NamespaceUID - } -} diff --git a/pkg/services/ngalert/api/api_testing_test.go b/pkg/services/ngalert/api/api_testing_test.go index becad5a73cc..a8bee7838dd 100644 --- a/pkg/services/ngalert/api/api_testing_test.go +++ b/pkg/services/ngalert/api/api_testing_test.go @@ -174,8 +174,9 @@ func TestRouteTestGrafanaRuleConfig(t *testing.T) { }) t.Run("should return Forbidden if user cannot query a data source", func(t *testing.T) { - data1 := models.GenerateAlertQuery() - data2 := models.GenerateAlertQuery() + gen := models.RuleGen + data1 := gen.GenerateQuery() + data2 := gen.GenerateQuery() ac := acMock.New().WithPermissions([]ac.Permission{ {Action: datasources.ActionQuery, Scope: datasources.ScopeProvider.GetResourceScopeUID(data1.DatasourceUID)}, @@ -195,12 +196,14 @@ func TestRouteTestGrafanaRuleConfig(t *testing.T) { NamespaceTitle: f.Title, }) + t.Log(string(response.Body())) require.Equal(t, http.StatusForbidden, response.Status()) }) t.Run("should return 200 if user can query all data sources", func(t *testing.T) { - data1 := models.GenerateAlertQuery() - data2 := models.GenerateAlertQuery() + gen := models.RuleGen + data1 := gen.GenerateQuery() + data2 := gen.GenerateQuery() ac := acMock.New().WithPermissions([]ac.Permission{ {Action: datasources.ActionQuery, Scope: datasources.ScopeProvider.GetResourceScopeUID(data1.DatasourceUID)}, @@ -252,8 +255,9 @@ func TestRouteEvalQueries(t *testing.T) { } t.Run("should return Forbidden if user cannot query a data source", func(t *testing.T) { - data1 := models.GenerateAlertQuery() - data2 := models.GenerateAlertQuery() + g := models.RuleGen + data1 := g.GenerateQuery() + data2 := g.GenerateQuery() srv := &TestingApiSrv{ authz: accesscontrol.NewRuleService(acMock.New().WithPermissions([]ac.Permission{ diff --git a/pkg/services/ngalert/api/test-data/post-rulegroup-101-export.hcl b/pkg/services/ngalert/api/test-data/post-rulegroup-101-export.hcl index 2cfb1ba5b0e..142aead24d0 100644 --- a/pkg/services/ngalert/api/test-data/post-rulegroup-101-export.hcl +++ b/pkg/services/ngalert/api/test-data/post-rulegroup-101-export.hcl @@ -79,12 +79,12 @@ resource "grafana_rule_group" "rule_group_0000" { is_paused = false notification_settings { - receiver = "Test-Receiver" - group_by = ["alertname", "grafana_folder", "test"] - group_wait = "1s" - group_interval = "5s" - repeat_interval = "5m" - mute_time_intervals = ["test-mute"] + contact_point = "Test-Receiver" + group_by = ["alertname", "grafana_folder", "test"] + group_wait = "1s" + group_interval = "5s" + repeat_interval = "5m" + mute_timings = ["test-mute"] } } } diff --git a/pkg/services/ngalert/api/tooling/definitions/provisioning_alert_rules.go b/pkg/services/ngalert/api/tooling/definitions/provisioning_alert_rules.go index bd8e1548861..b25b41c3395 100644 --- a/pkg/services/ngalert/api/tooling/definitions/provisioning_alert_rules.go +++ b/pkg/services/ngalert/api/tooling/definitions/provisioning_alert_rules.go @@ -291,12 +291,15 @@ type RelativeTimeRangeExport struct { } // AlertRuleNotificationSettingsExport is the provisioned export of models.NotificationSettings. +// Field name mismatches with Terraform provider schema are noted where applicable. type AlertRuleNotificationSettingsExport struct { - Receiver string `yaml:"receiver,omitempty" json:"receiver,omitempty" hcl:"receiver"` + // TF -> `contact_point` + Receiver string `yaml:"receiver,omitempty" json:"receiver,omitempty" hcl:"contact_point"` - GroupBy []string `yaml:"group_by,omitempty" json:"group_by,omitempty" hcl:"group_by"` - GroupWait *string `yaml:"group_wait,omitempty" json:"group_wait,omitempty" hcl:"group_wait,optional"` - GroupInterval *string `yaml:"group_interval,omitempty" json:"group_interval,omitempty" hcl:"group_interval,optional"` - RepeatInterval *string `yaml:"repeat_interval,omitempty" json:"repeat_interval,omitempty" hcl:"repeat_interval,optional"` - MuteTimeIntervals []string `yaml:"mute_time_intervals,omitempty" json:"mute_time_intervals,omitempty" hcl:"mute_time_intervals"` + GroupBy []string `yaml:"group_by,omitempty" json:"group_by,omitempty" hcl:"group_by"` + GroupWait *string `yaml:"group_wait,omitempty" json:"group_wait,omitempty" hcl:"group_wait,optional"` + GroupInterval *string `yaml:"group_interval,omitempty" json:"group_interval,omitempty" hcl:"group_interval,optional"` + RepeatInterval *string `yaml:"repeat_interval,omitempty" json:"repeat_interval,omitempty" hcl:"repeat_interval,optional"` + // TF -> `mute_timings` + MuteTimeIntervals []string `yaml:"mute_time_intervals,omitempty" json:"mute_time_intervals,omitempty" hcl:"mute_timings"` } diff --git a/pkg/services/ngalert/api/util_test.go b/pkg/services/ngalert/api/util_test.go index 55f5731aa5c..e700898084d 100644 --- a/pkg/services/ngalert/api/util_test.go +++ b/pkg/services/ngalert/api/util_test.go @@ -143,15 +143,16 @@ func TestAlertingProxy_createProxyContext(t *testing.T) { } func Test_containsProvisionedAlerts(t *testing.T) { + gen := models2.RuleGen t.Run("should return true if at least one rule is provisioned", func(t *testing.T) { - _, rules := models2.GenerateUniqueAlertRules(rand.Intn(4)+2, models2.AlertRuleGen()) + rules := gen.GenerateManyRef(2, 6) provenance := map[string]models2.Provenance{ rules[rand.Intn(len(rules))].UID: []models2.Provenance{models2.ProvenanceAPI, models2.ProvenanceFile}[rand.Intn(2)], } require.Truef(t, containsProvisionedAlerts(provenance, rules), "the group of rules is expected to be considered as provisioned but it isn't. Provenances: %v", provenance) }) t.Run("should return false if map does not contain or has ProvenanceNone", func(t *testing.T) { - _, rules := models2.GenerateUniqueAlertRules(rand.Intn(5)+1, models2.AlertRuleGen()) + rules := gen.GenerateManyRef(1, 6) provenance := make(map[string]models2.Provenance) numProvenanceNone := rand.Intn(len(rules)) for i := 0; i < numProvenanceNone; i++ { diff --git a/pkg/services/ngalert/backtesting/engine_test.go b/pkg/services/ngalert/backtesting/engine_test.go index f8d058f1410..26bb317aaad 100644 --- a/pkg/services/ngalert/backtesting/engine_test.go +++ b/pkg/services/ngalert/backtesting/engine_test.go @@ -189,7 +189,8 @@ func TestEvaluatorTest(t *testing.T) { return manager }, } - rule := models.AlertRuleGen(models.WithInterval(time.Second))() + gen := models.RuleGen + rule := gen.With(gen.WithInterval(time.Second)).GenerateRef() ruleInterval := time.Duration(rule.IntervalSeconds) * time.Second t.Run("should return data frame in specific format", func(t *testing.T) { diff --git a/pkg/services/ngalert/models/alert_rule_test.go b/pkg/services/ngalert/models/alert_rule_test.go index 22d2f65a9e6..414507582f5 100644 --- a/pkg/services/ngalert/models/alert_rule_test.go +++ b/pkg/services/ngalert/models/alert_rule_test.go @@ -197,11 +197,10 @@ func TestSetDashboardAndPanelFromAnnotations(t *testing.T) { for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - rule := AlertRuleGen(func(rule *AlertRule) { - rule.Annotations = tc.annotations - rule.DashboardUID = nil - rule.PanelID = nil - })() + rule := RuleGen.With( + RuleMuts.WithDashboardAndPanel(nil, nil), + RuleMuts.WithAnnotations(tc.annotations), + ).Generate() err := rule.SetDashboardAndPanelFromAnnotations() require.Equal(t, tc.expectedError, err) @@ -256,14 +255,16 @@ func TestPatchPartialAlertRule(t *testing.T) { }, } + gen := RuleGen.With( + RuleMuts.WithFor(time.Duration(rand.Int63n(1000) + 1)), + ) + for _, testCase := range testCases { t.Run(testCase.name, func(t *testing.T) { var existing *AlertRuleWithOptionals - for { - rule := AlertRuleGen(func(rule *AlertRule) { - rule.For = time.Duration(rand.Int63n(1000) + 1) - })() - existing = &AlertRuleWithOptionals{AlertRule: *rule} + for i := 0; i < 10; i++ { + rule := gen.Generate() + existing = &AlertRuleWithOptionals{AlertRule: rule} cloned := *existing testCase.mutator(&cloned) if !cmp.Equal(existing, cloned, cmp.FilterPath(func(path cmp.Path) bool { @@ -343,15 +344,20 @@ func TestPatchPartialAlertRule(t *testing.T) { }, } + gen := RuleGen.With( + RuleMuts.WithUniqueID(), + RuleMuts.WithFor(time.Duration(rand.Int63n(1000)+1)), + ) + for _, testCase := range testCases { t.Run(testCase.name, func(t *testing.T) { var existing *AlertRule for { - existing = AlertRuleGen()() - cloned := *existing + existing = gen.GenerateRef() + cloned := CopyRule(existing) // make sure the generated rule does not match the mutated one - testCase.mutator(&cloned) - if !cmp.Equal(*existing, cloned, cmp.FilterPath(func(path cmp.Path) bool { + testCase.mutator(cloned) + if !cmp.Equal(existing, cloned, cmp.FilterPath(func(path cmp.Path) bool { return path.String() == "Data.modelProps" }, cmp.Ignore())) { break @@ -368,14 +374,14 @@ func TestPatchPartialAlertRule(t *testing.T) { func TestDiff(t *testing.T) { t.Run("should return nil if there is no diff", func(t *testing.T) { - rule1 := AlertRuleGen()() + rule1 := RuleGen.GenerateRef() rule2 := CopyRule(rule1) result := rule1.Diff(rule2) require.Emptyf(t, result, "expected diff to be empty. rule1: %#v, rule2: %#v\ndiff: %s", rule1, rule2, result) }) t.Run("should respect fields to ignore", func(t *testing.T) { - rule1 := AlertRuleGen()() + rule1 := RuleGen.GenerateRef() rule2 := CopyRule(rule1) rule2.ID = rule1.ID/2 + 1 rule2.Version = rule1.Version/2 + 1 @@ -385,8 +391,8 @@ func TestDiff(t *testing.T) { }) t.Run("should find diff in simple fields", func(t *testing.T) { - rule1 := AlertRuleGen()() - rule2 := AlertRuleGen()() + rule1 := RuleGen.GenerateRef() + rule2 := RuleGen.GenerateRef() diffs := rule1.Diff(rule2, "Data", "Annotations", "Labels", "NotificationSettings") // these fields will be tested separately @@ -508,7 +514,7 @@ func TestDiff(t *testing.T) { }) t.Run("should not see difference between nil and empty Annotations", func(t *testing.T) { - rule1 := AlertRuleGen()() + rule1 := RuleGen.GenerateRef() rule1.Annotations = make(map[string]string) rule2 := CopyRule(rule1) rule2.Annotations = nil @@ -518,7 +524,7 @@ func TestDiff(t *testing.T) { }) t.Run("should detect changes in Annotations", func(t *testing.T) { - rule1 := AlertRuleGen()() + rule1 := RuleGen.GenerateRef() rule2 := CopyRule(rule1) rule1.Annotations = map[string]string{ @@ -555,7 +561,7 @@ func TestDiff(t *testing.T) { }) t.Run("should not see difference between nil and empty Labels", func(t *testing.T) { - rule1 := AlertRuleGen()() + rule1 := RuleGen.GenerateRef() rule1.Annotations = make(map[string]string) rule2 := CopyRule(rule1) rule2.Annotations = nil @@ -565,7 +571,7 @@ func TestDiff(t *testing.T) { }) t.Run("should detect changes in Labels", func(t *testing.T) { - rule1 := AlertRuleGen()() + rule1 := RuleGen.GenerateRef() rule2 := CopyRule(rule1) rule1.Labels = map[string]string{ @@ -602,7 +608,7 @@ func TestDiff(t *testing.T) { }) t.Run("should detect changes in Data", func(t *testing.T) { - rule1 := AlertRuleGen()() + rule1 := RuleGen.GenerateRef() rule2 := CopyRule(rule1) query1 := AlertQuery{ @@ -658,11 +664,11 @@ func TestDiff(t *testing.T) { t.Run("should correctly detect no change with '<' and '>' in query", func(t *testing.T) { old := query1 - new := query1 + newQuery := query1 old.Model = json.RawMessage(`{"field1": "$A \u003c 1"}`) - new.Model = json.RawMessage(`{"field1": "$A < 1"}`) + newQuery.Model = json.RawMessage(`{"field1": "$A < 1"}`) rule1.Data = []AlertQuery{old} - rule2.Data = []AlertQuery{new} + rule2.Data = []AlertQuery{newQuery} diff := rule1.Diff(rule2) assert.Nil(t, diff) @@ -699,7 +705,7 @@ func TestDiff(t *testing.T) { }) t.Run("should detect changes in NotificationSettings", func(t *testing.T) { - rule1 := AlertRuleGen()() + rule1 := RuleGen.GenerateRef() baseSettings := NotificationSettingsGen(NSMuts.WithGroupBy("test1", "test2"))() rule1.NotificationSettings = []NotificationSettings{baseSettings} @@ -824,7 +830,9 @@ func TestSortByGroupIndex(t *testing.T) { } t.Run("should sort rules by GroupIndex", func(t *testing.T) { - rules := GenerateAlertRules(rand.Intn(15)+5, AlertRuleGen(WithUniqueGroupIndex())) + rules := RuleGen.With( + RuleMuts.WithUniqueGroupIndex(), + ).GenerateManyRef(5, 20) ensureNotSorted(t, rules, func(i, j int) bool { return rules[i].RuleGroupIndex < rules[j].RuleGroupIndex }) @@ -835,7 +843,10 @@ func TestSortByGroupIndex(t *testing.T) { }) t.Run("should sort by ID if same GroupIndex", func(t *testing.T) { - rules := GenerateAlertRules(rand.Intn(15)+5, AlertRuleGen(WithUniqueID(), WithGroupIndex(rand.Int()))) + rules := RuleGen.With( + RuleMuts.WithUniqueID(), + RuleMuts.WithGroupIndex(rand.Int()), + ).GenerateManyRef(5, 20) ensureNotSorted(t, rules, func(i, j int) bool { return rules[i].ID < rules[j].ID }) diff --git a/pkg/services/ngalert/models/silence.go b/pkg/services/ngalert/models/silence.go new file mode 100644 index 00000000000..dbf9a0946de --- /dev/null +++ b/pkg/services/ngalert/models/silence.go @@ -0,0 +1,11 @@ +package models + +// Silence is the model-layer representation of an alertmanager silence. +type Silence struct { // TODO implement using matchers + ID *string + RuleUID *string +} + +func (s *Silence) GetRuleUID() *string { + return s.RuleUID +} diff --git a/pkg/services/ngalert/models/testing.go b/pkg/services/ngalert/models/testing.go index 71ffd9312a9..27deff6c000 100644 --- a/pkg/services/ngalert/models/testing.go +++ b/pkg/services/ngalert/models/testing.go @@ -9,7 +9,6 @@ import ( "testing" "time" - "github.com/google/uuid" "github.com/grafana/grafana-plugin-sdk-go/data" "github.com/prometheus/common/model" "github.com/stretchr/testify/require" @@ -20,222 +19,312 @@ import ( "github.com/grafana/grafana/pkg/util" ) -type AlertRuleMutator func(*AlertRule) +var ( + RuleMuts = AlertRuleMutators{} + NSMuts = NotificationSettingsMutators{} + RuleGen = &AlertRuleGenerator{ + mutators: []AlertRuleMutator{ + RuleMuts.WithUniqueUID(), RuleMuts.WithUniqueTitle(), + }, + } +) -// AlertRuleGen provides a factory function that generates a random AlertRule. -// The mutators arguments allows changing fields of the resulting structure -func AlertRuleGen(mutators ...AlertRuleMutator) func() *AlertRule { - return func() *AlertRule { - randNoDataState := func() NoDataState { - s := [...]NoDataState{ - Alerting, - NoData, - OK, - } - return s[rand.Intn(len(s))] - } +type AlertRuleMutator func(r *AlertRule) - randErrState := func() ExecutionErrorState { - s := [...]ExecutionErrorState{ - AlertingErrState, - ErrorErrState, - OkErrState, - } - return s[rand.Intn(len(s))] - } +type AlertRuleGenerator struct { + AlertRuleMutators + mutators []AlertRuleMutator +} - interval := (rand.Int63n(6) + 1) * 10 - forInterval := time.Duration(interval*rand.Int63n(6)) * time.Second - - var annotations map[string]string = nil - if rand.Int63()%2 == 0 { - annotations = GenerateAlertLabels(rand.Intn(5), "ann-") - } - var labels map[string]string = nil - if rand.Int63()%2 == 0 { - labels = GenerateAlertLabels(rand.Intn(5), "lbl-") - } - - var dashUID *string = nil - var panelID *int64 = nil - if rand.Int63()%2 == 0 { - d := util.GenerateShortUID() - dashUID = &d - p := rand.Int63n(1500) - panelID = &p - } - - var ns []NotificationSettings - if rand.Int63()%2 == 0 { - ns = append(ns, NotificationSettingsGen()()) - } - - rule := &AlertRule{ - ID: rand.Int63n(1500), - OrgID: rand.Int63n(1500) + 1, // Prevent OrgID=0 as this does not pass alert rule validation. - Title: "TEST-ALERT-" + util.GenerateShortUID(), - Condition: "A", - Data: []AlertQuery{GenerateAlertQuery()}, - Updated: time.Now().Add(-time.Duration(rand.Intn(100) + 1)), - IntervalSeconds: rand.Int63n(60) + 1, - Version: rand.Int63n(1500), // Don't generate a rule ID too big for postgres - UID: util.GenerateShortUID(), - NamespaceUID: util.GenerateShortUID(), - DashboardUID: dashUID, - PanelID: panelID, - RuleGroup: "TEST-GROUP-" + util.GenerateShortUID(), - RuleGroupIndex: rand.Intn(1500), - NoDataState: randNoDataState(), - ExecErrState: randErrState(), - For: forInterval, - Annotations: annotations, - Labels: labels, - NotificationSettings: ns, - } - - for _, mutator := range mutators { - mutator(rule) - } - return rule +func (g *AlertRuleGenerator) With(mutators ...AlertRuleMutator) *AlertRuleGenerator { + return &AlertRuleGenerator{ + AlertRuleMutators: g.AlertRuleMutators, + mutators: append(g.mutators, mutators...), } } -func WithNotEmptyLabels(count int, prefix string) AlertRuleMutator { +func (g *AlertRuleGenerator) Generate() AlertRule { + randNoDataState := func() NoDataState { + s := [...]NoDataState{ + Alerting, + NoData, + OK, + } + return s[rand.Intn(len(s))] + } + + randErrState := func() ExecutionErrorState { + s := [...]ExecutionErrorState{ + AlertingErrState, + ErrorErrState, + OkErrState, + } + return s[rand.Intn(len(s))] + } + + interval := (rand.Int63n(6) + 1) * 10 + forInterval := time.Duration(interval*rand.Int63n(6)) * time.Second + + var annotations map[string]string = nil + if rand.Int63()%2 == 0 { + annotations = GenerateAlertLabels(rand.Intn(5), "ann-") + } + var labels map[string]string = nil + if rand.Int63()%2 == 0 { + labels = GenerateAlertLabels(rand.Intn(5), "lbl-") + } + + var dashUID *string = nil + var panelID *int64 = nil + if rand.Int63()%2 == 0 { + d := util.GenerateShortUID() + dashUID = &d + p := rand.Int63n(1500) + panelID = &p + } + + var ns []NotificationSettings + if rand.Int63()%2 == 0 { + ns = append(ns, NotificationSettingsGen()()) + } + + rule := AlertRule{ + ID: 0, + OrgID: rand.Int63n(1500) + 1, // Prevent OrgID=0 as this does not pass alert rule validation. + Title: fmt.Sprintf("title-%s", util.GenerateShortUID()), + Condition: "A", + Data: []AlertQuery{g.GenerateQuery()}, + Updated: time.Now().Add(-time.Duration(rand.Intn(100) + 1)), + IntervalSeconds: rand.Int63n(60) + 1, + Version: rand.Int63n(1500), // Don't generate a rule ID too big for postgres + UID: util.GenerateShortUID(), + NamespaceUID: util.GenerateShortUID(), + DashboardUID: dashUID, + PanelID: panelID, + RuleGroup: fmt.Sprintf("group-%s,", util.GenerateShortUID()), + RuleGroupIndex: rand.Intn(1500), + NoDataState: randNoDataState(), + ExecErrState: randErrState(), + For: forInterval, + Annotations: annotations, + Labels: labels, + NotificationSettings: ns, + } + + for _, mutator := range g.mutators { + mutator(&rule) + } + return rule +} + +func (g *AlertRuleGenerator) GenerateRef() *AlertRule { + r := g.Generate() + return &r +} + +func (g *AlertRuleGenerator) getCount(bounds ...int) int { + count := 0 + if len(bounds) == 0 { + count = rand.Intn(5) + 1 + } + if len(bounds) == 1 { + count = bounds[0] + } + if len(bounds) == 2 { + if bounds[0] > bounds[1] { + panic("min should not be greater than max") + } else if bounds[0] < bounds[1] { + count = rand.Intn(bounds[1]-bounds[0]) + bounds[0] + } else { + count = bounds[0] + } + } + if len(bounds) > 2 { + panic("invalid number of parameter must be up to 2") + } + return count +} + +func (g *AlertRuleGenerator) GenerateMany(bounds ...int) []AlertRule { + count := g.getCount(bounds...) + result := make([]AlertRule, 0, count) + for i := 0; i < count; i++ { + result = append(result, g.Generate()) + } + return result +} + +func (g *AlertRuleGenerator) GenerateManyRef(bounds ...int) []*AlertRule { + count := g.getCount(bounds...) + + result := make([]*AlertRule, 0) + for i := 0; i < count; i++ { + r := g.Generate() + result = append(result, &r) + } + return result +} + +type AlertRuleMutators struct { +} + +func (a *AlertRuleMutators) WithNotEmptyLabels(count int, prefix string) AlertRuleMutator { return func(rule *AlertRule) { rule.Labels = GenerateAlertLabels(count, prefix) } } -func WithUniqueID() AlertRuleMutator { - usedID := make(map[int64]struct{}) +func (a *AlertRuleMutators) WithUniqueID() AlertRuleMutator { + ids := sync.Map{} return func(rule *AlertRule) { + id := rule.ID for { - id := rand.Int63n(1500) - if _, ok := usedID[id]; !ok { - usedID[id] = struct{}{} + _, exists := ids.LoadOrStore(id, struct{}{}) + if !exists { rule.ID = id return } + id = rand.Int63n(1500) + 1 } } } -func WithGroupIndex(groupIndex int) AlertRuleMutator { +func (a *AlertRuleMutators) WithGroupIndex(groupIndex int) AlertRuleMutator { return func(rule *AlertRule) { rule.RuleGroupIndex = groupIndex } } -func WithUniqueGroupIndex() AlertRuleMutator { - usedIdx := make(map[int]struct{}) +func (a *AlertRuleMutators) WithUniqueGroupIndex() AlertRuleMutator { + usedIdx := sync.Map{} return func(rule *AlertRule) { + idx := rule.RuleGroupIndex for { - idx := rand.Int() - if _, ok := usedIdx[idx]; !ok { - usedIdx[idx] = struct{}{} + if _, exists := usedIdx.LoadOrStore(idx, struct{}{}); !exists { rule.RuleGroupIndex = idx return } + idx = rand.Int() } } } -func WithSequentialGroupIndex() AlertRuleMutator { +func (a *AlertRuleMutators) WithSequentialGroupIndex() AlertRuleMutator { idx := 1 + m := sync.Mutex{} return func(rule *AlertRule) { + m.Lock() + defer m.Unlock() rule.RuleGroupIndex = idx idx++ } } -func WithOrgID(orgId int64) AlertRuleMutator { +func (a *AlertRuleMutators) WithOrgID(orgId int64) AlertRuleMutator { return func(rule *AlertRule) { rule.OrgID = orgId } } -func WithUniqueOrgID() AlertRuleMutator { - orgs := map[int64]struct{}{} +func (a *AlertRuleMutators) WithUniqueOrgID() AlertRuleMutator { + orgs := sync.Map{} return func(rule *AlertRule) { - var orgID int64 + orgID := rule.OrgID for { - orgID = rand.Int63() - if _, ok := orgs[orgID]; !ok { - break + if _, exist := orgs.LoadOrStore(orgID, struct{}{}); !exist { + rule.OrgID = orgID + return } + orgID = rand.Int63() } - orgs[orgID] = struct{}{} - rule.OrgID = orgID } } // WithNamespaceUIDNotIn generates a random namespace UID if it is among excluded -func WithNamespaceUIDNotIn(exclude ...string) AlertRuleMutator { +func (a *AlertRuleMutators) WithNamespaceUIDNotIn(exclude ...string) AlertRuleMutator { return func(rule *AlertRule) { for { if !slices.Contains(exclude, rule.NamespaceUID) { return } - rule.NamespaceUID = uuid.NewString() + rule.NamespaceUID = util.GenerateShortUID() } } } -func WithNamespace(namespace *folder.Folder) AlertRuleMutator { +func (a *AlertRuleMutators) WithNamespaceUID(namespaceUID string) AlertRuleMutator { return func(rule *AlertRule) { - rule.NamespaceUID = namespace.UID + rule.NamespaceUID = namespaceUID } } -func WithInterval(interval time.Duration) AlertRuleMutator { +func (a *AlertRuleMutators) WithNamespace(namespace *folder.Folder) AlertRuleMutator { + return a.WithNamespaceUID(namespace.UID) +} + +func (a *AlertRuleMutators) WithInterval(interval time.Duration) AlertRuleMutator { return func(rule *AlertRule) { rule.IntervalSeconds = int64(interval.Seconds()) } } -func WithIntervalBetween(min, max int64) AlertRuleMutator { +func (a *AlertRuleMutators) WithIntervalSeconds(seconds int64) AlertRuleMutator { + return func(rule *AlertRule) { + rule.IntervalSeconds = seconds + } +} + +// WithIntervalMatching mutator that generates random interval and `for` duration that are times of the provided base interval. +func (a *AlertRuleMutators) WithIntervalMatching(baseInterval time.Duration) AlertRuleMutator { + return func(rule *AlertRule) { + rule.IntervalSeconds = int64(baseInterval.Seconds()) * (rand.Int63n(10) + 1) + rule.For = time.Duration(rule.IntervalSeconds*rand.Int63n(9)+1) * time.Second + } +} + +func (a *AlertRuleMutators) WithIntervalBetween(min, max int64) AlertRuleMutator { return func(rule *AlertRule) { rule.IntervalSeconds = rand.Int63n(max-min) + min } } -func WithTitle(title string) AlertRuleMutator { +func (a *AlertRuleMutators) WithTitle(title string) AlertRuleMutator { return func(rule *AlertRule) { rule.Title = title } } -func WithFor(duration time.Duration) AlertRuleMutator { +func (a *AlertRuleMutators) WithFor(duration time.Duration) AlertRuleMutator { return func(rule *AlertRule) { rule.For = duration } } -func WithForNTimes(timesOfInterval int64) AlertRuleMutator { +func (a *AlertRuleMutators) WithForNTimes(timesOfInterval int64) AlertRuleMutator { return func(rule *AlertRule) { rule.For = time.Duration(rule.IntervalSeconds*timesOfInterval) * time.Second } } -func WithNoDataExecAs(nodata NoDataState) AlertRuleMutator { +func (a *AlertRuleMutators) WithNoDataExecAs(nodata NoDataState) AlertRuleMutator { return func(rule *AlertRule) { rule.NoDataState = nodata } } -func WithErrorExecAs(err ExecutionErrorState) AlertRuleMutator { +func (a *AlertRuleMutators) WithErrorExecAs(err ExecutionErrorState) AlertRuleMutator { return func(rule *AlertRule) { rule.ExecErrState = err } } -func WithAnnotations(a data.Labels) AlertRuleMutator { +func (a *AlertRuleMutators) WithAnnotations(lbls data.Labels) AlertRuleMutator { return func(rule *AlertRule) { - rule.Annotations = a + rule.Annotations = lbls } } -func WithAnnotation(key, value string) AlertRuleMutator { +func (a *AlertRuleMutators) WithAnnotation(key, value string) AlertRuleMutator { return func(rule *AlertRule) { if rule.Annotations == nil { rule.Annotations = data.Labels{} @@ -244,13 +333,13 @@ func WithAnnotation(key, value string) AlertRuleMutator { } } -func WithLabels(a data.Labels) AlertRuleMutator { +func (a *AlertRuleMutators) WithLabels(lbls data.Labels) AlertRuleMutator { return func(rule *AlertRule) { - rule.Labels = a + rule.Labels = lbls } } -func WithLabel(key, value string) AlertRuleMutator { +func (a *AlertRuleMutators) WithLabel(key, value string) AlertRuleMutator { return func(rule *AlertRule) { if rule.Labels == nil { rule.Labels = data.Labels{} @@ -259,35 +348,80 @@ func WithLabel(key, value string) AlertRuleMutator { } } -func WithUniqueUID(knownUids *sync.Map) AlertRuleMutator { +func (a *AlertRuleMutators) WithDashboardAndPanel(dashboardUID *string, panelID *int64) AlertRuleMutator { + return func(rule *AlertRule) { + rule.DashboardUID = dashboardUID + rule.PanelID = panelID + } +} + +// WithUniqueUID returns AlertRuleMutator that generates a random UID if it is among UIDs known by the instance of mutator. +// NOTE: two instances of the mutator do not share known UID. +// Example #1 reuse mutator instance: +// +// mut := WithUniqueUID() +// rule1 := RuleGen.With(mut).Generate() +// rule2 := RuleGen.With(mut).Generate() +// +// Example #2 reuse generator: +// +// gen := RuleGen.With(WithUniqueUID()) +// rule1 := gen.Generate() +// rule2 := gen.Generate() +// +// Example #3 non-unique: +// +// rule1 := RuleGen.With(WithUniqueUID()).Generate +// rule2 := RuleGen.With(WithUniqueUID()).Generate +func (a *AlertRuleMutators) WithUniqueUID() AlertRuleMutator { + uids := sync.Map{} return func(rule *AlertRule) { uid := rule.UID for { - _, ok := knownUids.LoadOrStore(uid, struct{}{}) - if !ok { + _, exist := uids.LoadOrStore(uid, struct{}{}) + if !exist { rule.UID = uid return } - uid = uuid.NewString() + uid = util.GenerateShortUID() } } } -func WithUniqueTitle(knownTitles *sync.Map) AlertRuleMutator { +// WithUniqueTitle returns AlertRuleMutator that generates a random title if the rule's title is among titles known by the instance of mutator. +// Two instances of the mutator do not share known titles. +// Example #1 reuse mutator instance: +// +// mut := WithUniqueTitle() +// rule1 := RuleGen.With(mut).Generate() +// rule2 := RuleGen.With(mut).Generate() +// +// Example #2 reuse generator: +// +// gen := RuleGen.With(WithUniqueTitle()) +// rule1 := gen.Generate() +// rule2 := gen.Generate() +// +// Example #3 non-unique: +// +// rule1 := RuleGen.With(WithUniqueTitle()).Generate +// rule2 := RuleGen.With(WithUniqueTitle()).Generate +func (a *AlertRuleMutators) WithUniqueTitle() AlertRuleMutator { + titles := sync.Map{} return func(rule *AlertRule) { title := rule.Title for { - _, ok := knownTitles.LoadOrStore(title, struct{}{}) - if !ok { + _, exist := titles.LoadOrStore(title, struct{}{}) + if !exist { rule.Title = title return } - title = uuid.NewString() + title = fmt.Sprintf("title-%s", util.GenerateShortUID()) } } } -func WithQuery(query ...AlertQuery) AlertRuleMutator { +func (a *AlertRuleMutators) WithQuery(query ...AlertQuery) AlertRuleMutator { return func(rule *AlertRule) { rule.Data = query if len(query) > 1 { @@ -296,7 +430,19 @@ func WithQuery(query ...AlertQuery) AlertRuleMutator { } } -func WithGroupKey(groupKey AlertRuleGroupKey) AlertRuleMutator { +func (a *AlertRuleMutators) WithGroupName(groupName string) AlertRuleMutator { + return func(rule *AlertRule) { + rule.RuleGroup = groupName + } +} + +func (a *AlertRuleMutators) WithGroupPrefix(prefix string) AlertRuleMutator { + return func(rule *AlertRule) { + rule.RuleGroup = fmt.Sprintf("%s%s", prefix, util.GenerateShortUID()) + } +} + +func (a *AlertRuleMutators) WithGroupKey(groupKey AlertRuleGroupKey) AlertRuleMutator { return func(rule *AlertRule) { rule.RuleGroup = groupKey.RuleGroup rule.OrgID = groupKey.OrgID @@ -304,19 +450,48 @@ func WithGroupKey(groupKey AlertRuleGroupKey) AlertRuleMutator { } } -func WithNotificationSettingsGen(ns func() NotificationSettings) AlertRuleMutator { +// WithSameGroup generates a random group name and assigns it to all rules passed to it +func (a *AlertRuleMutators) WithSameGroup() AlertRuleMutator { + once := sync.Once{} + name := "" + return func(rule *AlertRule) { + once.Do(func() { + name = util.GenerateShortUID() + }) + rule.RuleGroup = name + } +} + +func (a *AlertRuleMutators) WithNotificationSettingsGen(ns func() NotificationSettings) AlertRuleMutator { return func(rule *AlertRule) { rule.NotificationSettings = []NotificationSettings{ns()} } } +func (a *AlertRuleMutators) WithNotificationSettings(ns NotificationSettings) AlertRuleMutator { + return func(rule *AlertRule) { + rule.NotificationSettings = []NotificationSettings{ns} + } +} -func WithNoNotificationSettings() AlertRuleMutator { +func (a *AlertRuleMutators) WithNoNotificationSettings() AlertRuleMutator { return func(rule *AlertRule) { rule.NotificationSettings = nil } } -func GenerateAlertLabels(count int, prefix string) data.Labels { +func (a *AlertRuleMutators) WithIsPaused(paused bool) AlertRuleMutator { + return func(rule *AlertRule) { + rule.IsPaused = paused + } +} + +func (g *AlertRuleGenerator) GenerateLabels(min, max int, prefix string) data.Labels { + count := max + if min > max { + panic("min should not be greater than max") + } else if min < max { + count = rand.Intn(max-min) + min + } labels := make(data.Labels, count) for i := 0; i < count; i++ { labels[prefix+"key-"+util.GenerateShortUID()] = prefix + "value-" + util.GenerateShortUID() @@ -324,7 +499,15 @@ func GenerateAlertLabels(count int, prefix string) data.Labels { return labels } +func GenerateAlertLabels(count int, prefix string) data.Labels { + return RuleGen.GenerateLabels(count, count, prefix) +} + func GenerateAlertQuery() AlertQuery { + return RuleGen.GenerateQuery() +} + +func (g *AlertRuleGenerator) GenerateQuery() AlertQuery { f := rand.Intn(10) + 5 t := rand.Intn(f) @@ -343,35 +526,10 @@ func GenerateAlertQuery() AlertQuery { } } -// GenerateUniqueAlertRules generates many random alert rules and makes sure that they have unique UID. -// It returns a tuple where first element is a map where keys are UID of alert rule and the second element is a slice of the same rules -func GenerateUniqueAlertRules(count int, f func() *AlertRule) (map[string]*AlertRule, []*AlertRule) { - uIDs := make(map[string]*AlertRule, count) - result := make([]*AlertRule, 0, count) - for len(result) < count { - rule := f() - if _, ok := uIDs[rule.UID]; ok { - continue - } - result = append(result, rule) - uIDs[rule.UID] = rule +func (g *AlertRuleGenerator) WithCondition(condition string) AlertRuleMutator { + return func(r *AlertRule) { + r.Condition = condition } - return uIDs, result -} - -// GenerateAlertRulesSmallNonEmpty generates 1 to 5 rules using the provided generator -func GenerateAlertRulesSmallNonEmpty(f func() *AlertRule) []*AlertRule { - return GenerateAlertRules(rand.Intn(4)+1, f) -} - -// GenerateAlertRules generates many random alert rules. Does not guarantee that rules are unique (by UID) -func GenerateAlertRules(count int, f func() *AlertRule) []*AlertRule { - result := make([]*AlertRule, 0, count) - for len(result) < count { - rule := f() - result = append(result, rule) - } - return result } // GenerateRuleKey generates a random alert rule key @@ -392,7 +550,7 @@ func GenerateGroupKey(orgID int64) AlertRuleGroupKey { } // CopyRule creates a deep copy of AlertRule -func CopyRule(r *AlertRule) *AlertRule { +func CopyRule(r *AlertRule, mutators ...AlertRuleMutator) *AlertRule { result := AlertRule{ ID: r.ID, OrgID: r.OrgID, @@ -449,6 +607,12 @@ func CopyRule(r *AlertRule) *AlertRule { result.NotificationSettings = append(result.NotificationSettings, CopyNotificationSettings(s)) } + if len(mutators) > 0 { + for _, mutator := range mutators { + mutator(&result) + } + } + return &result } @@ -687,10 +851,6 @@ func NotificationSettingsGen(mutators ...Mutator[NotificationSettings]) func() N } } -var ( - NSMuts = NotificationSettingsMutators{} -) - type NotificationSettingsMutators struct{} func (n NotificationSettingsMutators) WithReceiver(receiver string) Mutator[NotificationSettings] { diff --git a/pkg/services/ngalert/ngalert.go b/pkg/services/ngalert/ngalert.go index 3b5d4b51ad5..6410c4be22d 100644 --- a/pkg/services/ngalert/ngalert.go +++ b/pkg/services/ngalert/ngalert.go @@ -480,7 +480,8 @@ func configureHistorianBackend(ctx context.Context, cfg setting.UnifiedAlertingS return nil, fmt.Errorf("invalid remote loki configuration: %w", err) } req := historian.NewRequester() - backend := historian.NewRemoteLokiBackend(lcfg, req, met) + lokiBackendLogger := log.New("ngalert.state.historian", "backend", "loki") + backend := historian.NewRemoteLokiBackend(lokiBackendLogger, lcfg, req, met) testConnCtx, cancelFunc := context.WithTimeout(ctx, 10*time.Second) defer cancelFunc() diff --git a/pkg/services/ngalert/ngalert_test.go b/pkg/services/ngalert/ngalert_test.go index b717f562cb1..ed7bee52d7c 100644 --- a/pkg/services/ngalert/ngalert_test.go +++ b/pkg/services/ngalert/ngalert_test.go @@ -29,7 +29,8 @@ func Test_subscribeToFolderChanges(t *testing.T) { UID: util.GenerateShortUID(), Title: "Folder" + util.GenerateShortUID(), } - rules := models.GenerateAlertRules(5, models.AlertRuleGen(models.WithOrgID(orgID), models.WithNamespace(folder))) + gen := models.RuleGen + rules := gen.With(gen.WithOrgID(orgID), gen.WithNamespace(folder)).GenerateManyRef(5) bus := bus.ProvideBus(tracing.InitializeTracerForTest()) db := fakes.NewRuleStore(t) diff --git a/pkg/services/ngalert/notifier/alertmanager_config.go b/pkg/services/ngalert/notifier/alertmanager_config.go index 32ae92baea2..232f17a3899 100644 --- a/pkg/services/ngalert/notifier/alertmanager_config.go +++ b/pkg/services/ngalert/notifier/alertmanager_config.go @@ -13,6 +13,20 @@ import ( "github.com/grafana/grafana/pkg/services/ngalert/models" "github.com/grafana/grafana/pkg/services/ngalert/store" "github.com/grafana/grafana/pkg/util" + "github.com/grafana/grafana/pkg/util/errutil" +) + +var ( + // ErrAlertmanagerReceiverInUse is primarily meant for when a receiver is used by a rule and is being deleted. + ErrAlertmanagerReceiverInUse = errutil.BadRequest("alerting.notifications.alertmanager.receiverInUse").MustTemplate("receiver [Name: {{ .Public.Receiver }}] is used by rule: {{ .Error }}", + errutil.WithPublic( + "receiver [Name: {{ .Public.Receiver }}] is used by rule", + )) + // ErrAlertmanagerTimeIntervalInUse is primarily meant for when a time interval is used by a rule and is being deleted. + ErrAlertmanagerTimeIntervalInUse = errutil.BadRequest("alerting.notifications.alertmanager.intervalInUse").MustTemplate("time interval [Name: {{ .Public.Interval }}] is used by rule: {{ .Error }}", + errutil.WithPublic( + "time interval [Name: {{ .Public.Interval }}] is used by rule", + )) ) type UnknownReceiverError struct { @@ -227,6 +241,14 @@ func (moa *MultiOrgAlertmanager) SaveAndApplyAlertmanagerConfiguration(ctx conte if err := am.SaveAndApplyConfig(ctx, &config); err != nil { moa.logger.Error("Unable to save and apply alertmanager configuration", "error", err) + errReceiverDoesNotExist := ErrorReceiverDoesNotExist{} + if errors.As(err, &errReceiverDoesNotExist) { + return ErrAlertmanagerReceiverInUse.Build(errutil.TemplateData{Public: map[string]interface{}{"Receiver": errReceiverDoesNotExist.Reference}, Error: err}) + } + errTimeIntervalDoesNotExist := ErrorTimeIntervalDoesNotExist{} + if errors.As(err, &errTimeIntervalDoesNotExist) { + return ErrAlertmanagerTimeIntervalInUse.Build(errutil.TemplateData{Public: map[string]interface{}{"Interval": errTimeIntervalDoesNotExist.Reference}, Error: err}) + } return AlertmanagerConfigRejectedError{err} } diff --git a/pkg/services/ngalert/notifier/channels_config/available_channels.go b/pkg/services/ngalert/notifier/channels_config/available_channels.go index bf6d5811c32..aaf2a938c83 100644 --- a/pkg/services/ngalert/notifier/channels_config/available_channels.go +++ b/pkg/services/ngalert/notifier/channels_config/available_channels.go @@ -1281,7 +1281,7 @@ func GetAvailableNotifiers() []*NotifierPlugin { }, { Label: "Override priority", Element: ElementTypeCheckbox, - Description: "Allow the alert priority to be set using the og_priority annotation", + Description: "Allow the alert priority to be set using the og_priority label.", PropertyName: "overridePriority", }, { diff --git a/pkg/services/ngalert/notifier/validation.go b/pkg/services/ngalert/notifier/validation.go index 48e0f20d2ad..835f2f10934 100644 --- a/pkg/services/ngalert/notifier/validation.go +++ b/pkg/services/ngalert/notifier/validation.go @@ -6,13 +6,33 @@ import ( "fmt" "sync" + "github.com/prometheus/alertmanager/config" + "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" "github.com/grafana/grafana/pkg/services/ngalert/models" "github.com/grafana/grafana/pkg/services/ngalert/store" - "github.com/prometheus/alertmanager/config" ) +type ErrorReferenceInvalid struct { + Reference string +} + +type ErrorReceiverDoesNotExist struct { + ErrorReferenceInvalid +} +type ErrorTimeIntervalDoesNotExist struct { + ErrorReferenceInvalid +} + +func (e ErrorReceiverDoesNotExist) Error() string { + return fmt.Sprintf("receiver %s does not exist", e.Reference) +} + +func (e ErrorTimeIntervalDoesNotExist) Error() string { + return fmt.Sprintf("time interval %s does not exist", e.Reference) +} + // NotificationSettingsValidator validates NotificationSettings against the current Alertmanager configuration type NotificationSettingsValidator interface { Validate(s models.NotificationSettings) error @@ -64,11 +84,11 @@ func (n staticValidator) Validate(settings models.NotificationSettings) error { } var errs []error if _, ok := n.availableReceivers[settings.Receiver]; !ok { - errs = append(errs, fmt.Errorf("receiver '%s' does not exist", settings.Receiver)) + errs = append(errs, ErrorReceiverDoesNotExist{ErrorReferenceInvalid: ErrorReferenceInvalid{Reference: settings.Receiver}}) } for _, interval := range settings.MuteTimeIntervals { if _, ok := n.availableTimeIntervals[interval]; !ok { - errs = append(errs, fmt.Errorf("mute time interval '%s' does not exist", interval)) + errs = append(errs, ErrorTimeIntervalDoesNotExist{ErrorReferenceInvalid: ErrorReferenceInvalid{Reference: interval}}) } } return errors.Join(errs...) diff --git a/pkg/services/ngalert/provisioning/accesscontrol_test.go b/pkg/services/ngalert/provisioning/accesscontrol_test.go index 04d9000c0ec..a55e44429b2 100644 --- a/pkg/services/ngalert/provisioning/accesscontrol_test.go +++ b/pkg/services/ngalert/provisioning/accesscontrol_test.go @@ -84,7 +84,7 @@ func TestCanWriteAllRules(t *testing.T) { func TestAuthorizeAccessToRuleGroup(t *testing.T) { testUser := &user.SignedInUser{} - rules := models.GenerateAlertRules(1, models.AlertRuleGen()) + rules := models.RuleGen.GenerateManyRef(1) t.Run("should return nil when user has provisioning permissions", func(t *testing.T) { rs := &fakes.FakeRuleService{} diff --git a/pkg/services/ngalert/provisioning/alert_rules_test.go b/pkg/services/ngalert/provisioning/alert_rules_test.go index fea81b19053..d035f5b78bf 100644 --- a/pkg/services/ngalert/provisioning/alert_rules_test.go +++ b/pkg/services/ngalert/provisioning/alert_rules_test.go @@ -551,7 +551,8 @@ func TestCreateAlertRule(t *testing.T) { u := &user.SignedInUser{OrgID: orgID} groupKey := models.GenerateGroupKey(orgID) groupIntervalSeconds := int64(30) - rules := models.GenerateAlertRules(3, models.AlertRuleGen(models.WithGroupKey(groupKey), models.WithInterval(time.Duration(groupIntervalSeconds)*time.Second))) + gen := models.RuleGen + rules := gen.With(gen.WithGroupKey(groupKey), gen.WithIntervalSeconds(groupIntervalSeconds)).GenerateManyRef(3) groupProvenance := models.ProvenanceAPI initServiceWithData := func(t *testing.T) (*AlertRuleService, *fakes.RuleStore, *fakes.FakeProvisioningStore, *fakeRuleAccessControlService) { @@ -567,14 +568,14 @@ func TestCreateAlertRule(t *testing.T) { t.Run("when user can write all rules", func(t *testing.T) { t.Run("and a new rule creates a new group", func(t *testing.T) { - rule := models.AlertRuleGen(models.WithOrgID(orgID))() + rule := gen.With(gen.WithOrgID(orgID)).Generate() service, ruleStore, provenanceStore, ac := initServiceWithData(t) ac.CanWriteAllRulesFunc = func(ctx context.Context, user identity.Requester) (bool, error) { return true, nil } - actualRule, err := service.CreateAlertRule(context.Background(), u, *rule, models.ProvenanceFile) + actualRule, err := service.CreateAlertRule(context.Background(), u, rule, models.ProvenanceFile) require.NoError(t, err) require.Len(t, ac.Calls, 1) @@ -601,14 +602,14 @@ func TestCreateAlertRule(t *testing.T) { }) }) t.Run("and it adds a rule to a group", func(t *testing.T) { - rule := models.AlertRuleGen(models.WithGroupKey(groupKey))() + rule := gen.With(gen.WithGroupKey(groupKey)).Generate() service, ruleStore, provenanceStore, ac := initServiceWithData(t) ac.CanWriteAllRulesFunc = func(ctx context.Context, user identity.Requester) (bool, error) { return true, nil } - actualRule, err := service.CreateAlertRule(context.Background(), u, *rule, models.ProvenanceNone) + actualRule, err := service.CreateAlertRule(context.Background(), u, rule, models.ProvenanceNone) require.NoError(t, err) require.Len(t, ac.Calls, 1) @@ -637,7 +638,7 @@ func TestCreateAlertRule(t *testing.T) { }) t.Run("when user cannot write all rules", func(t *testing.T) { t.Run("and it creates a new group", func(t *testing.T) { - rule := models.AlertRuleGen(models.WithOrgID(orgID))() + rule := gen.With(gen.WithOrgID(orgID)).Generate() t.Run("it should authorize the change", func(t *testing.T) { service, ruleStore, provenanceStore, ac := initServiceWithData(t) @@ -654,7 +655,7 @@ func TestCreateAlertRule(t *testing.T) { return nil } - actualRule, err := service.CreateAlertRule(context.Background(), u, *rule, models.ProvenanceFile) + actualRule, err := service.CreateAlertRule(context.Background(), u, rule, models.ProvenanceFile) require.NoError(t, err) require.Len(t, ac.Calls, 2) @@ -683,7 +684,7 @@ func TestCreateAlertRule(t *testing.T) { }) }) t.Run("and it adds a rule to a group", func(t *testing.T) { - rule := models.AlertRuleGen(models.WithGroupKey(groupKey))() + rule := gen.With(gen.WithGroupKey(groupKey)).Generate() t.Run("it should authorize the change to whole group", func(t *testing.T) { service, ruleStore, provenanceStore, ac := initServiceWithData(t) @@ -701,7 +702,7 @@ func TestCreateAlertRule(t *testing.T) { return nil } - actualRule, err := service.CreateAlertRule(context.Background(), u, *rule, models.ProvenanceNone) + actualRule, err := service.CreateAlertRule(context.Background(), u, rule, models.ProvenanceNone) require.NoError(t, err) require.Len(t, ac.Calls, 2) @@ -730,7 +731,7 @@ func TestCreateAlertRule(t *testing.T) { }) }) t.Run("it should not insert if not authorized", func(t *testing.T) { - rule := models.AlertRuleGen(models.WithGroupKey(groupKey))() + rule := gen.With(gen.WithGroupKey(groupKey)).Generate() service, ruleStore, _, ac := initServiceWithData(t) ac.CanWriteAllRulesFunc = func(ctx context.Context, user identity.Requester) (bool, error) { @@ -741,7 +742,7 @@ func TestCreateAlertRule(t *testing.T) { return expectedErr } - _, err := service.CreateAlertRule(context.Background(), u, *rule, models.ProvenanceFile) + _, err := service.CreateAlertRule(context.Background(), u, rule, models.ProvenanceFile) require.ErrorIs(t, expectedErr, err) require.Len(t, ac.Calls, 2) @@ -797,7 +798,8 @@ func TestUpdateAlertRule(t *testing.T) { u := &user.SignedInUser{OrgID: orgID} groupKey := models.GenerateGroupKey(orgID) groupIntervalSeconds := int64(30) - rules := models.GenerateAlertRules(3, models.AlertRuleGen(models.WithGroupKey(groupKey), models.WithInterval(time.Duration(groupIntervalSeconds)*time.Second))) + gen := models.RuleGen + rules := gen.With(gen.WithGroupKey(groupKey), gen.WithIntervalSeconds(groupIntervalSeconds)).GenerateManyRef(3) groupProvenance := models.ProvenanceAPI initServiceWithData := func(t *testing.T) (*AlertRuleService, *fakes.RuleStore, *fakes.FakeProvisioningStore, *fakeRuleAccessControlService) { @@ -899,7 +901,8 @@ func TestDeleteAlertRule(t *testing.T) { u := &user.SignedInUser{OrgID: orgID} groupKey := models.GenerateGroupKey(orgID) groupIntervalSeconds := int64(30) - rules := models.GenerateAlertRules(3, models.AlertRuleGen(models.WithGroupKey(groupKey), models.WithInterval(time.Duration(groupIntervalSeconds)*time.Second))) + gen := models.RuleGen + rules := gen.With(gen.WithGroupKey(groupKey), gen.WithIntervalSeconds(groupIntervalSeconds)).GenerateManyRef(3) groupProvenance := models.ProvenanceAPI initServiceWithData := func(t *testing.T) (*AlertRuleService, *fakes.RuleStore, *fakes.FakeProvisioningStore, *fakeRuleAccessControlService) { @@ -990,7 +993,8 @@ func TestGetAlertRule(t *testing.T) { orgID := rand.Int63() u := &user.SignedInUser{OrgID: orgID} groupKey := models.GenerateGroupKey(orgID) - rules := models.GenerateAlertRules(3, models.AlertRuleGen(models.WithGroupKey(groupKey))) + gen := models.RuleGen + rules := gen.With(gen.WithGroupKey(groupKey)).GenerateManyRef(3) rule := rules[0] expectedProvenance := models.ProvenanceAPI @@ -1118,7 +1122,8 @@ func TestGetRuleGroup(t *testing.T) { u := &user.SignedInUser{OrgID: orgID} groupKey := models.GenerateGroupKey(orgID) intervalSeconds := int64(30) - rules := models.GenerateAlertRules(3, models.AlertRuleGen(models.WithGroupKey(groupKey), models.WithInterval(time.Duration(intervalSeconds)*time.Second))) + gen := models.RuleGen + rules := gen.With(gen.WithGroupKey(groupKey), gen.WithIntervalSeconds(intervalSeconds)).GenerateManyRef(3) derefRules := make([]models.AlertRule, 0, len(rules)) for _, rule := range rules { derefRules = append(derefRules, *rule) @@ -1226,9 +1231,10 @@ func TestGetAlertRules(t *testing.T) { u := &user.SignedInUser{OrgID: orgID} groupKey1 := models.GenerateGroupKey(orgID) groupKey2 := models.GenerateGroupKey(orgID) - rules1 := models.GenerateAlertRules(3, models.AlertRuleGen(models.WithGroupKey(groupKey1))) + gen := models.RuleGen + rules1 := gen.With(gen.WithGroupKey(groupKey1), gen.WithUniqueGroupIndex()).GenerateManyRef(3) models.RulesGroup(rules1).SortByGroupIndex() - rules2 := models.GenerateAlertRules(4, models.AlertRuleGen(models.WithGroupKey(groupKey2))) + rules2 := gen.With(gen.WithGroupKey(groupKey2), gen.WithUniqueGroupIndex()).GenerateManyRef(4) models.RulesGroup(rules2).SortByGroupIndex() allRules := append(rules1, rules2...) expectedProvenance := models.ProvenanceAPI @@ -1325,7 +1331,8 @@ func TestReplaceGroup(t *testing.T) { u := &user.SignedInUser{OrgID: orgID} groupKey := models.GenerateGroupKey(orgID) groupIntervalSeconds := int64(30) - rules := models.GenerateAlertRules(3, models.AlertRuleGen(models.WithGroupKey(groupKey), models.WithInterval(time.Duration(groupIntervalSeconds)*time.Second))) + gen := models.RuleGen + rules := gen.With(gen.WithGroupKey(groupKey), gen.WithIntervalSeconds(groupIntervalSeconds)).GenerateManyRef(3) groupProvenance := models.ProvenanceAPI initServiceWithData := func(t *testing.T) (*AlertRuleService, *fakes.RuleStore, *fakes.FakeProvisioningStore, *fakeRuleAccessControlService) { @@ -1438,7 +1445,8 @@ func TestDeleteRuleGroup(t *testing.T) { u := &user.SignedInUser{OrgID: orgID} groupKey := models.GenerateGroupKey(orgID) groupIntervalSeconds := int64(30) - rules := models.GenerateAlertRules(3, models.AlertRuleGen(models.WithGroupKey(groupKey), models.WithInterval(time.Duration(groupIntervalSeconds)*time.Second))) + gen := models.RuleGen + rules := gen.With(gen.WithGroupKey(groupKey), gen.WithIntervalSeconds(groupIntervalSeconds)).GenerateManyRef(3) groupProvenance := models.ProvenanceAPI initServiceWithData := func(t *testing.T) (*AlertRuleService, *fakes.RuleStore, *fakes.FakeProvisioningStore, *fakeRuleAccessControlService) { diff --git a/pkg/services/ngalert/remote/alertmanager.go b/pkg/services/ngalert/remote/alertmanager.go index eda40a57f16..6862fba4e99 100644 --- a/pkg/services/ngalert/remote/alertmanager.go +++ b/pkg/services/ngalert/remote/alertmanager.go @@ -269,8 +269,21 @@ func (am *Alertmanager) CompareAndSendState(ctx context.Context) error { return nil } +// SaveAndApplyConfig decrypts and sends a configuration to the remote Alertmanager. func (am *Alertmanager) SaveAndApplyConfig(ctx context.Context, cfg *apimodels.PostableUserConfig) error { - return nil + // Get the hash for the encrypted configuration. + rawCfg, err := json.Marshal(cfg) + if err != nil { + return err + } + hash := fmt.Sprintf("%x", md5.Sum(rawCfg)) + + // Decrypt and send. + decrypted, err := am.decryptConfiguration(ctx, cfg) + if err != nil { + return err + } + return am.sendConfiguration(ctx, decrypted, hash, time.Now().Unix(), false) } // SaveAndApplyDefaultConfig sends the default Grafana Alertmanager configuration to the remote Alertmanager. diff --git a/pkg/services/ngalert/remote/alertmanager_test.go b/pkg/services/ngalert/remote/alertmanager_test.go index 9d812551331..188889fd56b 100644 --- a/pkg/services/ngalert/remote/alertmanager_test.go +++ b/pkg/services/ngalert/remote/alertmanager_test.go @@ -145,7 +145,7 @@ func TestApplyConfig(t *testing.T) { // The encrypted configuration should be different than the one we will send. encryptedConfig, err := json.Marshal(c) require.NoError(t, err) - require.NotEqual(t, testGrafanaConfig, encryptedConfig) + require.NotEqual(t, testGrafanaConfigWithSecret, encryptedConfig) // ApplyConfig performs a readiness check at startup. // A non-200 response should result in an error. @@ -316,7 +316,7 @@ func TestIntegrationRemoteAlertmanagerConfiguration(t *testing.T) { require.NoError(t, store.Set(ctx, cfg.OrgID, "alertmanager", notifier.SilencesFilename, testSilence1)) require.NoError(t, store.Set(ctx, cfg.OrgID, "alertmanager", notifier.NotificationLogFilename, testNflog1)) - secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore()) + secretsService := secretsManager.SetupTestService(t, database.ProvideSecretsStore(db.InitTestDB(t))) m := metrics.NewRemoteAlertmanagerMetrics(prometheus.NewRegistry()) am, err := NewAlertmanager(cfg, fstore, secretsService.Decrypt, defaultGrafanaConfig, m) require.NoError(t, err) @@ -386,6 +386,36 @@ func TestIntegrationRemoteAlertmanagerConfiguration(t *testing.T) { require.Equal(t, encodedFullState, state.State) } + // `SaveAndApplyConfig` is called whenever a user manually changes the Alertmanager configuration. + // Calling this method should decrypt and send a configuration to the remote Alertmanager. + { + postableCfg, err := notifier.Load([]byte(testGrafanaConfigWithSecret)) + require.NoError(t, err) + err = notifier.EncryptReceiverConfigs(postableCfg.AlertmanagerConfig.Receivers, func(ctx context.Context, payload []byte) ([]byte, error) { + return secretsService.Encrypt(ctx, payload, secrets.WithoutScope()) + }) + require.NoError(t, err) + + // The encrypted configuration should be different than the one we will send. + encryptedConfig, err := json.Marshal(postableCfg) + require.NoError(t, err) + require.NotEqual(t, testGrafanaConfigWithSecret, encryptedConfig) + + // Call `SaveAndApplyConfig` with the encrypted configuration. + require.NoError(t, err) + require.NoError(t, am.SaveAndApplyConfig(ctx, postableCfg)) + + // Check that the configuration was uploaded to the remote Alertmanager. + config, err := am.mimirClient.GetGrafanaAlertmanagerConfig(ctx) + require.NoError(t, err) + got, err := json.Marshal(config.GrafanaAlertmanagerConfig) + require.NoError(t, err) + + require.JSONEq(t, testGrafanaConfigWithSecret, string(got)) + require.Equal(t, fmt.Sprintf("%x", md5.Sum(encryptedConfig)), config.Hash) + require.False(t, config.Default) + } + // `SaveAndApplyDefaultConfig` should send the default Alertmanager configuration to the remote Alertmanager. { require.NoError(t, am.SaveAndApplyDefaultConfig(ctx)) diff --git a/pkg/services/ngalert/remote/forked_alertmanager_test.go b/pkg/services/ngalert/remote/forked_alertmanager_test.go index ca829cb068e..52a04116fbb 100644 --- a/pkg/services/ngalert/remote/forked_alertmanager_test.go +++ b/pkg/services/ngalert/remote/forked_alertmanager_test.go @@ -107,7 +107,7 @@ func TestForkedAlertmanager_ModeRemoteSecondary(t *testing.T) { }) t.Run("SaveAndApplyDefaultConfig", func(tt *testing.T) { - // SaveAndApplyDefaultConfig should only be called on the remote Alertmanager. + // SaveAndApplyDefaultConfig should only be called on the internal Alertmanager. // State and configuration are updated on an interval. internal, _, forked := genTestAlertmanagers(tt, modeRemoteSecondary) internal.EXPECT().SaveAndApplyDefaultConfig(ctx).Return(nil).Once() @@ -368,25 +368,62 @@ func TestForkedAlertmanager_ModeRemotePrimary(t *testing.T) { expErr := errors.New("test error") t.Run("ApplyConfig", func(tt *testing.T) { - { - // If the remote Alertmanager is not ready, ApplyConfig should be called on both Alertmanagers, - // first on the remote, then on the internal. - internal, remote, forked := genTestAlertmanagers(tt, modeRemotePrimary) - remoteCall := remote.EXPECT().ApplyConfig(ctx, mock.Anything).Return(nil).Once() - internal.EXPECT().ApplyConfig(ctx, mock.Anything).Return(nil).Once().NotBefore(remoteCall) - require.NoError(tt, forked.ApplyConfig(ctx, &models.AlertConfiguration{})) + // If the remote Alertmanager is not ready, ApplyConfig should be called on both Alertmanagers, + // first on the remote, then on the internal. + internal, remote, forked := genTestAlertmanagers(tt, modeRemotePrimary) + remoteCall := remote.EXPECT().ApplyConfig(ctx, mock.Anything).Return(nil).Once() + internal.EXPECT().ApplyConfig(ctx, mock.Anything).Return(nil).Once().NotBefore(remoteCall) + require.NoError(tt, forked.ApplyConfig(ctx, &models.AlertConfiguration{})) - // An error in the remote Alertmanager should be returned. - _, remote, forked = genTestAlertmanagers(tt, modeRemotePrimary) - remote.EXPECT().ApplyConfig(ctx, mock.Anything).Return(expErr).Once() - require.ErrorIs(tt, forked.ApplyConfig(ctx, &models.AlertConfiguration{}), expErr) + // An error in the remote Alertmanager should be returned. + _, remote, forked = genTestAlertmanagers(tt, modeRemotePrimary) + remote.EXPECT().ApplyConfig(ctx, mock.Anything).Return(expErr).Once() + require.ErrorIs(tt, forked.ApplyConfig(ctx, &models.AlertConfiguration{}), expErr) - // An error in the internal Alertmanager should not be returned. - internal, remote, forked = genTestAlertmanagers(tt, modeRemotePrimary) - remote.EXPECT().ApplyConfig(ctx, mock.Anything).Return(nil).Once() - internal.EXPECT().ApplyConfig(ctx, mock.Anything).Return(expErr).Once() - require.NoError(tt, forked.ApplyConfig(ctx, &models.AlertConfiguration{})) - } + // An error in the internal Alertmanager should not be returned. + internal, remote, forked = genTestAlertmanagers(tt, modeRemotePrimary) + remote.EXPECT().ApplyConfig(ctx, mock.Anything).Return(nil).Once() + internal.EXPECT().ApplyConfig(ctx, mock.Anything).Return(expErr).Once() + require.NoError(tt, forked.ApplyConfig(ctx, &models.AlertConfiguration{})) + }) + + t.Run("SaveAndApplyDefaultConfig", func(tt *testing.T) { + // SaveAndApplyDefaultConfig should be called on both Alertmanagers. + internal, remote, forked := genTestAlertmanagers(tt, modeRemotePrimary) + remote.EXPECT().SaveAndApplyDefaultConfig(ctx).Return(nil).Once() + internal.EXPECT().SaveAndApplyDefaultConfig(ctx).Return(nil).Once() + require.NoError(tt, forked.SaveAndApplyDefaultConfig(ctx)) + + // An error in the remote Alertmanager should be returned. + _, remote, forked = genTestAlertmanagers(tt, modeRemotePrimary) + remote.EXPECT().SaveAndApplyDefaultConfig(ctx).Return(expErr).Once() + require.ErrorIs(tt, forked.SaveAndApplyDefaultConfig(ctx), expErr) + + // An error in the internal Alertmanager should not be returned. + internal, remote, forked = genTestAlertmanagers(tt, modeRemotePrimary) + remote.EXPECT().SaveAndApplyDefaultConfig(ctx).Return(nil).Once() + internal.EXPECT().SaveAndApplyDefaultConfig(ctx).Return(expErr).Once() + require.NoError(tt, forked.SaveAndApplyDefaultConfig(ctx)) + }) + + t.Run("SaveAndApplyConfig", func(tt *testing.T) { + // SaveAndApplyConfig should first be called on the remote Alertmanager + // and then on the internal one. + internal, remote, forked := genTestAlertmanagers(tt, modeRemotePrimary) + remoteCall := remote.EXPECT().SaveAndApplyConfig(ctx, mock.Anything).Return(nil).Once() + internal.EXPECT().SaveAndApplyConfig(ctx, mock.Anything).Return(nil).Once().NotBefore(remoteCall) + require.NoError(tt, forked.SaveAndApplyConfig(ctx, &apimodels.PostableUserConfig{})) + + // If there's an error in the remote Alertmanager, it should be returned. + _, remote, forked = genTestAlertmanagers(tt, modeRemotePrimary) + remote.EXPECT().SaveAndApplyConfig(ctx, mock.Anything).Return(expErr).Once() + require.ErrorIs(tt, expErr, forked.SaveAndApplyConfig(ctx, &apimodels.PostableUserConfig{})) + + // An error in the internal Alertmanager should not be returned. + internal, remote, forked = genTestAlertmanagers(tt, modeRemotePrimary) + remote.EXPECT().SaveAndApplyConfig(ctx, mock.Anything).Return(nil).Once() + internal.EXPECT().SaveAndApplyConfig(ctx, mock.Anything).Return(expErr).Once() + require.NoError(tt, forked.SaveAndApplyConfig(ctx, &apimodels.PostableUserConfig{})) }) t.Run("GetStatus", func(tt *testing.T) { @@ -398,31 +435,51 @@ func TestForkedAlertmanager_ModeRemotePrimary(t *testing.T) { }) t.Run("CreateSilence", func(tt *testing.T) { - // We should create the silence in the remote Alertmanager. - _, remote, forked := genTestAlertmanagers(tt, modeRemotePrimary) - + // We should create the silence in both Alertmanagers using the same uid. + testSilence := &apimodels.PostableSilence{} expID := "test-id" - remote.EXPECT().CreateSilence(mock.Anything, mock.Anything).Return(expID, nil).Once() - id, err := forked.CreateSilence(ctx, nil) + + internal, remote, forked := genTestAlertmanagers(tt, modeRemotePrimary) + remote.EXPECT().CreateSilence(mock.Anything, testSilence).Return(expID, nil).Once() + internal.EXPECT().CreateSilence(mock.Anything, testSilence).Return(testSilence.ID, nil).Once() + id, err := forked.CreateSilence(ctx, testSilence) require.NoError(tt, err) + require.Equal(tt, expID, testSilence.ID) require.Equal(tt, expID, id) // If there's an error in the remote Alertmanager, the error should be returned. - remote.EXPECT().CreateSilence(mock.Anything, mock.Anything).Return("", expErr).Maybe() - _, err = forked.CreateSilence(ctx, nil) + _, remote, forked = genTestAlertmanagers(tt, modeRemotePrimary) + remote.EXPECT().CreateSilence(mock.Anything, mock.Anything).Return("", expErr).Once() + _, err = forked.CreateSilence(ctx, testSilence) require.ErrorIs(tt, expErr, err) + + // An error in the internal Alertmanager should not be returned. + internal, remote, forked = genTestAlertmanagers(tt, modeRemotePrimary) + remote.EXPECT().CreateSilence(mock.Anything, mock.Anything).Return(expID, nil).Once() + internal.EXPECT().CreateSilence(mock.Anything, mock.Anything).Return("", expErr).Once() + id, err = forked.CreateSilence(ctx, testSilence) + require.NoError(tt, err) + require.Equal(tt, expID, id) }) t.Run("DeleteSilence", func(tt *testing.T) { - // We should delete the silence in the remote Alertmanager. - _, remote, forked := genTestAlertmanagers(tt, modeRemotePrimary) - remote.EXPECT().DeleteSilence(mock.Anything, mock.Anything).Return(nil).Once() - require.NoError(tt, forked.DeleteSilence(ctx, "")) + // We should delete the silence in both Alertmanagers. + testID := "test-id" + internal, remote, forked := genTestAlertmanagers(tt, modeRemotePrimary) + remote.EXPECT().DeleteSilence(mock.Anything, testID).Return(nil).Once() + internal.EXPECT().DeleteSilence(mock.Anything, testID).Return(nil).Once() + require.NoError(tt, forked.DeleteSilence(ctx, testID)) // If there's an error in the remote Alertmanager, the error should be returned. _, remote, forked = genTestAlertmanagers(tt, modeRemotePrimary) - remote.EXPECT().DeleteSilence(mock.Anything, mock.Anything).Return(expErr).Maybe() - require.ErrorIs(tt, expErr, forked.DeleteSilence(ctx, "")) + remote.EXPECT().DeleteSilence(mock.Anything, testID).Return(expErr).Maybe() + require.ErrorIs(tt, expErr, forked.DeleteSilence(ctx, testID)) + + // An error in the internal Alertmanager should not be returned. + internal, remote, forked = genTestAlertmanagers(tt, modeRemotePrimary) + remote.EXPECT().DeleteSilence(mock.Anything, testID).Return(nil).Maybe() + internal.EXPECT().DeleteSilence(mock.Anything, testID).Return(nil).Maybe() + require.NoError(tt, forked.DeleteSilence(ctx, testID)) }) t.Run("GetSilence", func(tt *testing.T) { diff --git a/pkg/services/ngalert/remote/remote_primary_forked_alertmanager.go b/pkg/services/ngalert/remote/remote_primary_forked_alertmanager.go index 37b955db493..127d9d82e6f 100644 --- a/pkg/services/ngalert/remote/remote_primary_forked_alertmanager.go +++ b/pkg/services/ngalert/remote/remote_primary_forked_alertmanager.go @@ -34,18 +34,36 @@ func (fam *RemotePrimaryForkedAlertmanager) ApplyConfig(ctx context.Context, con } if err := fam.internal.ApplyConfig(ctx, config); err != nil { + // An error in the internal Alertmanager shouldn't make the whole operation fail. + // We're replicating writes in the internal Alertmanager just for comparing and in case we need to roll back. fam.log.Error("Error applying config to the internal Alertmanager", "err", err) } return nil } -// TODO: save the new configuration hash in memory. func (fam *RemotePrimaryForkedAlertmanager) SaveAndApplyConfig(ctx context.Context, config *apimodels.PostableUserConfig) error { + if err := fam.remote.SaveAndApplyConfig(ctx, config); err != nil { + return err + } + + if err := fam.internal.SaveAndApplyConfig(ctx, config); err != nil { + // An error in the internal Alertmanager shouldn't make the whole operation fail. + // We're replicating writes in the internal Alertmanager just for comparing and in case we need to roll back. + fam.log.Error("Error applying config to the internal Alertmanager", "err", err) + } return nil } -// TODO: save the new configuration hash in memory. func (fam *RemotePrimaryForkedAlertmanager) SaveAndApplyDefaultConfig(ctx context.Context) error { + if err := fam.remote.SaveAndApplyDefaultConfig(ctx); err != nil { + return fmt.Errorf("failed to send the default configuration to the remote Alertmanager: %w", err) + } + + if err := fam.internal.SaveAndApplyDefaultConfig(ctx); err != nil { + // An error in the internal Alertmanager shouldn't make the whole operation fail. + // We're replicating writes in the internal Alertmanager just for comparing and in case we need to roll back. + fam.log.Error("Error applying the default configuration to the internal Alertmanager", "err", err) + } return nil } @@ -54,11 +72,26 @@ func (fam *RemotePrimaryForkedAlertmanager) GetStatus() apimodels.GettableStatus } func (fam *RemotePrimaryForkedAlertmanager) CreateSilence(ctx context.Context, silence *apimodels.PostableSilence) (string, error) { - return fam.remote.CreateSilence(ctx, silence) + uid, err := fam.remote.CreateSilence(ctx, silence) + if err != nil { + return "", err + } + + silence.ID = uid + if _, err := fam.internal.CreateSilence(ctx, silence); err != nil { + fam.log.Error("Error creating silence in the internal Alertmanager", "err", err, "silence", silence) + } + return uid, nil } func (fam *RemotePrimaryForkedAlertmanager) DeleteSilence(ctx context.Context, id string) error { - return fam.remote.DeleteSilence(ctx, id) + if err := fam.remote.DeleteSilence(ctx, id); err != nil { + return err + } + if err := fam.internal.DeleteSilence(ctx, id); err != nil { + fam.log.Error("Error deleting silence in the internal Alertmanager", "err", err, "id", id) + } + return nil } func (fam *RemotePrimaryForkedAlertmanager) GetSilence(ctx context.Context, id string) (apimodels.GettableSilence, error) { diff --git a/pkg/services/ngalert/schedule/alert_rule_test.go b/pkg/services/ngalert/schedule/alert_rule_test.go index f910376cfba..3eb82f141e1 100644 --- a/pkg/services/ngalert/schedule/alert_rule_test.go +++ b/pkg/services/ngalert/schedule/alert_rule_test.go @@ -13,20 +13,22 @@ import ( alertingModels "github.com/grafana/alerting/models" "github.com/grafana/grafana-plugin-sdk-go/data" - definitions "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" - "github.com/grafana/grafana/pkg/services/ngalert/eval" - models "github.com/grafana/grafana/pkg/services/ngalert/models" - "github.com/grafana/grafana/pkg/services/ngalert/state" - "github.com/grafana/grafana/pkg/util" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/testutil" prometheusModel "github.com/prometheus/common/model" "github.com/stretchr/testify/assert" mock "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" + + definitions "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" + "github.com/grafana/grafana/pkg/services/ngalert/eval" + models "github.com/grafana/grafana/pkg/services/ngalert/models" + "github.com/grafana/grafana/pkg/services/ngalert/state" + "github.com/grafana/grafana/pkg/util" ) func TestAlertRule(t *testing.T) { + gen := models.RuleGen type evalResponse struct { success bool droppedEval *Evaluation @@ -78,7 +80,7 @@ func TestAlertRule(t *testing.T) { resultCh := make(chan evalResponse) data := &Evaluation{ scheduledAt: expected, - rule: models.AlertRuleGen()(), + rule: gen.GenerateRef(), folderTitle: util.GenerateShortUID(), } go func() { @@ -103,7 +105,7 @@ func TestAlertRule(t *testing.T) { resultCh2 := make(chan evalResponse) data := &Evaluation{ scheduledAt: time1, - rule: models.AlertRuleGen()(), + rule: gen.GenerateRef(), folderTitle: util.GenerateShortUID(), } data2 := &Evaluation{ @@ -146,7 +148,7 @@ func TestAlertRule(t *testing.T) { resultCh := make(chan evalResponse) data := &Evaluation{ scheduledAt: time.Now(), - rule: models.AlertRuleGen()(), + rule: gen.GenerateRef(), folderTitle: util.GenerateShortUID(), } go func() { @@ -176,7 +178,7 @@ func TestAlertRule(t *testing.T) { r.Stop(nil) data := &Evaluation{ scheduledAt: time.Now(), - rule: models.AlertRuleGen()(), + rule: gen.GenerateRef(), folderTitle: util.GenerateShortUID(), } success, dropped := r.Eval(data) @@ -225,7 +227,7 @@ func TestAlertRule(t *testing.T) { case 2: r.Eval(&Evaluation{ scheduledAt: time.Now(), - rule: models.AlertRuleGen()(), + rule: gen.GenerateRef(), folderTitle: util.GenerateShortUID(), }) case 3: @@ -245,6 +247,7 @@ func blankRuleForTests(ctx context.Context) *alertRule { } func TestRuleRoutine(t *testing.T) { + gen := models.RuleGen createSchedule := func( evalAppliedChan chan time.Time, senderMock *SyncAlertsSenderMock, @@ -270,7 +273,7 @@ func TestRuleRoutine(t *testing.T) { evalAppliedChan := make(chan time.Time) sch, ruleStore, instanceStore, reg := createSchedule(evalAppliedChan, nil) - rule := models.AlertRuleGen(withQueryForState(t, evalState))() + rule := gen.With(withQueryForState(t, evalState)).GenerateRef() ruleStore.PutRule(context.Background(), rule) folderTitle := ruleStore.getNamespaceTitle(rule.NamespaceUID) factory := ruleFactoryFromScheduler(sch) @@ -432,7 +435,7 @@ func TestRuleRoutine(t *testing.T) { stoppedChan := make(chan error) sch, _, _, _ := createSchedule(make(chan time.Time), nil) - rule := models.AlertRuleGen()() + rule := gen.GenerateRef() _ = sch.stateManager.ProcessEvalResults(context.Background(), sch.clock.Now(), rule, eval.GenerateResults(rand.Intn(5)+1, eval.ResultGen(eval.WithEvaluatedAt(sch.clock.Now()))), nil) expectedStates := sch.stateManager.GetStatesForRuleUID(rule.OrgID, rule.UID) require.NotEmpty(t, expectedStates) @@ -454,7 +457,7 @@ func TestRuleRoutine(t *testing.T) { stoppedChan := make(chan error) sch, _, _, _ := createSchedule(make(chan time.Time), nil) - rule := models.AlertRuleGen()() + rule := gen.GenerateRef() _ = sch.stateManager.ProcessEvalResults(context.Background(), sch.clock.Now(), rule, eval.GenerateResults(rand.Intn(5)+1, eval.ResultGen(eval.WithEvaluatedAt(sch.clock.Now()))), nil) require.NotEmpty(t, sch.stateManager.GetStatesForRuleUID(rule.OrgID, rule.UID)) @@ -474,7 +477,7 @@ func TestRuleRoutine(t *testing.T) { }) t.Run("when a message is sent to update channel", func(t *testing.T) { - rule := models.AlertRuleGen(withQueryForState(t, eval.Normal))() + rule := gen.With(withQueryForState(t, eval.Normal)).GenerateRef() folderTitle := "folderName" ruleFp := ruleWithFolder{rule, folderTitle}.Fingerprint() @@ -557,7 +560,7 @@ func TestRuleRoutine(t *testing.T) { }) t.Run("when evaluation fails", func(t *testing.T) { - rule := models.AlertRuleGen(withQueryForState(t, eval.Error))() + rule := gen.With(withQueryForState(t, eval.Error)).GenerateRef() rule.ExecErrState = models.ErrorErrState evalAppliedChan := make(chan time.Time) @@ -678,7 +681,7 @@ func TestRuleRoutine(t *testing.T) { t.Run("when there are alerts that should be firing", func(t *testing.T) { t.Run("it should call sender", func(t *testing.T) { // eval.Alerting makes state manager to create notifications for alertmanagers - rule := models.AlertRuleGen(withQueryForState(t, eval.Alerting))() + rule := gen.With(withQueryForState(t, eval.Alerting)).GenerateRef() evalAppliedChan := make(chan time.Time) @@ -712,7 +715,7 @@ func TestRuleRoutine(t *testing.T) { }) t.Run("when there are no alerts to send it should not call notifiers", func(t *testing.T) { - rule := models.AlertRuleGen(withQueryForState(t, eval.Normal))() + rule := gen.With(withQueryForState(t, eval.Normal)).GenerateRef() evalAppliedChan := make(chan time.Time) diff --git a/pkg/services/ngalert/schedule/jitter_test.go b/pkg/services/ngalert/schedule/jitter_test.go index ae7ead86fe7..9ffc358636c 100644 --- a/pkg/services/ngalert/schedule/jitter_test.go +++ b/pkg/services/ngalert/schedule/jitter_test.go @@ -4,14 +4,17 @@ import ( "testing" "time" - ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models" "github.com/stretchr/testify/require" + + ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models" ) func TestJitter(t *testing.T) { + gen := ngmodels.RuleGen + genWithInterval10to600 := gen.With(gen.WithIntervalBetween(10, 600)) t.Run("when strategy is JitterNever", func(t *testing.T) { t.Run("offset is always zero", func(t *testing.T) { - rules := createTestRules(100, ngmodels.WithIntervalBetween(10, 600)) + rules := genWithInterval10to600.GenerateManyRef(100) baseInterval := 10 * time.Second for _, r := range rules { @@ -23,7 +26,7 @@ func TestJitter(t *testing.T) { t.Run("when strategy is JitterByGroup", func(t *testing.T) { t.Run("offset is stable for the same rule", func(t *testing.T) { - rule := ngmodels.AlertRuleGen(ngmodels.WithIntervalBetween(10, 600))() + rule := genWithInterval10to600.GenerateRef() baseInterval := 10 * time.Second original := jitterOffsetInTicks(rule, baseInterval, JitterByGroup) @@ -35,7 +38,7 @@ func TestJitter(t *testing.T) { t.Run("offset is on the interval [0, interval/baseInterval)", func(t *testing.T) { baseInterval := 10 * time.Second - rules := createTestRules(1000, ngmodels.WithIntervalBetween(10, 600)) + rules := genWithInterval10to600.GenerateManyRef(1000) for _, r := range rules { offset := jitterOffsetInTicks(r, baseInterval, JitterByGroup) @@ -49,8 +52,8 @@ func TestJitter(t *testing.T) { baseInterval := 10 * time.Second group1 := ngmodels.AlertRuleGroupKey{} group2 := ngmodels.AlertRuleGroupKey{} - rules1 := createTestRules(1000, ngmodels.WithInterval(60*time.Second), ngmodels.WithGroupKey(group1)) - rules2 := createTestRules(1000, ngmodels.WithInterval(1*time.Hour), ngmodels.WithGroupKey(group2)) + rules1 := gen.With(gen.WithInterval(60*time.Second), gen.WithGroupKey(group1)).GenerateManyRef(1000) + rules2 := gen.With(gen.WithInterval(1*time.Hour), gen.WithGroupKey(group2)).GenerateManyRef(1000) group1Offset := jitterOffsetInTicks(rules1[0], baseInterval, JitterByGroup) for _, r := range rules1 { @@ -67,7 +70,7 @@ func TestJitter(t *testing.T) { t.Run("when strategy is JitterByRule", func(t *testing.T) { t.Run("offset is stable for the same rule", func(t *testing.T) { - rule := ngmodels.AlertRuleGen(ngmodels.WithIntervalBetween(10, 600))() + rule := genWithInterval10to600.GenerateRef() baseInterval := 10 * time.Second original := jitterOffsetInTicks(rule, baseInterval, JitterByRule) @@ -79,7 +82,7 @@ func TestJitter(t *testing.T) { t.Run("offset is on the interval [0, interval/baseInterval)", func(t *testing.T) { baseInterval := 10 * time.Second - rules := createTestRules(1000, ngmodels.WithIntervalBetween(10, 600)) + rules := genWithInterval10to600.GenerateManyRef(1000) for _, r := range rules { offset := jitterOffsetInTicks(r, baseInterval, JitterByRule) @@ -90,11 +93,3 @@ func TestJitter(t *testing.T) { }) }) } - -func createTestRules(n int, mutators ...ngmodels.AlertRuleMutator) []*ngmodels.AlertRule { - result := make([]*ngmodels.AlertRule, 0, n) - for i := 0; i < n; i++ { - result = append(result, ngmodels.AlertRuleGen(mutators...)()) - } - return result -} diff --git a/pkg/services/ngalert/schedule/loaded_metrics_reader_test.go b/pkg/services/ngalert/schedule/loaded_metrics_reader_test.go index 14648031c8a..0ecbeb4f008 100644 --- a/pkg/services/ngalert/schedule/loaded_metrics_reader_test.go +++ b/pkg/services/ngalert/schedule/loaded_metrics_reader_test.go @@ -13,7 +13,7 @@ import ( ) func TestLoadedResultsFromRuleState(t *testing.T) { - rule := ngmodels.AlertRuleGen()() + rule := ngmodels.RuleGen.GenerateRef() p := &FakeRuleStateProvider{ map[ngmodels.AlertRuleKey][]*state.State{ rule.GetKey(): { diff --git a/pkg/services/ngalert/schedule/registry_bench_test.go b/pkg/services/ngalert/schedule/registry_bench_test.go index 7ab26df885c..2b036133702 100644 --- a/pkg/services/ngalert/schedule/registry_bench_test.go +++ b/pkg/services/ngalert/schedule/registry_bench_test.go @@ -12,12 +12,13 @@ import ( ) func BenchmarkRuleWithFolderFingerprint(b *testing.B) { - rules := models.GenerateAlertRules(b.N, models.AlertRuleGen(func(rule *models.AlertRule) { + gen := models.RuleGen + rules := gen.With(func(rule *models.AlertRule) { rule.Data = make([]models.AlertQuery, 0, 5) for i := 0; i < rand.Intn(5)+1; i++ { - rule.Data = append(rule.Data, models.GenerateAlertQuery()) + rule.Data = append(rule.Data, gen.GenerateQuery()) } - })) + }).GenerateManyRef(b.N) folder := uuid.NewString() b.ReportAllocs() b.ResetTimer() diff --git a/pkg/services/ngalert/schedule/registry_test.go b/pkg/services/ngalert/schedule/registry_test.go index c617d91a562..d7628cdd356 100644 --- a/pkg/services/ngalert/schedule/registry_test.go +++ b/pkg/services/ngalert/schedule/registry_test.go @@ -78,7 +78,8 @@ func TestSchedulableAlertRulesRegistry(t *testing.T) { } func TestSchedulableAlertRulesRegistry_set(t *testing.T) { - _, initialRules := models.GenerateUniqueAlertRules(100, models.AlertRuleGen()) + gen := models.RuleGen + initialRules := gen.GenerateManyRef(100) init := make(map[models.AlertRuleKey]*models.AlertRule, len(initialRules)) for _, rule := range initialRules { init[rule.GetKey()] = rule @@ -95,7 +96,7 @@ func TestSchedulableAlertRulesRegistry_set(t *testing.T) { t.Run("should return empty diff if version does not change", func(t *testing.T) { newRules := make([]*models.AlertRule, 0, len(initialRules)) // generate random and then override rule key + version - _, randomNew := models.GenerateUniqueAlertRules(len(initialRules), models.AlertRuleGen()) + randomNew := gen.GenerateManyRef(len(initialRules)) for i := 0; i < len(initialRules); i++ { rule := randomNew[i] oldRule := initialRules[i] @@ -128,7 +129,7 @@ func TestSchedulableAlertRulesRegistry_set(t *testing.T) { } func TestRuleWithFolderFingerprint(t *testing.T) { - rule := models.AlertRuleGen()() + rule := models.RuleGen.GenerateRef() title := uuid.NewString() f := ruleWithFolder{rule: rule, folderTitle: title}.Fingerprint() t.Run("should calculate a fingerprint", func(t *testing.T) { diff --git a/pkg/services/ngalert/schedule/schedule_unit_test.go b/pkg/services/ngalert/schedule/schedule_unit_test.go index 4808f8312b8..50258cb2247 100644 --- a/pkg/services/ngalert/schedule/schedule_unit_test.go +++ b/pkg/services/ngalert/schedule/schedule_unit_test.go @@ -101,9 +101,9 @@ func TestProcessTicks(t *testing.T) { } tick := time.Time{} - + gen := models.RuleGen // create alert rule under main org with one second interval - alertRule1 := models.AlertRuleGen(models.WithOrgID(mainOrgID), models.WithInterval(cfg.BaseInterval), models.WithTitle("rule-1"))() + alertRule1 := gen.With(gen.WithOrgID(mainOrgID), gen.WithInterval(cfg.BaseInterval), gen.WithTitle("rule-1")).GenerateRef() ruleStore.PutRule(ctx, alertRule1) t.Run("on 1st tick alert rule should be evaluated", func(t *testing.T) { @@ -132,7 +132,7 @@ func TestProcessTicks(t *testing.T) { }) // add alert rule under main org with three base intervals - alertRule2 := models.AlertRuleGen(models.WithOrgID(mainOrgID), models.WithInterval(3*cfg.BaseInterval), models.WithTitle("rule-2"))() + alertRule2 := gen.With(gen.WithOrgID(mainOrgID), gen.WithInterval(3*cfg.BaseInterval), gen.WithTitle("rule-2")).GenerateRef() ruleStore.PutRule(ctx, alertRule2) t.Run("on 2nd tick first alert rule should be evaluated", func(t *testing.T) { @@ -317,7 +317,7 @@ func TestProcessTicks(t *testing.T) { }) // create alert rule with one base interval - alertRule3 := models.AlertRuleGen(models.WithOrgID(mainOrgID), models.WithInterval(cfg.BaseInterval), models.WithTitle("rule-3"))() + alertRule3 := gen.With(gen.WithOrgID(mainOrgID), gen.WithInterval(cfg.BaseInterval), gen.WithTitle("rule-3")).GenerateRef() ruleStore.PutRule(ctx, alertRule3) t.Run("on 10th tick a new alert rule should be evaluated", func(t *testing.T) { @@ -361,7 +361,7 @@ func TestSchedule_deleteAlertRule(t *testing.T) { t.Run("it should stop evaluation loop and remove the controller from registry", func(t *testing.T) { sch := setupScheduler(t, nil, nil, nil, nil, nil) ruleFactory := ruleFactoryFromScheduler(sch) - rule := models.AlertRuleGen()() + rule := models.RuleGen.GenerateRef() key := rule.GetKey() info, _ := sch.registry.getOrCreate(context.Background(), key, ruleFactory) sch.deleteAlertRule(key) diff --git a/pkg/services/ngalert/state/cache_bench_test.go b/pkg/services/ngalert/state/cache_bench_test.go index 1373d1ae713..d8b0cdb04fa 100644 --- a/pkg/services/ngalert/state/cache_bench_test.go +++ b/pkg/services/ngalert/state/cache_bench_test.go @@ -16,7 +16,7 @@ import ( func BenchmarkGetOrCreateTest(b *testing.B) { cache := newCache() - rule := models.AlertRuleGen(func(rule *models.AlertRule) { + rule := models.RuleGen.With(func(rule *models.AlertRule) { for i := 0; i < 2; i++ { rule.Labels = data.Labels{ "label-1": "{{ $value }}", @@ -27,7 +27,7 @@ func BenchmarkGetOrCreateTest(b *testing.B) { "anno-2": "{{ $values.A.Labels.instance }} has value {{ $values.A }}", } } - })() + }).GenerateRef() result := eval.ResultGen(func(r *eval.Result) { r.Values = map[string]eval.NumberValueCapture{ "A": { diff --git a/pkg/services/ngalert/state/cache_test.go b/pkg/services/ngalert/state/cache_test.go index 58a0adbfafd..23292714226 100644 --- a/pkg/services/ngalert/state/cache_test.go +++ b/pkg/services/ngalert/state/cache_test.go @@ -126,7 +126,8 @@ func Test_getOrCreate(t *testing.T) { l := log.New("test") c := newCache() - generateRule := models.AlertRuleGen(models.WithNotEmptyLabels(5, "rule-")) + gen := models.RuleGen + generateRule := gen.With(gen.WithNotEmptyLabels(5, "rule-")).GenerateRef t.Run("should combine all labels", func(t *testing.T) { rule := generateRule() diff --git a/pkg/services/ngalert/state/historian/annotation_test.go b/pkg/services/ngalert/state/historian/annotation_test.go index 0192d321659..da09fdc333b 100644 --- a/pkg/services/ngalert/state/historian/annotation_test.go +++ b/pkg/services/ngalert/state/historian/annotation_test.go @@ -14,6 +14,7 @@ import ( "github.com/stretchr/testify/require" "github.com/grafana/grafana-plugin-sdk-go/data" + "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/services/annotations" @@ -128,7 +129,7 @@ func createTestAnnotationSutWithStore(t *testing.T, annotations AnnotationStore) met := metrics.NewHistorianMetrics(prometheus.NewRegistry(), metrics.Subsystem) rules := fakes.NewRuleStore(t) rules.Rules[1] = []*models.AlertRule{ - models.AlertRuleGen(withOrgID(1), withUID("my-rule"))(), + models.RuleGen.With(models.RuleMuts.WithOrgID(1), withUID("my-rule")).GenerateRef(), } return NewAnnotationBackend(annotations, rules, met) } @@ -138,7 +139,7 @@ func createTestAnnotationBackendSutWithMetrics(t *testing.T, met *metrics.Histor fakeAnnoRepo := annotationstest.NewFakeAnnotationsRepo() rules := fakes.NewRuleStore(t) rules.Rules[1] = []*models.AlertRule{ - models.AlertRuleGen(withOrgID(1), withUID("my-rule"))(), + models.RuleGen.With(models.RuleMuts.WithOrgID(1), withUID("my-rule")).GenerateRef(), } dbs := &dashboards.FakeDashboardService{} dbs.On("GetDashboard", mock.Anything, mock.Anything).Return(&dashboards.Dashboard{}, nil) @@ -150,7 +151,7 @@ func createFailingAnnotationSut(t *testing.T, met *metrics.Historian) *Annotatio fakeAnnoRepo := &failingAnnotationRepo{} rules := fakes.NewRuleStore(t) rules.Rules[1] = []*models.AlertRule{ - models.AlertRuleGen(withOrgID(1), withUID("my-rule"))(), + models.RuleGen.With(models.RuleMuts.WithOrgID(1), withUID("my-rule")).GenerateRef(), } dbs := &dashboards.FakeDashboardService{} dbs.On("GetDashboard", mock.Anything, mock.Anything).Return(&dashboards.Dashboard{}, nil) @@ -169,12 +170,6 @@ func createAnnotation() annotations.Item { } } -func withOrgID(orgId int64) func(rule *models.AlertRule) { - return func(rule *models.AlertRule) { - rule.OrgID = orgId - } -} - func TestBuildAnnotations(t *testing.T) { t.Run("data wraps nil values when values are nil", func(t *testing.T) { logger := log.NewNopLogger() @@ -230,7 +225,7 @@ func makeStateTransition() state.StateTransition { } } -func withUID(uid string) func(rule *models.AlertRule) { +func withUID(uid string) models.AlertRuleMutator { return func(rule *models.AlertRule) { rule.UID = uid } diff --git a/pkg/services/ngalert/state/historian/loki.go b/pkg/services/ngalert/state/historian/loki.go index 1c4c8b071bb..72add63518d 100644 --- a/pkg/services/ngalert/state/historian/loki.go +++ b/pkg/services/ngalert/state/historian/loki.go @@ -55,8 +55,7 @@ type RemoteLokiBackend struct { log log.Logger } -func NewRemoteLokiBackend(cfg LokiConfig, req client.Requester, metrics *metrics.Historian) *RemoteLokiBackend { - logger := log.New("ngalert.state.historian", "backend", "loki") +func NewRemoteLokiBackend(logger log.Logger, cfg LokiConfig, req client.Requester, metrics *metrics.Historian) *RemoteLokiBackend { return &RemoteLokiBackend{ client: NewLokiClient(cfg, req, metrics, logger), externalLabels: cfg.ExternalLabels, diff --git a/pkg/services/ngalert/state/historian/loki_test.go b/pkg/services/ngalert/state/historian/loki_test.go index caa0db0aa4d..dbdefd0c51c 100644 --- a/pkg/services/ngalert/state/historian/loki_test.go +++ b/pkg/services/ngalert/state/historian/loki_test.go @@ -513,7 +513,8 @@ func createTestLokiBackend(req client.Requester, met *metrics.Historian) *Remote Encoder: JsonEncoder{}, ExternalLabels: map[string]string{"externalLabelKey": "externalLabelValue"}, } - return NewRemoteLokiBackend(cfg, req, met) + lokiBackendLogger := log.New("ngalert.state.historian", "backend", "loki") + return NewRemoteLokiBackend(lokiBackendLogger, cfg, req, met) } func singleFromNormal(st *state.State) []state.StateTransition { diff --git a/pkg/services/ngalert/state/manager_private_test.go b/pkg/services/ngalert/state/manager_private_test.go index 798b37a0720..39c66375219 100644 --- a/pkg/services/ngalert/state/manager_private_test.go +++ b/pkg/services/ngalert/state/manager_private_test.go @@ -146,11 +146,7 @@ func TestProcessEvalResults_StateTransitions(t *testing.T) { } baseRuleWith := func(mutators ...ngmodels.AlertRuleMutator) *ngmodels.AlertRule { - r := ngmodels.CopyRule(baseRule) - for _, mutator := range mutators { - mutator(r) - } - return r + return ngmodels.CopyRule(baseRule, mutators...) } newEvaluation := func(evalTime time.Time, evalState eval.State) Evaluation { @@ -397,7 +393,7 @@ func TestProcessEvalResults_StateTransitions(t *testing.T) { }, { desc: "t1[1:alerting,2:normal] and 'for'>0 at t1", - alertRule: baseRuleWith(ngmodels.WithForNTimes(3)), + alertRule: baseRuleWith(ngmodels.RuleMuts.WithForNTimes(3)), results: map[time.Time]eval.Results{ t1: { newResult(eval.WithState(eval.Alerting), eval.WithLabels(labels1)), @@ -483,7 +479,7 @@ func TestProcessEvalResults_StateTransitions(t *testing.T) { }, { desc: "t1[1:alerting] t2[1:alerting] t3[1:alerting] and 'for'=2 at t1,t2,t3", - alertRule: baseRuleWith(ngmodels.WithForNTimes(2)), + alertRule: baseRuleWith(ngmodels.RuleMuts.WithForNTimes(2)), results: map[time.Time]eval.Results{ t1: { newResult(eval.WithState(eval.Alerting), eval.WithLabels(labels1)), @@ -547,7 +543,7 @@ func TestProcessEvalResults_StateTransitions(t *testing.T) { }, { desc: "t1[1:alerting], t2[1:normal] and 'for'=2 at t2", - alertRule: baseRuleWith(ngmodels.WithForNTimes(2)), + alertRule: baseRuleWith(ngmodels.RuleMuts.WithForNTimes(2)), results: map[time.Time]eval.Results{ t1: { newResult(eval.WithState(eval.Alerting), eval.WithLabels(labels1)), @@ -740,7 +736,7 @@ func TestProcessEvalResults_StateTransitions(t *testing.T) { }, { desc: "t1[{}:alerting] and 'for'>0 at t1", - alertRule: baseRuleWith(ngmodels.WithForNTimes(3)), + alertRule: baseRuleWith(ngmodels.RuleMuts.WithForNTimes(3)), results: map[time.Time]eval.Results{ t1: { newResult(eval.WithState(eval.Alerting)), @@ -809,10 +805,10 @@ func TestProcessEvalResults_StateTransitions(t *testing.T) { t.Run("no-data", func(t *testing.T) { rules := map[ngmodels.NoDataState]*ngmodels.AlertRule{ - ngmodels.NoData: baseRuleWith(ngmodels.WithNoDataExecAs(ngmodels.NoData)), - ngmodels.Alerting: baseRuleWith(ngmodels.WithNoDataExecAs(ngmodels.Alerting)), - ngmodels.OK: baseRuleWith(ngmodels.WithNoDataExecAs(ngmodels.OK)), - ngmodels.KeepLast: baseRuleWith(ngmodels.WithNoDataExecAs(ngmodels.KeepLast)), + ngmodels.NoData: baseRuleWith(ngmodels.RuleMuts.WithNoDataExecAs(ngmodels.NoData)), + ngmodels.Alerting: baseRuleWith(ngmodels.RuleMuts.WithNoDataExecAs(ngmodels.Alerting)), + ngmodels.OK: baseRuleWith(ngmodels.RuleMuts.WithNoDataExecAs(ngmodels.OK)), + ngmodels.KeepLast: baseRuleWith(ngmodels.RuleMuts.WithNoDataExecAs(ngmodels.KeepLast)), } type noDataTestCase struct { @@ -829,10 +825,7 @@ func TestProcessEvalResults_StateTransitions(t *testing.T) { for stateExec, rule := range rules { r := rule if len(tc.ruleMutators) > 0 { - r = ngmodels.CopyRule(r) - for _, mutateRule := range tc.ruleMutators { - mutateRule(r) - } + r = ngmodels.CopyRule(r, tc.ruleMutators...) } t.Run(fmt.Sprintf("execute as %s", stateExec), func(t *testing.T) { expectedTransitions, ok := tc.expectedTransitionsApplyNoDataErrorToAllStates[stateExec] @@ -1524,7 +1517,7 @@ func TestProcessEvalResults_StateTransitions(t *testing.T) { }, { desc: "t1[1:normal,2:alerting] t2[NoData] t3[NoData] and 'for'=1 at t2*,t3", - ruleMutators: []ngmodels.AlertRuleMutator{ngmodels.WithForNTimes(1)}, + ruleMutators: []ngmodels.AlertRuleMutator{ngmodels.RuleMuts.WithForNTimes(1)}, results: map[time.Time]eval.Results{ t1: { newResult(eval.WithState(eval.Normal), eval.WithLabels(labels1)), @@ -1953,7 +1946,7 @@ func TestProcessEvalResults_StateTransitions(t *testing.T) { }, { desc: "t1[1:alerting] t2[NoData] t3[1:alerting] and 'for'=2 at t3", - ruleMutators: []ngmodels.AlertRuleMutator{ngmodels.WithForNTimes(2)}, + ruleMutators: []ngmodels.AlertRuleMutator{ngmodels.RuleMuts.WithForNTimes(2)}, results: map[time.Time]eval.Results{ t1: { newResult(eval.WithState(eval.Alerting), eval.WithLabels(labels1)), @@ -2678,7 +2671,7 @@ func TestProcessEvalResults_StateTransitions(t *testing.T) { }, { desc: "t1[{}:alerting] t2[NoData] t3[{}:alerting] and 'for'=2 at t2*,t3", - ruleMutators: []ngmodels.AlertRuleMutator{ngmodels.WithForNTimes(2)}, + ruleMutators: []ngmodels.AlertRuleMutator{ngmodels.RuleMuts.WithForNTimes(2)}, results: map[time.Time]eval.Results{ t1: { newResult(eval.WithState(eval.Alerting)), @@ -2850,10 +2843,10 @@ func TestProcessEvalResults_StateTransitions(t *testing.T) { t.Run("error", func(t *testing.T) { rules := map[ngmodels.ExecutionErrorState]*ngmodels.AlertRule{ - ngmodels.ErrorErrState: baseRuleWith(ngmodels.WithErrorExecAs(ngmodels.ErrorErrState)), - ngmodels.AlertingErrState: baseRuleWith(ngmodels.WithErrorExecAs(ngmodels.AlertingErrState)), - ngmodels.OkErrState: baseRuleWith(ngmodels.WithErrorExecAs(ngmodels.OkErrState)), - ngmodels.KeepLastErrState: baseRuleWith(ngmodels.WithErrorExecAs(ngmodels.KeepLastErrState)), + ngmodels.ErrorErrState: baseRuleWith(ngmodels.RuleMuts.WithErrorExecAs(ngmodels.ErrorErrState)), + ngmodels.AlertingErrState: baseRuleWith(ngmodels.RuleMuts.WithErrorExecAs(ngmodels.AlertingErrState)), + ngmodels.OkErrState: baseRuleWith(ngmodels.RuleMuts.WithErrorExecAs(ngmodels.OkErrState)), + ngmodels.KeepLastErrState: baseRuleWith(ngmodels.RuleMuts.WithErrorExecAs(ngmodels.KeepLastErrState)), } cacheID := func(lbls data.Labels) string { @@ -2879,10 +2872,7 @@ func TestProcessEvalResults_StateTransitions(t *testing.T) { for stateExec, rule := range rules { r := rule if len(tc.ruleMutators) > 0 { - r = ngmodels.CopyRule(r) - for _, mutateRule := range tc.ruleMutators { - mutateRule(r) - } + r = ngmodels.CopyRule(r, tc.ruleMutators...) } t.Run(fmt.Sprintf("execute as %s", stateExec), func(t *testing.T) { expectedTransitions, ok := tc.expectedTransitionsApplyNoDataErrorToAllStates[stateExec] @@ -3084,7 +3074,7 @@ func TestProcessEvalResults_StateTransitions(t *testing.T) { }, { desc: "t1[1:alerting] t2[QueryError] and 'for'=1 at t2", - ruleMutators: []ngmodels.AlertRuleMutator{ngmodels.WithForNTimes(1)}, + ruleMutators: []ngmodels.AlertRuleMutator{ngmodels.RuleMuts.WithForNTimes(1)}, results: map[time.Time]eval.Results{ t1: { newResult(eval.WithState(eval.Alerting), eval.WithLabels(labels1)), @@ -3630,7 +3620,7 @@ func TestProcessEvalResults_StateTransitions(t *testing.T) { }, { desc: "t1[{}:alerting] t2[QueryError] and 'for'=1 at t1*,t2", - ruleMutators: []ngmodels.AlertRuleMutator{ngmodels.WithForNTimes(1)}, + ruleMutators: []ngmodels.AlertRuleMutator{ngmodels.RuleMuts.WithForNTimes(1)}, results: map[time.Time]eval.Results{ t1: { newResult(eval.WithState(eval.Alerting)), @@ -3736,7 +3726,7 @@ func TestProcessEvalResults_StateTransitions(t *testing.T) { }, { desc: "t1[{}:alerting] t2[QueryError] t3[{}:alerting] and 'for'=2 at t2,t3", - ruleMutators: []ngmodels.AlertRuleMutator{ngmodels.WithForNTimes(2)}, + ruleMutators: []ngmodels.AlertRuleMutator{ngmodels.RuleMuts.WithForNTimes(2)}, results: map[time.Time]eval.Results{ t1: { newResult(eval.WithState(eval.Alerting)), diff --git a/pkg/services/ngalert/state/manager_test.go b/pkg/services/ngalert/state/manager_test.go index 38a7519f846..dbe6b697a15 100644 --- a/pkg/services/ngalert/state/manager_test.go +++ b/pkg/services/ngalert/state/manager_test.go @@ -311,7 +311,7 @@ func TestProcessEvalResults(t *testing.T) { t2 := tn(2) t3 := tn(3) - + m := models.RuleMuts baseRule := &models.AlertRule{ OrgID: 1, Title: "test_title", @@ -340,10 +340,7 @@ func TestProcessEvalResults(t *testing.T) { } baseRuleWith := func(mutators ...models.AlertRuleMutator) *models.AlertRule { - r := models.CopyRule(baseRule) - for _, mutator := range mutators { - mutator(r) - } + r := models.CopyRule(baseRule, mutators...) return r } @@ -500,7 +497,7 @@ func TestProcessEvalResults(t *testing.T) { }, { desc: "normal -> alerting when For is set", - alertRule: baseRuleWith(models.WithForNTimes(2)), + alertRule: baseRuleWith(m.WithForNTimes(2)), evalResults: map[time.Time]eval.Results{ t1: { newResult(eval.WithState(eval.Normal), eval.WithLabels(labels1)), @@ -533,7 +530,7 @@ func TestProcessEvalResults(t *testing.T) { }, { desc: "normal -> alerting -> noData -> alerting when For is set", - alertRule: baseRuleWith(models.WithForNTimes(2)), + alertRule: baseRuleWith(m.WithForNTimes(2)), evalResults: map[time.Time]eval.Results{ t1: { newResult(eval.WithState(eval.Normal), eval.WithLabels(labels1)), @@ -569,7 +566,7 @@ func TestProcessEvalResults(t *testing.T) { }, { desc: "pending -> alerting -> noData when For is set and NoDataState is NoData", - alertRule: baseRuleWith(models.WithForNTimes(2)), + alertRule: baseRuleWith(m.WithForNTimes(2)), evalResults: map[time.Time]eval.Results{ t1: { newResult(eval.WithState(eval.Alerting), eval.WithLabels(labels1)), @@ -602,7 +599,7 @@ func TestProcessEvalResults(t *testing.T) { }, { desc: "normal -> pending when For is set but not exceeded and first result is normal", - alertRule: baseRuleWith(models.WithForNTimes(2)), + alertRule: baseRuleWith(m.WithForNTimes(2)), evalResults: map[time.Time]eval.Results{ t1: { newResult(eval.WithState(eval.Normal), eval.WithLabels(labels1)), @@ -630,7 +627,7 @@ func TestProcessEvalResults(t *testing.T) { }, { desc: "normal -> pending when For is set but not exceeded and first result is alerting", - alertRule: baseRuleWith(models.WithForNTimes(6)), + alertRule: baseRuleWith(m.WithForNTimes(6)), evalResults: map[time.Time]eval.Results{ t1: { newResult(eval.WithState(eval.Alerting), eval.WithLabels(labels1)), @@ -657,7 +654,7 @@ func TestProcessEvalResults(t *testing.T) { }, { desc: "normal -> pending when For is set but not exceeded, result is NoData and NoDataState is alerting", - alertRule: baseRuleWith(models.WithForNTimes(6), models.WithNoDataExecAs(models.Alerting)), + alertRule: baseRuleWith(m.WithForNTimes(6), m.WithNoDataExecAs(models.Alerting)), evalResults: map[time.Time]eval.Results{ t1: { newResult(eval.WithState(eval.Normal), eval.WithLabels(labels1)), @@ -685,7 +682,7 @@ func TestProcessEvalResults(t *testing.T) { }, { desc: "normal -> alerting when For is exceeded, result is NoData and NoDataState is alerting", - alertRule: baseRuleWith(models.WithForNTimes(3), models.WithNoDataExecAs(models.Alerting)), + alertRule: baseRuleWith(m.WithForNTimes(3), m.WithNoDataExecAs(models.Alerting)), evalResults: map[time.Time]eval.Results{ t1: { newResult(eval.WithState(eval.Normal), eval.WithLabels(labels1)), @@ -877,7 +874,7 @@ func TestProcessEvalResults(t *testing.T) { }, { desc: "normal -> normal (NoData, KeepLastState) -> alerting -> alerting (NoData, KeepLastState) - keeps last state when result is NoData and NoDataState is KeepLast", - alertRule: baseRuleWith(models.WithForNTimes(0), models.WithNoDataExecAs(models.KeepLast)), + alertRule: baseRuleWith(m.WithForNTimes(0), m.WithNoDataExecAs(models.KeepLast)), evalResults: map[time.Time]eval.Results{ t1: { newResult(eval.WithState(eval.Normal), eval.WithLabels(labels1)), @@ -913,7 +910,7 @@ func TestProcessEvalResults(t *testing.T) { }, { desc: "normal -> pending -> pending (NoData, KeepLastState) -> alerting (NoData, KeepLastState) - keep last state respects For when result is NoData", - alertRule: baseRuleWith(models.WithForNTimes(2), models.WithNoDataExecAs(models.KeepLast)), + alertRule: baseRuleWith(m.WithForNTimes(2), m.WithNoDataExecAs(models.KeepLast)), evalResults: map[time.Time]eval.Results{ t1: { newResult(eval.WithState(eval.Normal), eval.WithLabels(labels1)), @@ -947,7 +944,7 @@ func TestProcessEvalResults(t *testing.T) { }, { desc: "normal -> normal when result is NoData and NoDataState is ok", - alertRule: baseRuleWith(models.WithNoDataExecAs(models.OK)), + alertRule: baseRuleWith(m.WithNoDataExecAs(models.OK)), evalResults: map[time.Time]eval.Results{ t1: { newResult(eval.WithState(eval.Normal), eval.WithLabels(labels1)), @@ -975,7 +972,7 @@ func TestProcessEvalResults(t *testing.T) { }, { desc: "normal -> pending when For is set but not exceeded, result is Error and ExecErrState is Alerting", - alertRule: baseRuleWith(models.WithForNTimes(6), models.WithErrorExecAs(models.AlertingErrState)), + alertRule: baseRuleWith(m.WithForNTimes(6), m.WithErrorExecAs(models.AlertingErrState)), evalResults: map[time.Time]eval.Results{ t1: { newResult(eval.WithState(eval.Normal), eval.WithLabels(labels1)), @@ -1004,7 +1001,7 @@ func TestProcessEvalResults(t *testing.T) { }, { desc: "normal -> alerting when For is exceeded, result is Error and ExecErrState is Alerting", - alertRule: baseRuleWith(models.WithForNTimes(3), models.WithErrorExecAs(models.AlertingErrState)), + alertRule: baseRuleWith(m.WithForNTimes(3), m.WithErrorExecAs(models.AlertingErrState)), evalResults: map[time.Time]eval.Results{ t1: { newResult(eval.WithState(eval.Normal), eval.WithLabels(labels1)), @@ -1043,7 +1040,7 @@ func TestProcessEvalResults(t *testing.T) { }, { desc: "normal -> error when result is Error and ExecErrState is Error", - alertRule: baseRuleWith(models.WithForNTimes(6), models.WithErrorExecAs(models.ErrorErrState)), + alertRule: baseRuleWith(m.WithForNTimes(6), m.WithErrorExecAs(models.ErrorErrState)), evalResults: map[time.Time]eval.Results{ t1: { newResult(eval.WithState(eval.Normal), eval.WithLabels(labels1)), @@ -1084,7 +1081,7 @@ func TestProcessEvalResults(t *testing.T) { }, { desc: "normal -> normal (Error, KeepLastState) -> alerting -> alerting (Error, KeepLastState) - keeps last state when result is Error and ExecErrState is KeepLast", - alertRule: baseRuleWith(models.WithForNTimes(0), models.WithErrorExecAs(models.KeepLastErrState)), + alertRule: baseRuleWith(m.WithForNTimes(0), m.WithErrorExecAs(models.KeepLastErrState)), evalResults: map[time.Time]eval.Results{ t1: { newResult(eval.WithState(eval.Normal), eval.WithLabels(labels1)), @@ -1120,7 +1117,7 @@ func TestProcessEvalResults(t *testing.T) { }, { desc: "normal -> pending -> pending (Error, KeepLastState) -> alerting (Error, KeepLastState) - keep last state respects For when result is Error", - alertRule: baseRuleWith(models.WithForNTimes(2), models.WithErrorExecAs(models.KeepLastErrState)), + alertRule: baseRuleWith(m.WithForNTimes(2), m.WithErrorExecAs(models.KeepLastErrState)), evalResults: map[time.Time]eval.Results{ t1: { newResult(eval.WithState(eval.Normal), eval.WithLabels(labels1)), @@ -1154,7 +1151,7 @@ func TestProcessEvalResults(t *testing.T) { }, { desc: "normal -> normal when result is Error and ExecErrState is OK", - alertRule: baseRuleWith(models.WithForNTimes(6), models.WithErrorExecAs(models.OkErrState)), + alertRule: baseRuleWith(m.WithForNTimes(6), m.WithErrorExecAs(models.OkErrState)), evalResults: map[time.Time]eval.Results{ t1: { newResult(eval.WithState(eval.Normal), eval.WithLabels(labels1)), @@ -1182,7 +1179,7 @@ func TestProcessEvalResults(t *testing.T) { }, { desc: "alerting -> normal when result is Error and ExecErrState is OK", - alertRule: baseRuleWith(models.WithForNTimes(6), models.WithErrorExecAs(models.OkErrState)), + alertRule: baseRuleWith(m.WithForNTimes(6), m.WithErrorExecAs(models.OkErrState)), evalResults: map[time.Time]eval.Results{ t1: { newResult(eval.WithState(eval.Alerting), eval.WithLabels(labels1)), @@ -1210,7 +1207,7 @@ func TestProcessEvalResults(t *testing.T) { }, { desc: "normal -> alerting -> error when result is Error and ExecErrorState is Error", - alertRule: baseRuleWith(models.WithForNTimes(2)), + alertRule: baseRuleWith(m.WithForNTimes(2)), evalResults: map[time.Time]eval.Results{ t1: { newResult(eval.WithState(eval.Alerting), eval.WithLabels(labels1)), @@ -1250,7 +1247,7 @@ func TestProcessEvalResults(t *testing.T) { }, { desc: "normal -> alerting -> error -> alerting - it should clear the error", - alertRule: baseRuleWith(models.WithForNTimes(3)), + alertRule: baseRuleWith(m.WithForNTimes(3)), evalResults: map[time.Time]eval.Results{ t1: { newResult(eval.WithState(eval.Normal), eval.WithLabels(labels1)), @@ -1284,7 +1281,7 @@ func TestProcessEvalResults(t *testing.T) { }, { desc: "normal -> alerting -> error -> no data - it should clear the error", - alertRule: baseRuleWith(models.WithForNTimes(3)), + alertRule: baseRuleWith(m.WithForNTimes(3)), evalResults: map[time.Time]eval.Results{ t1: { newResult(eval.WithState(eval.Normal), eval.WithLabels(labels1)), @@ -1319,8 +1316,8 @@ func TestProcessEvalResults(t *testing.T) { { desc: "template is correctly expanded", alertRule: baseRuleWith( - models.WithAnnotations(map[string]string{"summary": "{{$labels.pod}} is down in {{$labels.cluster}} cluster -> {{$labels.namespace}} namespace"}), - models.WithLabels(map[string]string{"label": "test", "job": "{{$labels.namespace}}/{{$labels.pod}}"}), + m.WithAnnotations(map[string]string{"summary": "{{$labels.pod}} is down in {{$labels.cluster}} cluster -> {{$labels.namespace}} namespace"}), + m.WithLabels(map[string]string{"label": "test", "job": "{{$labels.namespace}}/{{$labels.pod}}"}), ), evalResults: map[time.Time]eval.Results{ t1: { @@ -1359,7 +1356,7 @@ func TestProcessEvalResults(t *testing.T) { }, { desc: "classic condition, execution Error as Error (alerting -> query error -> alerting)", - alertRule: baseRuleWith(models.WithErrorExecAs(models.ErrorErrState)), + alertRule: baseRuleWith(m.WithErrorExecAs(models.ErrorErrState)), expectedAnnotations: 3, evalResults: map[time.Time]eval.Results{ t1: { @@ -1512,7 +1509,7 @@ func TestProcessEvalResults(t *testing.T) { } statePersister := state.NewSyncStatePersisiter(log.New("ngalert.state.manager.persist"), cfg) st := state.NewManager(cfg, statePersister) - rule := models.AlertRuleGen()() + rule := models.RuleGen.GenerateRef() var results = eval.GenerateResults(rand.Intn(4)+1, eval.ResultGen(eval.WithEvaluatedAt(clk.Now()))) states := st.ProcessEvalResults(context.Background(), clk.Now(), rule, results, make(data.Labels)) @@ -1747,7 +1744,8 @@ func TestStaleResults(t *testing.T) { } st := state.NewManager(cfg, state.NewNoopPersister()) - rule := models.AlertRuleGen(models.WithFor(0))() + gen := models.RuleGen + rule := gen.With(gen.WithFor(0)).GenerateRef() initResults := eval.Results{ eval.ResultGen(eval.WithEvaluatedAt(clk.Now()))(), diff --git a/pkg/services/ngalert/state/state_test.go b/pkg/services/ngalert/state/state_test.go index 1684b7036c1..7bb69b7d43f 100644 --- a/pkg/services/ngalert/state/state_test.go +++ b/pkg/services/ngalert/state/state_test.go @@ -706,8 +706,7 @@ func TestParseFormattedState(t *testing.T) { func TestGetRuleExtraLabels(t *testing.T) { logger := log.New() - rule := ngmodels.AlertRuleGen()() - rule.NotificationSettings = nil + rule := ngmodels.RuleGen.With(ngmodels.RuleMuts.WithNoNotificationSettings()).GenerateRef() folderTitle := uuid.NewString() ns := ngmodels.NotificationSettings{ diff --git a/pkg/services/ngalert/store/alert_rule.go b/pkg/services/ngalert/store/alert_rule.go index 2570657ea01..0ae77f063d5 100644 --- a/pkg/services/ngalert/store/alert_rule.go +++ b/pkg/services/ngalert/store/alert_rule.go @@ -169,7 +169,7 @@ func (st DBstore) InsertAlertRules(ctx context.Context, rules []ngmodels.AlertRu for i := range newRules { if _, err := sess.Insert(&newRules[i]); err != nil { if st.SQLStore.GetDialect().IsUniqueConstraintViolation(err) { - return ngmodels.ErrAlertRuleConflict(newRules[i], ngmodels.ErrAlertRuleUniqueConstraintViolation) + return ruleConstraintViolationToErr(newRules[i], err) } return fmt.Errorf("failed to create new rules: %w", err) } @@ -215,7 +215,7 @@ func (st DBstore) UpdateAlertRules(ctx context.Context, rules []ngmodels.UpdateR if updated, err := sess.ID(r.Existing.ID).AllCols().Update(r.New); err != nil || updated == 0 { if err != nil { if st.SQLStore.GetDialect().IsUniqueConstraintViolation(err) { - return ngmodels.ErrAlertRuleConflict(r.New, ngmodels.ErrAlertRuleUniqueConstraintViolation) + return ruleConstraintViolationToErr(r.New, err) } return fmt.Errorf("failed to update rule [%s] %s: %w", r.New.UID, r.New.Title, err) } @@ -758,3 +758,14 @@ func (st DBstore) RenameReceiverInNotificationSettings(ctx context.Context, orgI } return len(updates), st.UpdateAlertRules(ctx, updates) } + +func ruleConstraintViolationToErr(rule ngmodels.AlertRule, err error) error { + msg := err.Error() + if strings.Contains(msg, "UQE_alert_rule_org_id_namespace_uid_title") || strings.Contains(msg, "alert_rule.org_id, alert_rule.namespace_uid, alert_rule.title") { + return ngmodels.ErrAlertRuleConflict(rule, ngmodels.ErrAlertRuleUniqueConstraintViolation) + } else if strings.Contains(msg, "UQE_alert_rule_org_id_uid") || strings.Contains(msg, "alert_rule.org_id, alert_rule.uid") { + return ngmodels.ErrAlertRuleConflict(rule, errors.New("rule UID under the same organisation should be unique")) + } else { + return ngmodels.ErrAlertRuleConflict(rule, err) + } +} diff --git a/pkg/services/ngalert/store/alert_rule_test.go b/pkg/services/ngalert/store/alert_rule_test.go index 97aa3433d3e..4958c69b5f3 100644 --- a/pkg/services/ngalert/store/alert_rule_test.go +++ b/pkg/services/ngalert/store/alert_rule_test.go @@ -5,7 +5,6 @@ import ( "errors" "fmt" "strings" - "sync" "testing" "time" @@ -49,10 +48,11 @@ func TestIntegrationUpdateAlertRules(t *testing.T) { FolderService: setupFolderService(t, sqlStore, cfg, featuremgmt.WithFeatures()), Logger: &logtest.Fake{}, } - generator := models.AlertRuleGen(withIntervalMatching(store.Cfg.BaseInterval), models.WithUniqueID()) + gen := models.RuleGen + gen = gen.With(gen.WithIntervalMatching(store.Cfg.BaseInterval)) t.Run("should increase version", func(t *testing.T) { - rule := createRule(t, store, generator) + rule := createRule(t, store, gen) newRule := models.CopyRule(rule) newRule.Title = util.GenerateShortUID() err := store.UpdateAlertRules(context.Background(), []models.UpdateRule{{ @@ -74,7 +74,7 @@ func TestIntegrationUpdateAlertRules(t *testing.T) { }) t.Run("should fail due to optimistic locking if version does not match", func(t *testing.T) { - rule := createRule(t, store, generator) + rule := createRule(t, store, gen) rule.Version-- // simulate version discrepancy newRule := models.CopyRule(rule) @@ -104,13 +104,14 @@ func TestIntegrationUpdateAlertRulesWithUniqueConstraintViolation(t *testing.T) Logger: &logtest.Fake{}, } - idMutator := models.WithUniqueID() + gen := models.RuleGen createRuleInFolder := func(title string, orgID int64, namespaceUID string) *models.AlertRule { - generator := models.AlertRuleGen(withIntervalMatching(store.Cfg.BaseInterval), idMutator, models.WithNamespace(&folder.Folder{ - UID: namespaceUID, - Title: namespaceUID, - }), withOrgID(orgID), models.WithTitle(title)) - return createRule(t, store, generator) + gen := gen.With( + gen.WithOrgID(orgID), + gen.WithIntervalMatching(store.Cfg.BaseInterval), + gen.WithNamespaceUID(namespaceUID), + ) + return createRule(t, store, gen) } t.Run("should handle update chains without unique constraint violation", func(t *testing.T) { @@ -360,9 +361,11 @@ func TestIntegration_GetAlertRulesForScheduling(t *testing.T) { FeatureToggles: featuremgmt.WithFeatures(), } - generator := models.AlertRuleGen(withIntervalMatching(store.Cfg.BaseInterval), models.WithUniqueID(), models.WithUniqueOrgID()) - rule1 := createRule(t, store, generator) - rule2 := createRule(t, store, generator) + gen := models.RuleGen + gen = gen.With(gen.WithIntervalMatching(store.Cfg.BaseInterval), gen.WithUniqueOrgID()) + + rule1 := createRule(t, store, gen) + rule2 := createRule(t, store, gen) parentFolderUid := uuid.NewString() parentFolderTitle := "Very Parent Folder" @@ -372,7 +375,7 @@ func TestIntegration_GetAlertRulesForScheduling(t *testing.T) { createFolder(t, store, rule1.NamespaceUID, rule1FolderTitle, rule1.OrgID, parentFolderUid) createFolder(t, store, rule2.NamespaceUID, rule2FolderTitle, rule2.OrgID, "") - createFolder(t, store, rule2.NamespaceUID, "same UID folder", generator().OrgID, "") // create a folder with the same UID but in the different org + createFolder(t, store, rule2.NamespaceUID, "same UID folder", gen.GenerateRef().OrgID, "") // create a folder with the same UID but in the different org tc := []struct { name string @@ -458,13 +461,6 @@ func TestIntegration_GetAlertRulesForScheduling(t *testing.T) { }) } -func withIntervalMatching(baseInterval time.Duration) func(*models.AlertRule) { - return func(rule *models.AlertRule) { - rule.IntervalSeconds = int64(baseInterval.Seconds()) * (rand.Int63n(10) + 1) - rule.For = time.Duration(rule.IntervalSeconds*rand.Int63n(9)+1) * time.Second - } -} - func TestIntegration_CountAlertRules(t *testing.T) { if testing.Short() { t.Skip("skipping integration test") @@ -603,6 +599,7 @@ func TestIntegrationInsertAlertRules(t *testing.T) { t.Skip("skipping integration test") } + orgID := int64(1) sqlStore := db.InitTestDB(t) cfg := setting.NewCfg() cfg.UnifiedAlerting.BaseInterval = 1 * time.Second @@ -613,7 +610,12 @@ func TestIntegrationInsertAlertRules(t *testing.T) { Cfg: cfg.UnifiedAlerting, } - rules := models.GenerateAlertRules(5, models.AlertRuleGen(models.WithOrgID(1), withIntervalMatching(store.Cfg.BaseInterval))) + gen := models.RuleGen + rules := gen.With( + gen.WithOrgID(orgID), + gen.WithIntervalMatching(store.Cfg.BaseInterval), + ).GenerateManyRef(5) + deref := make([]models.AlertRule, 0, len(rules)) for _, rule := range rules { deref = append(deref, *rule) @@ -641,14 +643,30 @@ func TestIntegrationInsertAlertRules(t *testing.T) { require.Truef(t, found, "Rule with key %#v was not found in database", keyWithID) } - _, err = store.InsertAlertRules(context.Background(), []models.AlertRule{deref[0]}) - require.ErrorIs(t, err, models.ErrAlertRuleUniqueConstraintViolation) - require.NotEqual(t, deref[0].UID, "") - require.NotEqual(t, deref[0].Title, "") - require.NotEqual(t, deref[0].NamespaceUID, "") - require.ErrorContains(t, err, deref[0].UID) - require.ErrorContains(t, err, deref[0].Title) - require.ErrorContains(t, err, deref[0].NamespaceUID) + t.Run("fail to insert rules with same ID", func(t *testing.T) { + _, err = store.InsertAlertRules(context.Background(), []models.AlertRule{deref[0]}) + require.ErrorIs(t, err, models.ErrAlertRuleConflictBase) + }) + t.Run("fail insert rules with the same title in a folder", func(t *testing.T) { + cp := models.CopyRule(&deref[0]) + cp.UID = cp.UID + "-new" + _, err = store.InsertAlertRules(context.Background(), []models.AlertRule{*cp}) + require.ErrorIs(t, err, models.ErrAlertRuleConflictBase) + require.ErrorIs(t, err, models.ErrAlertRuleUniqueConstraintViolation) + require.NotEqual(t, deref[0].UID, "") + require.NotEqual(t, deref[0].Title, "") + require.NotEqual(t, deref[0].NamespaceUID, "") + require.ErrorContains(t, err, deref[0].UID) + require.ErrorContains(t, err, deref[0].Title) + require.ErrorContains(t, err, deref[0].NamespaceUID) + }) + t.Run("should not let insert rules with the same UID", func(t *testing.T) { + cp := models.CopyRule(&deref[0]) + cp.Title = "unique-test-title" + _, err = store.InsertAlertRules(context.Background(), []models.AlertRule{*cp}) + require.ErrorIs(t, err, models.ErrAlertRuleConflictBase) + require.ErrorContains(t, err, "rule UID under the same organisation should be unique") + }) } func TestIntegrationAlertRulesNotificationSettings(t *testing.T) { @@ -666,21 +684,14 @@ func TestIntegrationAlertRulesNotificationSettings(t *testing.T) { Cfg: cfg.UnifiedAlerting, } - uniqueUids := &sync.Map{} receiverName := "receiver\"-" + uuid.NewString() - rules := models.GenerateAlertRules(3, models.AlertRuleGen(models.WithOrgID(1), withIntervalMatching(store.Cfg.BaseInterval), models.WithUniqueUID(uniqueUids))) - receiveRules := models.GenerateAlertRules(3, - models.AlertRuleGen( - models.WithOrgID(1), - withIntervalMatching(store.Cfg.BaseInterval), - models.WithUniqueUID(uniqueUids), - models.WithNotificationSettingsGen(models.NotificationSettingsGen(models.NSMuts.WithReceiver(receiverName))))) - noise := models.GenerateAlertRules(3, - models.AlertRuleGen( - models.WithOrgID(1), - withIntervalMatching(store.Cfg.BaseInterval), - models.WithUniqueUID(uniqueUids), - models.WithNotificationSettingsGen(models.NotificationSettingsGen(models.NSMuts.WithMuteTimeIntervals(receiverName))))) // simulate collision of names of receiver and mute timing + + gen := models.RuleGen + gen = gen.With(gen.WithOrgID(1), gen.WithIntervalMatching(store.Cfg.BaseInterval)) + rules := gen.GenerateManyRef(3) + receiveRules := gen.With(gen.WithNotificationSettingsGen(models.NotificationSettingsGen(models.NSMuts.WithReceiver(receiverName)))).GenerateManyRef(3) + noise := gen.With(gen.WithNotificationSettingsGen(models.NotificationSettingsGen(models.NSMuts.WithMuteTimeIntervals(receiverName)))).GenerateManyRef(3) + deref := make([]models.AlertRule, 0, len(rules)+len(receiveRules)+len(noise)) for _, rule := range append(append(rules, receiveRules...), noise...) { r := *rule @@ -751,36 +762,21 @@ func TestIntegrationListNotificationSettings(t *testing.T) { Cfg: cfg.UnifiedAlerting, } - uids := &sync.Map{} - titles := &sync.Map{} receiverName := `receiver%"-👍'test` - rulesWithNotifications := models.GenerateAlertRules(5, models.AlertRuleGen( - models.WithOrgID(1), - models.WithUniqueUID(uids), - models.WithUniqueTitle(titles), - withIntervalMatching(store.Cfg.BaseInterval), - models.WithNotificationSettingsGen(models.NotificationSettingsGen(models.NSMuts.WithReceiver(receiverName))), - )) - rulesInOtherOrg := models.GenerateAlertRules(5, models.AlertRuleGen( - models.WithOrgID(2), - models.WithUniqueUID(uids), - models.WithUniqueTitle(titles), - withIntervalMatching(store.Cfg.BaseInterval), - models.WithNotificationSettingsGen(models.NotificationSettingsGen()), - )) - rulesWithNoNotifications := models.GenerateAlertRules(5, models.AlertRuleGen( - models.WithOrgID(1), - models.WithUniqueUID(uids), - models.WithUniqueTitle(titles), - withIntervalMatching(store.Cfg.BaseInterval), - models.WithNoNotificationSettings(), - )) - deref := make([]models.AlertRule, 0, len(rulesWithNotifications)+len(rulesWithNoNotifications)+len(rulesInOtherOrg)) - for _, rule := range append(append(rulesWithNotifications, rulesWithNoNotifications...), rulesInOtherOrg...) { - r := *rule - r.ID = 0 - deref = append(deref, r) - } + gen := models.RuleGen + gen = gen.With(gen.WithOrgID(1), gen.WithIntervalMatching(store.Cfg.BaseInterval)) + + rulesWithNotifications := gen.With( + gen.WithNotificationSettingsGen(models.NotificationSettingsGen(models.NSMuts.WithReceiver(receiverName))), + ).GenerateMany(5) + rulesInOtherOrg := gen.With( + gen.WithOrgID(2), + gen.WithNotificationSettingsGen(models.NotificationSettingsGen()), + ).GenerateMany(5) + + rulesWithNoNotifications := gen.With(gen.WithNoNotificationSettings()).GenerateMany(5) + + deref := append(append(rulesWithNotifications, rulesWithNoNotifications...), rulesInOtherOrg...) _, err := store.InsertAlertRules(context.Background(), deref) require.NoError(t, err) @@ -815,12 +811,12 @@ func TestIntegrationListNotificationSettings(t *testing.T) { // createAlertRule creates an alert rule in the database and returns it. // If a generator is not specified, uniqueness of primary key is not guaranteed. -func createRule(t *testing.T, store *DBstore, generate func() *models.AlertRule) *models.AlertRule { +func createRule(t *testing.T, store *DBstore, generator *models.AlertRuleGenerator) *models.AlertRule { t.Helper() - if generate == nil { - generate = models.AlertRuleGen(withIntervalMatching(store.Cfg.BaseInterval)) + if generator == nil { + generator = models.RuleGen.With(models.RuleMuts.WithIntervalMatching(store.Cfg.BaseInterval)) } - rule := generate() + rule := generator.GenerateRef() err := store.SQLStore.WithDbSession(context.Background(), func(sess *db.Session) error { _, err := sess.Table(models.AlertRule{}).InsertOne(rule) if err != nil { diff --git a/pkg/services/ngalert/store/deltas_test.go b/pkg/services/ngalert/store/deltas_test.go index 5a39e4cc787..2ae30ae5ef0 100644 --- a/pkg/services/ngalert/store/deltas_test.go +++ b/pkg/services/ngalert/store/deltas_test.go @@ -19,15 +19,16 @@ import ( func TestCalculateChanges(t *testing.T) { orgId := int64(rand.Int31()) + gen := models.RuleGen t.Run("detects alerts that need to be added", func(t *testing.T) { fakeStore := fakes.NewRuleStore(t) groupKey := models.GenerateGroupKey(orgId) - rules := models.GenerateAlertRules(rand.Intn(5)+1, models.AlertRuleGen(withOrgID(orgId), simulateSubmitted, withoutUID)) + rules := gen.With(gen.WithOrgID(orgId), simulateSubmitted, withoutUID).GenerateMany(1, 5) submitted := make([]*models.AlertRuleWithOptionals, 0, len(rules)) for _, rule := range rules { - submitted = append(submitted, &models.AlertRuleWithOptionals{AlertRule: *rule}) + submitted = append(submitted, &models.AlertRuleWithOptionals{AlertRule: rule}) } changes, err := CalculateChanges(context.Background(), fakeStore, groupKey, submitted) @@ -50,8 +51,8 @@ func TestCalculateChanges(t *testing.T) { t.Run("detects alerts that need to be deleted", func(t *testing.T) { groupKey := models.GenerateGroupKey(orgId) - inDatabaseMap, inDatabase := models.GenerateUniqueAlertRules(rand.Intn(5)+1, models.AlertRuleGen(withGroupKey(groupKey))) - + inDatabase := gen.With(gen.WithGroupKey(groupKey)).GenerateManyRef(1, 5) + inDatabaseMap := groupByUID(t, inDatabase) fakeStore := fakes.NewRuleStore(t) fakeStore.PutRule(context.Background(), inDatabase...) @@ -73,8 +74,11 @@ func TestCalculateChanges(t *testing.T) { t.Run("should detect alerts that needs to be updated", func(t *testing.T) { groupKey := models.GenerateGroupKey(orgId) - inDatabaseMap, inDatabase := models.GenerateUniqueAlertRules(rand.Intn(5)+1, models.AlertRuleGen(withGroupKey(groupKey))) - submittedMap, rules := models.GenerateUniqueAlertRules(len(inDatabase), models.AlertRuleGen(simulateSubmitted, withGroupKey(groupKey), withUIDs(inDatabaseMap))) + inDatabase := gen.With(gen.WithGroupKey(groupKey)).GenerateManyRef(1, 5) + inDatabaseMap := groupByUID(t, inDatabase) + + rules := gen.With(simulateSubmitted, gen.WithGroupKey(groupKey), withUIDs(inDatabaseMap)).GenerateManyRef(len(inDatabase), len(inDatabase)) + submittedMap := groupByUID(t, rules) submitted := make([]*models.AlertRuleWithOptionals, 0, len(rules)) for _, rule := range rules { submitted = append(submitted, &models.AlertRuleWithOptionals{AlertRule: *rule}) @@ -104,7 +108,7 @@ func TestCalculateChanges(t *testing.T) { t.Run("should include only if there are changes ignoring specific fields", func(t *testing.T) { groupKey := models.GenerateGroupKey(orgId) - _, inDatabase := models.GenerateUniqueAlertRules(rand.Intn(5)+1, models.AlertRuleGen(withGroupKey(groupKey))) + inDatabase := gen.With(gen.WithGroupKey(groupKey)).GenerateManyRef(1, 5) submitted := make([]*models.AlertRuleWithOptionals, 0, len(inDatabase)) for _, rule := range inDatabase { @@ -132,7 +136,7 @@ func TestCalculateChanges(t *testing.T) { t.Run("should patch rule with UID specified by existing rule", func(t *testing.T) { testCases := []struct { name string - mutator func(r *models.AlertRule) + mutator models.AlertRuleMutator }{ { name: "title is empty", @@ -167,7 +171,7 @@ func TestCalculateChanges(t *testing.T) { }, } - dbRule := models.AlertRuleGen(withOrgID(orgId))() + dbRule := gen.With(gen.WithOrgID(orgId)).GenerateRef() fakeStore := fakes.NewRuleStore(t) fakeStore.PutRule(context.Background(), dbRule) @@ -176,7 +180,7 @@ func TestCalculateChanges(t *testing.T) { for _, testCase := range testCases { t.Run(testCase.name, func(t *testing.T) { - expected := models.AlertRuleGen(simulateSubmitted, testCase.mutator)() + expected := gen.With(simulateSubmitted, testCase.mutator).GenerateRef() expected.UID = dbRule.UID submitted := *expected changes, err := CalculateChanges(context.Background(), fakeStore, groupKey, []*models.AlertRuleWithOptionals{{AlertRule: submitted}}) @@ -193,7 +197,8 @@ func TestCalculateChanges(t *testing.T) { t.Run("should be able to find alerts by UID in other group/namespace", func(t *testing.T) { sourceGroupKey := models.GenerateGroupKey(orgId) - inDatabaseMap, inDatabase := models.GenerateUniqueAlertRules(rand.Intn(10)+10, models.AlertRuleGen(withGroupKey(sourceGroupKey))) + inDatabase := gen.With(gen.WithGroupKey(sourceGroupKey)).GenerateManyRef(10, 20) + inDatabaseMap := groupByUID(t, inDatabase) fakeStore := fakes.NewRuleStore(t) fakeStore.PutRule(context.Background(), inDatabase...) @@ -207,7 +212,8 @@ func TestCalculateChanges(t *testing.T) { RuleGroup: groupName, } - submittedMap, rules := models.GenerateUniqueAlertRules(rand.Intn(len(inDatabase)-5)+5, models.AlertRuleGen(simulateSubmitted, withGroupKey(groupKey), withUIDs(inDatabaseMap))) + rules := gen.With(simulateSubmitted, gen.WithGroupKey(groupKey), withUIDs(inDatabaseMap)).GenerateManyRef(5, len(inDatabase)) + submittedMap := groupByUID(t, rules) submitted := make([]*models.AlertRuleWithOptionals, 0, len(rules)) for _, rule := range rules { submitted = append(submitted, &models.AlertRuleWithOptionals{AlertRule: *rule}) @@ -237,10 +243,10 @@ func TestCalculateChanges(t *testing.T) { t.Run("should fail when submitted rule has UID that does not exist in db", func(t *testing.T) { fakeStore := fakes.NewRuleStore(t) groupKey := models.GenerateGroupKey(orgId) - submitted := models.AlertRuleGen(withOrgID(orgId), simulateSubmitted)() + submitted := gen.With(gen.WithOrgID(orgId), simulateSubmitted).Generate() require.NotEqual(t, "", submitted.UID) - _, err := CalculateChanges(context.Background(), fakeStore, groupKey, []*models.AlertRuleWithOptionals{{AlertRule: *submitted}}) + _, err := CalculateChanges(context.Background(), fakeStore, groupKey, []*models.AlertRuleWithOptionals{{AlertRule: submitted}}) require.Error(t, err) }) @@ -256,9 +262,9 @@ func TestCalculateChanges(t *testing.T) { } groupKey := models.GenerateGroupKey(orgId) - submitted := models.AlertRuleGen(withOrgID(orgId), simulateSubmitted, withoutUID)() + submitted := gen.With(gen.WithOrgID(orgId), simulateSubmitted, withoutUID).Generate() - _, err := CalculateChanges(context.Background(), fakeStore, groupKey, []*models.AlertRuleWithOptionals{{AlertRule: *submitted}}) + _, err := CalculateChanges(context.Background(), fakeStore, groupKey, []*models.AlertRuleWithOptionals{{AlertRule: submitted}}) require.ErrorIs(t, err, expectedErr) }) @@ -274,19 +280,20 @@ func TestCalculateChanges(t *testing.T) { } groupKey := models.GenerateGroupKey(orgId) - submitted := models.AlertRuleGen(withOrgID(orgId), simulateSubmitted)() + submitted := gen.With(gen.WithOrgID(orgId), simulateSubmitted).Generate() - _, err := CalculateChanges(context.Background(), fakeStore, groupKey, []*models.AlertRuleWithOptionals{{AlertRule: *submitted}}) + _, err := CalculateChanges(context.Background(), fakeStore, groupKey, []*models.AlertRuleWithOptionals{{AlertRule: submitted}}) require.ErrorIs(t, err, expectedErr) }) } func TestCalculateAutomaticChanges(t *testing.T) { orgID := rand.Int63() + gen := models.RuleGen t.Run("should mark all rules in affected groups", func(t *testing.T) { group := models.GenerateGroupKey(orgID) - rules := models.GenerateAlertRules(10, models.AlertRuleGen(withGroupKey(group))) + rules := gen.With(gen.WithGroupKey(group)).GenerateManyRef(10) // copy rules to make sure that the function does not modify the original rules copies := make([]*models.AlertRule, 0, len(rules)) for _, rule := range rules { @@ -309,7 +316,7 @@ func TestCalculateAutomaticChanges(t *testing.T) { AffectedGroups: map[models.AlertRuleGroupKey]models.RulesGroup{ group: copies, }, - New: models.GenerateAlertRules(2, models.AlertRuleGen(withGroupKey(group))), + New: gen.With(gen.WithGroupKey(group)).GenerateManyRef(2), Update: updates, Delete: rules[5:7], } @@ -337,9 +344,9 @@ func TestCalculateAutomaticChanges(t *testing.T) { t.Run("should re-index rules in affected groups other than updated", func(t *testing.T) { group := models.GenerateGroupKey(orgID) - rules := models.GenerateAlertRules(3, models.AlertRuleGen(withGroupKey(group), models.WithSequentialGroupIndex())) + rules := gen.With(gen.WithGroupKey(group), gen.WithSequentialGroupIndex()).GenerateManyRef(3) group2 := models.GenerateGroupKey(orgID) - rules2 := models.GenerateAlertRules(4, models.AlertRuleGen(withGroupKey(group2), models.WithSequentialGroupIndex())) + rules2 := gen.With(gen.WithGroupKey(group2), gen.WithSequentialGroupIndex()).GenerateManyRef(4) movedIndex := rand.Intn(len(rules2)) movedRule := rules2[movedIndex] @@ -417,9 +424,10 @@ func TestCalculateAutomaticChanges(t *testing.T) { } func TestCalculateRuleGroupDelete(t *testing.T) { + gen := models.RuleGen fakeStore := fakes.NewRuleStore(t) groupKey := models.GenerateGroupKey(1) - otherRules := models.GenerateAlertRules(3, models.AlertRuleGen(models.WithOrgID(groupKey.OrgID), models.WithNamespaceUIDNotIn(groupKey.NamespaceUID))) + otherRules := gen.With(gen.WithOrgID(groupKey.OrgID), gen.WithNamespaceUIDNotIn(groupKey.NamespaceUID)).GenerateManyRef(3) fakeStore.Rules[groupKey.OrgID] = otherRules t.Run("NotFound when group does not exist", func(t *testing.T) { @@ -429,7 +437,7 @@ func TestCalculateRuleGroupDelete(t *testing.T) { }) t.Run("set AffectedGroups when a rule refers to an existing group", func(t *testing.T) { - groupRules := models.GenerateAlertRules(3, models.AlertRuleGen(models.WithGroupKey(groupKey))) + groupRules := gen.With(gen.WithGroupKey(groupKey)).GenerateManyRef(3) fakeStore.Rules[groupKey.OrgID] = append(fakeStore.Rules[groupKey.OrgID], groupRules...) delta, err := CalculateRuleGroupDelete(context.Background(), fakeStore, groupKey) @@ -447,9 +455,10 @@ func TestCalculateRuleGroupDelete(t *testing.T) { } func TestCalculateRuleDelete(t *testing.T) { + gen := models.RuleGen fakeStore := fakes.NewRuleStore(t) - rule := models.AlertRuleGen()() - otherRules := models.GenerateAlertRules(3, models.AlertRuleGen(models.WithOrgID(rule.OrgID), models.WithNamespaceUIDNotIn(rule.NamespaceUID))) + rule := gen.GenerateRef() + otherRules := gen.With(gen.WithOrgID(rule.OrgID), gen.WithNamespaceUIDNotIn(rule.NamespaceUID)).GenerateManyRef(3) fakeStore.Rules[rule.OrgID] = otherRules t.Run("nil when a rule does not exist", func(t *testing.T) { @@ -459,7 +468,7 @@ func TestCalculateRuleDelete(t *testing.T) { }) t.Run("set AffectedGroups when a rule refers to an existing group", func(t *testing.T) { - groupRules := models.GenerateAlertRules(3, models.AlertRuleGen(models.WithGroupKey(rule.GetGroupKey()))) + groupRules := gen.With(gen.WithGroupKey(rule.GetGroupKey())).GenerateManyRef(3) groupRules = append(groupRules, rule) fakeStore.Rules[rule.OrgID] = append(fakeStore.Rules[rule.OrgID], groupRules...) @@ -479,10 +488,11 @@ func TestCalculateRuleDelete(t *testing.T) { } func TestCalculateRuleUpdate(t *testing.T) { + gen := models.RuleGen fakeStore := fakes.NewRuleStore(t) - rule := models.AlertRuleGen()() - otherRules := models.GenerateAlertRules(3, models.AlertRuleGen(models.WithOrgID(rule.OrgID), models.WithNamespaceUIDNotIn(rule.NamespaceUID))) - groupRules := models.GenerateAlertRules(3, models.AlertRuleGen(models.WithGroupKey(rule.GetGroupKey()))) + rule := gen.GenerateRef() + otherRules := gen.With(gen.WithOrgID(rule.OrgID), gen.WithNamespaceUIDNotIn(rule.NamespaceUID)).GenerateManyRef(3) + groupRules := gen.With(gen.WithGroupKey(rule.GetGroupKey())).GenerateManyRef(3) groupRules = append(groupRules, rule) fakeStore.Rules[rule.OrgID] = append(otherRules, groupRules...) @@ -520,7 +530,7 @@ func TestCalculateRuleUpdate(t *testing.T) { t.Run("when a rule is moved between groups", func(t *testing.T) { sourceGroupKey := rule.GetGroupKey() targetGroupKey := models.GenerateGroupKey(rule.OrgID) - targetGroup := models.GenerateAlertRules(3, models.AlertRuleGen(models.WithGroupKey(targetGroupKey))) + targetGroup := gen.With(gen.WithGroupKey(targetGroupKey)).GenerateManyRef(3) fakeStore.Rules[rule.OrgID] = append(fakeStore.Rules[rule.OrgID], targetGroup...) cp := models.CopyRule(rule) @@ -548,9 +558,10 @@ func TestCalculateRuleUpdate(t *testing.T) { } func TestCalculateRuleCreate(t *testing.T) { + gen := models.RuleGen t.Run("when a rule refers to a new group", func(t *testing.T) { fakeStore := fakes.NewRuleStore(t) - rule := models.AlertRuleGen()() + rule := gen.GenerateRef() delta, err := CalculateRuleCreate(context.Background(), fakeStore, rule) require.NoError(t, err) @@ -565,10 +576,10 @@ func TestCalculateRuleCreate(t *testing.T) { t.Run("when a rule refers to an existing group", func(t *testing.T) { fakeStore := fakes.NewRuleStore(t) - rule := models.AlertRuleGen()() + rule := gen.GenerateRef() - groupRules := models.GenerateAlertRules(3, models.AlertRuleGen(models.WithGroupKey(rule.GetGroupKey()))) - otherRules := models.GenerateAlertRules(3, models.AlertRuleGen(models.WithOrgID(rule.OrgID), models.WithNamespaceUIDNotIn(rule.NamespaceUID))) + groupRules := gen.With(gen.WithGroupKey(rule.GetGroupKey())).GenerateManyRef(3) + otherRules := gen.With(gen.WithGroupKey(rule.GetGroupKey()), gen.WithNamespaceUIDNotIn(rule.NamespaceUID)).GenerateManyRef(3) fakeStore.Rules[rule.OrgID] = append(groupRules, otherRules...) delta, err := CalculateRuleCreate(context.Background(), fakeStore, rule) @@ -591,25 +602,11 @@ func simulateSubmitted(rule *models.AlertRule) { rule.Updated = time.Time{} } -func withOrgID(orgId int64) func(rule *models.AlertRule) { - return func(rule *models.AlertRule) { - rule.OrgID = orgId - } -} - func withoutUID(rule *models.AlertRule) { rule.UID = "" } -func withGroupKey(groupKey models.AlertRuleGroupKey) func(rule *models.AlertRule) { - return func(rule *models.AlertRule) { - rule.RuleGroup = groupKey.RuleGroup - rule.OrgID = groupKey.OrgID - rule.NamespaceUID = groupKey.NamespaceUID - } -} - -func withUIDs(uids map[string]*models.AlertRule) func(rule *models.AlertRule) { +func withUIDs(uids map[string]*models.AlertRule) models.AlertRuleMutator { unused := make([]string, 0, len(uids)) for s := range uids { unused = append(unused, s) @@ -635,3 +632,14 @@ func randFolder() *folder.Folder { CreatedBy: 0, } } + +func groupByUID(t *testing.T, list []*models.AlertRule) map[string]*models.AlertRule { + result := make(map[string]*models.AlertRule, len(list)) + for _, rule := range list { + if _, ok := result[rule.UID]; ok { + t.Fatalf("expected unique UID for rule %s but duplicate", rule.UID) + } + result[rule.UID] = rule + } + return result +} diff --git a/pkg/services/ngalert/tests/fakes/rules.go b/pkg/services/ngalert/tests/fakes/rules.go index 1714011d0bf..9f8bea0c31e 100644 --- a/pkg/services/ngalert/tests/fakes/rules.go +++ b/pkg/services/ngalert/tests/fakes/rules.go @@ -68,10 +68,12 @@ mainloop: } if existing == nil { metrics.MFolderIDsServiceCount.WithLabelValues(metrics.NGAlerts).Inc() + title := "TEST-FOLDER-" + util.GenerateShortUID() folders = append(folders, &folder.Folder{ - ID: rand.Int63(), // nolint:staticcheck - UID: r.NamespaceUID, - Title: "TEST-FOLDER-" + util.GenerateShortUID(), + ID: rand.Int63(), // nolint:staticcheck + UID: r.NamespaceUID, + Title: title, + Fullpath: "fullpath_" + title, }) f.Folders[r.OrgID] = folders } diff --git a/pkg/services/oauthtoken/oauth_token_test.go b/pkg/services/oauthtoken/oauth_token_test.go index ebcbb76fb3f..873a25cc9ce 100644 --- a/pkg/services/oauthtoken/oauth_token_test.go +++ b/pkg/services/oauthtoken/oauth_token_test.go @@ -174,13 +174,13 @@ func TestService_TryTokenRefresh(t *testing.T) { { desc: "should skip sync when identity is not a user", setup: func(env *environment) { - env.identity = &authn.Identity{ID: "service-account:1"} + env.identity = &authn.Identity{ID: authn.MustParseNamespaceID("service-account:1")} }, }, { desc: "should skip token refresh and return nil if namespace and id cannot be converted to user ID", setup: func(env *environment) { - env.identity = &authn.Identity{ID: "user:invalidIdentifierFormat"} + env.identity = &authn.Identity{ID: authn.MustParseNamespaceID("user:invalidIdentifierFormat")} }, }, { @@ -203,28 +203,28 @@ func TestService_TryTokenRefresh(t *testing.T) { env.identity = &authn.Identity{ AuthenticatedBy: login.GenericOAuthModule, - ID: "user:1234", + ID: authn.MustParseNamespaceID("user:1234"), } }, }, { desc: "should skip token refresh if the expiration check has already been cached", setup: func(env *environment) { - env.identity = &authn.Identity{ID: "user:1234"} + env.identity = &authn.Identity{ID: authn.MustParseNamespaceID("user:1234")} env.cache.Set("oauth-refresh-token-1234", true, 1*time.Minute) }, }, { desc: "should skip token refresh if there's an unexpected error while looking up the user oauth entry, additionally, no error should be returned", setup: func(env *environment) { - env.identity = &authn.Identity{ID: "user:1234"} + env.identity = &authn.Identity{ID: authn.MustParseNamespaceID("user:1234")} env.authInfoService.ExpectedError = errors.New("some error") }, }, { desc: "should skip token refresh if the user doesn't have an oauth entry", setup: func(env *environment) { - env.identity = &authn.Identity{ID: "user:1234"} + env.identity = &authn.Identity{ID: authn.MustParseNamespaceID("user:1234")} env.authInfoService.ExpectedUserAuth = &login.UserAuth{ AuthModule: login.SAMLAuthModule, } @@ -233,7 +233,7 @@ func TestService_TryTokenRefresh(t *testing.T) { { desc: "should do token refresh if access token or id token have not expired yet", setup: func(env *environment) { - env.identity = &authn.Identity{ID: "user:1234"} + env.identity = &authn.Identity{ID: authn.MustParseNamespaceID("user:1234")} env.authInfoService.ExpectedUserAuth = &login.UserAuth{ AuthModule: login.GenericOAuthModule, } @@ -242,7 +242,7 @@ func TestService_TryTokenRefresh(t *testing.T) { { desc: "should skip token refresh when no oauth provider was found", setup: func(env *environment) { - env.identity = &authn.Identity{ID: "user:1234"} + env.identity = &authn.Identity{ID: authn.MustParseNamespaceID("user:1234")} env.authInfoService.ExpectedUserAuth = &login.UserAuth{ AuthModule: login.GenericOAuthModule, OAuthIdToken: EXPIRED_JWT, @@ -252,7 +252,7 @@ func TestService_TryTokenRefresh(t *testing.T) { { desc: "should skip token refresh when oauth provider token handling is disabled (UseRefreshToken is false)", setup: func(env *environment) { - env.identity = &authn.Identity{ID: "user:1234"} + env.identity = &authn.Identity{ID: authn.MustParseNamespaceID("user:1234")} env.authInfoService.ExpectedUserAuth = &login.UserAuth{ AuthModule: login.GenericOAuthModule, OAuthIdToken: EXPIRED_JWT, @@ -265,7 +265,7 @@ func TestService_TryTokenRefresh(t *testing.T) { { desc: "should skip token refresh when there is no refresh token", setup: func(env *environment) { - env.identity = &authn.Identity{ID: "user:1234"} + env.identity = &authn.Identity{ID: authn.MustParseNamespaceID("user:1234")} env.authInfoService.ExpectedUserAuth = &login.UserAuth{ AuthModule: login.GenericOAuthModule, OAuthIdToken: EXPIRED_JWT, @@ -285,7 +285,7 @@ func TestService_TryTokenRefresh(t *testing.T) { Expiry: time.Now().Add(-time.Hour), TokenType: "Bearer", } - env.identity = &authn.Identity{ID: "user:1234"} + env.identity = &authn.Identity{ID: authn.MustParseNamespaceID("user:1234")} env.socialService.ExpectedAuthInfoProvider = &social.OAuthInfo{ UseRefreshToken: true, } @@ -310,7 +310,7 @@ func TestService_TryTokenRefresh(t *testing.T) { Expiry: time.Now().Add(time.Hour), TokenType: "Bearer", } - env.identity = &authn.Identity{ID: "user:1234"} + env.identity = &authn.Identity{ID: authn.MustParseNamespaceID("user:1234")} env.socialService.ExpectedAuthInfoProvider = &social.OAuthInfo{ UseRefreshToken: true, } diff --git a/pkg/services/org/orgimpl/org.go b/pkg/services/org/orgimpl/org.go index f9395027cdf..47ecc8d44e1 100644 --- a/pkg/services/org/orgimpl/org.go +++ b/pkg/services/org/orgimpl/org.go @@ -27,7 +27,6 @@ func ProvideService(db db.DB, cfg *setting.Cfg, quotaService quota.Service) (org db: db, dialect: db.GetDialect(), log: log, - cfg: cfg, }, cfg: cfg, log: log, diff --git a/pkg/services/org/orgimpl/store.go b/pkg/services/org/orgimpl/store.go index 59eb2dc1016..2d987b1539e 100644 --- a/pkg/services/org/orgimpl/store.go +++ b/pkg/services/org/orgimpl/store.go @@ -17,7 +17,6 @@ import ( "github.com/grafana/grafana/pkg/services/sqlstore" "github.com/grafana/grafana/pkg/services/sqlstore/migrator" "github.com/grafana/grafana/pkg/services/user" - "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/util" ) @@ -54,7 +53,6 @@ type sqlStore struct { dialect migrator.Dialect //TODO: moved to service log log.Logger - cfg *setting.Cfg deletes []string } diff --git a/pkg/services/org/orgimpl/store_test.go b/pkg/services/org/orgimpl/store_test.go index 1c62a471641..e946e97ca3d 100644 --- a/pkg/services/org/orgimpl/store_test.go +++ b/pkg/services/org/orgimpl/store_test.go @@ -11,6 +11,7 @@ import ( "github.com/stretchr/testify/require" "github.com/grafana/grafana/pkg/infra/db" + "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/auth/identity" "github.com/grafana/grafana/pkg/services/org" @@ -257,7 +258,6 @@ func TestIntegrationOrgUserDataAccess(t *testing.T) { orgUserStore := sqlStore{ db: ss, dialect: ss.GetDialect(), - cfg: setting.NewCfg(), } t.Run("org user inserted", func(t *testing.T) { @@ -330,8 +330,8 @@ func TestIntegrationOrgUserDataAccess(t *testing.T) { require.NoError(t, err) }) t.Run("GetOrgUsers and UpdateOrgUsers", func(t *testing.T) { - ss := db.InitTestDB(t) - _, usrSvc := createOrgAndUserSvc(t, ss, ss.Cfg) + ss, cfg := db.InitTestDBWithCfg(t) + _, usrSvc := createOrgAndUserSvc(t, ss, cfg) ac1cmd := &user.CreateUserCommand{Login: "ac1", Email: "ac1@test.com", Name: "ac1 name"} ac2cmd := &user.CreateUserCommand{Login: "ac2", Email: "ac2@test.com", Name: "ac2 name", IsAdmin: true} ac1, err := usrSvc.Create(context.Background(), ac1cmd) @@ -475,12 +475,12 @@ func TestIntegrationOrgUserDataAccess(t *testing.T) { }) t.Run("Given single org and 2 users inserted", func(t *testing.T) { - ss = db.InitTestDB(t) - ss.Cfg.AutoAssignOrg = true - ss.Cfg.AutoAssignOrgId = 1 - ss.Cfg.AutoAssignOrgRole = "Viewer" + ss, cfg := db.InitTestDBWithCfg(t) + cfg.AutoAssignOrg = true + cfg.AutoAssignOrgId = 1 + cfg.AutoAssignOrgRole = "Viewer" - orgSvc, usrSvc := createOrgAndUserSvc(t, ss, ss.Cfg) + orgSvc, usrSvc := createOrgAndUserSvc(t, ss, cfg) testUser := &user.SignedInUser{ Permissions: map[int64]map[string][]string{ @@ -535,16 +535,18 @@ func TestIntegrationSQLStore_AddOrgUser(t *testing.T) { t.Skip("skipping integration test") } - store := db.InitTestDB(t) - store.Cfg.AutoAssignOrg = true - store.Cfg.AutoAssignOrgId = 1 - store.Cfg.AutoAssignOrgRole = "Viewer" + store, cfg := db.InitTestDBWithCfg(t) + defer func() { + cfg.AutoAssignOrg, cfg.AutoAssignOrgId, cfg.AutoAssignOrgRole = false, 0, "" + }() + cfg.AutoAssignOrg = true + cfg.AutoAssignOrgId = 1 + cfg.AutoAssignOrgRole = "Viewer" orgUserStore := sqlStore{ db: store, dialect: store.GetDialect(), - cfg: setting.NewCfg(), } - orgSvc, usrSvc := createOrgAndUserSvc(t, store, store.Cfg) + orgSvc, usrSvc := createOrgAndUserSvc(t, store, cfg) o, err := orgSvc.CreateWithMember(context.Background(), &org.CreateOrgCommand{Name: "test org"}) require.NoError(t, err) @@ -604,19 +606,17 @@ func TestIntegration_SQLStore_GetOrgUsers(t *testing.T) { t.Skip("skipping integration test") } - store := db.InitTestDB(t) + store, cfg := db.InitTestDBWithCfg(t) orgUserStore := sqlStore{ db: store, dialect: store.GetDialect(), - cfg: setting.NewCfg(), } - orgUserStore.cfg.IsEnterprise = true + cfg.IsEnterprise = true defer func() { - orgUserStore.cfg.IsEnterprise = false + cfg.IsEnterprise = false }() - store.Cfg = setting.NewCfg() - orgSvc, userSvc := createOrgAndUserSvc(t, store, store.Cfg) + orgSvc, userSvc := createOrgAndUserSvc(t, store, cfg) o, err := orgSvc.CreateWithMember(context.Background(), &org.CreateOrgCommand{Name: "test org"}) require.NoError(t, err) @@ -724,13 +724,12 @@ func TestIntegration_SQLStore_GetOrgUsers_PopulatesCorrectly(t *testing.T) { userimpl.MockTimeNow(constNow) defer userimpl.ResetTimeNow() - store := db.InitTestDB(t, sqlstore.InitTestDBOpt{}) + store, cfg := db.InitTestDBWithCfg(t, sqlstore.InitTestDBOpt{}) orgUserStore := sqlStore{ db: store, dialect: store.GetDialect(), - cfg: setting.NewCfg(), } - _, usrSvc := createOrgAndUserSvc(t, store, store.Cfg) + _, usrSvc := createOrgAndUserSvc(t, store, cfg) id, err := orgUserStore.Insert(context.Background(), &org.Org{ @@ -786,14 +785,13 @@ func TestIntegration_SQLStore_SearchOrgUsers(t *testing.T) { t.Skip("skipping integration test") } - store := db.InitTestDB(t, sqlstore.InitTestDBOpt{}) + store, cfg := db.InitTestDBWithCfg(t, sqlstore.InitTestDBOpt{}) orgUserStore := sqlStore{ db: store, dialect: store.GetDialect(), - cfg: setting.NewCfg(), } // orgUserStore.cfg.Skip - orgSvc, userSvc := createOrgAndUserSvc(t, store, store.Cfg) + orgSvc, userSvc := createOrgAndUserSvc(t, store, cfg) o, err := orgSvc.CreateWithMember(context.Background(), &org.CreateOrgCommand{Name: "test org"}) require.NoError(t, err) @@ -863,13 +861,12 @@ func TestIntegration_SQLStore_RemoveOrgUser(t *testing.T) { if testing.Short() { t.Skip("skipping integration test") } - store := db.InitTestDB(t) + store, cfg := db.InitTestDBWithCfg(t) orgUserStore := sqlStore{ db: store, dialect: store.GetDialect(), - cfg: setting.NewCfg(), } - orgSvc, usrSvc := createOrgAndUserSvc(t, store, store.Cfg) + orgSvc, usrSvc := createOrgAndUserSvc(t, store, cfg) o, err := orgSvc.CreateWithMember(context.Background(), &org.CreateOrgCommand{Name: MainOrgName}) require.NoError(t, err) @@ -912,7 +909,10 @@ func createOrgAndUserSvc(t *testing.T, store db.DB, cfg *setting.Cfg) (org.Servi quotaService := quotaimpl.ProvideService(store, cfg) orgService, err := ProvideService(store, cfg, quotaService) require.NoError(t, err) - usrSvc, err := userimpl.ProvideService(store, orgService, cfg, nil, nil, quotaService, supportbundlestest.NewFakeBundleService()) + usrSvc, err := userimpl.ProvideService( + store, orgService, cfg, nil, nil, tracing.InitializeTracerForTest(), + quotaService, supportbundlestest.NewFakeBundleService(), + ) require.NoError(t, err) return orgService, usrSvc diff --git a/pkg/services/pluginsintegration/angulardetectorsprovider/dynamic.go b/pkg/services/pluginsintegration/angulardetectorsprovider/dynamic.go index bc3146a6c9f..5a4d37b97d5 100644 --- a/pkg/services/pluginsintegration/angulardetectorsprovider/dynamic.go +++ b/pkg/services/pluginsintegration/angulardetectorsprovider/dynamic.go @@ -62,7 +62,7 @@ func ProvideDynamic(cfg *setting.Cfg, store angularpatternsstore.Service) (*Dyna log: log.New("plugin.angulardetectorsprovider.dynamic"), store: store, httpClient: makeHttpClient(), - baseURL: cfg.GrafanaComURL, + baseURL: cfg.GrafanaComAPIURL, backgroundJobInterval: backgroundJobInterval, } d.log.Debug("Providing dynamic angular detection patterns", "baseURL", d.baseURL, "interval", d.backgroundJobInterval) diff --git a/pkg/services/pluginsintegration/angulardetectorsprovider/dynamic_test.go b/pkg/services/pluginsintegration/angulardetectorsprovider/dynamic_test.go index eea3555e903..0550f9c4a19 100644 --- a/pkg/services/pluginsintegration/angulardetectorsprovider/dynamic_test.go +++ b/pkg/services/pluginsintegration/angulardetectorsprovider/dynamic_test.go @@ -580,7 +580,7 @@ func provideDynamic(t *testing.T, gcomURL string, opts ...provideDynamicOpts) *D if opt.cfg == nil { opt.cfg = setting.NewCfg() } - opt.cfg.GrafanaComURL = gcomURL + opt.cfg.GrafanaComAPIURL = gcomURL + "/api" d, err := ProvideDynamic(opt.cfg, opt.store) require.NoError(t, err) return d diff --git a/pkg/services/pluginsintegration/angulardetectorsprovider/gcom.go b/pkg/services/pluginsintegration/angulardetectorsprovider/gcom.go index d2958b01aec..cd55ed448c7 100644 --- a/pkg/services/pluginsintegration/angulardetectorsprovider/gcom.go +++ b/pkg/services/pluginsintegration/angulardetectorsprovider/gcom.go @@ -9,7 +9,7 @@ import ( ) // gcomAngularPatternsPath is the relative path to the GCOM API handler that returns angular detection patterns. -const gcomAngularPatternsPath = "/api/plugins/angular_patterns" +const gcomAngularPatternsPath = "/plugins/angular_patterns" // GCOMPatternType is a pattern type returned by the GCOM API. type GCOMPatternType string diff --git a/pkg/services/pluginsintegration/keyretriever/dynamic/dynamic_retriever.go b/pkg/services/pluginsintegration/keyretriever/dynamic/dynamic_retriever.go index a038caecd0c..f3d982a0bf1 100644 --- a/pkg/services/pluginsintegration/keyretriever/dynamic/dynamic_retriever.go +++ b/pkg/services/pluginsintegration/keyretriever/dynamic/dynamic_retriever.go @@ -115,7 +115,7 @@ func (kr *KeyRetriever) downloadKeys(ctx context.Context) error { Items []ManifestKeys } - url, err := url.JoinPath(kr.cfg.GrafanaComURL, "/api/plugins/ci/keys") // nolint:gosec URL is provided by config + url, err := url.JoinPath(kr.cfg.GrafanaComAPIURL, "/plugins/ci/keys") // nolint:gosec URL is provided by config if err != nil { return err } diff --git a/pkg/services/pluginsintegration/keyretriever/dynamic/dynamic_retriever_test.go b/pkg/services/pluginsintegration/keyretriever/dynamic/dynamic_retriever_test.go index 79288dee477..5c769adaf50 100644 --- a/pkg/services/pluginsintegration/keyretriever/dynamic/dynamic_retriever_test.go +++ b/pkg/services/pluginsintegration/keyretriever/dynamic/dynamic_retriever_test.go @@ -45,7 +45,7 @@ func Test_PublicKeyUpdate(t *testing.T) { cfg := &setting.Cfg{} expectedKey := "fake" s, done := setFakeAPIServer(t, expectedKey, "7e4d0c6a708866e7") - cfg.GrafanaComURL = s.URL + cfg.GrafanaComAPIURL = s.URL + "/api" v := ProvideService(cfg, keystore.ProvideService(kvstore.NewFakeKVStore())) go func() { err := v.Run(context.Background()) @@ -66,7 +66,7 @@ func Test_PublicKeyUpdate(t *testing.T) { cfg := &setting.Cfg{} expectedKey := "fake" s, done := setFakeAPIServer(t, expectedKey, "7e4d0c6a708866e7") - cfg.GrafanaComURL = s.URL + cfg.GrafanaComAPIURL = s.URL + "/api" v := ProvideService(cfg, keystore.ProvideService(kvstore.NewFakeKVStore())) go func() { err := v.Run(context.Background()) @@ -86,7 +86,7 @@ func Test_PublicKeyUpdate(t *testing.T) { cfg := &setting.Cfg{} expectedKey := "fake" s, done := setFakeAPIServer(t, expectedKey, "other") - cfg.GrafanaComURL = s.URL + cfg.GrafanaComAPIURL = s.URL + "/api" v := ProvideService(cfg, keystore.ProvideService(kvstore.NewFakeKVStore())) go func() { err := v.Run(context.Background()) @@ -113,7 +113,7 @@ func Test_PublicKeyUpdate(t *testing.T) { } expectedKey := "fake" s, done := setFakeAPIServer(t, expectedKey, "7e4d0c6a708866e7") - cfg.GrafanaComURL = s.URL + cfg.GrafanaComAPIURL = s.URL + "/api" v := ProvideService(cfg, keystore.ProvideService(kvstore.NewFakeKVStore())) // Simulate an updated key err := v.kv.SetLastUpdated(context.Background()) diff --git a/pkg/services/pluginsintegration/pluginconfig/config.go b/pkg/services/pluginsintegration/pluginconfig/config.go index cfc32a0377d..83ec526c33f 100644 --- a/pkg/services/pluginsintegration/pluginconfig/config.go +++ b/pkg/services/pluginsintegration/pluginconfig/config.go @@ -34,7 +34,7 @@ func ProvidePluginManagementConfig(cfg *setting.Cfg, settingProvider setting.Pro SkipHostEnvVarsEnabled: features.IsEnabledGlobally(featuremgmt.FlagPluginsSkipHostEnvVars), }, cfg.AngularSupportEnabled, - cfg.GrafanaComURL, + cfg.GrafanaComAPIURL, cfg.DisablePlugins, cfg.HideAngularDeprecation, cfg.ForwardHostEnvVars, diff --git a/pkg/services/pluginsintegration/plugins_integration_test.go b/pkg/services/pluginsintegration/plugins_integration_test.go index 726b47f8e8f..27ec459cd84 100644 --- a/pkg/services/pluginsintegration/plugins_integration_test.go +++ b/pkg/services/pluginsintegration/plugins_integration_test.go @@ -89,7 +89,8 @@ func TestIntegrationPluginManager(t *testing.T) { pg := postgres.ProvideService(cfg) my := mysql.ProvideService() ms := mssql.ProvideService(cfg) - sv2 := searchV2.ProvideService(cfg, db.InitTestDB(t, sqlstore.InitTestDBOpt{Cfg: cfg}), nil, nil, tracer, features, nil, nil, nil) + db := db.InitTestDB(t, sqlstore.InitTestDBOpt{Cfg: cfg}) + sv2 := searchV2.ProvideService(cfg, db, nil, nil, tracer, features, nil, nil, nil) graf := grafanads.ProvideService(sv2, nil) pyroscope := pyroscope.ProvideService(hcp) parca := parca.ProvideService(hcp) diff --git a/pkg/services/publicdashboards/api/query_test.go b/pkg/services/publicdashboards/api/query_test.go index b92f142a1e0..385fe19dc47 100644 --- a/pkg/services/publicdashboards/api/query_test.go +++ b/pkg/services/publicdashboards/api/query_test.go @@ -40,7 +40,6 @@ import ( "github.com/grafana/grafana/pkg/services/quota/quotatest" "github.com/grafana/grafana/pkg/services/tag/tagimpl" "github.com/grafana/grafana/pkg/services/user" - "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/util/errutil" "github.com/grafana/grafana/pkg/web" ) @@ -258,7 +257,7 @@ func TestIntegrationUnauthenticatedUserCanGetPubdashPanelQueryData(t *testing.T) if testing.Short() { t.Skip("skipping integration test") } - db := db.InitTestDB(t) + db, cfg := db.InitTestDBWithCfg(t) cacheService := datasourcesService.ProvideCacheService(localcache.ProvideService(), db, guardian.ProvideGuardian()) qds := buildQueryDataService(t, cacheService, nil, db) @@ -300,7 +299,7 @@ func TestIntegrationUnauthenticatedUserCanGetPubdashPanelQueryData(t *testing.T) } // create dashboard - dashboardStoreService, err := dashboardStore.ProvideDashboardStore(db, db.Cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(db), quotatest.New(false, nil)) + dashboardStoreService, err := dashboardStore.ProvideDashboardStore(db, cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(db), quotatest.New(false, nil)) require.NoError(t, err) dashboard, err := dashboardStoreService.SaveDashboard(context.Background(), saveDashboardCmd) require.NoError(t, err) @@ -318,8 +317,7 @@ func TestIntegrationUnauthenticatedUserCanGetPubdashPanelQueryData(t *testing.T) annotationsService := annotationstest.NewFakeAnnotationsRepo() // create public dashboard - store := publicdashboardsStore.ProvideStore(db, db.Cfg, featuremgmt.WithFeatures()) - cfg := setting.NewCfg() + store := publicdashboardsStore.ProvideStore(db, cfg, featuremgmt.WithFeatures()) cfg.PublicDashboardsEnabled = true ac := acmock.New() ws := publicdashboardsService.ProvideServiceWrapper(store) diff --git a/pkg/services/publicdashboards/database/database_test.go b/pkg/services/publicdashboards/database/database_test.go index 8108bab714d..fd8273cc198 100644 --- a/pkg/services/publicdashboards/database/database_test.go +++ b/pkg/services/publicdashboards/database/database_test.go @@ -63,7 +63,7 @@ func TestIntegrationListPublicDashboard(t *testing.T) { var publicdashboardStore *PublicDashboardStoreImpl setup := func() { - sqlStore, cfg = db.InitTestDBwithCfg(t, db.InitTestDBOpt{}) + sqlStore, cfg = db.InitTestDBWithCfg(t, db.InitTestDBOpt{}) quotaService := quotatest.New(false, nil) dashboardStore, err := dashboardsDB.ProvideDashboardStore(sqlStore, cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore), quotaService) require.NoError(t, err) @@ -178,7 +178,7 @@ func TestIntegrationExistsEnabledByAccessToken(t *testing.T) { var savedDashboard *dashboards.Dashboard setup := func() { - sqlStore, cfg = db.InitTestDBwithCfg(t) + sqlStore, cfg = db.InitTestDBWithCfg(t) quotaService := quotatest.New(false, nil) store, err := dashboardsDB.ProvideDashboardStore(sqlStore, cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore), quotaService) require.NoError(t, err) @@ -251,7 +251,7 @@ func TestIntegrationExistsEnabledByDashboardUid(t *testing.T) { var savedDashboard *dashboards.Dashboard setup := func() { - sqlStore, cfg = db.InitTestDBwithCfg(t) + sqlStore, cfg = db.InitTestDBWithCfg(t) quotaService := quotatest.New(false, nil) store, err := dashboardsDB.ProvideDashboardStore(sqlStore, cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore), quotaService) require.NoError(t, err) @@ -316,7 +316,7 @@ func TestIntegrationFindByDashboardUid(t *testing.T) { var savedDashboard *dashboards.Dashboard setup := func() { - sqlStore, cfg = db.InitTestDBwithCfg(t) + sqlStore, cfg = db.InitTestDBWithCfg(t) quotaService := quotatest.New(false, nil) store, err := dashboardsDB.ProvideDashboardStore(sqlStore, cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore), quotaService) require.NoError(t, err) @@ -385,7 +385,7 @@ func TestIntegrationFindByAccessToken(t *testing.T) { var err error setup := func() { - sqlStore, cfg = db.InitTestDBwithCfg(t) + sqlStore, cfg = db.InitTestDBWithCfg(t) dashboardStore, err = dashboardsDB.ProvideDashboardStore(sqlStore, cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore), quotatest.New(false, nil)) require.NoError(t, err) publicdashboardStore = ProvideStore(sqlStore, cfg, featuremgmt.WithFeatures()) @@ -453,7 +453,7 @@ func TestIntegrationCreatePublicDashboard(t *testing.T) { var savedDashboard2 *dashboards.Dashboard setup := func() { - sqlStore, cfg = db.InitTestDBwithCfg(t, db.InitTestDBOpt{}) + sqlStore, cfg = db.InitTestDBWithCfg(t, db.InitTestDBOpt{}) quotaService := quotatest.New(false, nil) store, err := dashboardsDB.ProvideDashboardStore(sqlStore, cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore), quotaService) require.NoError(t, err) @@ -533,7 +533,7 @@ func TestIntegrationUpdatePublicDashboard(t *testing.T) { var err error setup := func() { - sqlStore, cfg = db.InitTestDBwithCfg(t, db.InitTestDBOpt{}) + sqlStore, cfg = db.InitTestDBWithCfg(t, db.InitTestDBOpt{}) quotaService := quotatest.New(false, nil) dashboardStore, err = dashboardsDB.ProvideDashboardStore(sqlStore, cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore), quotaService) require.NoError(t, err) @@ -637,7 +637,7 @@ func TestIntegrationGetOrgIdByAccessToken(t *testing.T) { var err error setup := func() { - sqlStore, cfg = db.InitTestDBwithCfg(t) + sqlStore, cfg = db.InitTestDBWithCfg(t) quotaService := quotatest.New(false, nil) dashboardStore, err = dashboardsDB.ProvideDashboardStore(sqlStore, cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore), quotaService) require.NoError(t, err) @@ -710,7 +710,7 @@ func TestIntegrationDelete(t *testing.T) { var err error setup := func() { - sqlStore, cfg = db.InitTestDBwithCfg(t) + sqlStore, cfg = db.InitTestDBWithCfg(t) dashboardStore, err = dashboardsDB.ProvideDashboardStore(sqlStore, cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore), quotatest.New(false, nil)) require.NoError(t, err) publicdashboardStore = ProvideStore(sqlStore, cfg, featuremgmt.WithFeatures()) @@ -742,9 +742,9 @@ func TestIntegrationDelete(t *testing.T) { func TestFindByFolder(t *testing.T) { t.Run("returns nil when dashboard is not a folder", func(t *testing.T) { - sqlStore, _ := db.InitTestDBwithCfg(t) + sqlStore, cfg := db.InitTestDBWithCfg(t) dashboard := &dashboards.Dashboard{OrgID: 1, UID: "dashboarduid", IsFolder: false} - store := ProvideStore(sqlStore, sqlStore.Cfg, featuremgmt.WithFeatures()) + store := ProvideStore(sqlStore, cfg, featuremgmt.WithFeatures()) pubdashes, err := store.FindByFolder(context.Background(), dashboard.OrgID, dashboard.UID) require.NoError(t, err) @@ -752,8 +752,8 @@ func TestFindByFolder(t *testing.T) { }) t.Run("returns nil when parameters are empty", func(t *testing.T) { - sqlStore, _ := db.InitTestDBwithCfg(t) - store := ProvideStore(sqlStore, sqlStore.Cfg, featuremgmt.WithFeatures()) + sqlStore, cfg := db.InitTestDBWithCfg(t) + store := ProvideStore(sqlStore, cfg, featuremgmt.WithFeatures()) pubdashes, err := store.FindByFolder(context.Background(), 0, "") require.NoError(t, err) @@ -761,11 +761,11 @@ func TestFindByFolder(t *testing.T) { }) t.Run("can get all pubdashes for dashboard folder and org", func(t *testing.T) { - sqlStore, _ := db.InitTestDBwithCfg(t) + sqlStore, cfg := db.InitTestDBWithCfg(t) quotaService := quotatest.New(false, nil) - dashboardStore, err := dashboardsDB.ProvideDashboardStore(sqlStore, sqlStore.Cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore), quotaService) + dashboardStore, err := dashboardsDB.ProvideDashboardStore(sqlStore, cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore), quotaService) require.NoError(t, err) - pubdashStore := ProvideStore(sqlStore, sqlStore.Cfg, featuremgmt.WithFeatures()) + pubdashStore := ProvideStore(sqlStore, cfg, featuremgmt.WithFeatures()) // insert folders folder := insertTestDashboard(t, dashboardStore, "This is a folder", 1, "", true, PublicShareType) folder2 := insertTestDashboard(t, dashboardStore, "This is another folder", 1, "", true, PublicShareType) @@ -800,7 +800,7 @@ func TestGetMetrics(t *testing.T) { var savedDashboard4 *dashboards.Dashboard setup := func() { - sqlStore, cfg = db.InitTestDBwithCfg(t, db.InitTestDBOpt{}) + sqlStore, cfg = db.InitTestDBWithCfg(t, db.InitTestDBOpt{}) quotaService := quotatest.New(false, nil) store, err := dashboardsDB.ProvideDashboardStore(sqlStore, cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore), quotaService) require.NoError(t, err) diff --git a/pkg/services/publicdashboards/service/common_test.go b/pkg/services/publicdashboards/service/common_test.go index da383640f65..1124b687288 100644 --- a/pkg/services/publicdashboards/service/common_test.go +++ b/pkg/services/publicdashboards/service/common_test.go @@ -14,7 +14,6 @@ import ( "github.com/grafana/grafana/pkg/services/publicdashboards/database" . "github.com/grafana/grafana/pkg/services/publicdashboards/models" "github.com/grafana/grafana/pkg/services/publicdashboards/service/intervalv2" - "github.com/grafana/grafana/pkg/services/sqlstore" "github.com/grafana/grafana/pkg/services/tag/tagimpl" "github.com/grafana/grafana/pkg/setting" ) @@ -27,14 +26,14 @@ func newPublicDashboardServiceImpl( ) (*PublicDashboardServiceImpl, db.DB, *setting.Cfg) { t.Helper() - db := sqlstore.InitTestDB(t) + db, cfg := db.InitTestDBWithCfg(t) tagService := tagimpl.ProvideService(db) if annotationsRepo == nil { - annotationsRepo = annotationsimpl.ProvideService(db, db.Cfg, featuremgmt.WithFeatures(), tagService) + annotationsRepo = annotationsimpl.ProvideService(db, cfg, featuremgmt.WithFeatures(), tagService) } if publicDashboardStore == nil { - publicDashboardStore = database.ProvideStore(db, db.Cfg, featuremgmt.WithFeatures()) + publicDashboardStore = database.ProvideStore(db, cfg, featuremgmt.WithFeatures()) } serviceWrapper := ProvideServiceWrapper(publicDashboardStore) @@ -50,5 +49,5 @@ func newPublicDashboardServiceImpl( serviceWrapper: serviceWrapper, license: license, features: featuremgmt.WithFeatures(), - }, db, db.Cfg + }, db, cfg } diff --git a/pkg/services/publicdashboards/service/query_test.go b/pkg/services/publicdashboards/service/query_test.go index 3aada6964bb..b53bb878603 100644 --- a/pkg/services/publicdashboards/service/query_test.go +++ b/pkg/services/publicdashboards/service/query_test.go @@ -1320,8 +1320,8 @@ func TestBuildMetricRequest(t *testing.T) { } func TestBuildAnonymousUser(t *testing.T) { - sqlStore := db.InitTestDB(t) - dashboardStore, err := dashboardsDB.ProvideDashboardStore(sqlStore, sqlStore.Cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore), quotatest.New(false, nil)) + sqlStore, cfg := db.InitTestDBWithCfg(t) + dashboardStore, err := dashboardsDB.ProvideDashboardStore(sqlStore, cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore), quotatest.New(false, nil)) require.NoError(t, err) dashboard := insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, "", true, []map[string]interface{}{}, nil) features := featuremgmt.WithFeatures() diff --git a/pkg/services/query/query_test.go b/pkg/services/query/query_test.go index fdaf22fbcd4..2df66a19e8b 100644 --- a/pkg/services/query/query_test.go +++ b/pkg/services/query/query_test.go @@ -465,7 +465,7 @@ func setup(t *testing.T) *testContext { dc := &fakeDataSourceCache{cache: dss} rv := &fakePluginRequestValidator{} - sqlStore := db.InitTestDB(t) + sqlStore, cfg := db.InitTestDBWithCfg(t) secretsService := secretsmng.SetupTestService(t, fakes.NewFakeSecretsStore()) ss := secretskvs.NewSQLSecretsKVStore(sqlStore, secretsService, log.New("test.logger")) fakeDatasourceService := &fakeDatasources.FakeDataSourceService{ @@ -473,7 +473,7 @@ func setup(t *testing.T) *testContext { SimulatePluginFailure: false, } - pCtxProvider := plugincontext.ProvideService(sqlStore.Cfg, + pCtxProvider := plugincontext.ProvideService(cfg, localcache.ProvideService(), &pluginstore.FakePluginStore{ PluginList: []pluginstore.Plugin{ {JSONData: plugins.JSONData{ID: "postgres"}}, diff --git a/pkg/services/queryhistory/queryhistory_test.go b/pkg/services/queryhistory/queryhistory_test.go index 80535378d86..5e7b8cad7d8 100644 --- a/pkg/services/queryhistory/queryhistory_test.go +++ b/pkg/services/queryhistory/queryhistory_test.go @@ -15,6 +15,7 @@ import ( "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/infra/db" + "github.com/grafana/grafana/pkg/infra/tracing" contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/org/orgimpl" @@ -55,7 +56,7 @@ func testScenario(t *testing.T, desc string, fn func(t *testing.T, sc scenarioCo Form: url.Values{}, }} ctx.Req.Header.Add("Content-Type", "application/json") - sqlStore := db.InitTestDB(t) + sqlStore, cfg := db.InitTestDBWithCfg(t) service := QueryHistoryService{ Cfg: setting.NewCfg(), store: sqlStore, @@ -63,9 +64,12 @@ func testScenario(t *testing.T, desc string, fn func(t *testing.T, sc scenarioCo } service.Cfg.QueryHistoryEnabled = true quotaService := quotatest.New(false, nil) - orgSvc, err := orgimpl.ProvideService(sqlStore, sqlStore.Cfg, quotaService) + orgSvc, err := orgimpl.ProvideService(sqlStore, cfg, quotaService) require.NoError(t, err) - usrSvc, err := userimpl.ProvideService(sqlStore, orgSvc, sqlStore.Cfg, nil, nil, quotaService, supportbundlestest.NewFakeBundleService()) + usrSvc, err := userimpl.ProvideService( + sqlStore, orgSvc, cfg, nil, nil, tracing.InitializeTracerForTest(), + quotaService, supportbundlestest.NewFakeBundleService(), + ) require.NoError(t, err) usr := user.SignedInUser{ diff --git a/pkg/services/quota/quotaimpl/quota_test.go b/pkg/services/quota/quotaimpl/quota_test.go index f83418a3b71..aa13fa80b03 100644 --- a/pkg/services/quota/quotaimpl/quota_test.go +++ b/pkg/services/quota/quotaimpl/quota_test.go @@ -39,7 +39,6 @@ import ( "github.com/grafana/grafana/pkg/services/secrets/fakes" secretskvs "github.com/grafana/grafana/pkg/services/secrets/kvstore" secretsmng "github.com/grafana/grafana/pkg/services/secrets/manager" - "github.com/grafana/grafana/pkg/services/sqlstore" storesrv "github.com/grafana/grafana/pkg/services/store" "github.com/grafana/grafana/pkg/services/supportbundles/supportbundlestest" "github.com/grafana/grafana/pkg/services/tag/tagimpl" @@ -65,8 +64,8 @@ func TestIntegrationQuotaCommandsAndQueries(t *testing.T) { if testing.Short() { t.Skip("skipping integration test") } - sqlStore := sqlstore.InitTestDB(t) - sqlStore.Cfg.Quota = setting.QuotaSettings{ + sqlStore, cfg := db.InitTestDBWithCfg(t) + cfg.Quota = setting.QuotaSettings{ Enabled: true, Org: setting.OrgQuota{ @@ -92,12 +91,15 @@ func TestIntegrationQuotaCommandsAndQueries(t *testing.T) { } b := bus.ProvideBus(tracing.InitializeTracerForTest()) - quotaService := ProvideService(sqlStore, sqlStore.Cfg) - orgService, err := orgimpl.ProvideService(sqlStore, sqlStore.Cfg, quotaService) + quotaService := ProvideService(sqlStore, cfg) + orgService, err := orgimpl.ProvideService(sqlStore, cfg, quotaService) require.NoError(t, err) - userService, err := userimpl.ProvideService(sqlStore, orgService, sqlStore.Cfg, nil, nil, quotaService, supportbundlestest.NewFakeBundleService()) + userService, err := userimpl.ProvideService( + sqlStore, orgService, cfg, nil, nil, tracing.InitializeTracerForTest(), + quotaService, supportbundlestest.NewFakeBundleService(), + ) require.NoError(t, err) - setupEnv(t, sqlStore, sqlStore.Cfg, b, quotaService) + setupEnv(t, sqlStore, cfg, b, quotaService) u, err := userService.Create(context.Background(), &user.CreateUserCommand{ Name: "TestUser", @@ -126,28 +128,28 @@ func TestIntegrationQuotaCommandsAndQueries(t *testing.T) { } tag, err := quota.NewTag(quota.TargetSrv(org.QuotaTargetSrv), quota.Target(org.OrgQuotaTarget), scope) require.NoError(t, err) - require.Equal(t, sqlStore.Cfg.Quota.Global.Org, defaultGlobalLimits[tag]) + require.Equal(t, cfg.Quota.Global.Org, defaultGlobalLimits[tag]) tag, err = quota.NewTag(quota.TargetSrv(user.QuotaTargetSrv), quota.Target(user.QuotaTarget), scope) require.NoError(t, err) - require.Equal(t, sqlStore.Cfg.Quota.Global.User, defaultGlobalLimits[tag]) + require.Equal(t, cfg.Quota.Global.User, defaultGlobalLimits[tag]) tag, err = quota.NewTag(dashboards.QuotaTargetSrv, dashboards.QuotaTarget, scope) require.NoError(t, err) - require.Equal(t, sqlStore.Cfg.Quota.Global.Dashboard, defaultGlobalLimits[tag]) + require.Equal(t, cfg.Quota.Global.Dashboard, defaultGlobalLimits[tag]) tag, err = quota.NewTag(datasources.QuotaTargetSrv, datasources.QuotaTarget, scope) require.NoError(t, err) - require.Equal(t, sqlStore.Cfg.Quota.Global.DataSource, defaultGlobalLimits[tag]) + require.Equal(t, cfg.Quota.Global.DataSource, defaultGlobalLimits[tag]) tag, err = quota.NewTag(apikey.QuotaTargetSrv, apikey.QuotaTarget, scope) require.NoError(t, err) - require.Equal(t, sqlStore.Cfg.Quota.Global.ApiKey, defaultGlobalLimits[tag]) + require.Equal(t, cfg.Quota.Global.ApiKey, defaultGlobalLimits[tag]) tag, err = quota.NewTag(auth.QuotaTargetSrv, auth.QuotaTarget, scope) require.NoError(t, err) - require.Equal(t, sqlStore.Cfg.Quota.Global.Session, defaultGlobalLimits[tag]) + require.Equal(t, cfg.Quota.Global.Session, defaultGlobalLimits[tag]) tag, err = quota.NewTag(ngalertmodels.QuotaTargetSrv, ngalertmodels.QuotaTarget, scope) require.NoError(t, err) - require.Equal(t, sqlStore.Cfg.Quota.Global.AlertRule, defaultGlobalLimits[tag]) + require.Equal(t, cfg.Quota.Global.AlertRule, defaultGlobalLimits[tag]) tag, err = quota.NewTag(storesrv.QuotaTargetSrv, storesrv.QuotaTarget, scope) require.NoError(t, err) - require.Equal(t, sqlStore.Cfg.Quota.Global.File, defaultGlobalLimits[tag]) + require.Equal(t, cfg.Quota.Global.File, defaultGlobalLimits[tag]) // fetch default limit/usage for org defaultOrgLimits := make(map[quota.Tag]int64) @@ -163,19 +165,19 @@ func TestIntegrationQuotaCommandsAndQueries(t *testing.T) { } tag, err = quota.NewTag(quota.TargetSrv(org.QuotaTargetSrv), quota.Target(org.OrgUserQuotaTarget), scope) require.NoError(t, err) - require.Equal(t, sqlStore.Cfg.Quota.Org.User, defaultOrgLimits[tag]) + require.Equal(t, cfg.Quota.Org.User, defaultOrgLimits[tag]) tag, err = quota.NewTag(dashboards.QuotaTargetSrv, dashboards.QuotaTarget, scope) require.NoError(t, err) - require.Equal(t, sqlStore.Cfg.Quota.Org.Dashboard, defaultOrgLimits[tag]) + require.Equal(t, cfg.Quota.Org.Dashboard, defaultOrgLimits[tag]) tag, err = quota.NewTag(datasources.QuotaTargetSrv, datasources.QuotaTarget, scope) require.NoError(t, err) - require.Equal(t, sqlStore.Cfg.Quota.Org.DataSource, defaultOrgLimits[tag]) + require.Equal(t, cfg.Quota.Org.DataSource, defaultOrgLimits[tag]) tag, err = quota.NewTag(apikey.QuotaTargetSrv, apikey.QuotaTarget, scope) require.NoError(t, err) - require.Equal(t, sqlStore.Cfg.Quota.Org.ApiKey, defaultOrgLimits[tag]) + require.Equal(t, cfg.Quota.Org.ApiKey, defaultOrgLimits[tag]) tag, err = quota.NewTag(ngalertmodels.QuotaTargetSrv, ngalertmodels.QuotaTarget, scope) require.NoError(t, err) - require.Equal(t, sqlStore.Cfg.Quota.Org.AlertRule, defaultOrgLimits[tag]) + require.Equal(t, cfg.Quota.Org.AlertRule, defaultOrgLimits[tag]) // fetch default limit/usage for user defaultUserLimits := make(map[quota.Tag]int64) @@ -191,7 +193,7 @@ func TestIntegrationQuotaCommandsAndQueries(t *testing.T) { } tag, err = quota.NewTag(quota.TargetSrv(org.QuotaTargetSrv), quota.Target(org.OrgUserQuotaTarget), scope) require.NoError(t, err) - require.Equal(t, sqlStore.Cfg.Quota.User.Org, defaultUserLimits[tag]) + require.Equal(t, cfg.Quota.User.Org, defaultUserLimits[tag]) t.Run("Given saved org quota for users", func(t *testing.T) { // update quota for the created org and limit users to 1 @@ -225,10 +227,13 @@ func TestIntegrationQuotaCommandsAndQueries(t *testing.T) { t.Run("Should be able to get zero used org alert quota when table does not exist (ngalert is not enabled - default case)", func(t *testing.T) { // disable Grafana Alerting - cfg := *sqlStore.Cfg + alertingCfg := cfg.UnifiedAlerting + defer func() { + cfg.UnifiedAlerting = alertingCfg + }() cfg.UnifiedAlerting = setting.UnifiedAlertingSettings{Enabled: util.Pointer(false)} - quotaSrv := ProvideService(sqlStore, &cfg) + quotaSrv := ProvideService(sqlStore, cfg) q, err := getQuotaBySrvTargetScope(t, quotaSrv, ngalertmodels.QuotaTargetSrv, ngalertmodels.QuotaTarget, quota.OrgScope, "a.ScopeParameters{OrgID: o.ID}) require.NoError(t, err) diff --git a/pkg/services/secrets/migrator/migrator.go b/pkg/services/secrets/migrator/migrator.go index 3361bc097f8..02872935b9f 100644 --- a/pkg/services/secrets/migrator/migrator.go +++ b/pkg/services/secrets/migrator/migrator.go @@ -45,6 +45,7 @@ func ProvideSecretsMigrator( jsonSecret{tableName: "plugin_setting"}, b64Secret{simpleSecret: simpleSecret{tableName: "signing_key", columnName: "private_key"}, encoding: base64.StdEncoding}, alertingSecret{}, + ssoSettingsSecret{}, } return &SecretsMigrator{ @@ -157,6 +158,8 @@ type jsonSecret struct { type alertingSecret struct{} +type ssoSettingsSecret struct{} + func nowInUTC() string { return time.Now().UTC().Format("2006-01-02 15:04:05") } diff --git a/pkg/services/secrets/migrator/reencrypt.go b/pkg/services/secrets/migrator/reencrypt.go index 990774e6b86..6c1271277a4 100644 --- a/pkg/services/secrets/migrator/reencrypt.go +++ b/pkg/services/secrets/migrator/reencrypt.go @@ -11,6 +11,8 @@ import ( "github.com/grafana/grafana/pkg/services/secrets" "github.com/grafana/grafana/pkg/services/secrets/manager" "github.com/grafana/grafana/pkg/services/sqlstore" + "github.com/grafana/grafana/pkg/services/ssosettings/models" + "github.com/grafana/grafana/pkg/services/ssosettings/ssosettingsimpl" ) func (s simpleSecret) ReEncrypt(ctx context.Context, secretsSrv *manager.SecretsService, sqlStore db.DB) bool { @@ -289,3 +291,90 @@ func (s alertingSecret) ReEncrypt(ctx context.Context, secretsSrv *manager.Secre return !anyFailure } + +func (s ssoSettingsSecret) ReEncrypt(ctx context.Context, secretsSrv *manager.SecretsService, sqlStore db.DB) bool { + results := make([]*models.SSOSettings, 0) + + err := sqlStore.WithDbSession(ctx, func(sess *db.Session) error { + return sess.Find(&results) + }) + + if err != nil { + logger.Warn("Failed to fetch SSO settings to re-encrypt", "err", err) + return false + } + + var anyFailure bool + + for _, result := range results { + err := sqlStore.InTransaction(ctx, func(ctx context.Context) error { + for field, value := range result.Settings { + if ssosettingsimpl.IsSecretField(field) { + decrypted, err := s.decryptValue(ctx, value, secretsSrv) + if err != nil { + logger.Warn("Could not decrypt SSO settings secret", "id", result.ID, "field", field, "error", err) + return err + } + + if decrypted == nil { + continue + } + + reencrypted, err := secretsSrv.Encrypt(ctx, decrypted, secrets.WithoutScope()) + if err != nil { + logger.Warn("Could not re-encrypt SSO settings secret", "id", result.ID, "field", field, "error", err) + return err + } + + result.Settings[field] = base64.RawStdEncoding.EncodeToString(reencrypted) + } + } + + err = sqlStore.WithDbSession(ctx, func(sess *db.Session) error { + _, err := sess.Where("id = ?", result.ID).Update(result) + return err + }) + if err != nil { + logger.Warn("Could not update SSO settings secrets while re-encrypting it", "id", result.ID, "error", err) + return err + } + + return nil + }) + + if err != nil { + anyFailure = true + } + } + + if anyFailure { + logger.Warn("SSO settings secrets have been re-encrypted with errors") + } else { + logger.Info("SSO settings secrets have been re-encrypted successfully") + } + + return !anyFailure +} + +func (s ssoSettingsSecret) decryptValue(ctx context.Context, value any, secretsSrv *manager.SecretsService) ([]byte, error) { + strValue, ok := value.(string) + if !ok { + return nil, fmt.Errorf("SSO secret value is not a string") + } + + if strValue == "" { + return nil, nil + } + + decoded, err := base64.RawStdEncoding.DecodeString(strValue) + if err != nil { + return nil, fmt.Errorf("could not decode base64-encoded SSO settings secret: %w", err) + } + + decrypted, err := secretsSrv.Decrypt(ctx, decoded) + if err != nil { + return nil, fmt.Errorf("could not decrypt SSO settings secret: %w", err) + } + + return decrypted, nil +} diff --git a/pkg/services/secrets/migrator/rollback.go b/pkg/services/secrets/migrator/rollback.go index c68c0962771..8a8838c0f96 100644 --- a/pkg/services/secrets/migrator/rollback.go +++ b/pkg/services/secrets/migrator/rollback.go @@ -10,6 +10,8 @@ import ( "github.com/grafana/grafana/pkg/services/encryption" "github.com/grafana/grafana/pkg/services/ngalert/notifier" "github.com/grafana/grafana/pkg/services/secrets/manager" + "github.com/grafana/grafana/pkg/services/ssosettings/models" + "github.com/grafana/grafana/pkg/services/ssosettings/ssosettingsimpl" ) func (s simpleSecret) Rollback( @@ -294,3 +296,71 @@ func (s alertingSecret) Rollback( return anyFailure } + +func (s ssoSettingsSecret) Rollback( + ctx context.Context, + secretsSrv *manager.SecretsService, + encryptionSrv encryption.Internal, + sqlStore db.DB, + secretKey string, +) (anyFailure bool) { + results := make([]*models.SSOSettings, 0) + + err := sqlStore.WithDbSession(ctx, func(sess *db.Session) error { + return sess.Find(&results) + }) + + if err != nil { + logger.Warn("Failed to fetch SSO settings to roll back") + return true + } + + for _, result := range results { + err := sqlStore.WithTransactionalDbSession(ctx, func(sess *db.Session) error { + for field, value := range result.Settings { + if ssosettingsimpl.IsSecretField(field) { + decrypted, err := s.decryptValue(ctx, value, secretsSrv) + if err != nil { + logger.Warn("Could not decrypt SSO settings secret", "id", result.ID, "field", field, "error", err) + return err + } + + if decrypted == nil { + continue + } + + reencrypted, err := encryptionSrv.Encrypt(ctx, decrypted, secretKey) + if err != nil { + logger.Warn("Could not re-encrypt SSO settings secret", "id", result.ID, "field", field, "error", err) + return err + } + + result.Settings[field] = base64.RawStdEncoding.EncodeToString(reencrypted) + } + } + + err = sqlStore.WithDbSession(ctx, func(sess *db.Session) error { + _, err := sess.Where("id = ?", result.ID).Update(result) + return err + }) + if err != nil { + logger.Warn("Could not update SSO settings secrets while re-encrypting it", "id", result.ID, "error", err) + return err + } + + return nil + }) + + if err != nil { + anyFailure = true + } + } + + if anyFailure { + logger.Warn("SSO settings secrets have been rolled back with errors") + } else { + logger.Info("SSO settings secrets have been rolled back successfully") + } + + return anyFailure +} diff --git a/pkg/services/secrets/secrets.go b/pkg/services/secrets/secrets.go index c54535a1d0a..282c1d42f3f 100644 --- a/pkg/services/secrets/secrets.go +++ b/pkg/services/secrets/secrets.go @@ -10,6 +10,10 @@ import ( // Service is an envelope encryption service in charge of encrypting/decrypting secrets. // It is a replacement for encryption.Service // +// For all encrypted secrets stored in the database, a migrator is needed to re-encrypt +// the secrets every time the encryption key has been rotated. Please add your database +// secrets to the migrator slice available in ./migrator/migrator.go. +// //go:generate mockery --name Service --structname MockService --outpkg fakes --filename mock_service.go --output ./fakes/ type Service interface { // Encrypt MUST NOT be used within database transactions, it may cause database locks. diff --git a/pkg/services/serviceaccounts/database/store_test.go b/pkg/services/serviceaccounts/database/store_test.go index 1f60db4d4f4..5804a3fe8e6 100644 --- a/pkg/services/serviceaccounts/database/store_test.go +++ b/pkg/services/serviceaccounts/database/store_test.go @@ -9,6 +9,7 @@ import ( "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/kvstore" + "github.com/grafana/grafana/pkg/infra/tracing" ac "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/apikey/apikeyimpl" "github.com/grafana/grafana/pkg/services/org" @@ -221,15 +222,17 @@ func TestStore_DeleteServiceAccount(t *testing.T) { func setupTestDatabase(t *testing.T) (db.DB, *ServiceAccountsStoreImpl) { t.Helper() - db := db.InitTestDB(t) - cfg := db.Cfg + db, cfg := db.InitTestDBWithCfg(t) quotaService := quotatest.New(false, nil) apiKeyService, err := apikeyimpl.ProvideService(db, cfg, quotaService) require.NoError(t, err) kvStore := kvstore.ProvideService(db) orgService, err := orgimpl.ProvideService(db, cfg, quotaService) require.NoError(t, err) - userSvc, err := userimpl.ProvideService(db, orgService, cfg, nil, nil, quotaService, supportbundlestest.NewFakeBundleService()) + userSvc, err := userimpl.ProvideService( + db, orgService, cfg, nil, nil, tracing.InitializeTracerForTest(), + quotaService, supportbundlestest.NewFakeBundleService(), + ) require.NoError(t, err) return db, ProvideServiceAccountsStore(cfg, db, apiKeyService, kvStore, userSvc, orgService) } diff --git a/pkg/services/serviceaccounts/tests/common.go b/pkg/services/serviceaccounts/tests/common.go index dce8daeffe2..66d15e35b9d 100644 --- a/pkg/services/serviceaccounts/tests/common.go +++ b/pkg/services/serviceaccounts/tests/common.go @@ -7,6 +7,7 @@ import ( "github.com/stretchr/testify/require" "github.com/grafana/grafana/pkg/infra/db" + "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/services/apikey" "github.com/grafana/grafana/pkg/services/apikey/apikeyimpl" "github.com/grafana/grafana/pkg/services/org" @@ -44,7 +45,10 @@ func SetupUserServiceAccount(t *testing.T, db db.DB, cfg *setting.Cfg, testUser quotaService := quotaimpl.ProvideService(db, cfg) orgService, err := orgimpl.ProvideService(db, cfg, quotaService) require.NoError(t, err) - usrSvc, err := userimpl.ProvideService(db, orgService, cfg, nil, nil, quotaService, supportbundlestest.NewFakeBundleService()) + usrSvc, err := userimpl.ProvideService( + db, orgService, cfg, nil, nil, tracing.InitializeTracerForTest(), + quotaService, supportbundlestest.NewFakeBundleService(), + ) require.NoError(t, err) org, err := orgService.CreateWithMember(context.Background(), &org.CreateOrgCommand{ @@ -111,7 +115,10 @@ func SetupUsersServiceAccounts(t *testing.T, sqlStore db.DB, cfg *setting.Cfg, t quotaService := quotaimpl.ProvideService(sqlStore, cfg) orgService, err := orgimpl.ProvideService(sqlStore, cfg, quotaService) require.NoError(t, err) - usrSvc, err := userimpl.ProvideService(sqlStore, orgService, cfg, nil, nil, quotaService, supportbundlestest.NewFakeBundleService()) + usrSvc, err := userimpl.ProvideService( + sqlStore, orgService, cfg, nil, nil, tracing.InitializeTracerForTest(), + quotaService, supportbundlestest.NewFakeBundleService(), + ) require.NoError(t, err) org, err := orgService.CreateWithMember(context.Background(), &org.CreateOrgCommand{ diff --git a/pkg/services/sqlstore/bulk_test.go b/pkg/services/sqlstore/bulk_test.go index 1659ccd40ae..92063a1e88c 100644 --- a/pkg/services/sqlstore/bulk_test.go +++ b/pkg/services/sqlstore/bulk_test.go @@ -64,7 +64,7 @@ func TestIntegrationBulkOps(t *testing.T) { if testing.Short() { t.Skip("skipping integration test") } - db := InitTestDB(t) + db, _ := InitTestDB(t) err := db.engine.Sync(new(bulkTestItem)) require.NoError(t, err) diff --git a/pkg/services/sqlstore/migrations/user_mig.go b/pkg/services/sqlstore/migrations/user_mig.go index 77ed12e5876..8603314a3d1 100644 --- a/pkg/services/sqlstore/migrations/user_mig.go +++ b/pkg/services/sqlstore/migrations/user_mig.go @@ -5,7 +5,7 @@ import ( "xorm.io/xorm" - "github.com/grafana/grafana/pkg/services/sqlstore/migrations/user" + "github.com/grafana/grafana/pkg/services/sqlstore/migrations/usermig" . "github.com/grafana/grafana/pkg/services/sqlstore/migrator" "github.com/grafana/grafana/pkg/util" ) @@ -157,7 +157,10 @@ func addUserMigrations(mg *Migrator) { // Service accounts login were not unique per org. this migration is part of making it unique per org // to be able to create service accounts that are unique per org - mg.AddMigration(user.AllowSameLoginCrossOrgs, &user.ServiceAccountsSameLoginCrossOrgs{}) + mg.AddMigration(usermig.AllowSameLoginCrossOrgs, &usermig.ServiceAccountsSameLoginCrossOrgs{}) + + // Users login and email should be in lower case + mg.AddMigration(usermig.LowerCaseUserLoginAndEmail, &usermig.UsersLowerCaseLoginAndEmail{}) } const migSQLITEisServiceAccountNullable = `ALTER TABLE user ADD COLUMN tmp_service_account BOOLEAN DEFAULT 0; diff --git a/pkg/services/sqlstore/migrations/user/service_account_multiple_org_login_migrator.go b/pkg/services/sqlstore/migrations/usermig/service_account_multiple_org_login_migrator.go similarity index 99% rename from pkg/services/sqlstore/migrations/user/service_account_multiple_org_login_migrator.go rename to pkg/services/sqlstore/migrations/usermig/service_account_multiple_org_login_migrator.go index 064985c0f73..1d60a4f7f28 100644 --- a/pkg/services/sqlstore/migrations/user/service_account_multiple_org_login_migrator.go +++ b/pkg/services/sqlstore/migrations/usermig/service_account_multiple_org_login_migrator.go @@ -1,4 +1,4 @@ -package user +package usermig import ( "fmt" diff --git a/pkg/services/sqlstore/migrations/user/test/service_account_test.go b/pkg/services/sqlstore/migrations/usermig/test/service_account_test.go similarity index 98% rename from pkg/services/sqlstore/migrations/user/test/service_account_test.go rename to pkg/services/sqlstore/migrations/usermig/test/service_account_test.go index eab2f5ac444..043c9852245 100644 --- a/pkg/services/sqlstore/migrations/user/test/service_account_test.go +++ b/pkg/services/sqlstore/migrations/usermig/test/service_account_test.go @@ -8,7 +8,7 @@ import ( "github.com/stretchr/testify/require" "github.com/grafana/grafana/pkg/infra/log" - usermig "github.com/grafana/grafana/pkg/services/sqlstore/migrations/user" + "github.com/grafana/grafana/pkg/services/sqlstore/migrations/usermig" "github.com/grafana/grafana/pkg/services/sqlstore/migrator" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/setting" diff --git a/pkg/services/sqlstore/migrations/usermig/test/user_lowercase_login_and_email_test.go b/pkg/services/sqlstore/migrations/usermig/test/user_lowercase_login_and_email_test.go new file mode 100644 index 00000000000..33e206ca38c --- /dev/null +++ b/pkg/services/sqlstore/migrations/usermig/test/user_lowercase_login_and_email_test.go @@ -0,0 +1,253 @@ +package test + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/services/sqlstore/migrations/usermig" + "github.com/grafana/grafana/pkg/services/sqlstore/migrator" + "github.com/grafana/grafana/pkg/services/user" + "github.com/grafana/grafana/pkg/setting" +) + +func TestLowerCaseMigration(t *testing.T) { + type migrationTestCase struct { + desc string + users []*user.User + wantUsers []*user.User + } + testCases := []migrationTestCase{ + { + desc: "basic case updates login and email to lowercase", + users: []*user.User{ + { + ID: 1, + UID: "u1", + Login: "User1", + Email: "USER1@domain.com", + Name: "user1", + OrgID: 1, + Created: now, + Updated: now, + }, + { + ID: 2, + UID: "u2", + Login: "User2", + Email: "USER2@domain.com", + Name: "user2", + OrgID: 1, + Created: now, + Updated: now, + }, + }, + wantUsers: []*user.User{ + { + ID: 1, + Login: "user1", + Email: "user1@domain.com", + }, + { + ID: 2, + Login: "user2", + Email: "user2@domain.com", + }, + }, + }, + // "2 users - same login, one already lowercase" + { + desc: "2 users with same login one already has lowercase so we keep both", + users: []*user.User{ + { + ID: 1, + UID: "u1", + Login: "user1", + Email: "user1@email.com", + Name: "user1", + OrgID: 1, + Created: now, + Updated: now, + }, + { + ID: 2, + UID: "u2", + Login: "User1", + Email: "user1-new@email.com", + Name: "user2", + OrgID: 1, + Created: now, + Updated: now, + }, + }, + wantUsers: []*user.User{ + { + ID: 1, + Login: "user1", + Email: "user1@email.com", + }, + { + ID: 2, + Login: "User1", + Email: "user1-new@email.com", + }, + }, + }, + // "2 users - same login, one already lowercase" + { + desc: "2 users with same login one already has lowercase so we keep both case for uppercasing comes first in our loop", + users: []*user.User{ + { + ID: 1, + UID: "u1", + Login: "User1", + Email: "user1@email.com", + Name: "user1", + OrgID: 1, + Created: now, + Updated: now, + }, + { + ID: 2, + UID: "u2", + Login: "user1", + Email: "user1-new@email.com", + Name: "user2", + OrgID: 1, + Created: now, + Updated: now, + }, + }, + wantUsers: []*user.User{ + { + ID: 1, + Login: "User1", + Email: "user1@email.com", + }, + { + ID: 2, + Login: "user1", + Email: "user1-new@email.com", + }, + }, + }, + // "2 users - same email, one already lowercase" + { + desc: "2 users with same email one already has lowercase so we keep both", + users: []*user.User{ + { + ID: 1, + UID: "u1", + Login: "user1", + Email: "USER1@email.com", + Name: "user1", + OrgID: 1, + Created: now, + Updated: now, + }, + { + ID: 2, + UID: "u2", + Login: "user1-new-login", + Email: "user1@email.com", + Name: "user2", + OrgID: 1, + Created: now, + Updated: now, + }, + }, + wantUsers: []*user.User{ + { + ID: 1, + Login: "user1", + Email: "USER1@email.com", + }, + { + ID: 2, + Login: "user1-new-login", + Email: "user1@email.com", + }, + }, + }, + + // "2 users - same login, none lowercase" + { + desc: "2 users with same login noone is lowercased we pick the most recent user to lowercase", + users: []*user.User{ + { + ID: 1, + UID: "u1", + Login: "USER1", + Email: "user1@mail.com", + Name: "user1", + OrgID: 1, + Created: now.Add(-1 * time.Hour), + Updated: now.Add(-1 * time.Hour), + }, + { + ID: 2, + UID: "u2", + Login: "User1", + Email: "user1-new@mail.com", + Name: "user2", + OrgID: 1, + Created: now, + Updated: now, + }, + }, + wantUsers: []*user.User{ + { + ID: 1, + Login: "user1", + Email: "user1@mail.com", + }, + { + ID: 2, + Login: "User1", + Email: "user1-new@mail.com", + }, + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.desc, func(t *testing.T) { + // Run initial migration to have a working DB + x := setupTestDB(t) + // Remove migration + _, errDeleteMig := x.Exec(`DELETE FROM migration_log WHERE migration_id = ?`, usermig.LowerCaseUserLoginAndEmail) + require.NoError(t, errDeleteMig) + + // insert users + usersCount, err := x.Insert(tc.users) + require.NoError(t, err) + require.Equal(t, int64(len(tc.users)), usersCount) + + // run the migration + usermigrator := migrator.NewMigrator(x, &setting.Cfg{Logger: log.New("usermigration.test")}) + usermig.AddLowerCaseUserLoginAndEmail(usermigrator) + errRunningMig := usermigrator.Start(false, 0) + require.NoError(t, errRunningMig) + + // Check users + resultingUsers := []user.User{} + err = x.Table("user").Find(&resultingUsers) + require.NoError(t, err) + + // Check that the users have been updated + require.Equal(t, len(tc.wantUsers), len(resultingUsers)) + + for i := range tc.wantUsers { + for _, u := range resultingUsers { + if u.ID == tc.wantUsers[i].ID { + assert.Equal(t, tc.wantUsers[i].Login, u.Login) + assert.Equal(t, tc.wantUsers[i].Email, u.Email) + } + } + } + }) + } +} diff --git a/pkg/services/sqlstore/migrations/user/test/user_test.go b/pkg/services/sqlstore/migrations/usermig/test/user_test.go similarity index 100% rename from pkg/services/sqlstore/migrations/user/test/user_test.go rename to pkg/services/sqlstore/migrations/usermig/test/user_test.go diff --git a/pkg/services/sqlstore/migrations/usermig/user_lowercase_login_and_email.go b/pkg/services/sqlstore/migrations/usermig/user_lowercase_login_and_email.go new file mode 100644 index 00000000000..570b79ba52e --- /dev/null +++ b/pkg/services/sqlstore/migrations/usermig/user_lowercase_login_and_email.go @@ -0,0 +1,97 @@ +package usermig + +import ( + "strings" + + "github.com/grafana/grafana/pkg/services/sqlstore/migrator" + "github.com/grafana/grafana/pkg/services/user" + "xorm.io/xorm" +) + +const ( + LowerCaseUserLoginAndEmail = "update login and email fields to lowercase" +) + +// AddLowerCaseUserLoginAndEmail adds a migration that updates the login and email fields of all users to be in lower case. +func AddLowerCaseUserLoginAndEmail(mg *migrator.Migrator) { + mg.AddMigration(LowerCaseUserLoginAndEmail, &UsersLowerCaseLoginAndEmail{}) +} + +var _ migrator.CodeMigration = new(UsersLowerCaseLoginAndEmail) + +type UsersLowerCaseLoginAndEmail struct { + migrator.MigrationBase +} + +func (p *UsersLowerCaseLoginAndEmail) SQL(dialect migrator.Dialect) string { + return "code migration" +} + +func (p *UsersLowerCaseLoginAndEmail) Exec(sess *xorm.Session, mg *migrator.Migrator) error { + // Get all users + users := make([]*user.User, 0) + err := sess.Table("user").Find(&users) + if err != nil { + return err + } + processedLogins := make(map[string]bool) + processedEmails := make(map[string]bool) + + for _, usr := range users { + /* + LOGIN + */ + lowerLogin := strings.ToLower(usr.Login) + // only work through if login is not already in lower case + if usr.Login != lowerLogin && !processedLogins[lowerLogin] { + // Check if lower login exists + existingLowerCasedUserLogin := &user.User{} + + // lowercaseexists in database + hasLowerCasedLogin, err := sess.Table("user").Where("login = ?", lowerLogin).Get(existingLowerCasedUserLogin) + if err != nil { + return err + } + + // If exact login does not exist and lower case login does not exist, update the user's login to be in lower case + if !hasLowerCasedLogin { + uLogin := user.User{ + Name: usr.Name, + Login: lowerLogin, + } + _, err := sess.ID(usr.ID).Update(&uLogin) + if err != nil { + return err + } + } + } + processedLogins[lowerLogin] = true + + /* + EMAIL + */ + lowerEmail := strings.ToLower(usr.Email) + // only work through if email is not already in lower case + if usr.Email != lowerEmail && !processedEmails[lowerEmail] { + // Check if lower case email exists + existingUserEmail := &user.User{} + hasLowerCasedEmail, err := sess.Table("user").Where("email = ?", lowerEmail).Get(existingUserEmail) + if err != nil { + return err + } + // If lower case email does not exist, update the user's email to be in lower case + if !hasLowerCasedEmail { + uEmail := user.User{ + Name: usr.Name, + Email: lowerEmail, + } + _, err := sess.ID(usr.ID).Update(&uEmail) + if err != nil { + return err + } + } + } + processedEmails[lowerEmail] = true + } + return nil +} diff --git a/pkg/services/sqlstore/permissions/dashboard.go b/pkg/services/sqlstore/permissions/dashboard.go index 38f8ad92a60..293a19b6bcf 100644 --- a/pkg/services/sqlstore/permissions/dashboard.go +++ b/pkg/services/sqlstore/permissions/dashboard.go @@ -114,8 +114,21 @@ func (f *accessControlDashboardPermissionFilter) Where() (string, []any) { return f.where.string, f.where.params } +// Check if user has no permissions required for search to skip expensive query +func (f *accessControlDashboardPermissionFilter) hasRequiredActions() bool { + permissions := f.user.GetPermissions() + requiredActions := append(f.folderActions, f.dashboardActions...) + for _, action := range requiredActions { + if _, ok := permissions[action]; ok { + return true + } + } + + return false +} + func (f *accessControlDashboardPermissionFilter) buildClauses() { - if f.user == nil || f.user.IsNil() || len(f.user.GetPermissions()) == 0 { + if f.user == nil || f.user.IsNil() || !f.hasRequiredActions() { f.where = clause{string: "(1 = 0)"} return } diff --git a/pkg/services/sqlstore/permissions/dashboard_test.go b/pkg/services/sqlstore/permissions/dashboard_test.go index 00a77a7964e..1f214851880 100644 --- a/pkg/services/sqlstore/permissions/dashboard_test.go +++ b/pkg/services/sqlstore/permissions/dashboard_test.go @@ -697,13 +697,13 @@ func setupTest(t *testing.T, numFolders, numDashboards int, permissions []access func setupNestedTest(t *testing.T, usr *user.SignedInUser, perms []accesscontrol.Permission, orgID int64, features featuremgmt.FeatureToggles) db.DB { t.Helper() - db := sqlstore.InitTestDB(t) + db, cfg := db.InitTestDBWithCfg(t) // dashboard store commands that should be called. - dashStore, err := database.ProvideDashboardStore(db, db.Cfg, features, tagimpl.ProvideService(db), quotatest.New(false, nil)) + dashStore, err := database.ProvideDashboardStore(db, cfg, features, tagimpl.ProvideService(db), quotatest.New(false, nil)) require.NoError(t, err) - folderSvc := folderimpl.ProvideService(mock.New(), bus.ProvideBus(tracing.InitializeTracerForTest()), db.Cfg, dashStore, folderimpl.ProvideDashboardFolderStore(db), db, features, supportbundlestest.NewFakeBundleService(), nil) + folderSvc := folderimpl.ProvideService(mock.New(), bus.ProvideBus(tracing.InitializeTracerForTest()), cfg, dashStore, folderimpl.ProvideDashboardFolderStore(db), db, features, supportbundlestest.NewFakeBundleService(), nil) // create parent folder parent, err := folderSvc.Create(context.Background(), &folder.CreateFolderCommand{ diff --git a/pkg/services/sqlstore/permissions/dashboards_bench_test.go b/pkg/services/sqlstore/permissions/dashboards_bench_test.go index ca804da2462..7ced246ba09 100644 --- a/pkg/services/sqlstore/permissions/dashboards_bench_test.go +++ b/pkg/services/sqlstore/permissions/dashboards_bench_test.go @@ -77,14 +77,14 @@ func setupBenchMark(b *testing.B, usr user.SignedInUser, features featuremgmt.Fe nestingLevel = folder.MaxNestedFolderDepth } - store := db.InitTestDB(b) + store, cfg := db.InitTestDBWithCfg(b) quotaService := quotatest.New(false, nil) - dashboardWriteStore, err := database.ProvideDashboardStore(store, store.Cfg, features, tagimpl.ProvideService(store), quotaService) + dashboardWriteStore, err := database.ProvideDashboardStore(store, cfg, features, tagimpl.ProvideService(store), quotaService) require.NoError(b, err) - folderSvc := folderimpl.ProvideService(mock.New(), bus.ProvideBus(tracing.InitializeTracerForTest()), store.Cfg, dashboardWriteStore, folderimpl.ProvideDashboardFolderStore(store), store, features, supportbundlestest.NewFakeBundleService(), nil) + folderSvc := folderimpl.ProvideService(mock.New(), bus.ProvideBus(tracing.InitializeTracerForTest()), cfg, dashboardWriteStore, folderimpl.ProvideDashboardFolderStore(store), store, features, supportbundlestest.NewFakeBundleService(), nil) origNewGuardian := guardian.New guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{CanViewValue: true, CanSaveValue: true}) diff --git a/pkg/services/sqlstore/session_test.go b/pkg/services/sqlstore/session_test.go index df6a9f66d62..5021bb0230c 100644 --- a/pkg/services/sqlstore/session_test.go +++ b/pkg/services/sqlstore/session_test.go @@ -12,7 +12,7 @@ import ( ) func TestRetryingDisabled(t *testing.T) { - store := InitTestDB(t) + store, _ := InitTestDB(t) require.Equal(t, 0, store.dbCfg.QueryRetries) funcToTest := map[string]func(ctx context.Context, callback DBTransactionFunc) error{ @@ -63,7 +63,7 @@ func TestRetryingDisabled(t *testing.T) { } func TestRetryingOnFailures(t *testing.T) { - store := InitTestDB(t) + store, _ := InitTestDB(t) store.dbCfg.QueryRetries = 5 funcToTest := map[string]func(ctx context.Context, callback DBTransactionFunc) error{ diff --git a/pkg/services/sqlstore/sqlstore.go b/pkg/services/sqlstore/sqlstore.go index 59faffe6db4..4b17a54e37d 100644 --- a/pkg/services/sqlstore/sqlstore.go +++ b/pkg/services/sqlstore/sqlstore.go @@ -37,7 +37,7 @@ import ( type ContextSessionKey struct{} type SQLStore struct { - Cfg *setting.Cfg + cfg *setting.Cfg features featuremgmt.FeatureToggles sqlxsession *session.SessionDB @@ -45,7 +45,7 @@ type SQLStore struct { dbCfg *DatabaseConfig engine *xorm.Engine log log.Logger - Dialect migrator.Dialect + dialect migrator.Dialect skipEnsureDefaultOrgAndUser bool migrations registry.DatabaseMigrator tracer tracing.Tracer @@ -114,7 +114,7 @@ func NewSQLStoreWithoutSideEffects(cfg *setting.Cfg, func newSQLStore(cfg *setting.Cfg, engine *xorm.Engine, migrations registry.DatabaseMigrator, bus bus.Bus, tracer tracing.Tracer, opts ...InitTestDBOpt) (*SQLStore, error) { ss := &SQLStore{ - Cfg: cfg, + cfg: cfg, log: log.New("sqlstore"), skipEnsureDefaultOrgAndUser: false, migrations: migrations, @@ -131,7 +131,7 @@ func newSQLStore(cfg *setting.Cfg, engine *xorm.Engine, return nil, fmt.Errorf("%v: %w", "failed to connect to database", err) } - ss.Dialect = migrator.NewDialect(ss.engine.DriverName()) + ss.dialect = migrator.NewDialect(ss.engine.DriverName()) // if err := ss.Reset(); err != nil { // return nil, err @@ -155,7 +155,7 @@ func (ss *SQLStore) Migrate(isDatabaseLockingEnabled bool) error { return nil } - migrator := migrator.NewMigrator(ss.engine, ss.Cfg) + migrator := migrator.NewMigrator(ss.engine, ss.cfg) ss.migrations.AddMigration(migrator) return migrator.Start(isDatabaseLockingEnabled, ss.dbCfg.MigrationLockAttemptTimeout) @@ -178,7 +178,7 @@ func (ss *SQLStore) Quote(value string) string { // GetDialect return the dialect func (ss *SQLStore) GetDialect() migrator.Dialect { - return ss.Dialect + return ss.dialect } func (ss *SQLStore) GetDBType() core.DbType { @@ -210,7 +210,7 @@ func (ss *SQLStore) ensureMainOrgAndAdminUser(test bool) error { var stats stats.SystemUserCountStats // TODO: Should be able to rename "Count" to "count", for more standard SQL style // Just have to make sure it gets deserialized properly into models.SystemUserCountStats - rawSQL := `SELECT COUNT(id) AS Count FROM ` + ss.Dialect.Quote("user") + rawSQL := `SELECT COUNT(id) AS Count FROM ` + ss.dialect.Quote("user") if _, err := sess.SQL(rawSQL).Get(&stats); err != nil { return fmt.Errorf("could not determine if admin user exists: %w", err) } @@ -220,19 +220,19 @@ func (ss *SQLStore) ensureMainOrgAndAdminUser(test bool) error { } // ensure admin user - if !ss.Cfg.DisableInitAdminCreation { + if !ss.cfg.DisableInitAdminCreation { ss.log.Debug("Creating default admin user") if _, err := ss.createUser(ctx, sess, user.CreateUserCommand{ - Login: ss.Cfg.AdminUser, - Email: ss.Cfg.AdminEmail, - Password: user.Password(ss.Cfg.AdminPassword), + Login: ss.cfg.AdminUser, + Email: ss.cfg.AdminEmail, + Password: user.Password(ss.cfg.AdminPassword), IsAdmin: true, }); err != nil { return fmt.Errorf("failed to create admin user: %s", err) } - ss.log.Info("Created default admin", "user", ss.Cfg.AdminUser) + ss.log.Info("Created default admin", "user", ss.cfg.AdminUser) } ss.log.Debug("Creating default org", "name", mainOrgName) @@ -254,14 +254,14 @@ func (ss *SQLStore) initEngine(engine *xorm.Engine) error { return nil } - dbCfg, err := NewDatabaseConfig(ss.Cfg, ss.features) + dbCfg, err := NewDatabaseConfig(ss.cfg, ss.features) if err != nil { return err } ss.dbCfg = dbCfg - if ss.Cfg.DatabaseInstrumentQueries { + if ss.cfg.DatabaseInstrumentQueries { ss.dbCfg.Type = WrapDatabaseDriverWithHooks(ss.dbCfg.Type, ss.tracer) } @@ -316,7 +316,7 @@ func (ss *SQLStore) initEngine(engine *xorm.Engine) error { engine.SetConnMaxLifetime(time.Second * time.Duration(ss.dbCfg.ConnMaxLifetime)) // configure sql logging - debugSQL := ss.Cfg.Raw.Section("database").Key("log_queries").MustBool(false) + debugSQL := ss.cfg.Raw.Section("database").Key("log_queries").MustBool(false) if !debugSQL { engine.SetLogger(&xorm.DiscardLogger{}) } else { @@ -425,7 +425,7 @@ func InitTestDBWithMigration(t sqlutil.ITestDB, migration registry.DatabaseMigra } // InitTestDB initializes the test DB. -func InitTestDB(t sqlutil.ITestDB, opts ...InitTestDBOpt) *SQLStore { +func InitTestDB(t sqlutil.ITestDB, opts ...InitTestDBOpt) (*SQLStore, *setting.Cfg) { t.Helper() features := getFeaturesForTesting(opts...) cfg := getCfgForTesting(opts...) @@ -434,7 +434,7 @@ func InitTestDB(t sqlutil.ITestDB, opts ...InitTestDBOpt) *SQLStore { if err != nil { t.Fatalf("failed to initialize sql store: %s", err) } - return store + return store, store.cfg } func SetupTestDB() { @@ -605,9 +605,9 @@ func TestMain(m *testing.M) { } // nolint:staticcheck - testSQLStore.Cfg.IsFeatureToggleEnabled = features.IsEnabledGlobally + testSQLStore.cfg.IsFeatureToggleEnabled = features.IsEnabledGlobally - if err := testSQLStore.Dialect.TruncateDBTables(testSQLStore.GetEngine()); err != nil { + if err := testSQLStore.dialect.TruncateDBTables(testSQLStore.GetEngine()); err != nil { return nil, err } if err := testSQLStore.Reset(); err != nil { diff --git a/pkg/services/sqlstore/sqlstore_test.go b/pkg/services/sqlstore/sqlstore_test.go index 54da359d9f0..7c62a7a8c04 100644 --- a/pkg/services/sqlstore/sqlstore_test.go +++ b/pkg/services/sqlstore/sqlstore_test.go @@ -20,7 +20,7 @@ func TestMain(m *testing.M) { } func TestIntegrationIsUniqueConstraintViolation(t *testing.T) { - store := InitTestDB(t) + store, _ := InitTestDB(t) testCases := []struct { desc string @@ -32,12 +32,12 @@ func TestIntegrationIsUniqueConstraintViolation(t *testing.T) { // Attempt to insert org with provided ID (primary key) twice now := time.Now() org := org.Org{Name: "test org primary key violation", Created: now, Updated: now, ID: 42} - err := sess.InsertId(&org, store.Dialect) + err := sess.InsertId(&org, store.dialect) require.NoError(t, err) // Provide a different name to avoid unique constraint violation org.Name = "test org 2" - return sess.InsertId(&org, store.Dialect) + return sess.InsertId(&org, store.dialect) }, }, { @@ -46,12 +46,12 @@ func TestIntegrationIsUniqueConstraintViolation(t *testing.T) { // Attempt to insert org with reserved name now := time.Now() org := org.Org{Name: "test org unique constrain violation", Created: now, Updated: now, ID: 43} - err := sess.InsertId(&org, store.Dialect) + err := sess.InsertId(&org, store.dialect) require.NoError(t, err) // Provide a different ID to avoid primary key violation org.ID = 44 - return sess.InsertId(&org, store.Dialect) + return sess.InsertId(&org, store.dialect) }, }, } @@ -62,7 +62,7 @@ func TestIntegrationIsUniqueConstraintViolation(t *testing.T) { return tc.f(t, sess) }) require.Error(t, err) - assert.True(t, store.Dialect.IsUniqueConstraintViolation(err)) + assert.True(t, store.dialect.IsUniqueConstraintViolation(err)) }) } } diff --git a/pkg/services/sqlstore/transactions_test.go b/pkg/services/sqlstore/transactions_test.go index 508b0bb71db..bbf3ad3f85c 100644 --- a/pkg/services/sqlstore/transactions_test.go +++ b/pkg/services/sqlstore/transactions_test.go @@ -13,7 +13,7 @@ func TestIntegrationReuseSessionWithTransaction(t *testing.T) { if testing.Short() { t.Skip("skipping integration test") } - ss := InitTestDB(t) + ss, _ := InitTestDB(t) t.Run("top level transaction", func(t *testing.T) { var outerSession *DBSession @@ -73,7 +73,7 @@ func TestIntegrationPublishAfterCommitWithNestedTransactions(t *testing.T) { t.Skip("skipping integration test") } - ss := InitTestDB(t) + ss, _ := InitTestDB(t) ctx := context.Background() // On X success diff --git a/pkg/services/sqlstore/user.go b/pkg/services/sqlstore/user.go index 1b2a9249bd3..00861d9bc39 100644 --- a/pkg/services/sqlstore/user.go +++ b/pkg/services/sqlstore/user.go @@ -16,7 +16,7 @@ import ( const mainOrgName = "Main Org." func (ss *SQLStore) getOrgIDForNewUser(sess *DBSession, args user.CreateUserCommand) (int64, error) { - if ss.Cfg.AutoAssignOrg && args.OrgID != 0 { + if ss.cfg.AutoAssignOrg && args.OrgID != 0 { if err := verifyExistingOrg(sess, args.OrgID); err != nil { return -1, err } @@ -114,11 +114,11 @@ func (ss *SQLStore) createUser(ctx context.Context, sess *DBSession, args user.C Updated: time.Now(), } - if ss.Cfg.AutoAssignOrg && !usr.IsAdmin { + if ss.cfg.AutoAssignOrg && !usr.IsAdmin { if len(args.DefaultOrgRole) > 0 { orgUser.Role = org.RoleType(args.DefaultOrgRole) } else { - orgUser.Role = org.RoleType(ss.Cfg.AutoAssignOrgRole) + orgUser.Role = org.RoleType(ss.cfg.AutoAssignOrgRole) } } @@ -144,8 +144,8 @@ func verifyExistingOrg(sess *DBSession, orgId int64) error { func (ss *SQLStore) getOrCreateOrg(sess *DBSession, orgName string) (int64, error) { var org org.Org - if ss.Cfg.AutoAssignOrg { - has, err := sess.Where("id=?", ss.Cfg.AutoAssignOrgId).Get(&org) + if ss.cfg.AutoAssignOrg { + has, err := sess.Where("id=?", ss.cfg.AutoAssignOrgId).Get(&org) if err != nil { return 0, err } @@ -154,18 +154,18 @@ func (ss *SQLStore) getOrCreateOrg(sess *DBSession, orgName string) (int64, erro } ss.log.Debug("auto assigned organization not found") - if ss.Cfg.AutoAssignOrgId != 1 { + if ss.cfg.AutoAssignOrgId != 1 { ss.log.Error("Could not create user: organization ID does not exist", "orgID", - ss.Cfg.AutoAssignOrgId) + ss.cfg.AutoAssignOrgId) return 0, fmt.Errorf("could not create user: organization ID %d does not exist", - ss.Cfg.AutoAssignOrgId) + ss.cfg.AutoAssignOrgId) } org.Name = mainOrgName org.Created = time.Now() org.Updated = org.Created - org.ID = int64(ss.Cfg.AutoAssignOrgId) - if err := sess.InsertId(&org, ss.Dialect); err != nil { + org.ID = int64(ss.cfg.AutoAssignOrgId) + if err := sess.InsertId(&org, ss.dialect); err != nil { ss.log.Error("failed to insert organization with provided id", "org_id", org.ID, "err", err) // ignore failure if for some reason the organization exists if ss.GetDialect().IsUniqueConstraintViolation(err) { diff --git a/pkg/services/sqlstore/user_test.go b/pkg/services/sqlstore/user_test.go index 03131de4665..eb2e4fb7c9a 100644 --- a/pkg/services/sqlstore/user_test.go +++ b/pkg/services/sqlstore/user_test.go @@ -14,13 +14,13 @@ func TestIntegrationGetOrCreateOrg(t *testing.T) { if testing.Short() { t.Skip("Skipping integration test in short mode") } - ss := InitTestDB(t) + ss, _ := InitTestDB(t) err := ss.WithNewDbSession(context.Background(), func(sess *DBSession) error { // Create the org only: - ss.Cfg.AutoAssignOrg = true - ss.Cfg.DisableInitAdminCreation = true - ss.Cfg.AutoAssignOrgId = 1 + ss.cfg.AutoAssignOrg = true + ss.cfg.DisableInitAdminCreation = true + ss.cfg.AutoAssignOrgId = 1 createdOrgID, err := ss.getOrCreateOrg(sess, mainOrgName) require.NoError(t, err) require.Equal(t, int64(1), createdOrgID) diff --git a/pkg/services/ssosettings/ssosettingsimpl/service.go b/pkg/services/ssosettings/ssosettingsimpl/service.go index fbc6e06c3cd..1bb029821ad 100644 --- a/pkg/services/ssosettings/ssosettingsimpl/service.go +++ b/pkg/services/ssosettings/ssosettingsimpl/service.go @@ -8,6 +8,8 @@ import ( "strings" "time" + "github.com/prometheus/client_golang/prometheus" + "github.com/grafana/grafana/pkg/api/routing" "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/log" @@ -24,7 +26,6 @@ import ( "github.com/grafana/grafana/pkg/services/ssosettings/models" "github.com/grafana/grafana/pkg/services/ssosettings/strategies" "github.com/grafana/grafana/pkg/setting" - "github.com/prometheus/client_golang/prometheus" ) var _ ssosettings.Service = (*Service)(nil) @@ -37,9 +38,10 @@ type Service struct { secrets secrets.Service metrics *metrics - fbStrategies []ssosettings.FallbackStrategy - providersList []string - reloadables map[string]ssosettings.Reloadable + fbStrategies []ssosettings.FallbackStrategy + providersList []string + configurableProviders map[string]bool + reloadables map[string]ssosettings.Reloadable } func ProvideService(cfg *setting.Cfg, sqlStore db.DB, ac ac.AccessControl, @@ -50,27 +52,34 @@ func ProvideService(cfg *setting.Cfg, sqlStore db.DB, ac ac.AccessControl, strategies.NewOAuthStrategy(cfg), } + configurableProviders := make(map[string]bool) + for provider, enabled := range cfg.SSOSettingsConfigurableProviders { + configurableProviders[provider] = enabled + } + providersList := ssosettings.AllOAuthProviders if licensing.FeatureEnabled(social.SAMLProviderName) { fbStrategies = append(fbStrategies, strategies.NewSAMLStrategy(settingsProvider)) - if cfg.SSOSettingsConfigurableProviders[social.SAMLProviderName] { + if features.IsEnabledGlobally(featuremgmt.FlagSsoSettingsSAML) { providersList = append(providersList, social.SAMLProviderName) + configurableProviders[social.SAMLProviderName] = true } } store := database.ProvideStore(sqlStore) svc := &Service{ - logger: log.New("ssosettings.service"), - cfg: cfg, - store: store, - ac: ac, - fbStrategies: fbStrategies, - secrets: secrets, - metrics: newMetrics(registerer), - providersList: providersList, - reloadables: make(map[string]ssosettings.Reloadable), + logger: log.New("ssosettings.service"), + cfg: cfg, + store: store, + ac: ac, + fbStrategies: fbStrategies, + secrets: secrets, + metrics: newMetrics(registerer), + providersList: providersList, + configurableProviders: configurableProviders, + reloadables: make(map[string]ssosettings.Reloadable), } usageStats.RegisterMetricsFunc(svc.getUsageStats) @@ -160,7 +169,7 @@ func (s *Service) ListWithRedactedSecrets(ctx context.Context) ([]*models.SSOSet return nil, err } - configurableSettings := make([]*models.SSOSettings, 0, len(s.cfg.SSOSettingsConfigurableProviders)) + configurableSettings := make([]*models.SSOSettings, 0, len(s.configurableProviders)) for _, provider := range storeSettings { if s.isProviderConfigurable(provider.Provider) { configurableSettings = append(configurableSettings, provider) @@ -183,24 +192,28 @@ func (s *Service) Upsert(ctx context.Context, settings *models.SSOSettings, requ return ssosettings.ErrNotConfigurable } - social, ok := s.reloadables[settings.Provider] + reloadable, ok := s.reloadables[settings.Provider] if !ok { return ssosettings.ErrInvalidProvider.Errorf("provider %s not found in reloadables", settings.Provider) } - err := social.Validate(ctx, *settings, requester) - if err != nil { - return err - } - storedSettings, err := s.GetForProvider(ctx, settings.Provider) if err != nil { return err } - secrets := collectSecrets(settings, storedSettings) + settingsWithSecrets, err := mergeSecrets(settings.Settings, storedSettings.Settings) + if err != nil { + return err + } + settings.Settings = settingsWithSecrets - settings.Settings, err = s.encryptSecrets(ctx, settings.Settings, storedSettings.Settings) + err = reloadable.Validate(ctx, *settings, requester) + if err != nil { + return err + } + + settings.Settings, err = s.encryptSecrets(ctx, settings.Settings) if err != nil { return err } @@ -212,9 +225,9 @@ func (s *Service) Upsert(ctx context.Context, settings *models.SSOSettings, requ // make a copy of current settings for reload operation and apply overrides reloadSettings := *settings - reloadSettings.Settings = overrideMaps(storedSettings.Settings, settings.Settings, secrets) + reloadSettings.Settings = overrideMaps(storedSettings.Settings, settingsWithSecrets) - go s.reload(social, settings.Provider, reloadSettings) + go s.reload(reloadable, settings.Provider, reloadSettings) return nil } @@ -308,19 +321,15 @@ func (s *Service) getFallbackStrategyFor(provider string) (ssosettings.FallbackS return nil, false } -func (s *Service) encryptSecrets(ctx context.Context, settings, storedSettings map[string]any) (map[string]any, error) { +func (s *Service) encryptSecrets(ctx context.Context, settings map[string]any) (map[string]any, error) { result := make(map[string]any) for k, v := range settings { - if isSecret(k) && v != "" { + if IsSecretField(k) && v != "" { strValue, ok := v.(string) if !ok { return result, fmt.Errorf("failed to encrypt %s setting because it is not a string: %v", k, v) } - if !isNewSecretValue(strValue) { - strValue = storedSettings[k].(string) - } - encryptedSecret, err := s.secrets.Encrypt(ctx, []byte(strValue), secrets.WithoutScope()) if err != nil { return result, err @@ -405,7 +414,7 @@ func (s *Service) mergeSSOSettings(dbSettings, systemSettings *models.SSOSetting func (s *Service) decryptSecrets(ctx context.Context, settings map[string]any) (map[string]any, error) { for k, v := range settings { - if isSecret(k) && v != "" { + if IsSecretField(k) && v != "" { strValue, ok := v.(string) if !ok { s.logger.Error("Failed to parse secret value, it is not a string", "key", k) @@ -431,8 +440,8 @@ func (s *Service) decryptSecrets(ctx context.Context, settings map[string]any) ( } func (s *Service) isProviderConfigurable(provider string) bool { - _, ok := s.cfg.SSOSettingsConfigurableProviders[provider] - return ok + enabled, ok := s.configurableProviders[provider] + return ok && enabled } // removeSecrets removes all the secrets from the map and replaces them with a redacted password @@ -440,7 +449,7 @@ func (s *Service) isProviderConfigurable(provider string) bool { func removeSecrets(settings map[string]any) map[string]any { result := make(map[string]any) for k, v := range settings { - if isSecret(k) { + if IsSecretField(k) { result[k] = setting.RedactedPassword continue } @@ -461,7 +470,9 @@ func mergeSettings(storedSettings, systemSettings map[string]any) map[string]any for k, v := range systemSettings { if _, ok := settings[k]; !ok { - settings[k] = v + if isMergingAllowed(k) { + settings[k] = v + } } else if isURL(k) && isEmptyString(settings[k]) { // Overwrite all URL settings from the DB containing an empty string with their value // from the system settings. This fixes an issue with empty auth_url, api_url and token_url @@ -474,20 +485,40 @@ func mergeSettings(storedSettings, systemSettings map[string]any) map[string]any return settings } -// collectSecrets collects all the secrets from the request and the currently stored settings -// and returns a new map -func collectSecrets(settings *models.SSOSettings, storedSettings *models.SSOSettings) map[string]any { - secrets := map[string]any{} - for k, v := range settings.Settings { - if isSecret(k) { - if isNewSecretValue(v.(string)) { - secrets[k] = v.(string) // use the new value - continue - } - secrets[k] = storedSettings.Settings[k] // keep the currently stored value +// isMergingAllowed returns true if the field provided can be merged from the system settings. +// It won't allow SAML fields that are part of a group of settings to be merged from system settings +// because the DB settings already contain one valid setting from each group. +func isMergingAllowed(fieldName string) bool { + forbiddenMergePatterns := []string{"certificate", "private_key", "idp_metadata"} + + for _, v := range forbiddenMergePatterns { + if strings.Contains(strings.ToLower(fieldName), strings.ToLower(v)) { + return false } } - return secrets + return true +} + +// mergeSecrets returns a new map with the current value for secrets that have not been updated +func mergeSecrets(settings map[string]any, storedSettings map[string]any) (map[string]any, error) { + settingsWithSecrets := map[string]any{} + for k, v := range settings { + if IsSecretField(k) { + strValue, ok := v.(string) + if !ok { + return nil, fmt.Errorf("secret value is not a string") + } + + if isNewSecretValue(strValue) { + settingsWithSecrets[k] = strValue // use the new value + continue + } + settingsWithSecrets[k] = storedSettings[k] // keep the currently stored value + } else { + settingsWithSecrets[k] = v + } + } + return settingsWithSecrets, nil } func overrideMaps(maps ...map[string]any) map[string]any { @@ -500,7 +531,8 @@ func overrideMaps(maps ...map[string]any) map[string]any { return result } -func isSecret(fieldName string) bool { +// IsSecretField returns true if the SSO settings field provided is a secret +func IsSecretField(fieldName string) bool { secretFieldPatterns := []string{"secret", "private", "certificate"} for _, v := range secretFieldPatterns { diff --git a/pkg/services/ssosettings/ssosettingsimpl/service_test.go b/pkg/services/ssosettings/ssosettingsimpl/service_test.go index 8d576ce06f4..7f9cd0aba05 100644 --- a/pkg/services/ssosettings/ssosettingsimpl/service_test.go +++ b/pkg/services/ssosettings/ssosettingsimpl/service_test.go @@ -40,13 +40,15 @@ func TestService_GetForProvider(t *testing.T) { t.Parallel() testCases := []struct { - name string - setup func(env testEnv) - want *models.SSOSettings - wantErr bool + name string + provider string + setup func(env testEnv) + want *models.SSOSettings + wantErr bool }{ { - name: "should return successfully", + name: "should return successfully", + provider: "github", setup: func(env testEnv) { env.store.ExpectedSSOSetting = &models.SSOSettings{ Provider: "github", @@ -72,13 +74,15 @@ func TestService_GetForProvider(t *testing.T) { wantErr: false, }, { - name: "should return error if store returns an error different than not found", - setup: func(env testEnv) { env.store.ExpectedError = fmt.Errorf("error") }, - want: nil, - wantErr: true, + name: "should return error if store returns an error different than not found", + provider: "github", + setup: func(env testEnv) { env.store.ExpectedError = fmt.Errorf("error") }, + want: nil, + wantErr: true, }, { - name: "should fallback to the system settings if store returns not found", + name: "should fallback to the system settings if store returns not found", + provider: "github", setup: func(env testEnv) { env.store.ExpectedError = ssosettings.ErrNotFound env.fallbackStrategy.ExpectedIsMatch = true @@ -99,7 +103,8 @@ func TestService_GetForProvider(t *testing.T) { wantErr: false, }, { - name: "should return error if the fallback strategy was not found", + name: "should return error if the fallback strategy was not found", + provider: "github", setup: func(env testEnv) { env.store.ExpectedError = ssosettings.ErrNotFound env.fallbackStrategy.ExpectedIsMatch = false @@ -108,7 +113,8 @@ func TestService_GetForProvider(t *testing.T) { wantErr: true, }, { - name: "should return error if fallback strategy returns error", + name: "should return error if fallback strategy returns error", + provider: "github", setup: func(env testEnv) { env.store.ExpectedError = ssosettings.ErrNotFound env.fallbackStrategy.ExpectedIsMatch = true @@ -118,7 +124,8 @@ func TestService_GetForProvider(t *testing.T) { wantErr: true, }, { - name: "should decrypt secrets if data is coming from store", + name: "should decrypt secrets if data is coming from store", + provider: "github", setup: func(env testEnv) { env.store.ExpectedSSOSetting = &models.SSOSettings{ Provider: "github", @@ -152,7 +159,8 @@ func TestService_GetForProvider(t *testing.T) { wantErr: false, }, { - name: "should not decrypt secrets if data is coming from the fallback strategy", + name: "should not decrypt secrets if data is coming from the fallback strategy", + provider: "github", setup: func(env testEnv) { env.store.ExpectedError = ssosettings.ErrNotFound env.fallbackStrategy.ExpectedIsMatch = true @@ -176,7 +184,8 @@ func TestService_GetForProvider(t *testing.T) { wantErr: false, }, { - name: "should return an error if the data in the store is invalid", + name: "should return an error if the data in the store is invalid", + provider: "github", setup: func(env testEnv) { env.store.ExpectedSSOSetting = &models.SSOSettings{ Provider: "github", @@ -196,7 +205,8 @@ func TestService_GetForProvider(t *testing.T) { wantErr: true, }, { - name: "correctly merge the DB and system settings", + name: "correctly merge URLs from the DB and system settings", + provider: "github", setup: func(env testEnv) { env.store.ExpectedSSOSetting = &models.SSOSettings{ Provider: "github", @@ -231,6 +241,45 @@ func TestService_GetForProvider(t *testing.T) { }, wantErr: false, }, + { + name: "correctly merge group of settings for SAML", + provider: "saml", + setup: func(env testEnv) { + env.store.ExpectedSSOSetting = &models.SSOSettings{ + Provider: "saml", + Settings: map[string]any{ + "certificate": base64.RawStdEncoding.EncodeToString([]byte("valid-certificate")), + "private_key_path": base64.RawStdEncoding.EncodeToString([]byte("path/to/private/key")), + "idp_metadata_url": "https://idp-metadata.com", + }, + Source: models.DB, + } + env.fallbackStrategy.ExpectedIsMatch = true + env.fallbackStrategy.ExpectedConfigs = map[string]map[string]any{ + "saml": { + "name": "test-settings", + "certificate_path": "path/to/certificate", + "private_key": "this-is-a-valid-private-key", + "idp_metadata_path": "path/to/metadata", + "max_issue_delay": "1h", + }, + } + env.secrets.On("Decrypt", mock.Anything, []byte("valid-certificate"), mock.Anything).Return([]byte("decrypted-valid-certificate"), nil).Once() + env.secrets.On("Decrypt", mock.Anything, []byte("path/to/private/key"), mock.Anything).Return([]byte("decrypted/path/to/private/key"), nil).Once() + }, + want: &models.SSOSettings{ + Provider: "saml", + Settings: map[string]any{ + "name": "test-settings", + "certificate": "decrypted-valid-certificate", + "private_key_path": "decrypted/path/to/private/key", + "idp_metadata_url": "https://idp-metadata.com", + "max_issue_delay": "1h", + }, + Source: models.DB, + }, + wantErr: false, + }, } for _, tc := range testCases { @@ -241,12 +290,12 @@ func TestService_GetForProvider(t *testing.T) { t.Run(tc.name, func(t *testing.T) { t.Parallel() - env := setupTestEnv(t, false, false, nil) + env := setupTestEnv(t, true, false, true) if tc.setup != nil { tc.setup(env) } - actual, err := env.service.GetForProvider(context.Background(), "github") + actual, err := env.service.GetForProvider(context.Background(), tc.provider) if tc.wantErr { require.Error(t, err) @@ -350,7 +399,7 @@ func TestService_GetForProviderWithRedactedSecrets(t *testing.T) { t.Run(tc.name, func(t *testing.T) { t.Parallel() - env := setupTestEnv(t, false, false, nil) + env := setupTestEnv(t, false, false, false) if tc.setup != nil { tc.setup(env) } @@ -501,7 +550,7 @@ func TestService_List(t *testing.T) { t.Run(tc.name, func(t *testing.T) { t.Parallel() - env := setupTestEnv(t, false, false, nil) + env := setupTestEnv(t, false, false, false) if tc.setup != nil { tc.setup(env) } @@ -803,7 +852,7 @@ func TestService_ListWithRedactedSecrets(t *testing.T) { t.Run(tc.name, func(t *testing.T) { t.Parallel() - env := setupTestEnv(t, false, false, nil) + env := setupTestEnv(t, false, false, false) if tc.setup != nil { tc.setup(env) } @@ -827,7 +876,7 @@ func TestService_Upsert(t *testing.T) { t.Run("successfully upsert SSO settings", func(t *testing.T) { t.Parallel() - env := setupTestEnv(t, false, false, nil) + env := setupTestEnv(t, false, false, false) provider := social.AzureADProviderName settings := models.SSOSettings{ @@ -890,7 +939,7 @@ func TestService_Upsert(t *testing.T) { t.Run("returns error if provider is not configurable", func(t *testing.T) { t.Parallel() - env := setupTestEnv(t, false, false, nil) + env := setupTestEnv(t, false, false, false) provider := social.GrafanaComProviderName settings := &models.SSOSettings{ @@ -913,7 +962,7 @@ func TestService_Upsert(t *testing.T) { t.Run("returns error if provider was not found in reloadables", func(t *testing.T) { t.Parallel() - env := setupTestEnv(t, false, false, nil) + env := setupTestEnv(t, false, false, false) provider := social.AzureADProviderName settings := &models.SSOSettings{ @@ -937,7 +986,7 @@ func TestService_Upsert(t *testing.T) { t.Run("returns error if validation fails", func(t *testing.T) { t.Parallel() - env := setupTestEnv(t, false, false, nil) + env := setupTestEnv(t, false, false, false) provider := social.AzureADProviderName settings := models.SSOSettings{ @@ -961,7 +1010,7 @@ func TestService_Upsert(t *testing.T) { t.Run("returns error if a fallback strategy is not available for the provider", func(t *testing.T) { t.Parallel() - env := setupTestEnv(t, false, false, nil) + env := setupTestEnv(t, false, false, false) settings := &models.SSOSettings{ Provider: social.AzureADProviderName, @@ -979,10 +1028,33 @@ func TestService_Upsert(t *testing.T) { require.Error(t, err) }) + t.Run("returns error if a secret does not have the type string", func(t *testing.T) { + t.Parallel() + + env := setupTestEnv(t, false, false, false) + + provider := social.OktaProviderName + settings := models.SSOSettings{ + Provider: provider, + Settings: map[string]any{ + "client_id": "client-id", + "client_secret": 123, + "enabled": true, + }, + IsDeleted: false, + } + + reloadable := ssosettingstests.NewMockReloadable(t) + env.reloadables[provider] = reloadable + + err := env.service.Upsert(context.Background(), &settings, &user.SignedInUser{}) + require.Error(t, err) + }) + t.Run("returns error if secrets encryption failed", func(t *testing.T) { t.Parallel() - env := setupTestEnv(t, false, false, nil) + env := setupTestEnv(t, false, false, false) provider := social.OktaProviderName settings := models.SSOSettings{ @@ -1007,7 +1079,7 @@ func TestService_Upsert(t *testing.T) { t.Run("should not update the current secret if the secret has not been updated", func(t *testing.T) { t.Parallel() - env := setupTestEnv(t, false, false, nil) + env := setupTestEnv(t, false, false, false) provider := social.AzureADProviderName settings := models.SSOSettings{ @@ -1027,8 +1099,15 @@ func TestService_Upsert(t *testing.T) { }, } + expected := settings + expected.Settings = make(map[string]any) + for key, value := range settings.Settings { + expected.Settings[key] = value + } + expected.Settings["client_secret"] = "encrypted-client-secret" + reloadable := ssosettingstests.NewMockReloadable(t) - reloadable.On("Validate", mock.Anything, settings, mock.Anything).Return(nil) + reloadable.On("Validate", mock.Anything, expected, mock.Anything).Return(nil) reloadable.On("Reload", mock.Anything, mock.Anything).Return(nil).Maybe() env.reloadables[provider] = reloadable env.secrets.On("Decrypt", mock.Anything, []byte("current-client-secret"), mock.Anything).Return([]byte("encrypted-client-secret"), nil).Once() @@ -1037,14 +1116,67 @@ func TestService_Upsert(t *testing.T) { err := env.service.Upsert(context.Background(), &settings, &user.SignedInUser{}) require.NoError(t, err) - settings.Settings["client_secret"] = base64.RawStdEncoding.EncodeToString([]byte("current-client-secret")) - require.EqualValues(t, settings, env.store.ActualSSOSettings) + expected.Settings["client_secret"] = base64.RawStdEncoding.EncodeToString([]byte("current-client-secret")) + require.EqualValues(t, expected, env.store.ActualSSOSettings) + }) + + t.Run("run validation with all new and current secrets available in settings", func(t *testing.T) { + t.Parallel() + + env := setupTestEnv(t, false, false, false) + + provider := social.AzureADProviderName + settings := models.SSOSettings{ + Provider: provider, + Settings: map[string]any{ + "client_secret": setting.RedactedPassword, + "private_key": setting.RedactedPassword, + "certificate": "new-certificate", + }, + IsDeleted: false, + } + + env.store.ExpectedSSOSetting = &models.SSOSettings{ + Provider: provider, + Settings: map[string]any{ + "client_secret": base64.RawStdEncoding.EncodeToString([]byte("encrypted-current-client-secret")), + "private_key": base64.RawStdEncoding.EncodeToString([]byte("encrypted-current-private-key")), + "certificate": base64.RawStdEncoding.EncodeToString([]byte("encrypted-current-certificate")), + }, + } + + expected := settings + expected.Settings = make(map[string]any) + for key, value := range settings.Settings { + expected.Settings[key] = value + } + expected.Settings["client_secret"] = "current-client-secret" + expected.Settings["private_key"] = "current-private-key" + + reloadable := ssosettingstests.NewMockReloadable(t) + reloadable.On("Validate", mock.Anything, expected, mock.Anything).Return(nil) + reloadable.On("Reload", mock.Anything, mock.Anything).Return(nil).Maybe() + env.reloadables[provider] = reloadable + env.secrets.On("Decrypt", mock.Anything, []byte("encrypted-current-client-secret"), mock.Anything).Return([]byte("current-client-secret"), nil).Once() + env.secrets.On("Decrypt", mock.Anything, []byte("encrypted-current-private-key"), mock.Anything).Return([]byte("current-private-key"), nil).Once() + env.secrets.On("Decrypt", mock.Anything, []byte("encrypted-current-certificate"), mock.Anything).Return([]byte("current-certificate"), nil).Once() + env.secrets.On("Encrypt", mock.Anything, []byte("current-client-secret"), mock.Anything).Return([]byte("encrypted-current-client-secret"), nil).Once() + env.secrets.On("Encrypt", mock.Anything, []byte("current-private-key"), mock.Anything).Return([]byte("encrypted-current-private-key"), nil).Once() + env.secrets.On("Encrypt", mock.Anything, []byte("new-certificate"), mock.Anything).Return([]byte("encrypted-new-certificate"), nil).Once() + + err := env.service.Upsert(context.Background(), &settings, &user.SignedInUser{}) + require.NoError(t, err) + + expected.Settings["client_secret"] = base64.RawStdEncoding.EncodeToString([]byte("encrypted-current-client-secret")) + expected.Settings["private_key"] = base64.RawStdEncoding.EncodeToString([]byte("encrypted-current-private-key")) + expected.Settings["certificate"] = base64.RawStdEncoding.EncodeToString([]byte("encrypted-new-certificate")) + require.EqualValues(t, expected, env.store.ActualSSOSettings) }) t.Run("returns error if store failed to upsert settings", func(t *testing.T) { t.Parallel() - env := setupTestEnv(t, false, false, nil) + env := setupTestEnv(t, false, false, false) provider := social.AzureADProviderName settings := models.SSOSettings{ @@ -1076,7 +1208,7 @@ func TestService_Upsert(t *testing.T) { t.Run("successfully upsert SSO settings if reload fails", func(t *testing.T) { t.Parallel() - env := setupTestEnv(t, false, false, nil) + env := setupTestEnv(t, false, false, false) provider := social.AzureADProviderName settings := models.SSOSettings{ @@ -1109,7 +1241,7 @@ func TestService_Delete(t *testing.T) { t.Run("successfully delete SSO settings", func(t *testing.T) { t.Parallel() - env := setupTestEnv(t, false, false, nil) + env := setupTestEnv(t, false, false, false) var wg sync.WaitGroup wg.Add(1) @@ -1147,7 +1279,7 @@ func TestService_Delete(t *testing.T) { t.Run("return error if SSO setting was not found for the specified provider", func(t *testing.T) { t.Parallel() - env := setupTestEnv(t, false, false, nil) + env := setupTestEnv(t, false, false, false) provider := social.AzureADProviderName reloadable := ssosettingstests.NewMockReloadable(t) @@ -1163,7 +1295,7 @@ func TestService_Delete(t *testing.T) { t.Run("should not delete the SSO settings if the provider is not configurable", func(t *testing.T) { t.Parallel() - env := setupTestEnv(t, false, false, nil) + env := setupTestEnv(t, false, false, false) env.cfg.SSOSettingsConfigurableProviders = map[string]bool{social.AzureADProviderName: true} provider := social.GrafanaComProviderName @@ -1176,7 +1308,7 @@ func TestService_Delete(t *testing.T) { t.Run("return error when store fails to delete the SSO settings for the specified provider", func(t *testing.T) { t.Parallel() - env := setupTestEnv(t, false, false, nil) + env := setupTestEnv(t, false, false, false) provider := social.AzureADProviderName env.store.ExpectedError = errors.New("delete sso settings failed") @@ -1189,7 +1321,7 @@ func TestService_Delete(t *testing.T) { t.Run("return successfully when the deletion was successful but reloading the settings fail", func(t *testing.T) { t.Parallel() - env := setupTestEnv(t, false, false, nil) + env := setupTestEnv(t, false, false, false) provider := social.AzureADProviderName reloadable := ssosettingstests.NewMockReloadable(t) @@ -1211,7 +1343,7 @@ func TestService_DoReload(t *testing.T) { t.Run("successfully reload settings", func(t *testing.T) { t.Parallel() - env := setupTestEnv(t, false, false, nil) + env := setupTestEnv(t, false, false, false) settingsList := []*models.SSOSettings{ { @@ -1251,7 +1383,7 @@ func TestService_DoReload(t *testing.T) { t.Run("successfully reload settings when some providers have empty settings", func(t *testing.T) { t.Parallel() - env := setupTestEnv(t, false, false, nil) + env := setupTestEnv(t, false, false, false) settingsList := []*models.SSOSettings{ { @@ -1281,7 +1413,7 @@ func TestService_DoReload(t *testing.T) { t.Run("failed fetching the SSO settings", func(t *testing.T) { t.Parallel() - env := setupTestEnv(t, false, false, nil) + env := setupTestEnv(t, false, false, false) provider := "github" @@ -1382,7 +1514,7 @@ func TestService_decryptSecrets(t *testing.T) { t.Run(tc.name, func(t *testing.T) { t.Parallel() - env := setupTestEnv(t, false, false, nil) + env := setupTestEnv(t, false, false, false) if tc.setup != nil { tc.setup(env) @@ -1407,12 +1539,12 @@ func Test_ProviderService(t *testing.T) { tests := []struct { name string isLicenseEnabled bool - configurableProviders map[string]bool + samlEnabled bool expectedProvidersList []string strategiesLength int }{ { - name: "should return all OAuth providers but not saml because the licensing feature is not enabled", + name: "should return all OAuth providers but not SAML because the licensing feature is not enabled", isLicenseEnabled: false, expectedProvidersList: []string{ "github", @@ -1426,7 +1558,7 @@ func Test_ProviderService(t *testing.T) { strategiesLength: 1, }, { - name: "should return all fallback strategies and it should return all OAuth providers but not saml because the licensing feature is enabled but the configurable provider is not setup", + name: "should return all fallback strategies and it should return all OAuth providers but not SAML because the licensing feature is enabled but the configurable provider is not setup", isLicenseEnabled: true, expectedProvidersList: []string{ "github", @@ -1440,9 +1572,9 @@ func Test_ProviderService(t *testing.T) { strategiesLength: 2, }, { - name: "should return all fallback strategies and it should return all OAuth providers and saml because the licensing feature is enabled and the provider is setup", - isLicenseEnabled: true, - configurableProviders: map[string]bool{"saml": true}, + name: "should return all fallback strategies and it should return all OAuth providers and SAML because the licensing feature is enabled and the provider is setup", + isLicenseEnabled: true, + samlEnabled: true, expectedProvidersList: []string{ "github", "gitlab", @@ -1461,7 +1593,7 @@ func Test_ProviderService(t *testing.T) { t.Run(tc.name, func(t *testing.T) { t.Parallel() - env := setupTestEnv(t, tc.isLicenseEnabled, true, tc.configurableProviders) + env := setupTestEnv(t, tc.isLicenseEnabled, true, tc.samlEnabled) require.Equal(t, tc.expectedProvidersList, env.service.providersList) require.Equal(t, tc.strategiesLength, len(env.service.fbStrategies)) @@ -1469,7 +1601,7 @@ func Test_ProviderService(t *testing.T) { } } -func setupTestEnv(t *testing.T, isLicensingEnabled, keepFallbackStratergies bool, extraConfigurableProviders map[string]bool) testEnv { +func setupTestEnv(t *testing.T, isLicensingEnabled, keepFallbackStratergies, samlEnabled bool) testEnv { t.Helper() store := ssosettingstests.NewFakeStore() @@ -1491,10 +1623,6 @@ func setupTestEnv(t *testing.T, isLicensingEnabled, keepFallbackStratergies bool "gitlab": true, } - for k, v := range extraConfigurableProviders { - configurableProviders[k] = v - } - cfg := &setting.Cfg{ SSOSettingsConfigurableProviders: configurableProviders, Raw: iniFile, @@ -1503,12 +1631,17 @@ func setupTestEnv(t *testing.T, isLicensingEnabled, keepFallbackStratergies bool licensing := licensingtest.NewFakeLicensing() licensing.On("FeatureEnabled", "saml").Return(isLicensingEnabled) + featureManager := featuremgmt.WithManager() + if samlEnabled { + featureManager = featuremgmt.WithManager(featuremgmt.FlagSsoSettingsSAML) + } + svc := ProvideService( cfg, &dbtest.FakeDB{}, accessControl, routing.NewRouteRegister(), - featuremgmt.WithManager(nil), + featureManager, secretsFakes.NewMockService(t), &usagestats.UsageStatsMock{}, prometheus.NewRegistry(), diff --git a/pkg/services/stats/statsimpl/stats_test.go b/pkg/services/stats/statsimpl/stats_test.go index 36565ee1835..e63d51fddc0 100644 --- a/pkg/services/stats/statsimpl/stats_test.go +++ b/pkg/services/stats/statsimpl/stats_test.go @@ -16,7 +16,6 @@ import ( "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/org/orgimpl" "github.com/grafana/grafana/pkg/services/quota/quotatest" - "github.com/grafana/grafana/pkg/services/sqlstore" "github.com/grafana/grafana/pkg/services/stats" "github.com/grafana/grafana/pkg/services/supportbundles/supportbundlestest" "github.com/grafana/grafana/pkg/services/user" @@ -33,9 +32,9 @@ func TestIntegrationStatsDataAccess(t *testing.T) { if testing.Short() { t.Skip("skipping integration test") } - db := sqlstore.InitTestDB(t) + db, cfg := db.InitTestDBWithCfg(t) statsService := &sqlStatsService{db: db} - populateDB(t, db, db.Cfg) + populateDB(t, db, cfg) t.Run("Get system stats should not results in error", func(t *testing.T) { query := stats.GetSystemStatsQuery{} @@ -50,7 +49,7 @@ func TestIntegrationStatsDataAccess(t *testing.T) { assert.Equal(t, int64(0), result.APIKeys) assert.Equal(t, int64(2), result.Correlations) assert.NotNil(t, result.DatabaseCreatedTime) - assert.Equal(t, db.Dialect.DriverName(), result.DatabaseDriver) + assert.Equal(t, db.GetDialect().DriverName(), result.DatabaseDriver) }) t.Run("Get system user count stats should not results in error", func(t *testing.T) { @@ -88,7 +87,10 @@ func populateDB(t *testing.T, db db.DB, cfg *setting.Cfg) { t.Helper() orgService, _ := orgimpl.ProvideService(db, cfg, quotatest.New(false, nil)) - userSvc, _ := userimpl.ProvideService(db, orgService, cfg, nil, nil, "atest.FakeQuotaService{}, supportbundlestest.NewFakeBundleService()) + userSvc, _ := userimpl.ProvideService( + db, orgService, cfg, nil, nil, tracing.InitializeTracerForTest(), + "atest.FakeQuotaService{}, supportbundlestest.NewFakeBundleService(), + ) bus := bus.ProvideBus(tracing.InitializeTracerForTest()) correlationsSvc := correlationstest.New(db, cfg, bus) @@ -155,8 +157,8 @@ func TestIntegration_GetAdminStats(t *testing.T) { if testing.Short() { t.Skip("skipping integration test") } - db := sqlstore.InitTestDB(t) - statsService := ProvideService(&setting.Cfg{}, db) + db, cfg := db.InitTestDBWithCfg(t) + statsService := ProvideService(cfg, db) query := stats.GetAdminStatsQuery{} _, err := statsService.GetAdminStats(context.Background(), &query) diff --git a/pkg/services/store/entity/client_wrapper.go b/pkg/services/store/entity/client_wrapper.go index 18d151e9851..a4b7572d7a2 100644 --- a/pkg/services/store/entity/client_wrapper.go +++ b/pkg/services/store/entity/client_wrapper.go @@ -3,7 +3,7 @@ package entity import ( "github.com/fullstorydev/grpchan" "github.com/fullstorydev/grpchan/inprocgrpc" - grpcAuth "github.com/grpc-ecosystem/go-grpc-middleware/auth" + grpcAuth "github.com/grpc-ecosystem/go-grpc-middleware/v2/interceptors/auth" "google.golang.org/grpc" grpcUtils "github.com/grafana/grafana/pkg/services/store/entity/grpc" diff --git a/pkg/services/store/entity/entity.pb.go b/pkg/services/store/entity/entity.pb.go index 76a04593bd1..1674b8d7fdc 100644 --- a/pkg/services/store/entity/entity.pb.go +++ b/pkg/services/store/entity/entity.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.32.0 -// protoc v4.25.2 +// protoc-gen-go v1.33.0 +// protoc v5.26.1 // source: entity.proto package entity diff --git a/pkg/services/store/entity/entity_grpc.pb.go b/pkg/services/store/entity/entity_grpc.pb.go index e76302c91dc..c1ab389ca2d 100644 --- a/pkg/services/store/entity/entity_grpc.pb.go +++ b/pkg/services/store/entity/entity_grpc.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go-grpc. DO NOT EDIT. // versions: // - protoc-gen-go-grpc v1.3.0 -// - protoc v4.25.2 +// - protoc v5.26.1 // source: entity.proto package entity diff --git a/pkg/services/store/entity/sqlstash/sql_storage_server_test.go b/pkg/services/store/entity/sqlstash/sql_storage_server_test.go index 00353826fad..bd4558e553e 100644 --- a/pkg/services/store/entity/sqlstash/sql_storage_server_test.go +++ b/pkg/services/store/entity/sqlstash/sql_storage_server_test.go @@ -11,7 +11,6 @@ import ( "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/store/entity" "github.com/grafana/grafana/pkg/services/store/entity/db/dbimpl" - "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/tests/testsuite" ) @@ -125,15 +124,15 @@ func TestCreate(t *testing.T) { } func setUpTestServer(t *testing.T) entity.EntityStoreServer { - sqlStore := db.InitTestDB(t) + sqlStore, cfg := db.InitTestDBWithCfg(t) entityDB, err := dbimpl.ProvideEntityDB( sqlStore, - setting.NewCfg(), + cfg, featuremgmt.WithFeatures(featuremgmt.FlagUnifiedStorage)) require.NoError(t, err) - traceConfig, err := tracing.ParseTracingConfig(sqlStore.Cfg) + traceConfig, err := tracing.ParseTracingConfig(cfg) require.NoError(t, err) tracer, err := tracing.ProvideService(traceConfig) require.NoError(t, err) diff --git a/pkg/services/team/model.go b/pkg/services/team/model.go index a4d4fe0a6a6..0426368de17 100644 --- a/pkg/services/team/model.go +++ b/pkg/services/team/model.go @@ -21,6 +21,8 @@ var ( ErrTeamMemberAlreadyAdded = errors.New("user is already added to this team") ) +const MemberPermissionName = "Member" + // Team model type Team struct { ID int64 `json:"id" xorm:"pk autoincr 'id'"` @@ -124,16 +126,10 @@ type TeamMember struct { type AddTeamMemberCommand struct { UserID int64 `json:"userId" binding:"Required"` - OrgID int64 `json:"-"` - TeamID int64 `json:"-"` - External bool `json:"-"` Permission dashboardaccess.PermissionType `json:"-"` } type UpdateTeamMemberCommand struct { - UserID int64 `json:"-"` - OrgID int64 `json:"-"` - TeamID int64 `json:"-"` Permission dashboardaccess.PermissionType `json:"permission"` } diff --git a/pkg/services/team/team.go b/pkg/services/team/team.go index 5f1bca40244..3a44a8883fa 100644 --- a/pkg/services/team/team.go +++ b/pkg/services/team/team.go @@ -2,22 +2,17 @@ package team import ( "context" - - "github.com/grafana/grafana/pkg/services/dashboards/dashboardaccess" ) type Service interface { - CreateTeam(name, email string, orgID int64) (Team, error) + CreateTeam(ctx context.Context, name, email string, orgID int64) (Team, error) UpdateTeam(ctx context.Context, cmd *UpdateTeamCommand) error DeleteTeam(ctx context.Context, cmd *DeleteTeamCommand) error SearchTeams(ctx context.Context, query *SearchTeamsQuery) (SearchTeamQueryResult, error) GetTeamByID(ctx context.Context, query *GetTeamByIDQuery) (*TeamDTO, error) GetTeamsByUser(ctx context.Context, query *GetTeamsByUserQuery) ([]*TeamDTO, error) GetTeamIDsByUser(ctx context.Context, query *GetTeamIDsByUserQuery) ([]int64, error) - AddTeamMember(ctx context.Context, userID, orgID, teamID int64, isExternal bool, permission dashboardaccess.PermissionType) error - UpdateTeamMember(ctx context.Context, cmd *UpdateTeamMemberCommand) error - IsTeamMember(orgId int64, teamId int64, userId int64) (bool, error) - RemoveTeamMember(ctx context.Context, cmd *RemoveTeamMemberCommand) error + IsTeamMember(ctx context.Context, orgId int64, teamId int64, userId int64) (bool, error) RemoveUsersMemberships(tx context.Context, userID int64) error GetUserTeamMemberships(ctx context.Context, orgID, userID int64, external bool) ([]*TeamMemberDTO, error) GetTeamMembers(ctx context.Context, query *GetTeamMembersQuery) ([]*TeamMemberDTO, error) diff --git a/pkg/services/team/teamapi/team.go b/pkg/services/team/teamapi/team.go index 70fcc9ae8e4..0f5911a9198 100644 --- a/pkg/services/team/teamapi/team.go +++ b/pkg/services/team/teamapi/team.go @@ -34,7 +34,7 @@ func (tapi *TeamAPI) createTeam(c *contextmodel.ReqContext) response.Response { return response.Error(http.StatusBadRequest, "bad request data", err) } - t, err := tapi.teamService.CreateTeam(cmd.Name, cmd.Email, c.SignedInUser.GetOrgID()) + t, err := tapi.teamService.CreateTeam(c.Req.Context(), cmd.Name, cmd.Email, c.SignedInUser.GetOrgID()) if err != nil { if errors.Is(err, team.ErrTeamNameTaken) { return response.Error(http.StatusConflict, "Team name taken", err) diff --git a/pkg/services/team/teamapi/team_members.go b/pkg/services/team/teamapi/team_members.go index 89419b8ff09..92be74bb551 100644 --- a/pkg/services/team/teamapi/team_members.go +++ b/pkg/services/team/teamapi/team_members.go @@ -77,13 +77,12 @@ func (tapi *TeamAPI) addTeamMember(c *contextmodel.ReqContext) response.Response if err := web.Bind(c.Req, &cmd); err != nil { return response.Error(http.StatusBadRequest, "bad request data", err) } - cmd.OrgID = c.SignedInUser.GetOrgID() - cmd.TeamID, err = strconv.ParseInt(web.Params(c.Req)[":teamId"], 10, 64) + teamID, err := strconv.ParseInt(web.Params(c.Req)[":teamId"], 10, 64) if err != nil { return response.Error(http.StatusBadRequest, "teamId is invalid", err) } - isTeamMember, err := tapi.teamService.IsTeamMember(c.SignedInUser.GetOrgID(), cmd.TeamID, cmd.UserID) + isTeamMember, err := tapi.teamService.IsTeamMember(c.Req.Context(), c.SignedInUser.GetOrgID(), teamID, cmd.UserID) if err != nil { return response.Error(http.StatusInternalServerError, "Failed to add team member.", err) } @@ -91,7 +90,7 @@ func (tapi *TeamAPI) addTeamMember(c *contextmodel.ReqContext) response.Response return response.Error(http.StatusBadRequest, "User is already added to this team", nil) } - err = addOrUpdateTeamMember(c.Req.Context(), tapi.teamPermissionsService, cmd.UserID, cmd.OrgID, cmd.TeamID, getPermissionName(cmd.Permission)) + err = addOrUpdateTeamMember(c.Req.Context(), tapi.teamPermissionsService, cmd.UserID, c.SignedInUser.GetOrgID(), teamID, team.MemberPermissionName) if err != nil { return response.Error(http.StatusInternalServerError, "Failed to add Member to Team", err) } @@ -126,7 +125,7 @@ func (tapi *TeamAPI) updateTeamMember(c *contextmodel.ReqContext) response.Respo } orgId := c.SignedInUser.GetOrgID() - isTeamMember, err := tapi.teamService.IsTeamMember(orgId, teamId, userId) + isTeamMember, err := tapi.teamService.IsTeamMember(c.Req.Context(), orgId, teamId, userId) if err != nil { return response.Error(http.StatusInternalServerError, "Failed to update team member.", err) } diff --git a/pkg/services/team/teamimpl/store.go b/pkg/services/team/teamimpl/store.go index f8a85618d99..2076c5cf95d 100644 --- a/pkg/services/team/teamimpl/store.go +++ b/pkg/services/team/teamimpl/store.go @@ -26,10 +26,7 @@ type store interface { GetByUser(ctx context.Context, query *team.GetTeamsByUserQuery) ([]*team.TeamDTO, error) GetIDsByUser(ctx context.Context, query *team.GetTeamIDsByUserQuery) ([]int64, error) RemoveUsersMemberships(ctx context.Context, userID int64) error - AddMember(ctx context.Context, userID, orgID, teamID int64, isExternal bool, permission dashboardaccess.PermissionType) error - UpdateMember(ctx context.Context, cmd *team.UpdateTeamMemberCommand) error IsMember(orgId int64, teamId int64, userId int64) (bool, error) - RemoveMember(ctx context.Context, cmd *team.RemoveTeamMemberCommand) error GetMemberships(ctx context.Context, orgID, userID int64, external bool) ([]*team.TeamMemberDTO, error) GetMembers(ctx context.Context, query *team.GetTeamMembersQuery) ([]*team.TeamMemberDTO, error) RegisterDelete(query string) @@ -350,19 +347,6 @@ WHERE tm.user_id=? AND tm.org_id=?;`, query.UserID, query.OrgID).Find(&queryResu return queryResult, nil } -// AddTeamMember adds a user to a team -func (ss *xormStore) AddMember(ctx context.Context, userID, orgID, teamID int64, isExternal bool, permission dashboardaccess.PermissionType) error { - return ss.db.WithTransactionalDbSession(ctx, func(sess *db.Session) error { - if isMember, err := isTeamMember(sess, orgID, teamID, userID); err != nil { - return err - } else if isMember { - return team.ErrTeamMemberAlreadyAdded - } - - return addTeamMember(sess, orgID, teamID, userID, isExternal, permission) - }) -} - func getTeamMember(sess *db.Session, orgId int64, teamId int64, userId int64) (team.TeamMember, error) { rawSQL := `SELECT * FROM team_member WHERE org_id=? and team_id=? and user_id=?` var member team.TeamMember @@ -378,13 +362,6 @@ func getTeamMember(sess *db.Session, orgId int64, teamId int64, userId int64) (t return member, nil } -// UpdateTeamMember updates a team member -func (ss *xormStore) UpdateMember(ctx context.Context, cmd *team.UpdateTeamMemberCommand) error { - return ss.db.WithTransactionalDbSession(ctx, func(sess *db.Session) error { - return updateTeamMember(sess, cmd.OrgID, cmd.TeamID, cmd.UserID, cmd.Permission) - }) -} - func (ss *xormStore) IsMember(orgId int64, teamId int64, userId int64) (bool, error) { var isMember bool @@ -458,13 +435,6 @@ func updateTeamMember(sess *db.Session, orgID, teamID, userID int64, permission return err } -// RemoveTeamMember removes a member from a team -func (ss *xormStore) RemoveMember(ctx context.Context, cmd *team.RemoveTeamMemberCommand) error { - return ss.db.WithTransactionalDbSession(ctx, func(sess *db.Session) error { - return removeTeamMember(sess, cmd) - }) -} - // RemoveTeamMemberHook is called from team resource permission service // it removes a member from a team within the given transaction session func RemoveTeamMemberHook(sess *db.Session, cmd *team.RemoveTeamMemberCommand) error { diff --git a/pkg/services/team/teamimpl/store_test.go b/pkg/services/team/teamimpl/store_test.go index d23ab695b02..4fdeaab2ba4 100644 --- a/pkg/services/team/teamimpl/store_test.go +++ b/pkg/services/team/teamimpl/store_test.go @@ -10,6 +10,7 @@ import ( "github.com/stretchr/testify/require" "github.com/grafana/grafana/pkg/infra/db" + "github.com/grafana/grafana/pkg/infra/tracing" ac "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/auth/identity" "github.com/grafana/grafana/pkg/services/dashboards/dashboardaccess" @@ -34,8 +35,8 @@ func TestIntegrationTeamCommandsAndQueries(t *testing.T) { t.Skip("skipping integration test") } t.Run("Testing Team commands and queries", func(t *testing.T) { - sqlStore := db.InitTestDB(t) - teamSvc, err := ProvideService(sqlStore, sqlStore.Cfg) + sqlStore, cfg := db.InitTestDBWithCfg(t) + teamSvc, err := ProvideService(sqlStore, cfg, tracing.InitializeTracerForTest()) require.NoError(t, err) testUser := &user.SignedInUser{ OrgID: 1, @@ -47,11 +48,13 @@ func TestIntegrationTeamCommandsAndQueries(t *testing.T) { }, }, } - quotaService := quotaimpl.ProvideService(sqlStore, sqlStore.Cfg) - orgSvc, err := orgimpl.ProvideService(sqlStore, sqlStore.Cfg, quotaService) + quotaService := quotaimpl.ProvideService(sqlStore, cfg) + orgSvc, err := orgimpl.ProvideService(sqlStore, cfg, quotaService) require.NoError(t, err) - userSvc, err := userimpl.ProvideService(sqlStore, orgSvc, sqlStore.Cfg, teamSvc, nil, quotaService, - supportbundlestest.NewFakeBundleService()) + userSvc, err := userimpl.ProvideService( + sqlStore, orgSvc, cfg, teamSvc, nil, tracing.InitializeTracerForTest(), + quotaService, supportbundlestest.NewFakeBundleService(), + ) require.NoError(t, err) t.Run("Given saved users and two teams", func(t *testing.T) { @@ -73,9 +76,9 @@ func TestIntegrationTeamCommandsAndQueries(t *testing.T) { require.NoError(t, err) userIds = append(userIds, usr.ID) } - team1, err = teamSvc.CreateTeam("group1 name", "test1@test.com", testOrgID) + team1, err = teamSvc.CreateTeam(context.Background(), "group1 name", "test1@test.com", testOrgID) require.NoError(t, err) - team2, err = teamSvc.CreateTeam("group2 name", "test2@test.com", testOrgID) + team2, err = teamSvc.CreateTeam(context.Background(), "group2 name", "test2@test.com", testOrgID) require.NoError(t, err) } setup() @@ -92,9 +95,13 @@ func TestIntegrationTeamCommandsAndQueries(t *testing.T) { require.Equal(t, team1.OrgID, testOrgID) require.EqualValues(t, team1.MemberCount, 0) - err = teamSvc.AddTeamMember(context.Background(), userIds[0], testOrgID, team1.ID, false, 0) - require.NoError(t, err) - err = teamSvc.AddTeamMember(context.Background(), userIds[1], testOrgID, team1.ID, true, 0) + err = sqlStore.WithDbSession(context.Background(), func(sess *db.Session) error { + err := AddOrUpdateTeamMemberHook(sess, userIds[0], testOrgID, team1.ID, false, 0) + if err != nil { + return err + } + return AddOrUpdateTeamMemberHook(sess, userIds[1], testOrgID, team1.ID, true, 0) + }) require.NoError(t, err) q1 := &team.GetTeamMembersQuery{OrgID: testOrgID, TeamID: team1.ID, SignedInUser: testUser} @@ -152,7 +159,9 @@ func TestIntegrationTeamCommandsAndQueries(t *testing.T) { team1 := teamQueryResult.Teams[0] - err = teamSvc.AddTeamMember(context.Background(), userId, testOrgID, team1.ID, true, 0) + err = sqlStore.WithDbSession(context.Background(), func(sess *db.Session) error { + return AddOrUpdateTeamMemberHook(sess, userId, testOrgID, team1.ID, true, 0) + }) require.NoError(t, err) memberQuery := &team.GetTeamMembersQuery{OrgID: testOrgID, TeamID: team1.ID, External: true, SignedInUser: testUser} @@ -168,7 +177,9 @@ func TestIntegrationTeamCommandsAndQueries(t *testing.T) { t.Run("Should be able to update users in a team", func(t *testing.T) { userId := userIds[0] - err = teamSvc.AddTeamMember(context.Background(), userId, testOrgID, team1.ID, false, 0) + err = sqlStore.WithDbSession(context.Background(), func(sess *db.Session) error { + return AddOrUpdateTeamMemberHook(sess, userId, testOrgID, team1.ID, false, 0) + }) require.NoError(t, err) qBeforeUpdate := &team.GetTeamMembersQuery{OrgID: testOrgID, TeamID: team1.ID, SignedInUser: testUser} @@ -176,13 +187,9 @@ func TestIntegrationTeamCommandsAndQueries(t *testing.T) { require.NoError(t, err) require.EqualValues(t, qBeforeUpdateResult[0].Permission, 0) - err = teamSvc.UpdateTeamMember(context.Background(), &team.UpdateTeamMemberCommand{ - UserID: userId, - OrgID: testOrgID, - TeamID: team1.ID, - Permission: dashboardaccess.PERMISSION_ADMIN, + err = sqlStore.WithDbSession(context.Background(), func(sess *db.Session) error { + return AddOrUpdateTeamMemberHook(sess, userId, testOrgID, team1.ID, false, dashboardaccess.PERMISSION_ADMIN) }) - require.NoError(t, err) qAfterUpdate := &team.GetTeamMembersQuery{OrgID: testOrgID, TeamID: team1.ID, SignedInUser: testUser} @@ -195,7 +202,10 @@ func TestIntegrationTeamCommandsAndQueries(t *testing.T) { sqlStore = db.InitTestDB(t) setup() userID := userIds[0] - err = teamSvc.AddTeamMember(context.Background(), userID, testOrgID, team1.ID, false, 0) + + err = sqlStore.WithDbSession(context.Background(), func(sess *db.Session) error { + return AddOrUpdateTeamMemberHook(sess, userID, testOrgID, team1.ID, false, 0) + }) require.NoError(t, err) qBeforeUpdate := &team.GetTeamMembersQuery{OrgID: testOrgID, TeamID: team1.ID, SignedInUser: testUser} @@ -204,13 +214,9 @@ func TestIntegrationTeamCommandsAndQueries(t *testing.T) { require.EqualValues(t, qBeforeUpdateResult[0].Permission, 0) invalidPermissionLevel := dashboardaccess.PERMISSION_EDIT - err = teamSvc.UpdateTeamMember(context.Background(), &team.UpdateTeamMemberCommand{ - UserID: userID, - OrgID: testOrgID, - TeamID: team1.ID, - Permission: invalidPermissionLevel, + err = sqlStore.WithDbSession(context.Background(), func(sess *db.Session) error { + return AddOrUpdateTeamMemberHook(sess, userID, testOrgID, team1.ID, false, invalidPermissionLevel) }) - require.NoError(t, err) qAfterUpdate := &team.GetTeamMembersQuery{OrgID: testOrgID, TeamID: team1.ID, SignedInUser: testUser} @@ -219,19 +225,6 @@ func TestIntegrationTeamCommandsAndQueries(t *testing.T) { require.EqualValues(t, qAfterUpdateResult[0].Permission, 0) }) - t.Run("Shouldn't be able to update a user not in the team.", func(t *testing.T) { - sqlStore = db.InitTestDB(t) - setup() - err = teamSvc.UpdateTeamMember(context.Background(), &team.UpdateTeamMemberCommand{ - UserID: 1, - OrgID: testOrgID, - TeamID: team1.ID, - Permission: dashboardaccess.PERMISSION_ADMIN, - }) - - require.Error(t, err, team.ErrTeamMemberNotFound) - }) - t.Run("Should be able to search for teams", func(t *testing.T) { query := &team.SearchTeamsQuery{OrgID: testOrgID, Query: "group", Page: 1, SignedInUser: testUser} queryResult, err := teamSvc.SearchTeams(context.Background(), query) @@ -251,11 +244,30 @@ func TestIntegrationTeamCommandsAndQueries(t *testing.T) { require.NoError(t, err) // Add a team member - err = teamSvc.AddTeamMember(context.Background(), userIds[0], testOrgID, team2.ID, false, 0) + err = sqlStore.WithDbSession(context.Background(), func(sess *db.Session) error { + err := AddOrUpdateTeamMemberHook(sess, userIds[2], testOrgID, team1.ID, false, 0) + if err != nil { + return err + } + err = AddOrUpdateTeamMemberHook(sess, userIds[3], testOrgID, team1.ID, false, 0) + if err != nil { + return err + } + return AddOrUpdateTeamMemberHook(sess, userIds[2], testOrgID, team2.ID, false, 0) + }) require.NoError(t, err) defer func() { - err := teamSvc.RemoveTeamMember(context.Background(), - &team.RemoveTeamMemberCommand{OrgID: testOrgID, UserID: userIds[0], TeamID: team2.ID}) + err = sqlStore.WithDbSession(context.Background(), func(sess *db.Session) error { + err := RemoveTeamMemberHook(sess, &team.RemoveTeamMemberCommand{OrgID: testOrgID, UserID: userIds[2], TeamID: team1.ID}) + if err != nil { + return err + } + err = RemoveTeamMemberHook(sess, &team.RemoveTeamMemberCommand{OrgID: testOrgID, UserID: userIds[3], TeamID: team1.ID}) + if err != nil { + return err + } + return RemoveTeamMemberHook(sess, &team.RemoveTeamMemberCommand{OrgID: testOrgID, UserID: userIds[2], TeamID: team2.ID}) + }) require.NoError(t, err) }() @@ -304,7 +316,9 @@ func TestIntegrationTeamCommandsAndQueries(t *testing.T) { sqlStore = db.InitTestDB(t) setup() groupId := team2.ID - err := teamSvc.AddTeamMember(context.Background(), userIds[0], testOrgID, groupId, false, 0) + err = sqlStore.WithDbSession(context.Background(), func(sess *db.Session) error { + return AddOrUpdateTeamMemberHook(sess, userIds[0], testOrgID, groupId, false, 0) + }) require.NoError(t, err) query := &team.GetTeamsByUserQuery{ @@ -323,10 +337,14 @@ func TestIntegrationTeamCommandsAndQueries(t *testing.T) { }) t.Run("Should be able to remove users from a group", func(t *testing.T) { - err = teamSvc.AddTeamMember(context.Background(), userIds[0], testOrgID, team1.ID, false, 0) + err = sqlStore.WithDbSession(context.Background(), func(sess *db.Session) error { + return AddOrUpdateTeamMemberHook(sess, userIds[0], testOrgID, team1.ID, false, 0) + }) require.NoError(t, err) - err = teamSvc.RemoveTeamMember(context.Background(), &team.RemoveTeamMemberCommand{OrgID: testOrgID, TeamID: team1.ID, UserID: userIds[0]}) + err = sqlStore.WithDbSession(context.Background(), func(sess *db.Session) error { + return RemoveTeamMemberHook(sess, &team.RemoveTeamMemberCommand{OrgID: testOrgID, TeamID: team1.ID, UserID: userIds[0]}) + }) require.NoError(t, err) q2 := &team.GetTeamMembersQuery{OrgID: testOrgID, TeamID: team1.ID, SignedInUser: testUser} @@ -336,16 +354,22 @@ func TestIntegrationTeamCommandsAndQueries(t *testing.T) { }) t.Run("Should have empty teams", func(t *testing.T) { - err = teamSvc.AddTeamMember(context.Background(), userIds[0], testOrgID, team1.ID, false, dashboardaccess.PERMISSION_ADMIN) + err = sqlStore.WithDbSession(context.Background(), func(sess *db.Session) error { + return AddOrUpdateTeamMemberHook(sess, userIds[0], testOrgID, team1.ID, false, dashboardaccess.PERMISSION_ADMIN) + }) require.NoError(t, err) t.Run("A user should be able to remove the admin permission for the last admin", func(t *testing.T) { - err = teamSvc.UpdateTeamMember(context.Background(), &team.UpdateTeamMemberCommand{OrgID: testOrgID, TeamID: team1.ID, UserID: userIds[0], Permission: 0}) + err = sqlStore.WithDbSession(context.Background(), func(sess *db.Session) error { + return AddOrUpdateTeamMemberHook(sess, userIds[0], testOrgID, team1.ID, false, 0) + }) require.NoError(t, err) }) t.Run("A user should be able to remove the last member", func(t *testing.T) { - err = teamSvc.RemoveTeamMember(context.Background(), &team.RemoveTeamMemberCommand{OrgID: testOrgID, TeamID: team1.ID, UserID: userIds[0]}) + err = sqlStore.WithDbSession(context.Background(), func(sess *db.Session) error { + return RemoveTeamMemberHook(sess, &team.RemoveTeamMemberCommand{OrgID: testOrgID, TeamID: team1.ID, UserID: userIds[0]}) + }) require.NoError(t, err) }) @@ -353,12 +377,17 @@ func TestIntegrationTeamCommandsAndQueries(t *testing.T) { sqlStore = db.InitTestDB(t) setup() - err = teamSvc.AddTeamMember(context.Background(), userIds[0], testOrgID, team1.ID, false, dashboardaccess.PERMISSION_ADMIN) + err = sqlStore.WithDbSession(context.Background(), func(sess *db.Session) error { + err := AddOrUpdateTeamMemberHook(sess, userIds[0], testOrgID, team1.ID, false, dashboardaccess.PERMISSION_ADMIN) + if err != nil { + return err + } + return AddOrUpdateTeamMemberHook(sess, userIds[1], testOrgID, team1.ID, false, dashboardaccess.PERMISSION_ADMIN) + }) require.NoError(t, err) - - err = teamSvc.AddTeamMember(context.Background(), userIds[1], testOrgID, team1.ID, false, dashboardaccess.PERMISSION_ADMIN) - require.NoError(t, err) - err = teamSvc.UpdateTeamMember(context.Background(), &team.UpdateTeamMemberCommand{OrgID: testOrgID, TeamID: team1.ID, UserID: userIds[0], Permission: 0}) + err = sqlStore.WithDbSession(context.Background(), func(sess *db.Session) error { + return AddOrUpdateTeamMemberHook(sess, userIds[0], testOrgID, team1.ID, false, 0) + }) require.NoError(t, err) }) }) @@ -379,12 +408,17 @@ func TestIntegrationTeamCommandsAndQueries(t *testing.T) { hiddenUsers := map[string]struct{}{"loginuser0": {}, "loginuser1": {}} teamId := team1.ID - err = teamSvc.AddTeamMember(context.Background(), userIds[0], testOrgID, teamId, false, 0) - require.NoError(t, err) - err = teamSvc.AddTeamMember(context.Background(), userIds[1], testOrgID, teamId, false, 0) - require.NoError(t, err) - err = teamSvc.AddTeamMember(context.Background(), userIds[2], testOrgID, teamId, false, 0) - require.NoError(t, err) + err = sqlStore.WithDbSession(context.Background(), func(sess *db.Session) error { + err := AddOrUpdateTeamMemberHook(sess, userIds[0], testOrgID, teamId, false, 0) + if err != nil { + return err + } + err = AddOrUpdateTeamMemberHook(sess, userIds[1], testOrgID, teamId, false, 0) + if err != nil { + return err + } + return AddOrUpdateTeamMemberHook(sess, userIds[2], testOrgID, teamId, false, 0) + }) searchQuery := &team.SearchTeamsQuery{OrgID: testOrgID, Page: 1, Limit: 10, SignedInUser: signedInUser, HiddenUsers: hiddenUsers} searchQueryResult, err := teamSvc.SearchTeams(context.Background(), searchQuery) @@ -401,10 +435,13 @@ func TestIntegrationTeamCommandsAndQueries(t *testing.T) { t.Run("Should be able to exclude service accounts from teamembers", func(t *testing.T) { sqlStore = db.InitTestDB(t) - quotaService := quotaimpl.ProvideService(sqlStore, sqlStore.Cfg) - orgSvc, err := orgimpl.ProvideService(sqlStore, sqlStore.Cfg, quotaService) + quotaService := quotaimpl.ProvideService(sqlStore, cfg) + orgSvc, err := orgimpl.ProvideService(sqlStore, cfg, quotaService) require.NoError(t, err) - userSvc, err := userimpl.ProvideService(sqlStore, orgSvc, sqlStore.Cfg, teamSvc, nil, quotaService, supportbundlestest.NewFakeBundleService()) + userSvc, err := userimpl.ProvideService( + sqlStore, orgSvc, cfg, teamSvc, nil, tracing.InitializeTracerForTest(), + quotaService, supportbundlestest.NewFakeBundleService(), + ) require.NoError(t, err) setup() userCmd = user.CreateUserCommand{ @@ -417,13 +454,16 @@ func TestIntegrationTeamCommandsAndQueries(t *testing.T) { require.NoError(t, err) groupId := team2.ID - // add service account to team - err = teamSvc.AddTeamMember(context.Background(), serviceAccount.ID, testOrgID, groupId, false, 0) - require.NoError(t, err) - - // add user to team - err = teamSvc.AddTeamMember(context.Background(), userIds[0], testOrgID, groupId, false, 0) - require.NoError(t, err) + dbErr := sqlStore.WithDbSession(context.Background(), func(sess *db.Session) error { + // add service account to team + err := AddOrUpdateTeamMemberHook(sess, serviceAccount.ID, testOrgID, groupId, false, 0) + if err != nil { + return err + } + // add user to team + return AddOrUpdateTeamMemberHook(sess, userIds[0], testOrgID, groupId, false, 0) + }) + require.NoError(t, dbErr) teamMembersQuery := &team.GetTeamMembersQuery{ OrgID: testOrgID, @@ -489,13 +529,13 @@ func TestIntegrationSQLStore_SearchTeams(t *testing.T) { }, } - store := db.InitTestDB(t, db.InitTestDBOpt{}) - teamSvc, err := ProvideService(store, store.Cfg) + store, cfg := db.InitTestDBWithCfg(t, db.InitTestDBOpt{}) + teamSvc, err := ProvideService(store, cfg, tracing.InitializeTracerForTest()) require.NoError(t, err) // Seed 10 teams for i := 1; i <= 10; i++ { - _, err := teamSvc.CreateTeam(fmt.Sprintf("team-%d", i), fmt.Sprintf("team-%d@example.org", i), 1) + _, err := teamSvc.CreateTeam(context.Background(), fmt.Sprintf("team-%d", i), fmt.Sprintf("team-%d@example.org", i), 1) require.NoError(t, err) } @@ -527,16 +567,19 @@ func TestIntegrationSQLStore_GetTeamMembers_ACFilter(t *testing.T) { // Seed 2 teams with 2 members setup := func(store db.DB, cfg *setting.Cfg) { - teamSvc, err := ProvideService(store, cfg) + teamSvc, err := ProvideService(store, cfg, tracing.InitializeTracerForTest()) require.NoError(t, err) - team1, errCreateTeam := teamSvc.CreateTeam("group1 name", "test1@example.org", testOrgID) + team1, errCreateTeam := teamSvc.CreateTeam(context.Background(), "group1 name", "test1@example.org", testOrgID) require.NoError(t, errCreateTeam) - team2, errCreateTeam := teamSvc.CreateTeam("group2 name", "test2@example.org", testOrgID) + team2, errCreateTeam := teamSvc.CreateTeam(context.Background(), "group2 name", "test2@example.org", testOrgID) require.NoError(t, errCreateTeam) quotaService := quotaimpl.ProvideService(store, cfg) orgSvc, err := orgimpl.ProvideService(store, cfg, quotaService) require.NoError(t, err) - userSvc, err := userimpl.ProvideService(store, orgSvc, cfg, teamSvc, nil, quotaService, supportbundlestest.NewFakeBundleService()) + userSvc, err := userimpl.ProvideService( + store, orgSvc, cfg, teamSvc, nil, tracing.InitializeTracerForTest(), + quotaService, supportbundlestest.NewFakeBundleService(), + ) require.NoError(t, err) for i := 0; i < 4; i++ { @@ -550,19 +593,27 @@ func TestIntegrationSQLStore_GetTeamMembers_ACFilter(t *testing.T) { userIds[i] = user.ID } - errAddMember := teamSvc.AddTeamMember(context.Background(), userIds[0], testOrgID, team1.ID, false, 0) - require.NoError(t, errAddMember) - errAddMember = teamSvc.AddTeamMember(context.Background(), userIds[1], testOrgID, team1.ID, false, 0) - require.NoError(t, errAddMember) - errAddMember = teamSvc.AddTeamMember(context.Background(), userIds[2], testOrgID, team2.ID, false, 0) - require.NoError(t, errAddMember) - errAddMember = teamSvc.AddTeamMember(context.Background(), userIds[3], testOrgID, team2.ID, false, 0) - require.NoError(t, errAddMember) + errAddMembers := store.WithDbSession(context.Background(), func(sess *db.Session) error { + err := AddOrUpdateTeamMemberHook(sess, userIds[0], testOrgID, team1.ID, false, 0) + if err != nil { + return err + } + err = AddOrUpdateTeamMemberHook(sess, userIds[1], testOrgID, team1.ID, false, 0) + if err != nil { + return err + } + err = AddOrUpdateTeamMemberHook(sess, userIds[2], testOrgID, team2.ID, false, 0) + if err != nil { + return err + } + return AddOrUpdateTeamMemberHook(sess, userIds[3], testOrgID, team2.ID, false, 0) + }) + require.NoError(t, errAddMembers) } - store := db.InitTestDB(t, db.InitTestDBOpt{}) - setup(store, store.Cfg) - teamSvc, err := ProvideService(store, store.Cfg) + store, cfg := db.InitTestDBWithCfg(t, db.InitTestDBOpt{}) + setup(store, cfg) + teamSvc, err := ProvideService(store, cfg, tracing.InitializeTracerForTest()) require.NoError(t, err) type getTeamMembersTestCase struct { diff --git a/pkg/services/team/teamimpl/team.go b/pkg/services/team/teamimpl/team.go index 11cef2ab58e..1eb8e7c1b2d 100644 --- a/pkg/services/team/teamimpl/team.go +++ b/pkg/services/team/teamimpl/team.go @@ -3,78 +3,129 @@ package teamimpl import ( "context" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/trace" + "github.com/grafana/grafana/pkg/infra/db" - "github.com/grafana/grafana/pkg/services/dashboards/dashboardaccess" + "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/services/team" "github.com/grafana/grafana/pkg/setting" ) type Service struct { - store store + store store + tracer tracing.Tracer } -func ProvideService(db db.DB, cfg *setting.Cfg) (team.Service, error) { +func ProvideService(db db.DB, cfg *setting.Cfg, tracer tracing.Tracer) (team.Service, error) { store := &xormStore{db: db, cfg: cfg, deletes: []string{}} if err := store.uidMigration(); err != nil { return nil, err } - return &Service{store: &xormStore{db: db, cfg: cfg, deletes: []string{}}}, nil + return &Service{ + store: &xormStore{db: db, cfg: cfg, deletes: []string{}}, + tracer: tracer, + }, nil } -func (s *Service) CreateTeam(name, email string, orgID int64) (team.Team, error) { +func (s *Service) CreateTeam(ctx context.Context, name, email string, orgID int64) (team.Team, error) { + _, span := s.tracer.Start(ctx, "team.CreateTeam", trace.WithAttributes( + attribute.Int64("orgID", orgID), + attribute.String("name", name), + )) + defer span.End() return s.store.Create(name, email, orgID) } func (s *Service) UpdateTeam(ctx context.Context, cmd *team.UpdateTeamCommand) error { + ctx, span := s.tracer.Start(ctx, "team.UpdateTeam", trace.WithAttributes( + attribute.Int64("orgID", cmd.OrgID), + attribute.Int64("teamID", cmd.ID), + )) + defer span.End() return s.store.Update(ctx, cmd) } func (s *Service) DeleteTeam(ctx context.Context, cmd *team.DeleteTeamCommand) error { + ctx, span := s.tracer.Start(ctx, "team.DeleteTeam", trace.WithAttributes( + attribute.Int64("orgID", cmd.OrgID), + attribute.Int64("teamID", cmd.ID), + )) + defer span.End() return s.store.Delete(ctx, cmd) } func (s *Service) SearchTeams(ctx context.Context, query *team.SearchTeamsQuery) (team.SearchTeamQueryResult, error) { + ctx, span := s.tracer.Start(ctx, "team.SearchTeams", trace.WithAttributes( + attribute.Int64("orgID", query.OrgID), + attribute.String("query", query.Query), + )) + defer span.End() return s.store.Search(ctx, query) } func (s *Service) GetTeamByID(ctx context.Context, query *team.GetTeamByIDQuery) (*team.TeamDTO, error) { + ctx, span := s.tracer.Start(ctx, "team.GetTeamByID", trace.WithAttributes( + attribute.Int64("orgID", query.OrgID), + attribute.Int64("teamID", query.ID), + )) + defer span.End() return s.store.GetByID(ctx, query) } func (s *Service) GetTeamsByUser(ctx context.Context, query *team.GetTeamsByUserQuery) ([]*team.TeamDTO, error) { + ctx, span := s.tracer.Start(ctx, "team.GetTeamsByUser", trace.WithAttributes( + attribute.Int64("orgID", query.OrgID), + attribute.Int64("userID", query.UserID), + )) + defer span.End() return s.store.GetByUser(ctx, query) } func (s *Service) GetTeamIDsByUser(ctx context.Context, query *team.GetTeamIDsByUserQuery) ([]int64, error) { + ctx, span := s.tracer.Start(ctx, "team.GetTeamIDsByUser", trace.WithAttributes( + attribute.Int64("orgID", query.OrgID), + attribute.Int64("userID", query.UserID), + )) + defer span.End() return s.store.GetIDsByUser(ctx, query) } -func (s *Service) AddTeamMember(ctx context.Context, userID, orgID, teamID int64, isExternal bool, permission dashboardaccess.PermissionType) error { - return s.store.AddMember(ctx, userID, orgID, teamID, isExternal, permission) -} - -func (s *Service) UpdateTeamMember(ctx context.Context, cmd *team.UpdateTeamMemberCommand) error { - return s.store.UpdateMember(ctx, cmd) -} - -func (s *Service) IsTeamMember(orgId int64, teamId int64, userId int64) (bool, error) { +func (s *Service) IsTeamMember(ctx context.Context, orgId int64, teamId int64, userId int64) (bool, error) { + _, span := s.tracer.Start(ctx, "team.IsTeamMember", trace.WithAttributes( + attribute.Int64("orgID", orgId), + attribute.Int64("teamID", teamId), + attribute.Int64("userID", userId), + )) + defer span.End() return s.store.IsMember(orgId, teamId, userId) } -func (s *Service) RemoveTeamMember(ctx context.Context, cmd *team.RemoveTeamMemberCommand) error { - return s.store.RemoveMember(ctx, cmd) -} - func (s *Service) RemoveUsersMemberships(ctx context.Context, userID int64) error { + ctx, span := s.tracer.Start(ctx, "team.RemoveUsersMemberships", trace.WithAttributes( + attribute.Int64("userID", userID), + )) + defer span.End() return s.store.RemoveUsersMemberships(ctx, userID) } func (s *Service) GetUserTeamMemberships(ctx context.Context, orgID, userID int64, external bool) ([]*team.TeamMemberDTO, error) { + ctx, span := s.tracer.Start(ctx, "team.GetUserTeamMemberships", trace.WithAttributes( + attribute.Int64("orgID", orgID), + attribute.Int64("userID", userID), + )) + defer span.End() return s.store.GetMemberships(ctx, orgID, userID, external) } func (s *Service) GetTeamMembers(ctx context.Context, query *team.GetTeamMembersQuery) ([]*team.TeamMemberDTO, error) { + ctx, span := s.tracer.Start(ctx, "team.GetTeamMembers", trace.WithAttributes( + attribute.Int64("orgID", query.OrgID), + attribute.Int64("teamID", query.TeamID), + attribute.String("teamUID", query.TeamUID), + )) + defer span.End() return s.store.GetMembers(ctx, query) } diff --git a/pkg/services/team/teamtest/team.go b/pkg/services/team/teamtest/team.go index 2c6fccdf333..a561b7c4772 100644 --- a/pkg/services/team/teamtest/team.go +++ b/pkg/services/team/teamtest/team.go @@ -3,7 +3,6 @@ package teamtest import ( "context" - "github.com/grafana/grafana/pkg/services/dashboards/dashboardaccess" "github.com/grafana/grafana/pkg/services/team" ) @@ -21,7 +20,7 @@ func NewFakeService() *FakeService { return &FakeService{} } -func (s *FakeService) CreateTeam(name, email string, orgID int64) (team.Team, error) { +func (s *FakeService) CreateTeam(ctx context.Context, name, email string, orgID int64) (team.Team, error) { return s.ExpectedTeam, s.ExpectedError } @@ -45,22 +44,10 @@ func (s *FakeService) GetTeamsByUser(ctx context.Context, query *team.GetTeamsBy return s.ExpectedTeamsByUser, s.ExpectedError } -func (s *FakeService) AddTeamMember(ctx context.Context, userID, orgID, teamID int64, isExternal bool, permission dashboardaccess.PermissionType) error { - return s.ExpectedError -} - -func (s *FakeService) UpdateTeamMember(ctx context.Context, cmd *team.UpdateTeamMemberCommand) error { - return s.ExpectedError -} - -func (s *FakeService) IsTeamMember(orgId int64, teamId int64, userId int64) (bool, error) { +func (s *FakeService) IsTeamMember(ctx context.Context, orgId int64, teamId int64, userId int64) (bool, error) { return s.ExpectedIsMember, s.ExpectedError } -func (s *FakeService) RemoveTeamMember(ctx context.Context, cmd *team.RemoveTeamMemberCommand) error { - return s.ExpectedError -} - func (s *FakeService) RemoveUsersMemberships(ctx context.Context, userID int64) error { return s.ExpectedError } diff --git a/pkg/services/temp_user/tempuserimpl/store_test.go b/pkg/services/temp_user/tempuserimpl/store_test.go index 491850afe51..811dae0cd44 100644 --- a/pkg/services/temp_user/tempuserimpl/store_test.go +++ b/pkg/services/temp_user/tempuserimpl/store_test.go @@ -31,8 +31,8 @@ func TestIntegrationTempUserCommandsAndQueries(t *testing.T) { Status: tempuser.TmpUserInvitePending, } setup := func(t *testing.T) { - db := db.InitTestDB(t) - store = &xormStore{db: db, cfg: db.Cfg} + db, cfg := db.InitTestDBWithCfg(t) + store = &xormStore{db: db, cfg: cfg} tempUser, err = store.CreateTempUser(context.Background(), &cmd) require.Nil(t, err) } @@ -112,8 +112,8 @@ func TestIntegrationTempUserCommandsAndQueries(t *testing.T) { Status: tempuser.TmpUserEmailUpdateStarted, InvitedByUserID: userID, } - db := db.InitTestDB(t) - store = &xormStore{db: db, cfg: db.Cfg} + db, cfg := db.InitTestDBWithCfg(t) + store = &xormStore{db: db, cfg: cfg} for i := 0; i < verifications; i++ { tempUser, err = store.CreateTempUser(context.Background(), &cmd) @@ -152,8 +152,8 @@ func TestIntegrationTempUserCommandsAndQueries(t *testing.T) { Status: tempuser.TmpUserEmailUpdateStarted, InvitedByUserID: 99, } - db := db.InitTestDB(t) - store = &xormStore{db: db, cfg: db.Cfg} + db, cfg := db.InitTestDBWithCfg(t) + store = &xormStore{db: db, cfg: cfg} tempUser, err = store.CreateTempUser(context.Background(), &cmd) require.Nil(t, err) diff --git a/pkg/services/user/model.go b/pkg/services/user/model.go index 13aaed4ae78..73276afc8b6 100644 --- a/pkg/services/user/model.go +++ b/pkg/services/user/model.go @@ -39,7 +39,7 @@ type User struct { Company string EmailVerified bool Theme string - HelpFlags1 HelpFlags1 + HelpFlags1 HelpFlags1 `xorm:"help_flags1"` IsDisabled bool IsAdmin bool @@ -86,9 +86,13 @@ type UpdateUserCommand struct { IsDisabled *bool `json:"-"` EmailVerified *bool `json:"-"` IsGrafanaAdmin *bool `json:"-"` - - Password *Password `json:"-"` + // If password is included it will be validated, hashed and updated for user. + Password *Password `json:"-"` + // If old password is included it will be validated against users current password. OldPassword *Password `json:"-"` + // If OrgID is included update current org for user + OrgID *int64 `json:"-"` + HelpFlags1 *HelpFlags1 `json:"-"` } type UpdateUserLastSeenAtCommand struct { @@ -96,11 +100,6 @@ type UpdateUserLastSeenAtCommand struct { OrgID int64 } -type SetUsingOrgCommand struct { - UserID int64 - OrgID int64 -} - type SearchUsersQuery struct { SignedInUser identity.Requester OrgID int64 `xorm:"org_id"` @@ -179,11 +178,6 @@ type BatchDisableUsersCommand struct { IsDisabled bool } -type SetUserHelpFlagCommand struct { - HelpFlags1 HelpFlags1 - UserID int64 `xorm:"user_id"` -} - type GetSignedInUserQuery struct { UserID int64 `xorm:"user_id"` Login string diff --git a/pkg/services/user/user.go b/pkg/services/user/user.go index 06d8b113978..5d27249ac03 100644 --- a/pkg/services/user/user.go +++ b/pkg/services/user/user.go @@ -17,12 +17,10 @@ type Service interface { GetByEmail(context.Context, *GetUserByEmailQuery) (*User, error) Update(context.Context, *UpdateUserCommand) error UpdateLastSeenAt(context.Context, *UpdateUserLastSeenAtCommand) error - SetUsingOrg(context.Context, *SetUsingOrgCommand) error GetSignedInUserWithCacheCtx(context.Context, *GetSignedInUserQuery) (*SignedInUser, error) GetSignedInUser(context.Context, *GetSignedInUserQuery) (*SignedInUser, error) Search(context.Context, *SearchUsersQuery) (*SearchUserQueryResult, error) BatchDisableUsers(context.Context, *BatchDisableUsersCommand) error - SetUserHelpFlag(context.Context, *SetUserHelpFlagCommand) error GetProfile(context.Context, *GetUserProfileQuery) (*UserProfileDTO, error) } diff --git a/pkg/services/user/userimpl/store.go b/pkg/services/user/userimpl/store.go index c0bc043e499..d4c8d2513a1 100644 --- a/pkg/services/user/userimpl/store.go +++ b/pkg/services/user/userimpl/store.go @@ -19,20 +19,16 @@ import ( type store interface { Insert(context.Context, *user.User) (int64, error) - Get(context.Context, *user.User) (*user.User, error) GetByID(context.Context, int64) (*user.User, error) - GetNotServiceAccount(context.Context, int64) (*user.User, error) + GetByLogin(context.Context, *user.GetUserByLoginQuery) (*user.User, error) + GetByEmail(context.Context, *user.GetUserByEmailQuery) (*user.User, error) Delete(context.Context, int64) error LoginConflict(ctx context.Context, login, email string) error CaseInsensitiveLoginConflict(context.Context, string, string) error - GetByLogin(context.Context, *user.GetUserByLoginQuery) (*user.User, error) - GetByEmail(context.Context, *user.GetUserByEmailQuery) (*user.User, error) Update(context.Context, *user.UpdateUserCommand) error UpdateLastSeenAt(context.Context, *user.UpdateUserLastSeenAtCommand) error GetSignedInUser(context.Context, *user.GetSignedInUserQuery) (*user.SignedInUser, error) - UpdateUser(context.Context, *user.User) error GetProfile(context.Context, *user.GetUserProfileQuery) (*user.UserProfileDTO, error) - SetHelpFlag(context.Context, *user.SetUserHelpFlagCommand) error BatchDisableUsers(context.Context, *user.BatchDisableUsersCommand) error Search(context.Context, *user.SearchUsersQuery) (*user.SearchUserQueryResult, error) Count(ctx context.Context) (int64, error) @@ -79,37 +75,9 @@ func (ss *sqlStore) Insert(ctx context.Context, cmd *user.User) (int64, error) { return 0, err } - // verify that user was created and cmd.ID was updated with the actual new userID - _, err = ss.getAnyUserType(ctx, cmd.ID) - if err != nil { - return 0, err - } return cmd.ID, nil } -func (ss *sqlStore) Get(ctx context.Context, usr *user.User) (*user.User, error) { - ret := &user.User{} - err := ss.db.WithDbSession(ctx, func(sess *db.Session) error { - login := usr.Login - email := usr.Email - where := "LOWER(email)=LOWER(?) OR LOWER(login)=LOWER(?)" - - exists, err := sess.Where(where, email, login).Get(ret) - if !exists { - return user.ErrUserNotFound - } - if err != nil { - return err - } - return nil - }) - if err != nil { - return nil, err - } - - return ret, nil -} - func (ss *sqlStore) Delete(ctx context.Context, userID int64) error { err := ss.db.WithDbSession(ctx, func(sess *db.Session) error { var rawSQL = "DELETE FROM " + ss.dialect.Quote("user") + " WHERE id = ?" @@ -122,21 +90,6 @@ func (ss *sqlStore) Delete(ctx context.Context, userID int64) error { return nil } -func (ss *sqlStore) GetNotServiceAccount(ctx context.Context, userID int64) (*user.User, error) { - usr := user.User{ID: userID} - err := ss.db.WithDbSession(ctx, func(sess *db.Session) error { - has, err := sess.Where(ss.notServiceAccountFilter()).Get(&usr) - if err != nil { - return err - } - if !has { - return user.ErrUserNotFound - } - return nil - }) - return &usr, err -} - func (ss *sqlStore) GetByID(ctx context.Context, userID int64) (*user.User, error) { var usr user.User @@ -178,6 +131,9 @@ func (ss *sqlStore) CaseInsensitiveLoginConflict(ctx context.Context, login, ema } func (ss *sqlStore) GetByLogin(ctx context.Context, query *user.GetUserByLoginQuery) (*user.User, error) { + // enforcement of lowercase due to forcement of caseinsensitive login + query.LoginOrEmail = strings.ToLower(query.LoginOrEmail) + usr := &user.User{} err := ss.db.WithDbSession(ctx, func(sess *db.Session) error { if query.LoginOrEmail == "" { @@ -191,7 +147,7 @@ func (ss *sqlStore) GetByLogin(ctx context.Context, query *user.GetUserByLoginQu // Since username can be an email address, attempt login with email address // first if the login field has the "@" symbol. if strings.Contains(query.LoginOrEmail, "@") { - where = "LOWER(email)=LOWER(?)" + where = "email=?" has, err = sess.Where(ss.notServiceAccountFilter()).Where(where, query.LoginOrEmail).Get(usr) if err != nil { return err @@ -200,7 +156,7 @@ func (ss *sqlStore) GetByLogin(ctx context.Context, query *user.GetUserByLoginQu // Look for the login field instead of email if !has { - where = "LOWER(login)=LOWER(?)" + where = "login=?" has, err = sess.Where(ss.notServiceAccountFilter()).Where(where, query.LoginOrEmail).Get(usr) } @@ -209,9 +165,6 @@ func (ss *sqlStore) GetByLogin(ctx context.Context, query *user.GetUserByLoginQu } else if !has { return user.ErrUserNotFound } - if err := ss.userCaseInsensitiveLoginConflict(ctx, sess, usr.Login, usr.Email); err != nil { - return err - } return nil }) @@ -223,13 +176,16 @@ func (ss *sqlStore) GetByLogin(ctx context.Context, query *user.GetUserByLoginQu } func (ss *sqlStore) GetByEmail(ctx context.Context, query *user.GetUserByEmailQuery) (*user.User, error) { + // enforcement of lowercase due to forcement of caseinsensitive login + query.Email = strings.ToLower(query.Email) + usr := &user.User{} err := ss.db.WithDbSession(ctx, func(sess *db.Session) error { if query.Email == "" { return user.ErrUserNotFound } - where := "LOWER(email)=LOWER(?)" + where := "email=?" has, err := sess.Where(ss.notServiceAccountFilter()).Where(where, query.Email).Get(usr) if err != nil { @@ -237,10 +193,6 @@ func (ss *sqlStore) GetByEmail(ctx context.Context, query *user.GetUserByEmailQu } else if !has { return user.ErrUserNotFound } - - if err := ss.userCaseInsensitiveLoginConflict(ctx, sess, usr.Login, usr.Email); err != nil { - return err - } return nil }) if err != nil { @@ -249,103 +201,85 @@ func (ss *sqlStore) GetByEmail(ctx context.Context, query *user.GetUserByEmailQu return usr, nil } -func (ss *sqlStore) userCaseInsensitiveLoginConflict(ctx context.Context, sess *db.Session, login, email string) error { - users := make([]user.User, 0) - - if err := sess.Where("LOWER(email)=LOWER(?) OR LOWER(login)=LOWER(?)", - email, login).Find(&users); err != nil { - return err - } - - if len(users) > 1 { - return &user.ErrCaseInsensitiveLoginConflict{Users: users} - } - - return nil -} - // LoginConflict returns an error if the provided email or login are already // associated with a user. If caseInsensitive is true the search is not case // sensitive. func (ss *sqlStore) LoginConflict(ctx context.Context, login, email string) error { err := ss.db.WithDbSession(ctx, func(sess *db.Session) error { - return ss.loginConflict(ctx, sess, login, email) + users := make([]user.User, 0) + where := "email=? OR login=?" + login = strings.ToLower(login) + email = strings.ToLower(email) + + exists, err := sess.Where(where, email, login).Get(&user.User{}) + if err != nil { + return err + } + if exists { + return user.ErrUserAlreadyExists + } + if err := sess.Where("LOWER(email)=LOWER(?) OR LOWER(login)=LOWER(?)", + email, login).Find(&users); err != nil { + return err + } + + if len(users) > 1 { + return &user.ErrCaseInsensitiveLoginConflict{Users: users} + } + return nil }) return err } -func (ss *sqlStore) loginConflict(ctx context.Context, sess *db.Session, login, email string) error { - users := make([]user.User, 0) - where := "LOWER(email)=LOWER(?) OR LOWER(login)=LOWER(?)" - login = strings.ToLower(login) - email = strings.ToLower(email) - - exists, err := sess.Where(where, email, login).Get(&user.User{}) - if err != nil { - return err - } - if exists { - return user.ErrUserAlreadyExists - } - if err := sess.Where("LOWER(email)=LOWER(?) OR LOWER(login)=LOWER(?)", - email, login).Find(&users); err != nil { - return err - } - - if len(users) > 1 { - return &user.ErrCaseInsensitiveLoginConflict{Users: users} - } - return nil -} - func (ss *sqlStore) Update(ctx context.Context, cmd *user.UpdateUserCommand) error { + // enforcement of lowercase due to forcement of caseinsensitive login + cmd.Login = strings.ToLower(cmd.Login) + cmd.Email = strings.ToLower(cmd.Email) + return ss.db.WithTransactionalDbSession(ctx, func(sess *db.Session) error { - user := user.User{ + usr := user.User{ Name: cmd.Name, + Theme: cmd.Theme, Email: strings.ToLower(cmd.Email), Login: strings.ToLower(cmd.Login), - Theme: cmd.Theme, Updated: time.Now(), } q := sess.ID(cmd.UserID).Where(ss.notServiceAccountFilter()) - if cmd.Password != nil { - user.Password = *cmd.Password - } + setOptional(cmd.OrgID, func(v int64) { usr.OrgID = v }) + setOptional(cmd.Password, func(v user.Password) { usr.Password = v }) + setOptional(cmd.IsDisabled, func(v bool) { + q = q.UseBool("is_disabled") + usr.IsDisabled = v + }) + setOptional(cmd.EmailVerified, func(v bool) { + q = q.UseBool("email_verified") + usr.EmailVerified = v + }) + setOptional(cmd.IsGrafanaAdmin, func(v bool) { + q = q.UseBool("is_admin") + usr.IsAdmin = v + }) + setOptional(cmd.HelpFlags1, func(v user.HelpFlags1) { usr.HelpFlags1 = *cmd.HelpFlags1 }) - if cmd.IsDisabled != nil { - sess.UseBool("is_disabled") - user.IsDisabled = *cmd.IsDisabled - } - - if cmd.EmailVerified != nil { - q.UseBool("email_verified") - user.EmailVerified = *cmd.EmailVerified - } - - if cmd.IsGrafanaAdmin != nil { - q.UseBool("is_admin") - user.IsAdmin = *cmd.IsGrafanaAdmin - } - - if _, err := q.Update(&user); err != nil { + if _, err := q.Update(&usr); err != nil { return err } if cmd.IsGrafanaAdmin != nil && !*cmd.IsGrafanaAdmin { // validate that after update there is at least one server admin - if err := validateOneAdminLeft(ctx, sess); err != nil { + if err := validateOneAdminLeft(sess); err != nil { return err } } sess.PublishAfterCommit(&events.UserUpdated{ - Timestamp: user.Created, - Id: user.ID, - Name: user.Name, - Login: user.Login, - Email: user.Email, + Timestamp: usr.Created, + Id: usr.ID, + Name: usr.Name, + Login: usr.Login, + Email: usr.Email, }) return nil @@ -423,13 +357,6 @@ func (ss *sqlStore) GetSignedInUser(ctx context.Context, query *user.GetSignedIn return &signedInUser, err } -func (ss *sqlStore) UpdateUser(ctx context.Context, user *user.User) error { - return ss.db.WithTransactionalDbSession(ctx, func(sess *db.Session) error { - _, err := sess.ID(user.ID).Update(user) - return err - }) -} - func (ss *sqlStore) GetProfile(ctx context.Context, query *user.GetUserProfileQuery) (*user.UserProfileDTO, error) { var usr user.User var userProfile user.UserProfileDTO @@ -461,19 +388,6 @@ func (ss *sqlStore) GetProfile(ctx context.Context, query *user.GetUserProfileQu return &userProfile, err } -func (ss *sqlStore) SetHelpFlag(ctx context.Context, cmd *user.SetUserHelpFlagCommand) error { - return ss.db.WithTransactionalDbSession(ctx, func(sess *db.Session) error { - user := user.User{ - ID: cmd.UserID, - HelpFlags1: cmd.HelpFlags1, - Updated: time.Now(), - } - - _, err := sess.ID(cmd.UserID).Cols("help_flags1").Update(&user) - return err - }) -} - func (ss *sqlStore) Count(ctx context.Context) (int64, error) { type result struct { Count int64 @@ -512,7 +426,7 @@ func (ss *sqlStore) CountUserAccountsWithEmptyRole(ctx context.Context) (int64, } // validateOneAdminLeft validate that there is an admin user left -func validateOneAdminLeft(ctx context.Context, sess *db.Session) error { +func validateOneAdminLeft(sess *db.Session) error { count, err := sess.Where("is_admin=?", true).Count(&user.User{}) if err != nil { return err @@ -669,18 +583,8 @@ func (ss *sqlStore) Search(ctx context.Context, query *user.SearchUsersQuery) (* return &result, err } -// getAnyUserType searches for a user record by ID. The user account may be a service account. -func (ss *sqlStore) getAnyUserType(ctx context.Context, userID int64) (*user.User, error) { - usr := user.User{ID: userID} - err := ss.db.WithDbSession(ctx, func(sess *db.Session) error { - has, err := sess.Get(&usr) - if err != nil { - return err - } - if !has { - return user.ErrUserNotFound - } - return nil - }) - return &usr, err +func setOptional[T any](v *T, add func(v T)) { + if v != nil { + add(*v) + } } diff --git a/pkg/services/user/userimpl/store_test.go b/pkg/services/user/userimpl/store_test.go index cf559938a87..45ed734c2f7 100644 --- a/pkg/services/user/userimpl/store_test.go +++ b/pkg/services/user/userimpl/store_test.go @@ -10,6 +10,7 @@ import ( "github.com/stretchr/testify/require" "github.com/grafana/grafana/pkg/infra/db" + "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/org/orgimpl" @@ -27,93 +28,20 @@ func TestMain(m *testing.M) { testsuite.Run(m) } -func TestIntegrationUserGet(t *testing.T) { - testCases := []struct { - name string - wantErr error - searchLogin string - searchEmail string - }{ - { - name: "user found non exact", - wantErr: nil, - searchLogin: "test", - searchEmail: "Test@email.com", - }, - { - name: "user found exact", - wantErr: nil, - searchLogin: "test", - searchEmail: "test@email.com", - }, - { - name: "user found exact - case insensitive", - wantErr: nil, - searchLogin: "Test", - searchEmail: "Test@email.com", - }, - { - name: "user not found - case insensitive", - wantErr: user.ErrUserNotFound, - searchLogin: "Test_login", - searchEmail: "Test*@email.com", - }, - } - - if testing.Short() { - t.Skip("skipping integration test") - } - - ss := db.InitTestDB(t) - cfg := ss.Cfg - userStore := ProvideStore(ss, cfg) - - _, errUser := userStore.Insert(context.Background(), - &user.User{ - Email: "test@email.com", - Name: "test", - Login: "test", - Created: time.Now(), - Updated: time.Now(), - }, - ) - require.NoError(t, errUser) - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - if db.IsTestDbMySQL() { - t.Skip("mysql is always case insensitive") - } - usr, err := userStore.Get(context.Background(), - &user.User{ - Email: tc.searchEmail, - Login: tc.searchLogin, - }, - ) - - if tc.wantErr != nil { - require.Error(t, err) - require.Nil(t, usr) - } else { - require.NoError(t, err) - require.NotNil(t, usr) - require.NotEmpty(t, usr.UID) - } - }) - } -} - func TestIntegrationUserDataAccess(t *testing.T) { if testing.Short() { t.Skip("skipping integration test") } - ss := db.InitTestDB(t) - quotaService := quotaimpl.ProvideService(ss, ss.Cfg) - orgService, err := orgimpl.ProvideService(ss, ss.Cfg, quotaService) + ss, cfg := db.InitTestDBWithCfg(t) + quotaService := quotaimpl.ProvideService(ss, cfg) + orgService, err := orgimpl.ProvideService(ss, cfg, quotaService) require.NoError(t, err) userStore := ProvideStore(ss, setting.NewCfg()) - usrSvc, err := ProvideService(ss, orgService, ss.Cfg, nil, nil, quotaService, supportbundlestest.NewFakeBundleService()) + usrSvc, err := ProvideService( + ss, orgService, cfg, nil, nil, tracing.InitializeTracerForTest(), + quotaService, supportbundlestest.NewFakeBundleService(), + ) require.NoError(t, err) usr := &user.SignedInUser{ OrgID: 1, @@ -121,12 +49,8 @@ func TestIntegrationUserDataAccess(t *testing.T) { } t.Run("user not found", func(t *testing.T) { - _, err := userStore.Get(context.Background(), - &user.User{ - Email: "test@email.com", - Name: "test1", - Login: "test1", - }, + _, err := userStore.GetByEmail(context.Background(), + &user.GetUserByEmailQuery{Email: "test@email.com"}, ) require.Error(t, err, user.ErrUserNotFound) }) @@ -144,6 +68,13 @@ func TestIntegrationUserDataAccess(t *testing.T) { require.NoError(t, err) }) + t.Run("get user", func(t *testing.T) { + _, err := userStore.GetByEmail(context.Background(), + &user.GetUserByEmailQuery{Email: "test@email.com"}, + ) + require.NoError(t, err) + }) + t.Run("insert user (with known UID)", func(t *testing.T) { ctx := context.Background() id, err := userStore.Insert(ctx, @@ -170,20 +101,9 @@ func TestIntegrationUserDataAccess(t *testing.T) { require.Equal(t, "abcd", siu.UserUID) }) - t.Run("get user", func(t *testing.T) { - _, err := userStore.Get(context.Background(), - &user.User{ - Email: "test@email.com", - Name: "test1", - Login: "test1", - }, - ) - require.NoError(t, err) - }) - t.Run("Testing DB - creates and loads user", func(t *testing.T) { ss := db.InitTestDB(t) - _, usrSvc := createOrgAndUserSvc(t, ss, ss.Cfg) + _, usrSvc := createOrgAndUserSvc(t, ss, cfg) cmd := user.CreateUserCommand{ Email: "usertest@test.com", @@ -345,37 +265,6 @@ func TestIntegrationUserDataAccess(t *testing.T) { return nil }) require.NoError(t, err) - - t.Run("GetByEmail - email conflict", func(t *testing.T) { - query := user.GetUserByEmailQuery{Email: "confusertest@test.com"} - _, err = userStore.GetByEmail(context.Background(), &query) - require.Error(t, err) - }) - - t.Run("GetByEmail - login conflict", func(t *testing.T) { - query := user.GetUserByEmailQuery{Email: "user_test_login_conflict@test.com"} - _, err = userStore.GetByEmail(context.Background(), &query) - require.Error(t, err) - }) - - t.Run("GetByLogin - email conflict", func(t *testing.T) { - query := user.GetUserByLoginQuery{LoginOrEmail: "user_email_conflict_two"} - _, err = userStore.GetByLogin(context.Background(), &query) - require.Error(t, err) - }) - - t.Run("GetByLogin - login conflict", func(t *testing.T) { - query := user.GetUserByLoginQuery{LoginOrEmail: "user_test_login_conflict"} - _, err = userStore.GetByLogin(context.Background(), &query) - require.Error(t, err) - }) - - t.Run("GetByLogin - login conflict by email", func(t *testing.T) { - query := user.GetUserByLoginQuery{LoginOrEmail: "user_test_login_conflict@test.com"} - _, err = userStore.GetByLogin(context.Background(), &query) - require.Error(t, err) - }) - t.Run("GetByLogin - user2 uses user1.email as login", func(t *testing.T) { // create user_1 user1 := &user.User{ @@ -460,7 +349,7 @@ func TestIntegrationUserDataAccess(t *testing.T) { t.Run("get signed in user", func(t *testing.T) { ss := db.InitTestDB(t) - orgService, usrSvc := createOrgAndUserSvc(t, ss, ss.Cfg) + orgService, usrSvc := createOrgAndUserSvc(t, ss, cfg) users := createFiveTestUsers(t, usrSvc, func(i int) *user.CreateUserCommand { return &user.CreateUserCommand{ Email: fmt.Sprint("user", i, "@test.com"), @@ -490,18 +379,9 @@ func TestIntegrationUserDataAccess(t *testing.T) { } }) - t.Run("update user", func(t *testing.T) { - err := userStore.UpdateUser(context.Background(), &user.User{ID: 1, Name: "testtestest", Login: "loginloginlogin"}) - require.NoError(t, err) - result, err := userStore.GetByID(context.Background(), 1) - require.NoError(t, err) - assert.Equal(t, result.Name, "testtestest") - assert.Equal(t, result.Login, "loginloginlogin") - }) - t.Run("Testing DB - grafana admin users", func(t *testing.T) { ss := db.InitTestDB(t) - _, usrSvc := createOrgAndUserSvc(t, ss, ss.Cfg) + _, usrSvc := createOrgAndUserSvc(t, ss, cfg) usr, err := usrSvc.Create(context.Background(), &user.CreateUserCommand{ Email: "admin@test.com", Name: "admin", @@ -515,7 +395,7 @@ func TestIntegrationUserDataAccess(t *testing.T) { UserID: usr.ID, IsGrafanaAdmin: boolPtr(false), }) - require.ErrorIs(t, user.ErrLastGrafanaAdmin, err) + require.ErrorIs(t, err, user.ErrLastGrafanaAdmin) usr, err = userStore.GetByID(context.Background(), usr.ID) require.NoError(t, err) @@ -550,15 +430,34 @@ func TestIntegrationUserDataAccess(t *testing.T) { require.NoError(t, err) }) - t.Run("SetHelpFlag", func(t *testing.T) { - err := userStore.SetHelpFlag(context.Background(), &user.SetUserHelpFlagCommand{UserID: 1, HelpFlags1: user.HelpFlags1(1)}) + t.Run("Update HelpFlags", func(t *testing.T) { + id, err := userStore.Insert(context.Background(), &user.User{ + Email: "help@test.com", + Name: "help", + Login: "help", + Updated: time.Now(), + Created: time.Now(), + LastSeenAt: time.Now(), + }) require.NoError(t, err) + original, err := userStore.GetByID(context.Background(), id) + require.NoError(t, err) + + helpflags := user.HelpFlags1(1) + err = userStore.Update(context.Background(), &user.UpdateUserCommand{UserID: id, HelpFlags1: &helpflags}) + require.NoError(t, err) + + got, err := userStore.GetByID(context.Background(), id) + require.NoError(t, err) + + original.HelpFlags1 = helpflags + assertEqualUser(t, original, got) }) t.Run("Testing DB - return list users based on their is_disabled flag", func(t *testing.T) { ss = db.InitTestDB(t) - _, usrSvc := createOrgAndUserSvc(t, ss, ss.Cfg) - userStore := ProvideStore(ss, ss.Cfg) + _, usrSvc := createOrgAndUserSvc(t, ss, cfg) + userStore := ProvideStore(ss, cfg) createFiveTestUsers(t, usrSvc, func(i int) *user.CreateUserCommand { return &user.CreateUserCommand{ @@ -591,7 +490,7 @@ func TestIntegrationUserDataAccess(t *testing.T) { // Re-init DB ss := db.InitTestDB(t) - orgService, usrSvc = createOrgAndUserSvc(t, ss, ss.Cfg) + orgService, usrSvc = createOrgAndUserSvc(t, ss, cfg) users := createFiveTestUsers(t, usrSvc, func(i int) *user.CreateUserCommand { return &user.CreateUserCommand{ @@ -615,7 +514,7 @@ func TestIntegrationUserDataAccess(t *testing.T) { // A user is an org member and has been assigned permissions // Re-init DB ss = db.InitTestDB(t) - orgService, usrSvc = createOrgAndUserSvc(t, ss, ss.Cfg) + orgService, usrSvc = createOrgAndUserSvc(t, ss, cfg) users = createFiveTestUsers(t, usrSvc, func(i int) *user.CreateUserCommand { return &user.CreateUserCommand{ Email: fmt.Sprint("user", i, "@test.com"), @@ -657,9 +556,12 @@ func TestIntegrationUserDataAccess(t *testing.T) { t.Run("Testing DB - return list of users that the SignedInUser has permission to read", func(t *testing.T) { ss := db.InitTestDB(t) - orgService, err := orgimpl.ProvideService(ss, ss.Cfg, quotaService) + orgService, err := orgimpl.ProvideService(ss, cfg, quotaService) require.NoError(t, err) - usrSvc, err := ProvideService(ss, orgService, ss.Cfg, nil, nil, quotaService, supportbundlestest.NewFakeBundleService()) + usrSvc, err := ProvideService( + ss, orgService, cfg, nil, nil, tracing.InitializeTracerForTest(), + quotaService, supportbundlestest.NewFakeBundleService(), + ) require.NoError(t, err) createFiveTestUsers(t, usrSvc, func(i int) *user.CreateUserCommand { @@ -934,9 +836,9 @@ func TestIntegrationUserUpdate(t *testing.T) { t.Skip("skipping integration test") } - ss := db.InitTestDB(t) - userStore := ProvideStore(ss, setting.NewCfg()) - _, usrSvc := createOrgAndUserSvc(t, ss, ss.Cfg) + ss, cfg := db.InitTestDBWithCfg(t) + userStore := ProvideStore(ss, cfg) + _, usrSvc := createOrgAndUserSvc(t, ss, cfg) users := createFiveTestUsers(t, usrSvc, func(i int) *user.CreateUserCommand { return &user.CreateUserCommand{ @@ -999,13 +901,13 @@ func createFiveTestUsers(t *testing.T, svc user.Service, fn func(i int) *user.Cr } func TestMetricsUsage(t *testing.T) { - ss := db.InitTestDB(t) + ss, cfg := db.InitTestDBWithCfg(t) userStore := ProvideStore(ss, setting.NewCfg()) - quotaService := quotaimpl.ProvideService(ss, ss.Cfg) - orgService, err := orgimpl.ProvideService(ss, ss.Cfg, quotaService) + quotaService := quotaimpl.ProvideService(ss, cfg) + orgService, err := orgimpl.ProvideService(ss, cfg, quotaService) require.NoError(t, err) - _, usrSvc := createOrgAndUserSvc(t, ss, ss.Cfg) + _, usrSvc := createOrgAndUserSvc(t, ss, cfg) t.Run("Get empty role metrics for an org", func(t *testing.T) { orgId := int64(1) @@ -1045,13 +947,28 @@ func TestMetricsUsage(t *testing.T) { }) } +func assertEqualUser(t *testing.T, expected, got *user.User) { + // zero out time fields + expected.Updated = time.Time{} + expected.Created = time.Time{} + expected.LastSeenAt = time.Time{} + got.Updated = time.Time{} + got.Created = time.Time{} + got.LastSeenAt = time.Time{} + + assert.Equal(t, expected, got) +} + func createOrgAndUserSvc(t *testing.T, store db.DB, cfg *setting.Cfg) (org.Service, user.Service) { t.Helper() quotaService := quotaimpl.ProvideService(store, cfg) orgService, err := orgimpl.ProvideService(store, cfg, quotaService) require.NoError(t, err) - usrSvc, err := ProvideService(store, orgService, cfg, nil, nil, quotaService, supportbundlestest.NewFakeBundleService()) + usrSvc, err := ProvideService( + store, orgService, cfg, nil, nil, tracing.InitializeTracerForTest(), + quotaService, supportbundlestest.NewFakeBundleService(), + ) require.NoError(t, err) return orgService, usrSvc diff --git a/pkg/services/user/userimpl/user.go b/pkg/services/user/userimpl/user.go index 9a085c39ccb..c272934fa85 100644 --- a/pkg/services/user/userimpl/user.go +++ b/pkg/services/user/userimpl/user.go @@ -7,8 +7,12 @@ import ( "strings" "time" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/trace" + "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/localcache" + "github.com/grafana/grafana/pkg/infra/tracing" ac "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/quota" @@ -27,6 +31,7 @@ type Service struct { teamService team.Service cacheService *localcache.CacheService cfg *setting.Cfg + tracer tracing.Tracer } func ProvideService( @@ -34,9 +39,8 @@ func ProvideService( orgService org.Service, cfg *setting.Cfg, teamService team.Service, - cacheService *localcache.CacheService, - quotaService quota.Service, - bundleRegistry supportbundles.Service, + cacheService *localcache.CacheService, tracer tracing.Tracer, + quotaService quota.Service, bundleRegistry supportbundles.Service, ) (user.Service, error) { store := ProvideStore(db, cfg) s := &Service{ @@ -45,6 +49,7 @@ func ProvideService( cfg: cfg, teamService: teamService, cacheService: cacheService, + tracer: tracer, } defaultLimits, err := readQuotaConfig(cfg) @@ -55,7 +60,7 @@ func ProvideService( if err := quotaService.RegisterQuotaReporter("a.NewUsageReporter{ TargetSrv: quota.TargetSrv(user.QuotaTargetSrv), DefaultLimits: defaultLimits, - Reporter: s.Usage, + Reporter: s.usage, }); err != nil { return s, err } @@ -87,21 +92,10 @@ func (s *Service) GetUsageStats(ctx context.Context) map[string]any { return stats } -func (s *Service) Usage(ctx context.Context, _ *quota.ScopeParameters) (*quota.Map, error) { - u := "a.Map{} - if used, err := s.store.Count(ctx); err != nil { - return u, err - } else { - tag, err := quota.NewTag(quota.TargetSrv(user.QuotaTargetSrv), quota.Target(user.QuotaTarget), quota.GlobalScope) - if err != nil { - return u, err - } - u.Set(tag, used) - } - return u, nil -} - func (s *Service) Create(ctx context.Context, cmd *user.CreateUserCommand) (*user.User, error) { + ctx, span := s.tracer.Start(ctx, "user.Create") + defer span.End() + if len(cmd.Login) == 0 { cmd.Login = cmd.Email } @@ -126,8 +120,7 @@ func (s *Service) Create(ctx context.Context, cmd *user.CreateUserCommand) (*use cmd.Email = cmd.Login } - err = s.store.LoginConflict(ctx, cmd.Login, cmd.Email) - if err != nil { + if err := s.store.LoginConflict(ctx, cmd.Login, cmd.Email); err != nil { return nil, user.ErrUserAlreadyExists } @@ -202,27 +195,48 @@ func (s *Service) Create(ctx context.Context, cmd *user.CreateUserCommand) (*use } func (s *Service) Delete(ctx context.Context, cmd *user.DeleteUserCommand) error { - _, err := s.store.GetNotServiceAccount(ctx, cmd.UserID) + ctx, span := s.tracer.Start(ctx, "user.Delete", trace.WithAttributes( + attribute.Int64("userID", cmd.UserID), + )) + defer span.End() + + _, err := s.store.GetByID(ctx, cmd.UserID) if err != nil { return err } - // delete from all the stores + return s.store.Delete(ctx, cmd.UserID) } func (s *Service) GetByID(ctx context.Context, query *user.GetUserByIDQuery) (*user.User, error) { + ctx, span := s.tracer.Start(ctx, "user.GetByID", trace.WithAttributes( + attribute.Int64("userID", query.ID), + )) + defer span.End() + return s.store.GetByID(ctx, query.ID) } func (s *Service) GetByLogin(ctx context.Context, query *user.GetUserByLoginQuery) (*user.User, error) { + ctx, span := s.tracer.Start(ctx, "user.GetByLogin") + defer span.End() + return s.store.GetByLogin(ctx, query) } func (s *Service) GetByEmail(ctx context.Context, query *user.GetUserByEmailQuery) (*user.User, error) { + ctx, span := s.tracer.Start(ctx, "user.GetByEmail") + defer span.End() + return s.store.GetByEmail(ctx, query) } func (s *Service) Update(ctx context.Context, cmd *user.UpdateUserCommand) error { + ctx, span := s.tracer.Start(ctx, "user.Update", trace.WithAttributes( + attribute.Int64("userID", cmd.UserID), + )) + defer span.End() + usr, err := s.store.GetByID(ctx, cmd.UserID) if err != nil { return err @@ -251,10 +265,33 @@ func (s *Service) Update(ctx context.Context, cmd *user.UpdateUserCommand) error cmd.Password = &hashed } + if cmd.OrgID != nil { + orgs, err := s.orgService.GetUserOrgList(ctx, &org.GetUserOrgListQuery{UserID: cmd.UserID}) + if err != nil { + return err + } + + valid := false + for _, org := range orgs { + if org.OrgID == *cmd.OrgID { + valid = true + } + } + + if !valid { + return fmt.Errorf("user does not belong to org") + } + } + return s.store.Update(ctx, cmd) } func (s *Service) UpdateLastSeenAt(ctx context.Context, cmd *user.UpdateUserLastSeenAtCommand) error { + ctx, span := s.tracer.Start(ctx, "user.UpdateLastSeen", trace.WithAttributes( + attribute.Int64("userID", cmd.UserID), + )) + defer span.End() + u, err := s.GetSignedInUserWithCacheCtx(ctx, &user.GetSignedInUserQuery{ UserID: cmd.UserID, OrgID: cmd.OrgID, @@ -275,29 +312,13 @@ func shouldUpdateLastSeen(t time.Time) bool { return time.Since(t) > time.Minute*5 } -func (s *Service) SetUsingOrg(ctx context.Context, cmd *user.SetUsingOrgCommand) error { - getOrgsForUserCmd := &org.GetUserOrgListQuery{UserID: cmd.UserID} - orgsForUser, err := s.orgService.GetUserOrgList(ctx, getOrgsForUserCmd) - if err != nil { - return err - } - - valid := false - for _, other := range orgsForUser { - if other.OrgID == cmd.OrgID { - valid = true - } - } - if !valid { - return fmt.Errorf("user does not belong to org") - } - return s.store.UpdateUser(ctx, &user.User{ - ID: cmd.UserID, - OrgID: cmd.OrgID, - }) -} - func (s *Service) GetSignedInUserWithCacheCtx(ctx context.Context, query *user.GetSignedInUserQuery) (*user.SignedInUser, error) { + ctx, span := s.tracer.Start(ctx, "user.GetSignedInUserWithCacheCtx", trace.WithAttributes( + attribute.Int64("userID", query.UserID), + attribute.Int64("orgID", query.OrgID), + )) + defer span.End() + var signedInUser *user.SignedInUser // only check cache if we have a user ID and an org ID in query @@ -325,58 +346,62 @@ func newSignedInUserCacheKey(orgID, userID int64) string { } func (s *Service) GetSignedInUser(ctx context.Context, query *user.GetSignedInUserQuery) (*user.SignedInUser, error) { - signedInUser, err := s.store.GetSignedInUser(ctx, query) + ctx, span := s.tracer.Start(ctx, "user.GetSignedInUser", trace.WithAttributes( + attribute.Int64("userID", query.UserID), + attribute.Int64("orgID", query.OrgID), + )) + defer span.End() + + usr, err := s.store.GetSignedInUser(ctx, query) if err != nil { return nil, err } - getTeamsByUserQuery := &team.GetTeamIDsByUserQuery{ - OrgID: signedInUser.OrgID, - UserID: signedInUser.UserID, - } - signedInUser.Teams, err = s.teamService.GetTeamIDsByUser(ctx, getTeamsByUserQuery) + usr.Teams, err = s.teamService.GetTeamIDsByUser(ctx, &team.GetTeamIDsByUserQuery{ + OrgID: usr.OrgID, + UserID: usr.UserID, + }) if err != nil { return nil, err } - return signedInUser, err + return usr, err } func (s *Service) Search(ctx context.Context, query *user.SearchUsersQuery) (*user.SearchUserQueryResult, error) { + ctx, span := s.tracer.Start(ctx, "user.Search", trace.WithAttributes( + attribute.Int64("orgID", query.OrgID), + )) + defer span.End() + return s.store.Search(ctx, query) } func (s *Service) BatchDisableUsers(ctx context.Context, cmd *user.BatchDisableUsersCommand) error { + ctx, span := s.tracer.Start(ctx, "user.BatchDisableUsers", trace.WithAttributes( + attribute.Int64Slice("userIDs", cmd.UserIDs), + )) + defer span.End() + return s.store.BatchDisableUsers(ctx, cmd) } -func (s *Service) SetUserHelpFlag(ctx context.Context, cmd *user.SetUserHelpFlagCommand) error { - return s.store.SetHelpFlag(ctx, cmd) -} - func (s *Service) GetProfile(ctx context.Context, query *user.GetUserProfileQuery) (*user.UserProfileDTO, error) { - result, err := s.store.GetProfile(ctx, query) - return result, err -} + ctx, span := s.tracer.Start(ctx, "user.GetProfile", trace.WithAttributes( + attribute.Int64("userID", query.UserID), + )) + defer span.End() -func readQuotaConfig(cfg *setting.Cfg) (*quota.Map, error) { - limits := "a.Map{} - - if cfg == nil { - return limits, nil - } - - globalQuotaTag, err := quota.NewTag(quota.TargetSrv(user.QuotaTargetSrv), quota.Target(user.QuotaTarget), quota.GlobalScope) - if err != nil { - return limits, err - } - - limits.Set(globalQuotaTag, cfg.Quota.Global.User) - return limits, nil + return s.store.GetProfile(ctx, query) } // CreateServiceAccount creates a service account in the user table and adds service account to an organisation in the org_user table func (s *Service) CreateServiceAccount(ctx context.Context, cmd *user.CreateUserCommand) (*user.User, error) { + ctx, span := s.tracer.Start(ctx, "user.CreateServiceAccount", trace.WithAttributes( + attribute.Int64("orgID", cmd.OrgID), + )) + defer span.End() + cmd.Email = cmd.Login err := s.store.LoginConflict(ctx, cmd.Login, cmd.Email) if err != nil { @@ -470,6 +495,36 @@ func (s *Service) supportBundleCollector() supportbundles.Collector { } } +func (s *Service) usage(ctx context.Context, _ *quota.ScopeParameters) (*quota.Map, error) { + u := "a.Map{} + if used, err := s.store.Count(ctx); err != nil { + return u, err + } else { + tag, err := quota.NewTag(quota.TargetSrv(user.QuotaTargetSrv), quota.Target(user.QuotaTarget), quota.GlobalScope) + if err != nil { + return u, err + } + u.Set(tag, used) + } + return u, nil +} + +func readQuotaConfig(cfg *setting.Cfg) (*quota.Map, error) { + limits := "a.Map{} + + if cfg == nil { + return limits, nil + } + + globalQuotaTag, err := quota.NewTag(quota.TargetSrv(user.QuotaTargetSrv), quota.Target(user.QuotaTarget), quota.GlobalScope) + if err != nil { + return limits, err + } + + limits.Set(globalQuotaTag, cfg.Quota.Global.User) + return limits, nil +} + // This is just to ensure that all users have a valid uid. // To protect against upgrade / downgrade we need to run this for a couple of releases. // FIXME: Remove this migration and make uid field required https://github.com/grafana/identity-access-team/issues/552 diff --git a/pkg/services/user/userimpl/user_test.go b/pkg/services/user/userimpl/user_test.go index 6e362ae92ab..6cc9512507c 100644 --- a/pkg/services/user/userimpl/user_test.go +++ b/pkg/services/user/userimpl/user_test.go @@ -10,6 +10,7 @@ import ( "github.com/stretchr/testify/require" "github.com/grafana/grafana/pkg/infra/localcache" + "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/org/orgtest" "github.com/grafana/grafana/pkg/services/team/teamtest" @@ -25,6 +26,7 @@ func TestUserService(t *testing.T) { orgService: orgService, cacheService: localcache.ProvideService(), teamService: &teamtest.FakeService{}, + tracer: tracing.InitializeTracerForTest(), } userService.cfg = setting.NewCfg() @@ -100,6 +102,7 @@ func TestUserService(t *testing.T) { orgService: orgService, cacheService: localcache.ProvideService(), teamService: teamtest.NewFakeService(), + tracer: tracing.InitializeTracerForTest(), } usr := &user.SignedInUser{ OrgID: 1, @@ -126,58 +129,112 @@ func TestUserService(t *testing.T) { assert.Equal(t, query2.OrgID, result2.OrgID) }) - t.Run("Can set using org", func(t *testing.T) { - cmd := user.SetUsingOrgCommand{UserID: 2, OrgID: 1} - orgService.ExpectedUserOrgDTO = []*org.UserOrgDTO{{OrgID: 1}} + t.Run("SignedInUserQuery with a different org", func(t *testing.T) { + query := user.GetSignedInUserQuery{UserID: 2} + userStore.ExpectedSignedInUser = &user.SignedInUser{ + OrgID: 1, + Email: "ac2@test.com", + Name: "ac2 name", + Login: "ac2", + OrgName: "ac1@test.com", + } userStore.ExpectedError = nil - err := userService.SetUsingOrg(context.Background(), &cmd) + queryResult, err := userService.GetSignedInUser(context.Background(), &query) + require.NoError(t, err) - - t.Run("SignedInUserQuery with a different org", func(t *testing.T) { - query := user.GetSignedInUserQuery{UserID: 2} - userStore.ExpectedSignedInUser = &user.SignedInUser{ - OrgID: 1, - Email: "ac2@test.com", - Name: "ac2 name", - Login: "ac2", - OrgName: "ac1@test.com", - } - queryResult, err := userService.GetSignedInUser(context.Background(), &query) - - require.NoError(t, err) - require.EqualValues(t, queryResult.OrgID, 1) - require.Equal(t, queryResult.Email, "ac2@test.com") - require.Equal(t, queryResult.Name, "ac2 name") - require.Equal(t, queryResult.Login, "ac2") - require.Equal(t, queryResult.OrgName, "ac1@test.com") - }) + require.EqualValues(t, queryResult.OrgID, 1) + require.Equal(t, queryResult.Email, "ac2@test.com") + require.Equal(t, queryResult.Name, "ac2 name") + require.Equal(t, queryResult.Login, "ac2") + require.Equal(t, queryResult.OrgName, "ac1@test.com") }) } func TestService_Update(t *testing.T) { - t.Run("should return error if old password does not match stored password", func(t *testing.T) { - stored, err := user.Password("test").Hash("salt") - require.NoError(t, err) - service := &Service{store: &FakeUserStore{ExpectedUser: &user.User{Password: stored, Salt: "salt"}}} + setup := func(opts ...func(svc *Service)) *Service { + service := &Service{ + store: &FakeUserStore{}, + tracer: tracing.InitializeTracerForTest(), + } + for _, o := range opts { + o(service) + } + return service + } - err = service.Update(context.Background(), &user.UpdateUserCommand{ - OldPassword: passwordPtr("test123"), + t.Run("should return error if old password does not match stored password", func(t *testing.T) { + service := setup(func(svc *Service) { + stored, err := user.Password("test").Hash("salt") + require.NoError(t, err) + + svc.store = &FakeUserStore{ExpectedUser: &user.User{Password: stored, Salt: "salt"}} }) + err := service.Update(context.Background(), &user.UpdateUserCommand{ + OldPassword: passwordPtr("test123"), + }) assert.ErrorIs(t, err, user.ErrPasswordMissmatch) }) t.Run("should return error new password is not valid", func(t *testing.T) { - stored, err := user.Password("test").Hash("salt") - require.NoError(t, err) - service := &Service{cfg: setting.NewCfg(), store: &FakeUserStore{ExpectedUser: &user.User{Password: stored, Salt: "salt"}}} + service := setup(func(svc *Service) { + stored, err := user.Password("test").Hash("salt") + require.NoError(t, err) + svc.cfg = setting.NewCfg() + svc.store = &FakeUserStore{ExpectedUser: &user.User{Password: stored, Salt: "salt"}} + }) - err = service.Update(context.Background(), &user.UpdateUserCommand{ + err := service.Update(context.Background(), &user.UpdateUserCommand{ OldPassword: passwordPtr("test"), Password: passwordPtr("asd"), }) require.ErrorIs(t, err, user.ErrPasswordTooShort) }) + + t.Run("Can set using org", func(t *testing.T) { + orgID := int64(1) + service := setup(func(svc *Service) { + svc.orgService = &orgtest.FakeOrgService{ExpectedUserOrgDTO: []*org.UserOrgDTO{{OrgID: orgID}}} + }) + err := service.Update(context.Background(), &user.UpdateUserCommand{UserID: 2, OrgID: &orgID}) + require.NoError(t, err) + }) + + t.Run("Cannot set using org when user is not member of it", func(t *testing.T) { + orgID := int64(1) + service := setup(func(svc *Service) { + svc.orgService = &orgtest.FakeOrgService{ExpectedUserOrgDTO: []*org.UserOrgDTO{{OrgID: 2}}} + }) + err := service.Update(context.Background(), &user.UpdateUserCommand{UserID: 2, OrgID: &orgID}) + require.Error(t, err) + }) +} + +func TestUpdateLastSeenAt(t *testing.T) { + userStore := newUserStoreFake() + orgService := orgtest.NewOrgServiceFake() + userService := Service{ + store: userStore, + orgService: orgService, + cacheService: localcache.ProvideService(), + teamService: &teamtest.FakeService{}, + tracer: tracing.InitializeTracerForTest(), + } + userService.cfg = setting.NewCfg() + + t.Run("update last seen at", func(t *testing.T) { + userStore.ExpectedSignedInUser = &user.SignedInUser{UserID: 1, OrgID: 1, Email: "email", Login: "login", Name: "name", LastSeenAt: time.Now().Add(-10 * time.Minute)} + err := userService.UpdateLastSeenAt(context.Background(), &user.UpdateUserLastSeenAtCommand{UserID: 1, OrgID: 1}) + require.NoError(t, err) + }) + + userService.cacheService.Flush() + + t.Run("do not update last seen at", func(t *testing.T) { + userStore.ExpectedSignedInUser = &user.SignedInUser{UserID: 1, OrgID: 1, Email: "email", Login: "login", Name: "name", LastSeenAt: time.Now().Add(-1 * time.Minute)} + err := userService.UpdateLastSeenAt(context.Background(), &user.UpdateUserLastSeenAtCommand{UserID: 1, OrgID: 1}) + require.ErrorIs(t, err, user.ErrLastSeenUpToDate, err) + }) } func TestMetrics(t *testing.T) { @@ -189,6 +246,7 @@ func TestMetrics(t *testing.T) { orgService: orgService, cacheService: localcache.ProvideService(), teamService: &teamtest.FakeService{}, + tracer: tracing.InitializeTracerForTest(), } t.Run("update user with role None", func(t *testing.T) { @@ -220,10 +278,6 @@ func newUserStoreFake() *FakeUserStore { return &FakeUserStore{} } -func (f *FakeUserStore) Get(ctx context.Context, query *user.User) (*user.User, error) { - return f.ExpectedUser, f.ExpectedError -} - func (f *FakeUserStore) Insert(ctx context.Context, query *user.User) (int64, error) { return 0, f.ExpectedError } @@ -232,10 +286,6 @@ func (f *FakeUserStore) Delete(ctx context.Context, userID int64) error { return f.ExpectedDeleteUserError } -func (f *FakeUserStore) GetNotServiceAccount(ctx context.Context, userID int64) (*user.User, error) { - return f.ExpectedUser, f.ExpectedError -} - func (f *FakeUserStore) GetByID(context.Context, int64) (*user.User, error) { return f.ExpectedUser, f.ExpectedError } @@ -268,22 +318,10 @@ func (f *FakeUserStore) GetSignedInUser(ctx context.Context, query *user.GetSign return f.ExpectedSignedInUser, f.ExpectedError } -func (f *FakeUserStore) UpdateUser(ctx context.Context, user *user.User) error { - return f.ExpectedError -} - func (f *FakeUserStore) GetProfile(ctx context.Context, query *user.GetUserProfileQuery) (*user.UserProfileDTO, error) { return f.ExpectedUserProfile, f.ExpectedError } -func (f *FakeUserStore) SetHelpFlag(ctx context.Context, cmd *user.SetUserHelpFlagCommand) error { - return f.ExpectedError -} - -func (f *FakeUserStore) UpdatePermissions(ctx context.Context, userID int64, isAdmin bool) error { - return f.ExpectedError -} - func (f *FakeUserStore) BatchDisableUsers(ctx context.Context, cmd *user.BatchDisableUsersCommand) error { return f.ExpectedError } @@ -299,29 +337,3 @@ func (f *FakeUserStore) Count(ctx context.Context) (int64, error) { func (f *FakeUserStore) CountUserAccountsWithEmptyRole(ctx context.Context) (int64, error) { return f.ExpectedCountUserAccountsWithEmptyRoles, nil } - -func TestUpdateLastSeenAt(t *testing.T) { - userStore := newUserStoreFake() - orgService := orgtest.NewOrgServiceFake() - userService := Service{ - store: userStore, - orgService: orgService, - cacheService: localcache.ProvideService(), - teamService: &teamtest.FakeService{}, - } - userService.cfg = setting.NewCfg() - - t.Run("update last seen at", func(t *testing.T) { - userStore.ExpectedSignedInUser = &user.SignedInUser{UserID: 1, OrgID: 1, Email: "email", Login: "login", Name: "name", LastSeenAt: time.Now().Add(-10 * time.Minute)} - err := userService.UpdateLastSeenAt(context.Background(), &user.UpdateUserLastSeenAtCommand{UserID: 1, OrgID: 1}) - require.NoError(t, err) - }) - - userService.cacheService.Flush() - - t.Run("do not update last seen at", func(t *testing.T) { - userStore.ExpectedSignedInUser = &user.SignedInUser{UserID: 1, OrgID: 1, Email: "email", Login: "login", Name: "name", LastSeenAt: time.Now().Add(-1 * time.Minute)} - err := userService.UpdateLastSeenAt(context.Background(), &user.UpdateUserLastSeenAtCommand{UserID: 1, OrgID: 1}) - require.ErrorIs(t, err, user.ErrLastSeenUpToDate, err) - }) -} diff --git a/pkg/services/user/userimpl/verifier.go b/pkg/services/user/userimpl/verifier.go index 3017a9b6189..e4b62fa9d61 100644 --- a/pkg/services/user/userimpl/verifier.go +++ b/pkg/services/user/userimpl/verifier.go @@ -152,6 +152,6 @@ func (s *Verifier) Complete(ctx context.Context, cmd user.CompleteEmailVerifyCom // remove the current token, so a new one can be generated with correct values. return s.is.RemoveIDToken( ctx, - &user.SignedInUser{UserID: usr.ID, OrgID: usr.OrgID, NamespacedID: authn.NamespacedID(authn.NamespaceUser, usr.ID)}, + &authn.Identity{ID: authn.NewNamespaceIDUnchecked(authn.NamespaceUser, usr.ID), OrgID: usr.OrgID}, ) } diff --git a/pkg/services/user/usertest/fake.go b/pkg/services/user/usertest/fake.go index b33a04ce552..1d6e84ab44b 100644 --- a/pkg/services/user/usertest/fake.go +++ b/pkg/services/user/usertest/fake.go @@ -71,10 +71,6 @@ func (f *FakeUserService) UpdateLastSeenAt(ctx context.Context, cmd *user.Update return f.ExpectedError } -func (f *FakeUserService) SetUsingOrg(ctx context.Context, cmd *user.SetUsingOrgCommand) error { - return f.ExpectedSetUsingOrgError -} - func (f *FakeUserService) GetSignedInUserWithCacheCtx(ctx context.Context, query *user.GetSignedInUserQuery) (*user.SignedInUser, error) { return f.GetSignedInUser(ctx, query) } @@ -104,10 +100,6 @@ func (f *FakeUserService) BatchDisableUsers(ctx context.Context, cmd *user.Batch return f.ExpectedError } -func (f *FakeUserService) SetUserHelpFlag(ctx context.Context, cmd *user.SetUserHelpFlagCommand) error { - return f.ExpectedError -} - func (f *FakeUserService) GetProfile(ctx context.Context, query *user.GetUserProfileQuery) (*user.UserProfileDTO, error) { if f.ExpectedUserProfileDTO != nil { return f.ExpectedUserProfileDTO, f.ExpectedError diff --git a/pkg/services/user/usertest/mock.go b/pkg/services/user/usertest/mock.go index 7668bf3548f..01cfea90965 100644 --- a/pkg/services/user/usertest/mock.go +++ b/pkg/services/user/usertest/mock.go @@ -340,42 +340,6 @@ func (_m *MockService) Search(_a0 context.Context, _a1 *user.SearchUsersQuery) ( return r0, r1 } -// SetUserHelpFlag provides a mock function with given fields: _a0, _a1 -func (_m *MockService) SetUserHelpFlag(_a0 context.Context, _a1 *user.SetUserHelpFlagCommand) error { - ret := _m.Called(_a0, _a1) - - if len(ret) == 0 { - panic("no return value specified for SetUserHelpFlag") - } - - var r0 error - if rf, ok := ret.Get(0).(func(context.Context, *user.SetUserHelpFlagCommand) error); ok { - r0 = rf(_a0, _a1) - } else { - r0 = ret.Error(0) - } - - return r0 -} - -// SetUsingOrg provides a mock function with given fields: _a0, _a1 -func (_m *MockService) SetUsingOrg(_a0 context.Context, _a1 *user.SetUsingOrgCommand) error { - ret := _m.Called(_a0, _a1) - - if len(ret) == 0 { - panic("no return value specified for SetUsingOrg") - } - - var r0 error - if rf, ok := ret.Get(0).(func(context.Context, *user.SetUsingOrgCommand) error); ok { - r0 = rf(_a0, _a1) - } else { - r0 = ret.Error(0) - } - - return r0 -} - // Update provides a mock function with given fields: _a0, _a1 func (_m *MockService) Update(_a0 context.Context, _a1 *user.UpdateUserCommand) error { ret := _m.Called(_a0, _a1) diff --git a/pkg/setting/setting.go b/pkg/setting/setting.go index ed864575f6d..76406ffabec 100644 --- a/pkg/setting/setting.go +++ b/pkg/setting/setting.go @@ -470,10 +470,12 @@ type Cfg struct { RBACSingleOrganization bool // GRPC Server. - GRPCServerNetwork string - GRPCServerAddress string - GRPCServerTLSConfig *tls.Config - GRPCServerEnableLogging bool // log request and response of each unary gRPC call + GRPCServerNetwork string + GRPCServerAddress string + GRPCServerTLSConfig *tls.Config + GRPCServerEnableLogging bool // log request and response of each unary gRPC call + GRPCServerMaxRecvMsgSize int + GRPCServerMaxSendMsgSize int CustomResponseHeaders map[string]string @@ -516,6 +518,9 @@ type Cfg struct { // Experimental scope settings ScopesListScopesURL string ScopesListDashboardsURL string + + //Short Links + ShortLinkExpiration int } // AddChangePasswordLink returns if login form is disabled or not since @@ -1158,6 +1163,14 @@ func (cfg *Cfg) parseINIFile(iniFile *ini.File) error { queryHistory := iniFile.Section("query_history") cfg.QueryHistoryEnabled = queryHistory.Key("enabled").MustBool(true) + shortLinks := iniFile.Section("short_links") + cfg.ShortLinkExpiration = shortLinks.Key("expire_time").MustInt(7) + + if cfg.ShortLinkExpiration > 365 { + cfg.Logger.Warn("short_links expire_time must be less than 366 days. Setting to 365 days") + cfg.ShortLinkExpiration = 365 + } + panelsSection := iniFile.Section("panels") cfg.DisableSanitizeHtml = panelsSection.Key("disable_sanitize_html").MustBool(false) @@ -1758,6 +1771,8 @@ func readGRPCServerSettings(cfg *Cfg, iniFile *ini.File) error { cfg.GRPCServerNetwork = valueAsString(server, "network", "tcp") cfg.GRPCServerAddress = valueAsString(server, "address", "") cfg.GRPCServerEnableLogging = server.Key("enable_logging").MustBool(false) + cfg.GRPCServerMaxRecvMsgSize = server.Key("max_recv_msg_size").MustInt(0) + cfg.GRPCServerMaxSendMsgSize = server.Key("max_send_msg_size").MustInt(0) switch cfg.GRPCServerNetwork { case "unix": if cfg.GRPCServerAddress != "" { diff --git a/pkg/tests/api/alerting/api_alertmanager_test.go b/pkg/tests/api/alerting/api_alertmanager_test.go index afb2e920897..653def960c5 100644 --- a/pkg/tests/api/alerting/api_alertmanager_test.go +++ b/pkg/tests/api/alerting/api_alertmanager_test.go @@ -20,6 +20,7 @@ import ( "github.com/grafana/grafana/pkg/expr" "github.com/grafana/grafana/pkg/infra/db" + "github.com/grafana/grafana/pkg/infra/tracing" apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models" ngstore "github.com/grafana/grafana/pkg/services/ngalert/store" @@ -2649,7 +2650,10 @@ func createUser(t *testing.T, db db.DB, cfg *setting.Cfg, cmd user.CreateUserCom quotaService := quotaimpl.ProvideService(db, cfg) orgService, err := orgimpl.ProvideService(db, cfg, quotaService) require.NoError(t, err) - usrSvc, err := userimpl.ProvideService(db, orgService, cfg, nil, nil, quotaService, supportbundlestest.NewFakeBundleService()) + usrSvc, err := userimpl.ProvideService( + db, orgService, cfg, nil, nil, tracing.InitializeTracerForTest(), + quotaService, supportbundlestest.NewFakeBundleService(), + ) require.NoError(t, err) u, err := usrSvc.Create(context.Background(), &cmd) diff --git a/pkg/tests/api/correlations/common_test.go b/pkg/tests/api/correlations/common_test.go index a42a497eda3..c14f4b3a937 100644 --- a/pkg/tests/api/correlations/common_test.go +++ b/pkg/tests/api/correlations/common_test.go @@ -9,6 +9,7 @@ import ( "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/server" "github.com/grafana/grafana/pkg/services/correlations" "github.com/grafana/grafana/pkg/services/datasources" @@ -160,7 +161,10 @@ func (c TestContext) createUser(cmd user.CreateUserCommand) User { quotaService := quotaimpl.ProvideService(store, c.env.Cfg) orgService, err := orgimpl.ProvideService(store, c.env.Cfg, quotaService) require.NoError(c.t, err) - usrSvc, err := userimpl.ProvideService(store, orgService, c.env.Cfg, nil, nil, quotaService, supportbundlestest.NewFakeBundleService()) + usrSvc, err := userimpl.ProvideService( + store, orgService, c.env.Cfg, nil, nil, tracing.InitializeTracerForTest(), + quotaService, supportbundlestest.NewFakeBundleService(), + ) require.NoError(c.t, err) user, err := usrSvc.Create(context.Background(), &cmd) diff --git a/pkg/tests/api/dashboards/api_dashboards_test.go b/pkg/tests/api/dashboards/api_dashboards_test.go index 0b280eed3d3..604f0df60b8 100644 --- a/pkg/tests/api/dashboards/api_dashboards_test.go +++ b/pkg/tests/api/dashboards/api_dashboards_test.go @@ -19,6 +19,7 @@ import ( "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/infra/db" + "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/services/dashboardimport" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/folder" @@ -123,7 +124,10 @@ func createUser(t *testing.T, db db.DB, cfg *setting.Cfg, cmd user.CreateUserCom quotaService := quotaimpl.ProvideService(db, cfg) orgService, err := orgimpl.ProvideService(db, cfg, quotaService) require.NoError(t, err) - usrSvc, err := userimpl.ProvideService(db, orgService, cfg, nil, nil, quotaService, supportbundlestest.NewFakeBundleService()) + usrSvc, err := userimpl.ProvideService( + db, orgService, cfg, nil, nil, tracing.InitializeTracerForTest(), + quotaService, supportbundlestest.NewFakeBundleService(), + ) require.NoError(t, err) u, err := usrSvc.Create(context.Background(), &cmd) diff --git a/pkg/tests/api/folders/api_folder_test.go b/pkg/tests/api/folders/api_folder_test.go index 8c959967b31..c61e94b0cd9 100644 --- a/pkg/tests/api/folders/api_folder_test.go +++ b/pkg/tests/api/folders/api_folder_test.go @@ -10,6 +10,7 @@ import ( "github.com/grafana/grafana-openapi-client-go/client/folders" "github.com/grafana/grafana-openapi-client-go/models" "github.com/grafana/grafana/pkg/infra/db" + "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/org/orgimpl" @@ -215,7 +216,10 @@ func createUser(t *testing.T, db db.DB, cfg *setting.Cfg, cmd user.CreateUserCom quotaService := quotaimpl.ProvideService(db, cfg) orgService, err := orgimpl.ProvideService(db, cfg, quotaService) require.NoError(t, err) - usrSvc, err := userimpl.ProvideService(db, orgService, cfg, nil, nil, quotaService, supportbundlestest.NewFakeBundleService()) + usrSvc, err := userimpl.ProvideService( + db, orgService, cfg, nil, nil, tracing.InitializeTracerForTest(), + quotaService, supportbundlestest.NewFakeBundleService(), + ) require.NoError(t, err) u, err := usrSvc.Create(context.Background(), &cmd) diff --git a/pkg/tests/api/plugins/api_plugins_test.go b/pkg/tests/api/plugins/api_plugins_test.go index a026f9d8336..ba04251c1fb 100644 --- a/pkg/tests/api/plugins/api_plugins_test.go +++ b/pkg/tests/api/plugins/api_plugins_test.go @@ -18,6 +18,7 @@ import ( "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/infra/db" + "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/services/org/orgimpl" "github.com/grafana/grafana/pkg/services/quota/quotaimpl" @@ -201,7 +202,10 @@ func createUser(t *testing.T, db db.DB, cfg *setting.Cfg, cmd user.CreateUserCom quotaService := quotaimpl.ProvideService(db, cfg) orgService, err := orgimpl.ProvideService(db, cfg, quotaService) require.NoError(t, err) - usrSvc, err := userimpl.ProvideService(db, orgService, cfg, nil, nil, quotaService, supportbundlestest.NewFakeBundleService()) + usrSvc, err := userimpl.ProvideService( + db, orgService, cfg, nil, nil, tracing.InitializeTracerForTest(), + quotaService, supportbundlestest.NewFakeBundleService(), + ) require.NoError(t, err) _, err = usrSvc.Create(context.Background(), &cmd) diff --git a/pkg/tests/api/stats/admin_test.go b/pkg/tests/api/stats/admin_test.go index 019fd8608bc..3e264a75e0c 100644 --- a/pkg/tests/api/stats/admin_test.go +++ b/pkg/tests/api/stats/admin_test.go @@ -9,6 +9,7 @@ import ( "github.com/stretchr/testify/require" "github.com/grafana/grafana/pkg/infra/db" + "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/org/orgimpl" "github.com/grafana/grafana/pkg/services/quota/quotaimpl" @@ -89,7 +90,10 @@ func createUser(t *testing.T, db db.DB, cfg *setting.Cfg, cmd user.CreateUserCom quotaService := quotaimpl.ProvideService(db, cfg) orgService, err := orgimpl.ProvideService(db, cfg, quotaService) require.NoError(t, err) - usrSvc, err := userimpl.ProvideService(db, orgService, cfg, nil, nil, quotaService, supportbundlestest.NewFakeBundleService()) + usrSvc, err := userimpl.ProvideService( + db, orgService, cfg, nil, nil, tracing.InitializeTracerForTest(), + quotaService, supportbundlestest.NewFakeBundleService(), + ) require.NoError(t, err) u, err := usrSvc.Create(context.Background(), &cmd) diff --git a/pkg/tests/apis/helper.go b/pkg/tests/apis/helper.go index c44034c05dc..c491dea3847 100644 --- a/pkg/tests/apis/helper.go +++ b/pkg/tests/apis/helper.go @@ -25,6 +25,7 @@ import ( "k8s.io/client-go/rest" "github.com/grafana/grafana/pkg/infra/localcache" + "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/server" "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request" "github.com/grafana/grafana/pkg/services/auth/identity" @@ -392,12 +393,13 @@ func (c K8sTestHelper) createTestUsers(orgName string) OrgUsers { c.env.Cfg.AutoAssignOrg = true c.env.Cfg.AutoAssignOrgId = int(orgId) - teamSvc, err := teamimpl.ProvideService(store, c.env.Cfg) + teamSvc, err := teamimpl.ProvideService(store, c.env.Cfg, tracing.InitializeTracerForTest()) require.NoError(c.t, err) cache := localcache.ProvideService() - userSvc, err := userimpl.ProvideService(store, - orgService, c.env.Cfg, teamSvc, cache, quotaService, + userSvc, err := userimpl.ProvideService( + store, orgService, c.env.Cfg, teamSvc, + cache, tracing.InitializeTracerForTest(), quotaService, supportbundlestest.NewFakeBundleService()) require.NoError(c.t, err) diff --git a/pkg/tests/testinfra/testinfra.go b/pkg/tests/testinfra/testinfra.go index 24b046d7aed..c5c9b14452c 100644 --- a/pkg/tests/testinfra/testinfra.go +++ b/pkg/tests/testinfra/testinfra.go @@ -20,6 +20,7 @@ import ( "github.com/grafana/grafana/pkg/extensions" "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/fs" + "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/server" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/org/orgimpl" @@ -442,7 +443,9 @@ func CreateUser(t *testing.T, store db.DB, cfg *setting.Cfg, cmd user.CreateUser quotaService := quotaimpl.ProvideService(store, cfg) orgService, err := orgimpl.ProvideService(store, cfg, quotaService) require.NoError(t, err) - usrSvc, err := userimpl.ProvideService(store, orgService, cfg, nil, nil, quotaService, supportbundlestest.NewFakeBundleService()) + usrSvc, err := userimpl.ProvideService( + store, orgService, cfg, nil, nil, tracing.InitializeTracerForTest(), quotaService, supportbundlestest.NewFakeBundleService(), + ) require.NoError(t, err) o, err := orgService.CreateWithMember(context.Background(), &org.CreateOrgCommand{Name: fmt.Sprintf("test org %d", time.Now().UnixNano())}) diff --git a/pkg/tests/utils.go b/pkg/tests/utils.go index 4f4d7fcc28a..f81683c80f4 100644 --- a/pkg/tests/utils.go +++ b/pkg/tests/utils.go @@ -10,6 +10,7 @@ import ( "github.com/go-openapi/strfmt" goapi "github.com/grafana/grafana-openapi-client-go/client" "github.com/grafana/grafana/pkg/infra/db" + "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/services/accesscontrol/resourcepermissions" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/org/orgimpl" @@ -30,7 +31,10 @@ func CreateUser(t *testing.T, db db.DB, cfg *setting.Cfg, cmd user.CreateUserCom quotaService := quotaimpl.ProvideService(db, cfg) orgService, err := orgimpl.ProvideService(db, cfg, quotaService) require.NoError(t, err) - usrSvc, err := userimpl.ProvideService(db, orgService, cfg, nil, nil, quotaService, supportbundlestest.NewFakeBundleService()) + usrSvc, err := userimpl.ProvideService( + db, orgService, cfg, nil, nil, tracing.InitializeTracerForTest(), + quotaService, supportbundlestest.NewFakeBundleService(), + ) require.NoError(t, err) u, err := usrSvc.Create(context.Background(), &cmd) diff --git a/pkg/tsdb/azuremonitor/httpclient.go b/pkg/tsdb/azuremonitor/httpclient.go index 53fd0735d7a..1b6338a75de 100644 --- a/pkg/tsdb/azuremonitor/httpclient.go +++ b/pkg/tsdb/azuremonitor/httpclient.go @@ -39,6 +39,8 @@ func newHTTPClient(ctx context.Context, route types.AzRoute, model types.Datasou authOpts := azhttpclient.NewAuthOptions(azureSettings) authOpts.AllowUserIdentity() + // Allows requests from the same identity but different Grafana users to be identified as such by the server + authOpts.AddRateLimitSession(true) authOpts.Scopes(route.Scopes) azhttpclient.AddAzureAuthentication(&clientOpts, authOpts, model.Credentials) } diff --git a/pkg/tsdb/cloudwatch/get_dimension_values_for_wildcards.go b/pkg/tsdb/cloudwatch/get_dimension_values_for_wildcards.go index 3fc8f469942..96260038064 100644 --- a/pkg/tsdb/cloudwatch/get_dimension_values_for_wildcards.go +++ b/pkg/tsdb/cloudwatch/get_dimension_values_for_wildcards.go @@ -12,17 +12,27 @@ import ( ) // getDimensionValues gets the actual dimension values for dimensions with a wildcard -func (e *cloudWatchExecutor) getDimensionValuesForWildcards(ctx context.Context, region string, - client models.CloudWatchMetricsAPIProvider, origQueries []*models.CloudWatchQuery, tagValueCache *cache.Cache, listMetricsPageLimit int) ([]*models.CloudWatchQuery, error) { +func (e *cloudWatchExecutor) getDimensionValuesForWildcards( + ctx context.Context, + region string, + client models.CloudWatchMetricsAPIProvider, + origQueries []*models.CloudWatchQuery, + tagValueCache *cache.Cache, + listMetricsPageLimit int, + shouldSkip func(*models.CloudWatchQuery) bool) ([]*models.CloudWatchQuery, error) { metricsClient := clients.NewMetricsClient(client, listMetricsPageLimit) service := services.NewListMetricsService(metricsClient) // create copies of the original query. All the fields besides Dimensions are primitives queries := copyQueries(origQueries) + queries = addWildcardDimensionsForMetricQueryTypeQueries(queries) for _, query := range queries { + if shouldSkip(query) { + continue + } for dimensionKey, values := range query.Dimensions { // if the dimension is not a wildcard, skip it - if len(values) != 1 || query.MatchExact || (len(values) == 1 && values[0] != "*") { + if len(values) != 1 || (len(values) == 1 && values[0] != "*") { continue } @@ -85,3 +95,22 @@ func copyQueries(origQueries []*models.CloudWatchQuery) []*models.CloudWatchQuer } return newQueries } + +// addWildcardDimensionsForMetricQueryTypeQueries adds wildcard dimensions if there is +// a `GROUP BY` clause in the query. This is used for MetricQuery type queries so we can +// build labels when we build the data frame. +func addWildcardDimensionsForMetricQueryTypeQueries(queries []*models.CloudWatchQuery) []*models.CloudWatchQuery { + for i, q := range queries { + if q.MetricQueryType != models.MetricQueryTypeQuery || q.MetricEditorMode == models.MetricEditorModeRaw || q.Sql.GroupBy == nil || len(q.Sql.GroupBy.Expressions) == 0 { + continue + } + + for _, expr := range q.Sql.GroupBy.Expressions { + if expr.Property.Name != nil && *expr.Property.Name != "" { + queries[i].Dimensions[*expr.Property.Name] = []string{"*"} + } + } + } + + return queries +} diff --git a/pkg/tsdb/cloudwatch/get_dimension_values_for_wildcards_test.go b/pkg/tsdb/cloudwatch/get_dimension_values_for_wildcards_test.go index 7a18ba59f8c..2a3f5c36e78 100644 --- a/pkg/tsdb/cloudwatch/get_dimension_values_for_wildcards_test.go +++ b/pkg/tsdb/cloudwatch/get_dimension_values_for_wildcards_test.go @@ -6,6 +6,7 @@ import ( "github.com/aws/aws-sdk-go/service/cloudwatch" "github.com/grafana/grafana-plugin-sdk-go/backend/log" + "github.com/grafana/grafana/pkg/tsdb/cloudwatch/kinds/dataquery" "github.com/grafana/grafana/pkg/tsdb/cloudwatch/mocks" "github.com/grafana/grafana/pkg/tsdb/cloudwatch/models" "github.com/grafana/grafana/pkg/tsdb/cloudwatch/utils" @@ -13,96 +14,159 @@ import ( "github.com/stretchr/testify/assert" ) +func noSkip(q *models.CloudWatchQuery) bool { return false } +func skip(q *models.CloudWatchQuery) bool { return true } + func TestGetDimensionValuesForWildcards(t *testing.T) { executor := &cloudWatchExecutor{im: defaultTestInstanceManager(), logger: log.NewNullLogger()} ctx := context.Background() - tagValueCache := cache.New(0, 0) - t.Run("Should not change non-wildcard dimension value", func(t *testing.T) { - query := getBaseQuery() - query.MetricName = "Test_MetricName1" - query.Dimensions = map[string][]string{"Test_DimensionName1": {"Value1"}} - queries, err := executor.getDimensionValuesForWildcards(ctx, "us-east-1", nil, []*models.CloudWatchQuery{query}, tagValueCache, 50) - assert.Nil(t, err) - assert.Len(t, queries, 1) - assert.NotNil(t, queries[0].Dimensions["Test_DimensionName1"], 1) - assert.Equal(t, []string{"Value1"}, queries[0].Dimensions["Test_DimensionName1"]) + t.Run("Tag value cache", func(t *testing.T) { + tagValueCache := cache.New(0, 0) + + t.Run("Should use cache for previously fetched value", func(t *testing.T) { + query := getBaseQuery() + query.MetricName = "Test_MetricName" + query.Dimensions = map[string][]string{"Test_DimensionName": {"*"}} + query.MetricQueryType = models.MetricQueryTypeSearch + query.MatchExact = false + api := &mocks.MetricsAPI{Metrics: []*cloudwatch.Metric{ + {MetricName: utils.Pointer("Test_MetricName"), Dimensions: []*cloudwatch.Dimension{{Name: utils.Pointer("Test_DimensionName"), Value: utils.Pointer("Value")}}}, + }} + api.On("ListMetricsPagesWithContext").Return(nil) + _, err := executor.getDimensionValuesForWildcards(ctx, "us-east-1", api, []*models.CloudWatchQuery{query}, tagValueCache, 50, noSkip) + assert.Nil(t, err) + // make sure the original query wasn't altered + assert.Equal(t, map[string][]string{"Test_DimensionName": {"*"}}, query.Dimensions) + + //setting the api to nil confirms that it's using the cached value + queries, err := executor.getDimensionValuesForWildcards(ctx, "us-east-1", nil, []*models.CloudWatchQuery{query}, tagValueCache, 50, noSkip) + assert.Nil(t, err) + assert.Len(t, queries, 1) + assert.Equal(t, map[string][]string{"Test_DimensionName": {"Value"}}, queries[0].Dimensions) + api.AssertExpectations(t) + }) + + t.Run("Should not cache when no values are returned", func(t *testing.T) { + query := getBaseQuery() + query.MetricName = "Test_MetricName" + query.Dimensions = map[string][]string{"Test_DimensionName2": {"*"}} + query.MetricQueryType = models.MetricQueryTypeSearch + query.MatchExact = false + api := &mocks.MetricsAPI{Metrics: []*cloudwatch.Metric{}} + api.On("ListMetricsPagesWithContext").Return(nil) + queries, err := executor.getDimensionValuesForWildcards(ctx, "us-east-1", api, []*models.CloudWatchQuery{query}, tagValueCache, 50, noSkip) + assert.Nil(t, err) + assert.Len(t, queries, 1) + // assert that the values was set to an empty array + assert.Equal(t, map[string][]string{"Test_DimensionName2": {}}, queries[0].Dimensions) + + // Confirm that it calls the api again if the last call did not return any values + api.Metrics = []*cloudwatch.Metric{ + {MetricName: utils.Pointer("Test_MetricName"), Dimensions: []*cloudwatch.Dimension{{Name: utils.Pointer("Test_DimensionName2"), Value: utils.Pointer("Value")}}}, + } + api.On("ListMetricsPagesWithContext").Return(nil) + queries, err = executor.getDimensionValuesForWildcards(ctx, "us-east-1", api, []*models.CloudWatchQuery{query}, tagValueCache, 50, noSkip) + assert.Nil(t, err) + assert.Len(t, queries, 1) + assert.Equal(t, map[string][]string{"Test_DimensionName2": {"Value"}}, queries[0].Dimensions) + api.AssertExpectations(t) + }) }) - t.Run("Should not change exact dimension value", func(t *testing.T) { - query := getBaseQuery() - query.MetricName = "Test_MetricName1" - query.Dimensions = map[string][]string{"Test_DimensionName1": {"*"}} - queries, err := executor.getDimensionValuesForWildcards(ctx, "us-east-1", nil, []*models.CloudWatchQuery{query}, tagValueCache, 50) - assert.Nil(t, err) - assert.Len(t, queries, 1) - assert.NotNil(t, queries[0].Dimensions["Test_DimensionName1"]) - assert.Equal(t, []string{"*"}, queries[0].Dimensions["Test_DimensionName1"]) + t.Run("MetricSearch query type", func(t *testing.T) { + t.Run("Should not change non-wildcard dimension value", func(t *testing.T) { + query := getBaseQuery() + query.MetricName = "Test_MetricName1" + query.Dimensions = map[string][]string{"Test_DimensionName1": {"Value1"}} + query.MetricQueryType = models.MetricQueryTypeSearch + queries, err := executor.getDimensionValuesForWildcards(ctx, "us-east-1", nil, []*models.CloudWatchQuery{query}, cache.New(0, 0), 50, skip) + assert.Nil(t, err) + assert.Len(t, queries, 1) + assert.NotNil(t, queries[0].Dimensions["Test_DimensionName1"], 1) + assert.Equal(t, []string{"Value1"}, queries[0].Dimensions["Test_DimensionName1"]) + }) + + t.Run("Should not change exact dimension value", func(t *testing.T) { + query := getBaseQuery() + query.MetricName = "Test_MetricName1" + query.Dimensions = map[string][]string{"Test_DimensionName1": {"*"}} + query.MetricQueryType = models.MetricQueryTypeSearch + queries, err := executor.getDimensionValuesForWildcards(ctx, "us-east-1", nil, []*models.CloudWatchQuery{query}, cache.New(0, 0), 50, skip) + assert.Nil(t, err) + assert.Len(t, queries, 1) + assert.NotNil(t, queries[0].Dimensions["Test_DimensionName1"]) + assert.Equal(t, []string{"*"}, queries[0].Dimensions["Test_DimensionName1"]) + }) + + t.Run("Should change wildcard dimension value", func(t *testing.T) { + query := getBaseQuery() + query.MetricName = "Test_MetricName1" + query.Dimensions = map[string][]string{"Test_DimensionName1": {"*"}} + query.MetricQueryType = models.MetricQueryTypeSearch + query.MatchExact = false + api := &mocks.MetricsAPI{Metrics: []*cloudwatch.Metric{ + {MetricName: utils.Pointer("Test_MetricName1"), Dimensions: []*cloudwatch.Dimension{{Name: utils.Pointer("Test_DimensionName1"), Value: utils.Pointer("Value1")}, {Name: utils.Pointer("Test_DimensionName2"), Value: utils.Pointer("Value2")}}}, + {MetricName: utils.Pointer("Test_MetricName2"), Dimensions: []*cloudwatch.Dimension{{Name: utils.Pointer("Test_DimensionName1"), Value: utils.Pointer("Value3")}}}, + {MetricName: utils.Pointer("Test_MetricName3"), Dimensions: []*cloudwatch.Dimension{{Name: utils.Pointer("Test_DimensionName1"), Value: utils.Pointer("Value4")}}}, + {MetricName: utils.Pointer("Test_MetricName4"), Dimensions: []*cloudwatch.Dimension{{Name: utils.Pointer("Test_DimensionName1"), Value: utils.Pointer("Value2")}}}, + }} + api.On("ListMetricsPagesWithContext").Return(nil) + queries, err := executor.getDimensionValuesForWildcards(ctx, "us-east-1", api, []*models.CloudWatchQuery{query}, cache.New(0, 0), 50, noSkip) + assert.Nil(t, err) + assert.Len(t, queries, 1) + assert.Equal(t, map[string][]string{"Test_DimensionName1": {"Value1", "Value2", "Value3", "Value4"}}, queries[0].Dimensions) + api.AssertExpectations(t) + }) }) - t.Run("Should change wildcard dimension value", func(t *testing.T) { - query := getBaseQuery() - query.MetricName = "Test_MetricName1" - query.Dimensions = map[string][]string{"Test_DimensionName1": {"*"}} - query.MatchExact = false - api := &mocks.MetricsAPI{Metrics: []*cloudwatch.Metric{ - {MetricName: utils.Pointer("Test_MetricName1"), Dimensions: []*cloudwatch.Dimension{{Name: utils.Pointer("Test_DimensionName1"), Value: utils.Pointer("Value1")}, {Name: utils.Pointer("Test_DimensionName2"), Value: utils.Pointer("Value2")}}}, - {MetricName: utils.Pointer("Test_MetricName2"), Dimensions: []*cloudwatch.Dimension{{Name: utils.Pointer("Test_DimensionName1"), Value: utils.Pointer("Value3")}}}, - {MetricName: utils.Pointer("Test_MetricName3"), Dimensions: []*cloudwatch.Dimension{{Name: utils.Pointer("Test_DimensionName1"), Value: utils.Pointer("Value4")}}}, - {MetricName: utils.Pointer("Test_MetricName4"), Dimensions: []*cloudwatch.Dimension{{Name: utils.Pointer("Test_DimensionName1"), Value: utils.Pointer("Value2")}}}, - }} - api.On("ListMetricsPagesWithContext").Return(nil) - queries, err := executor.getDimensionValuesForWildcards(ctx, "us-east-1", api, []*models.CloudWatchQuery{query}, tagValueCache, 50) - assert.Nil(t, err) - assert.Len(t, queries, 1) - assert.Equal(t, map[string][]string{"Test_DimensionName1": {"Value1", "Value2", "Value3", "Value4"}}, queries[0].Dimensions) - api.AssertExpectations(t) - }) + t.Run("MetricQuery query type", func(t *testing.T) { + t.Run("Should fetch dimensions when there is a `GROUP BY` clause", func(t *testing.T) { + query := getBaseQuery() + query.MetricName = "Test_MetricName" + query.Dimensions = map[string][]string{} + query.Sql.GroupBy = &models.SQLExpressionGroupBy{ + Expressions: []dataquery.QueryEditorGroupByExpression{ + { + Property: dataquery.QueryEditorProperty{Name: utils.Pointer("Test_DimensionName1"), Type: "string"}, + Type: "groupBy", + }, + { + Property: dataquery.QueryEditorProperty{Name: utils.Pointer("Test_DimensionName2"), Type: "string"}, + Type: "groupBy", + }, + }, + Type: "and", + } + query.MetricQueryType = models.MetricQueryTypeQuery - t.Run("Should use cache for previously fetched value", func(t *testing.T) { - query := getBaseQuery() - query.MetricName = "Test_MetricName" - query.Dimensions = map[string][]string{"Test_DimensionName": {"*"}} - query.MatchExact = false - api := &mocks.MetricsAPI{Metrics: []*cloudwatch.Metric{ - {MetricName: utils.Pointer("Test_MetricName"), Dimensions: []*cloudwatch.Dimension{{Name: utils.Pointer("Test_DimensionName"), Value: utils.Pointer("Value")}}}, - }} - api.On("ListMetricsPagesWithContext").Return(nil) - _, err := executor.getDimensionValuesForWildcards(ctx, "us-east-1", api, []*models.CloudWatchQuery{query}, tagValueCache, 50) - assert.Nil(t, err) - // make sure the original query wasn't altered - assert.Equal(t, map[string][]string{"Test_DimensionName": {"*"}}, query.Dimensions) + api := &mocks.MetricsAPI{Metrics: []*cloudwatch.Metric{ + {MetricName: utils.Pointer("Test_MetricName"), Dimensions: []*cloudwatch.Dimension{{Name: utils.Pointer("Test_DimensionName1"), Value: utils.Pointer("Dimension1Value1")}, {Name: utils.Pointer("Test_DimensionName2"), Value: utils.Pointer("Dimension2Value1")}}}, + {MetricName: utils.Pointer("Test_MetricName"), Dimensions: []*cloudwatch.Dimension{{Name: utils.Pointer("Test_DimensionName1"), Value: utils.Pointer("Dimension1Value2")}, {Name: utils.Pointer("Test_DimensionName2"), Value: utils.Pointer("Dimension2Value2")}}}, + {MetricName: utils.Pointer("Test_MetricName"), Dimensions: []*cloudwatch.Dimension{{Name: utils.Pointer("Test_DimensionName1"), Value: utils.Pointer("Dimension1Value3")}, {Name: utils.Pointer("Test_DimensionName2"), Value: utils.Pointer("Dimension2Value3")}}}, + {MetricName: utils.Pointer("Test_MetricName"), Dimensions: []*cloudwatch.Dimension{{Name: utils.Pointer("Test_DimensionName1"), Value: utils.Pointer("Dimension1Value4")}, {Name: utils.Pointer("Test_DimensionName2"), Value: utils.Pointer("Dimension2Value4")}}}, + }} + api.On("ListMetricsPagesWithContext").Return(nil) + queries, err := executor.getDimensionValuesForWildcards(ctx, "us-east-1", api, []*models.CloudWatchQuery{query}, cache.New(0, 0), 50, noSkip) + assert.Nil(t, err) + assert.Len(t, queries, 1) + assert.Equal(t, map[string][]string{ + "Test_DimensionName1": {"Dimension1Value1", "Dimension1Value2", "Dimension1Value3", "Dimension1Value4"}, + "Test_DimensionName2": {"Dimension2Value1", "Dimension2Value2", "Dimension2Value3", "Dimension2Value4"}, + }, queries[0].Dimensions) + api.AssertExpectations(t) + }) - //setting the api to nil confirms that it's using the cached value - queries, err := executor.getDimensionValuesForWildcards(ctx, "us-east-1", nil, []*models.CloudWatchQuery{query}, tagValueCache, 50) - assert.Nil(t, err) - assert.Len(t, queries, 1) - assert.Equal(t, map[string][]string{"Test_DimensionName": {"Value"}}, queries[0].Dimensions) - api.AssertExpectations(t) - }) + t.Run("Should not fetch dimensions when there is not a `GROUP BY` clause", func(t *testing.T) { + query := getBaseQuery() + query.MetricName = "Test_MetricName" + query.Dimensions = map[string][]string{} + query.MetricQueryType = models.MetricQueryTypeQuery - t.Run("Should not cache when no values are returned", func(t *testing.T) { - query := getBaseQuery() - query.MetricName = "Test_MetricName" - query.Dimensions = map[string][]string{"Test_DimensionName2": {"*"}} - query.MatchExact = false - api := &mocks.MetricsAPI{Metrics: []*cloudwatch.Metric{}} - api.On("ListMetricsPagesWithContext").Return(nil) - queries, err := executor.getDimensionValuesForWildcards(ctx, "us-east-1", api, []*models.CloudWatchQuery{query}, tagValueCache, 50) - assert.Nil(t, err) - assert.Len(t, queries, 1) - // assert that the values was set to an empty array - assert.Equal(t, map[string][]string{"Test_DimensionName2": {}}, queries[0].Dimensions) - - // Confirm that it calls the api again if the last call did not return any values - api.Metrics = []*cloudwatch.Metric{ - {MetricName: utils.Pointer("Test_MetricName"), Dimensions: []*cloudwatch.Dimension{{Name: utils.Pointer("Test_DimensionName2"), Value: utils.Pointer("Value")}}}, - } - api.On("ListMetricsPagesWithContext").Return(nil) - queries, err = executor.getDimensionValuesForWildcards(ctx, "us-east-1", api, []*models.CloudWatchQuery{query}, tagValueCache, 50) - assert.Nil(t, err) - assert.Len(t, queries, 1) - assert.Equal(t, map[string][]string{"Test_DimensionName2": {"Value"}}, queries[0].Dimensions) - api.AssertExpectations(t) + queries, err := executor.getDimensionValuesForWildcards(ctx, "us-east-1", nil, []*models.CloudWatchQuery{query}, cache.New(0, 0), 50, noSkip) + assert.Nil(t, err) + assert.Len(t, queries, 1) + assert.Equal(t, map[string][]string{}, queries[0].Dimensions) + }) }) } diff --git a/pkg/tsdb/cloudwatch/models/cloudwatch_query.go b/pkg/tsdb/cloudwatch/models/cloudwatch_query.go index 4d6afa1e9e7..56709d8442f 100644 --- a/pkg/tsdb/cloudwatch/models/cloudwatch_query.go +++ b/pkg/tsdb/cloudwatch/models/cloudwatch_query.go @@ -50,6 +50,16 @@ const ( chinaConsoleURL = "console.amazonaws.cn" ) +type SQLExpressionGroupBy struct { + Expressions []dataquery.QueryEditorGroupByExpression `json:"expressions"` + Type dataquery.QueryEditorArrayExpressionType `json:"type"` +} + +type sqlExpression struct { + dataquery.SQLExpression + GroupBy *SQLExpressionGroupBy `json:"groupBy,omitempty"` +} + type CloudWatchQuery struct { logger log.Logger RefId string @@ -59,6 +69,7 @@ type CloudWatchQuery struct { MetricName string Statistic string Expression string + Sql sqlExpression SqlExpression string ReturnData bool Dimensions map[string][]string @@ -210,8 +221,9 @@ var validMetricDataID = regexp.MustCompile(`^[a-z][a-zA-Z0-9_]*$`) type metricsDataQuery struct { dataquery.CloudWatchMetricsQuery - Type string `json:"type"` - TimezoneUTCOffset string `json:"timezoneUTCOffset"` + Sql *sqlExpression `json:"sql,omitempty"` + Type string `json:"type"` + TimezoneUTCOffset string `json:"timezoneUTCOffset"` } // ParseMetricDataQueries decodes the metric data queries json, validates, sets default values and returns an array of CloudWatchQueries. @@ -254,6 +266,10 @@ func ParseMetricDataQueries(dataQueries []backend.DataQuery, startTime time.Time cwQuery.MetricQueryType = *mdq.MetricQueryType } + if mdq.Sql != nil { + cwQuery.Sql = *mdq.Sql + } + if mdq.SqlExpression != nil { cwQuery.SqlExpression = *mdq.SqlExpression } diff --git a/pkg/tsdb/cloudwatch/response_parser.go b/pkg/tsdb/cloudwatch/response_parser.go index f8953021a1f..2feaf906c2c 100644 --- a/pkg/tsdb/cloudwatch/response_parser.go +++ b/pkg/tsdb/cloudwatch/response_parser.go @@ -114,17 +114,28 @@ func parseLabels(cloudwatchLabel string, query *models.CloudWatchQuery) (string, return splitLabels[0], labels } -func getLabels(cloudwatchLabel string, query *models.CloudWatchQuery) data.Labels { +func getLabels(cloudwatchLabel string, query *models.CloudWatchQuery, addSeriesLabelAsFallback bool) data.Labels { dims := make([]string, 0, len(query.Dimensions)) for k := range query.Dimensions { dims = append(dims, k) } sort.Strings(dims) labels := data.Labels{} + + if addSeriesLabelAsFallback { + labels["Series"] = cloudwatchLabel + } + for _, dim := range dims { values := query.Dimensions[dim] if len(values) == 1 && values[0] != "*" { labels[dim] = values[0] + } else if len(values) == 0 { + // Metric Insights metrics might not have a value for a dimension specified in the `GROUP BY` clause for Metric Query type queries. When this happens, CloudWatch returns "Other" in the label for the dimension so `len(values)` would be 0. + // We manually add "Other" as the value for the dimension to match what CloudWatch returns in the label. + // See the note under `GROUP BY` in https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/cloudwatch-metrics-insights-querylanguage.html + labels[dim] = "Other" + continue } else { for _, value := range values { if value == cloudwatchLabel || value == "*" { @@ -195,10 +206,12 @@ func buildDataFrames(ctx context.Context, startTime time.Time, endTime time.Time name := label var labels data.Labels - if features.IsEnabled(ctx, features.FlagCloudWatchNewLabelParsing) { + if query.GetGetMetricDataAPIMode() == models.GMDApiModeSQLExpression { + labels = getLabels(label, query, true) + } else if features.IsEnabled(ctx, features.FlagCloudWatchNewLabelParsing) { name, labels = parseLabels(label, query) } else { - labels = getLabels(label, query) + labels = getLabels(label, query, false) } timestamps := []*time.Time{} points := []*float64{} diff --git a/pkg/tsdb/cloudwatch/response_parser_test.go b/pkg/tsdb/cloudwatch/response_parser_test.go index bd992d17f10..be1d28107ce 100644 --- a/pkg/tsdb/cloudwatch/response_parser_test.go +++ b/pkg/tsdb/cloudwatch/response_parser_test.go @@ -363,7 +363,7 @@ func Test_buildDataFrames_parse_label_to_name_and_labels(t *testing.T) { assert.Equal(t, "res", frames[1].Fields[1].Labels["Resource"]) }) - t.Run("when not using multi-value dimension filters", func(t *testing.T) { + t.Run("when not using multi-value dimension filters on a `MetricSearch` query", func(t *testing.T) { timestamp := time.Unix(0, 0) response := &models.QueryRowResponse{ Metrics: []*cloudwatch.MetricDataResult{ @@ -391,7 +391,7 @@ func Test_buildDataFrames_parse_label_to_name_and_labels(t *testing.T) { }, Statistic: "Average", Period: 60, - MetricQueryType: models.MetricQueryTypeQuery, + MetricQueryType: models.MetricQueryTypeSearch, MetricEditorMode: models.MetricEditorModeRaw, } frames, err := buildDataFrames(contextWithFeaturesEnabled(features.FlagCloudWatchNewLabelParsing), startTime, endTime, *response, query) @@ -403,7 +403,7 @@ func Test_buildDataFrames_parse_label_to_name_and_labels(t *testing.T) { assert.Equal(t, "res", frames[0].Fields[1].Labels["Resource"]) }) - t.Run("when non-static label set on query", func(t *testing.T) { + t.Run("when non-static label set on a `MetricSearch` query", func(t *testing.T) { timestamp := time.Unix(0, 0) response := &models.QueryRowResponse{ Metrics: []*cloudwatch.MetricDataResult{ @@ -431,7 +431,7 @@ func Test_buildDataFrames_parse_label_to_name_and_labels(t *testing.T) { }, Statistic: "Average", Period: 60, - MetricQueryType: models.MetricQueryTypeQuery, + MetricQueryType: models.MetricQueryTypeSearch, MetricEditorMode: models.MetricEditorModeBuilder, Label: "set ${AVG} label", } @@ -444,7 +444,7 @@ func Test_buildDataFrames_parse_label_to_name_and_labels(t *testing.T) { assert.Equal(t, "res", frames[0].Fields[1].Labels["Resource"]) }) - t.Run("when static label set on query", func(t *testing.T) { + t.Run("when static label set on a `MetricSearch` query", func(t *testing.T) { timestamp := time.Unix(0, 0) response := &models.QueryRowResponse{ Metrics: []*cloudwatch.MetricDataResult{ @@ -472,7 +472,7 @@ func Test_buildDataFrames_parse_label_to_name_and_labels(t *testing.T) { }, Statistic: "Average", Period: 60, - MetricQueryType: models.MetricQueryTypeQuery, + MetricQueryType: models.MetricQueryTypeSearch, MetricEditorMode: models.MetricEditorModeBuilder, Label: "actual", } @@ -485,6 +485,84 @@ func Test_buildDataFrames_parse_label_to_name_and_labels(t *testing.T) { assert.Equal(t, "res", frames[0].Fields[1].Labels["Resource"]) }) + t.Run("when `MetricQuery` query has no label set and `GROUP BY` clause has multiple fields", func(t *testing.T) { + timestamp := time.Unix(0, 0) + response := &models.QueryRowResponse{ + Metrics: []*cloudwatch.MetricDataResult{ + { + Id: aws.String("query1"), + Label: aws.String("EC2 vCPU"), + Timestamps: []*time.Time{ + aws.Time(timestamp), + }, + Values: []*float64{aws.Float64(23)}, + StatusCode: aws.String("Complete"), + }, + { + Id: aws.String("query2"), + Label: aws.String("Elastic Loading Balancing ApplicationLoadBalancersPerRegion"), + Timestamps: []*time.Time{ + aws.Time(timestamp), + }, + Values: []*float64{aws.Float64(23)}, + StatusCode: aws.String("Complete"), + }, + }, + } + + query := &models.CloudWatchQuery{ + RefId: "refId1", + Region: "us-east-1", + Statistic: "Average", + Period: 60, + MetricQueryType: models.MetricQueryTypeQuery, + MetricEditorMode: models.MetricEditorModeBuilder, + Dimensions: map[string][]string{"Service": {"EC2", "Elastic Loading Balancing"}, "Resource": {"vCPU", "ApplicationLoadBalancersPerRegion"}}, + SqlExpression: "SELECT AVG(ResourceCount) FROM SCHEMA(\"AWS/Usage\", Class, Resource, Service, Type) GROUP BY Service, Resource", + } + frames, err := buildDataFrames(contextWithFeaturesEnabled(features.FlagCloudWatchNewLabelParsing), startTime, endTime, *response, query) + require.NoError(t, err) + + assert.Equal(t, "EC2 vCPU", frames[0].Name) + assert.Equal(t, "EC2", frames[0].Fields[1].Labels["Service"]) + assert.Equal(t, "vCPU", frames[0].Fields[1].Labels["Resource"]) + assert.Equal(t, "Elastic Loading Balancing ApplicationLoadBalancersPerRegion", frames[1].Name) + assert.Equal(t, "Elastic Loading Balancing", frames[1].Fields[1].Labels["Service"]) + assert.Equal(t, "ApplicationLoadBalancersPerRegion", frames[1].Fields[1].Labels["Resource"]) + }) + + t.Run("when `MetricQuery` query has no `GROUP BY` clause", func(t *testing.T) { + timestamp := time.Unix(0, 0) + response := &models.QueryRowResponse{ + Metrics: []*cloudwatch.MetricDataResult{ + { + Id: aws.String("query1"), + Label: aws.String("cloudwatch-default-label"), + Timestamps: []*time.Time{ + aws.Time(timestamp), + }, + Values: []*float64{aws.Float64(23)}, + StatusCode: aws.String("Complete"), + }, + }, + } + + query := &models.CloudWatchQuery{ + RefId: "refId1", + Region: "us-east-1", + Statistic: "Average", + Period: 60, + MetricQueryType: models.MetricQueryTypeQuery, + MetricEditorMode: models.MetricEditorModeBuilder, + SqlExpression: "SELECT AVG(ResourceCount) FROM SCHEMA(\"AWS/Usage\", Class, Resource, Service, Type)", + } + frames, err := buildDataFrames(contextWithFeaturesEnabled(features.FlagCloudWatchNewLabelParsing), startTime, endTime, *response, query) + require.NoError(t, err) + + assert.Equal(t, "cloudwatch-default-label", frames[0].Name) + assert.Equal(t, "cloudwatch-default-label", frames[0].Fields[1].Labels["Series"]) + }) + t.Run("Parse cloudwatch response", func(t *testing.T) { timestamp := time.Unix(0, 0) response := &models.QueryRowResponse{ diff --git a/pkg/tsdb/cloudwatch/time_series_query.go b/pkg/tsdb/cloudwatch/time_series_query.go index 6b5777fc79e..e7d09de8ef0 100644 --- a/pkg/tsdb/cloudwatch/time_series_query.go +++ b/pkg/tsdb/cloudwatch/time_series_query.go @@ -96,11 +96,20 @@ func (e *cloudWatchExecutor) executeTimeSeriesQuery(ctx context.Context, req *ba return err } - if !features.IsEnabled(ctx, features.FlagCloudWatchNewLabelParsing) { - requestQueries, err = e.getDimensionValuesForWildcards(ctx, region, client, requestQueries, instance.tagValueCache, instance.Settings.GrafanaSettings.ListMetricsPageLimit) - if err != nil { - return err + newLabelParsingEnabled := features.IsEnabled(ctx, features.FlagCloudWatchNewLabelParsing) + requestQueries, err = e.getDimensionValuesForWildcards(ctx, region, client, requestQueries, instance.tagValueCache, instance.Settings.GrafanaSettings.ListMetricsPageLimit, func(q *models.CloudWatchQuery) bool { + if q.MetricQueryType == models.MetricQueryTypeSearch && (q.MatchExact || newLabelParsingEnabled) { + return true } + + if q.MetricQueryType == models.MetricQueryTypeQuery && q.MetricEditorMode == models.MetricEditorModeRaw { + return true + } + + return false + }) + if err != nil { + return err } res, err := e.parseResponse(ctx, startTime, endTime, mdo, requestQueries) diff --git a/pkg/tsdb/grafana-postgresql-datasource/sqleng/sql_engine_test.go b/pkg/tsdb/grafana-postgresql-datasource/sqleng/sql_engine_test.go index b7f422327ac..4d511a53f25 100644 --- a/pkg/tsdb/grafana-postgresql-datasource/sqleng/sql_engine_test.go +++ b/pkg/tsdb/grafana-postgresql-datasource/sqleng/sql_engine_test.go @@ -13,9 +13,10 @@ import ( "github.com/stretchr/testify/require" "github.com/grafana/grafana-plugin-sdk-go/backend/log" - "github.com/grafana/grafana/pkg/tsdb/grafana-postgresql-datasource/sqleng/util" ) +func Pointer[T any](v T) *T { return &v } + func TestSQLEngine(t *testing.T) { dt := time.Date(2018, 3, 14, 21, 20, 6, int(527345*time.Microsecond), time.UTC) @@ -73,19 +74,19 @@ func TestSQLEngine(t *testing.T) { tSeconds, }), data.NewField("time2", nil, []*int64{ - util.Pointer(tSeconds), + Pointer(tSeconds), }), data.NewField("time3", nil, []int64{ tMilliseconds, }), data.NewField("time4", nil, []*int64{ - util.Pointer(tMilliseconds), + Pointer(tMilliseconds), }), data.NewField("time5", nil, []int64{ tNanoSeconds, }), data.NewField("time6", nil, []*int64{ - util.Pointer(tNanoSeconds), + Pointer(tNanoSeconds), }), data.NewField("time7", nil, []*int64{ nilPointer, @@ -117,19 +118,19 @@ func TestSQLEngine(t *testing.T) { tSeconds, }), data.NewField("time2", nil, []*uint64{ - util.Pointer(tSeconds), + Pointer(tSeconds), }), data.NewField("time3", nil, []uint64{ tMilliseconds, }), data.NewField("time4", nil, []*uint64{ - util.Pointer(tMilliseconds), + Pointer(tMilliseconds), }), data.NewField("time5", nil, []uint64{ tNanoSeconds, }), data.NewField("time6", nil, []*uint64{ - util.Pointer(tNanoSeconds), + Pointer(tNanoSeconds), }), data.NewField("time7", nil, []*uint64{ nilPointer, @@ -159,7 +160,7 @@ func TestSQLEngine(t *testing.T) { tSeconds, }), data.NewField("time2", nil, []*int32{ - util.Pointer(tSeconds), + Pointer(tSeconds), }), data.NewField("time7", nil, []*int32{ nilInt, @@ -184,7 +185,7 @@ func TestSQLEngine(t *testing.T) { tSeconds, }), data.NewField("time2", nil, []*uint32{ - util.Pointer(tSeconds), + Pointer(tSeconds), }), data.NewField("time7", nil, []*uint32{ nilInt, @@ -210,19 +211,19 @@ func TestSQLEngine(t *testing.T) { tSeconds, }), data.NewField("time2", nil, []*float64{ - util.Pointer(tSeconds), + Pointer(tSeconds), }), data.NewField("time3", nil, []float64{ tMilliseconds, }), data.NewField("time4", nil, []*float64{ - util.Pointer(tMilliseconds), + Pointer(tMilliseconds), }), data.NewField("time5", nil, []float64{ tNanoSeconds, }), data.NewField("time6", nil, []*float64{ - util.Pointer(tNanoSeconds), + Pointer(tNanoSeconds), }), data.NewField("time7", nil, []*float64{ nilPointer, @@ -252,7 +253,7 @@ func TestSQLEngine(t *testing.T) { tSeconds, }), data.NewField("time2", nil, []*float32{ - util.Pointer(tSeconds), + Pointer(tSeconds), }), data.NewField("time7", nil, []*float32{ nilInt, @@ -273,61 +274,61 @@ func TestSQLEngine(t *testing.T) { int64(1), }), data.NewField("value2", nil, []*int64{ - util.Pointer(int64(1)), + Pointer(int64(1)), }), data.NewField("value3", nil, []int32{ int32(1), }), data.NewField("value4", nil, []*int32{ - util.Pointer(int32(1)), + Pointer(int32(1)), }), data.NewField("value5", nil, []int16{ int16(1), }), data.NewField("value6", nil, []*int16{ - util.Pointer(int16(1)), + Pointer(int16(1)), }), data.NewField("value7", nil, []int8{ int8(1), }), data.NewField("value8", nil, []*int8{ - util.Pointer(int8(1)), + Pointer(int8(1)), }), data.NewField("value9", nil, []float64{ float64(1), }), data.NewField("value10", nil, []*float64{ - util.Pointer(1.0), + Pointer(1.0), }), data.NewField("value11", nil, []float32{ float32(1), }), data.NewField("value12", nil, []*float32{ - util.Pointer(float32(1)), + Pointer(float32(1)), }), data.NewField("value13", nil, []uint64{ uint64(1), }), data.NewField("value14", nil, []*uint64{ - util.Pointer(uint64(1)), + Pointer(uint64(1)), }), data.NewField("value15", nil, []uint32{ uint32(1), }), data.NewField("value16", nil, []*uint32{ - util.Pointer(uint32(1)), + Pointer(uint32(1)), }), data.NewField("value17", nil, []uint16{ uint16(1), }), data.NewField("value18", nil, []*uint16{ - util.Pointer(uint16(1)), + Pointer(uint16(1)), }), data.NewField("value19", nil, []uint8{ uint8(1), }), data.NewField("value20", nil, []*uint8{ - util.Pointer(uint8(1)), + Pointer(uint8(1)), }), ) for i := 0; i < len(originFrame.Fields); i++ { diff --git a/pkg/tsdb/grafana-postgresql-datasource/sqleng/util/util.go b/pkg/tsdb/grafana-postgresql-datasource/sqleng/util/util.go deleted file mode 100644 index f6b5d330c17..00000000000 --- a/pkg/tsdb/grafana-postgresql-datasource/sqleng/util/util.go +++ /dev/null @@ -1,3 +0,0 @@ -package util - -func Pointer[T any](v T) *T { return &v } diff --git a/pkg/tsdb/legacydata/service/service_test.go b/pkg/tsdb/legacydata/service/service_test.go index a38e7b43bb1..df7e0965c69 100644 --- a/pkg/tsdb/legacydata/service/service_test.go +++ b/pkg/tsdb/legacydata/service/service_test.go @@ -42,16 +42,16 @@ func TestHandleRequest(t *testing.T) { actualReq = req return backend.NewQueryDataResponse(), nil } - sqlStore := db.InitTestDB(t) + sqlStore, cfg := db.InitTestDBWithCfg(t) secretsService := secretsmng.SetupTestService(t, fakes.NewFakeSecretsStore()) secretsStore := secretskvs.NewSQLSecretsKVStore(sqlStore, secretsService, log.New("test.logger")) datasourcePermissions := acmock.NewMockedPermissionsService() quotaService := quotatest.New(false, nil) dsCache := datasourceservice.ProvideCacheService(localcache.ProvideService(), sqlStore, guardian.ProvideGuardian()) - dsService, err := datasourceservice.ProvideService(nil, secretsService, secretsStore, sqlStore.Cfg, featuremgmt.WithFeatures(), acmock.New(), datasourcePermissions, quotaService, &pluginstore.FakePluginStore{}) + dsService, err := datasourceservice.ProvideService(nil, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), datasourcePermissions, quotaService, &pluginstore.FakePluginStore{}) require.NoError(t, err) - pCtxProvider := plugincontext.ProvideService(sqlStore.Cfg, localcache.ProvideService(), &pluginstore.FakePluginStore{ + pCtxProvider := plugincontext.ProvideService(cfg, localcache.ProvideService(), &pluginstore.FakePluginStore{ PluginList: []pluginstore.Plugin{{JSONData: plugins.JSONData{ID: "test"}}}, }, dsCache, dsService, pluginSettings.ProvideService(sqlStore, secretsService), pluginconfig.NewFakePluginRequestConfigProvider()) s := ProvideService(client, nil, dsService, pCtxProvider) diff --git a/pkg/tsdb/loki/api.go b/pkg/tsdb/loki/api.go index f914063f095..49f3d5a94b8 100644 --- a/pkg/tsdb/loki/api.go +++ b/pkg/tsdb/loki/api.go @@ -367,7 +367,7 @@ func setXScopeOrgIDHeader(req *http.Request, ctx context.Context) *http.Request if len(tenantids) == 0 { // We assume we are not using multi-tenant mode, which is fine logger.Debug("Tenant ID not present. Header not set") - } else if len(tenantids[0]) > 1 { + } else if len(tenantids) > 1 { // Loki supports multiple tenant IDs, but we should receive them from different contexts logger.Error(strconv.Itoa(len(tenantids)) + " tenant IDs found. Header not set") } else { diff --git a/pkg/util/xorm/go.mod b/pkg/util/xorm/go.mod index d499a5d5137..88a83bc208a 100644 --- a/pkg/util/xorm/go.mod +++ b/pkg/util/xorm/go.mod @@ -14,7 +14,7 @@ require ( github.com/go-sql-driver/mysql v1.7.1 // indirect github.com/kr/pretty v0.3.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect - github.com/rogpeppe/go-internal v1.11.0 // indirect + github.com/rogpeppe/go-internal v1.12.0 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/pkg/util/xorm/go.sum b/pkg/util/xorm/go.sum index 15e6719de9d..6a1b138bd40 100644 --- a/pkg/util/xorm/go.sum +++ b/pkg/util/xorm/go.sum @@ -23,8 +23,7 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN 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.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= -github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= -github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= +github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= 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.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= diff --git a/public/api-enterprise-spec.json b/public/api-enterprise-spec.json index e506c1d1ca6..c539729cf4c 100644 --- a/public/api-enterprise-spec.json +++ b/public/api-enterprise-spec.json @@ -760,6 +760,155 @@ } } }, + "/datasources/{dataSourceUID}/cache": { + "get": { + "description": "get cache config for a single data source", + "tags": [ + "enterprise" + ], + "operationId": "getDataSourceCacheConfig", + "parameters": [ + { + "type": "string", + "name": "dataSourceUID", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "CacheConfigResponse", + "schema": { + "$ref": "#/definitions/CacheConfigResponse" + } + }, + "500": { + "$ref": "#/responses/internalServerError" + } + } + }, + "post": { + "description": "set cache config for a single data source", + "tags": [ + "enterprise" + ], + "operationId": "setDataSourceCacheConfig", + "parameters": [ + { + "type": "string", + "name": "dataSourceUID", + "in": "path", + "required": true + }, + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/CacheConfigSetter" + } + } + ], + "responses": { + "200": { + "description": "CacheConfigResponse", + "schema": { + "$ref": "#/definitions/CacheConfigResponse" + } + }, + "400": { + "$ref": "#/responses/badRequestError" + }, + "500": { + "$ref": "#/responses/internalServerError" + } + } + } + }, + "/datasources/{dataSourceUID}/cache/clean": { + "post": { + "description": "clean cache for a single data source", + "tags": [ + "enterprise" + ], + "operationId": "cleanDataSourceCache", + "parameters": [ + { + "type": "string", + "name": "dataSourceUID", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "CacheConfigResponse", + "schema": { + "$ref": "#/definitions/CacheConfigResponse" + } + }, + "500": { + "$ref": "#/responses/internalServerError" + } + } + } + }, + "/datasources/{dataSourceUID}/cache/disable": { + "post": { + "description": "disable cache for a single data source", + "tags": [ + "enterprise" + ], + "operationId": "disableDataSourceCache", + "parameters": [ + { + "type": "string", + "name": "dataSourceUID", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "CacheConfigResponse", + "schema": { + "$ref": "#/definitions/CacheConfigResponse" + } + }, + "500": { + "$ref": "#/responses/internalServerError" + } + } + } + }, + "/datasources/{dataSourceUID}/cache/enable": { + "post": { + "description": "enable cache for a single data source", + "tags": [ + "enterprise" + ], + "operationId": "enableDataSourceCache", + "parameters": [ + { + "type": "string", + "name": "dataSourceUID", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "CacheConfigResponse", + "schema": { + "$ref": "#/definitions/CacheConfigResponse" + } + }, + "500": { + "$ref": "#/responses/internalServerError" + } + } + } + }, "/licensing/check": { "get": { "tags": [ @@ -2502,6 +2651,123 @@ "Value": {} } }, + "CacheConfig": { + "description": "Config defines the internal representation of a cache configuration, including fields not set by the API caller", + "type": "object", + "properties": { + "created": { + "type": "string", + "format": "date-time" + }, + "dataSourceID": { + "description": "Fields that can be set by the API caller - read/write", + "type": "integer", + "format": "int64" + }, + "dataSourceUID": { + "type": "string" + }, + "defaultTTLMs": { + "description": "These are returned by the HTTP API, but are managed internally - read-only\nNote: 'created' and 'updated' are special properties managed automatically by xorm, but we are setting them manually", + "type": "integer", + "format": "int64" + }, + "enabled": { + "type": "boolean" + }, + "ttlQueriesMs": { + "description": "TTL MS, or \"time to live\", is how long a cached item will stay in the cache before it is removed (in milliseconds)", + "type": "integer", + "format": "int64" + }, + "ttlResourcesMs": { + "type": "integer", + "format": "int64" + }, + "updated": { + "type": "string", + "format": "date-time" + }, + "useDefaultTTL": { + "description": "If UseDefaultTTL is enabled, then the TTLQueriesMS and TTLResourcesMS in this object is always sent as the default TTL located in grafana.ini", + "type": "boolean" + } + } + }, + "CacheConfigResponse": { + "type": "object", + "properties": { + "created": { + "type": "string", + "format": "date-time" + }, + "dataSourceID": { + "description": "Fields that can be set by the API caller - read/write", + "type": "integer", + "format": "int64" + }, + "dataSourceUID": { + "type": "string" + }, + "defaultTTLMs": { + "description": "These are returned by the HTTP API, but are managed internally - read-only\nNote: 'created' and 'updated' are special properties managed automatically by xorm, but we are setting them manually", + "type": "integer", + "format": "int64" + }, + "enabled": { + "type": "boolean" + }, + "message": { + "type": "string" + }, + "ttlQueriesMs": { + "description": "TTL MS, or \"time to live\", is how long a cached item will stay in the cache before it is removed (in milliseconds)", + "type": "integer", + "format": "int64" + }, + "ttlResourcesMs": { + "type": "integer", + "format": "int64" + }, + "updated": { + "type": "string", + "format": "date-time" + }, + "useDefaultTTL": { + "description": "If UseDefaultTTL is enabled, then the TTLQueriesMS and TTLResourcesMS in this object is always sent as the default TTL located in grafana.ini", + "type": "boolean" + } + } + }, + "CacheConfigSetter": { + "description": "ConfigSetter defines the cache parameters that users can configure per datasource\nThis is only intended to be consumed by the SetCache HTTP Handler", + "type": "object", + "properties": { + "dataSourceID": { + "type": "integer", + "format": "int64" + }, + "dataSourceUID": { + "type": "string" + }, + "enabled": { + "type": "boolean" + }, + "ttlQueriesMs": { + "description": "TTL MS, or \"time to live\", is how long a cached item will stay in the cache before it is removed (in milliseconds)", + "type": "integer", + "format": "int64" + }, + "ttlResourcesMs": { + "type": "integer", + "format": "int64" + }, + "useDefaultTTL": { + "description": "If UseDefaultTTL is enabled, then the TTLQueriesMS and TTLResourcesMS in this object is always sent as the default TTL located in grafana.ini", + "type": "boolean" + } + } + }, "CalculateDiffTarget": { "type": "object", "properties": { @@ -2666,7 +2932,15 @@ "type": "string" } }, + "Policies": { + "description": "Policies contains all policy identifiers included in the certificate.\nIn Go 1.22, encoding/gob cannot handle and ignores this field.", + "type": "array", + "items": { + "$ref": "#/definitions/OID" + } + }, "PolicyIdentifiers": { + "description": "PolicyIdentifiers contains asn1.ObjectIdentifiers, the components\nof which are limited to int32. If a certificate contains a policy which\ncannot be represented by asn1.ObjectIdentifier, it will not be included in\nPolicyIdentifiers, but will be present in Policies, which contains all parsed\npolicy OIDs.", "type": "array", "items": { "$ref": "#/definitions/ObjectIdentifier" @@ -4396,7 +4670,7 @@ "type": "string" }, "IPMask": { - "description": "See type IPNet and func ParseCIDR for details.", + "description": "See type [IPNet] and func [ParseCIDR] for details.", "type": "array", "title": "An IPMask is a bitmask that can be used to manipulate\nIP addresses for IP addressing and routing.", "items": { @@ -4945,7 +5219,7 @@ } }, "Name": { - "description": "Name represents an X.509 distinguished name. This only includes the common\nelements of a DN. Note that Name is only an approximation of the X.509\nstructure. If an accurate representation is needed, asn1.Unmarshal the raw\nsubject or issuer as an RDNSequence.", + "description": "Name represents an X.509 distinguished name. This only includes the common\nelements of a DN. Note that Name is only an approximation of the X.509\nstructure. If an accurate representation is needed, asn1.Unmarshal the raw\nsubject or issuer as an [RDNSequence].", "type": "object", "properties": { "Country": { @@ -5028,6 +5302,10 @@ "format": "int64", "title": "NoticeSeverity is a type for the Severity property of a Notice." }, + "OID": { + "type": "object", + "title": "An OID represents an ASN.1 OBJECT IDENTIFIER." + }, "ObjectIdentifier": { "type": "array", "title": "An ObjectIdentifier represents an ASN.1 OBJECT IDENTIFIER.", @@ -7081,7 +7359,7 @@ } }, "URL": { - "description": "The general form represented is:\n\n[scheme:][//[userinfo@]host][/]path[?query][#fragment]\n\nURLs that do not start with a slash after the scheme are interpreted as:\n\nscheme:opaque[?query][#fragment]\n\nNote that the Path field is stored in decoded form: /%47%6f%2f becomes /Go/.\nA consequence is that it is impossible to tell which slashes in the Path were\nslashes in the raw URL and which were %2f. This distinction is rarely important,\nbut when it is, the code should use the EscapedPath method, which preserves\nthe original encoding of Path.\n\nThe RawPath field is an optional field which is only set when the default\nencoding of Path is different from the escaped path. See the EscapedPath method\nfor more details.\n\nURL's String method uses the EscapedPath method to obtain the path.", + "description": "The general form represented is:\n\n[scheme:][//[userinfo@]host][/]path[?query][#fragment]\n\nURLs that do not start with a slash after the scheme are interpreted as:\n\nscheme:opaque[?query][#fragment]\n\nThe Host field contains the host and port subcomponents of the URL.\nWhen the port is present, it is separated from the host with a colon.\nWhen the host is an IPv6 address, it must be enclosed in square brackets:\n\"[fe80::1]:80\". The [net.JoinHostPort] function combines a host and port\ninto a string suitable for the Host field, adding square brackets to\nthe host when necessary.\n\nNote that the Path field is stored in decoded form: /%47%6f%2f becomes /Go/.\nA consequence is that it is impossible to tell which slashes in the Path were\nslashes in the raw URL and which were %2f. This distinction is rarely important,\nbut when it is, the code should use the [URL.EscapedPath] method, which preserves\nthe original encoding of Path.\n\nThe RawPath field is an optional field which is only set when the default\nencoding of Path is different from the escaped path. See the EscapedPath method\nfor more details.\n\nURL's String method uses the EscapedPath method to obtain the path.", "type": "object", "title": "A URL represents a parsed URL (technically, a URI reference).", "properties": { @@ -7693,7 +7971,7 @@ } }, "Userinfo": { - "description": "The Userinfo type is an immutable encapsulation of username and\npassword details for a URL. An existing Userinfo value is guaranteed\nto have a username set (potentially empty, as allowed by RFC 2396),\nand optionally a password.", + "description": "The Userinfo type is an immutable encapsulation of username and\npassword details for a [URL]. An existing Userinfo value is guaranteed\nto have a username set (potentially empty, as allowed by RFC 2396),\nand optionally a password.", "type": "object" }, "ValueMapping": { diff --git a/public/api-merged.json b/public/api-merged.json index 9a7e94be3a9..2dafddb8a3e 100644 --- a/public/api-merged.json +++ b/public/api-merged.json @@ -4297,6 +4297,155 @@ } } }, + "/datasources/{dataSourceUID}/cache": { + "get": { + "description": "get cache config for a single data source", + "tags": [ + "enterprise" + ], + "operationId": "getDataSourceCacheConfig", + "parameters": [ + { + "type": "string", + "name": "dataSourceUID", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "CacheConfigResponse", + "schema": { + "$ref": "#/definitions/CacheConfigResponse" + } + }, + "500": { + "$ref": "#/responses/internalServerError" + } + } + }, + "post": { + "description": "set cache config for a single data source", + "tags": [ + "enterprise" + ], + "operationId": "setDataSourceCacheConfig", + "parameters": [ + { + "type": "string", + "name": "dataSourceUID", + "in": "path", + "required": true + }, + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/CacheConfigSetter" + } + } + ], + "responses": { + "200": { + "description": "CacheConfigResponse", + "schema": { + "$ref": "#/definitions/CacheConfigResponse" + } + }, + "400": { + "$ref": "#/responses/badRequestError" + }, + "500": { + "$ref": "#/responses/internalServerError" + } + } + } + }, + "/datasources/{dataSourceUID}/cache/clean": { + "post": { + "description": "clean cache for a single data source", + "tags": [ + "enterprise" + ], + "operationId": "cleanDataSourceCache", + "parameters": [ + { + "type": "string", + "name": "dataSourceUID", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "CacheConfigResponse", + "schema": { + "$ref": "#/definitions/CacheConfigResponse" + } + }, + "500": { + "$ref": "#/responses/internalServerError" + } + } + } + }, + "/datasources/{dataSourceUID}/cache/disable": { + "post": { + "description": "disable cache for a single data source", + "tags": [ + "enterprise" + ], + "operationId": "disableDataSourceCache", + "parameters": [ + { + "type": "string", + "name": "dataSourceUID", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "CacheConfigResponse", + "schema": { + "$ref": "#/definitions/CacheConfigResponse" + } + }, + "500": { + "$ref": "#/responses/internalServerError" + } + } + } + }, + "/datasources/{dataSourceUID}/cache/enable": { + "post": { + "description": "enable cache for a single data source", + "tags": [ + "enterprise" + ], + "operationId": "enableDataSourceCache", + "parameters": [ + { + "type": "string", + "name": "dataSourceUID", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "CacheConfigResponse", + "schema": { + "$ref": "#/definitions/CacheConfigResponse" + } + }, + "500": { + "$ref": "#/responses/internalServerError" + } + } + } + }, "/datasources/{id}": { "get": { "description": "If you are running Grafana Enterprise and have Fine-grained access control enabled\nyou need to have a permission with action: `datasources:read` and scopes: `datasources:*`, `datasources:id:*` and `datasources:id:1` (single data source).\n\nPlease refer to [updated API](#/datasources/getDataSourceByUID) instead", @@ -11155,7 +11304,7 @@ }, "settings": { "type": "object", - "additionalProperties": false + "additionalProperties": {} } } } @@ -11706,7 +11855,7 @@ }, "model": { "type": "object", - "additionalProperties": false + "additionalProperties": {} }, "queryType": { "type": "string" @@ -12474,6 +12623,123 @@ } } }, + "CacheConfig": { + "description": "Config defines the internal representation of a cache configuration, including fields not set by the API caller", + "type": "object", + "properties": { + "created": { + "type": "string", + "format": "date-time" + }, + "dataSourceID": { + "description": "Fields that can be set by the API caller - read/write", + "type": "integer", + "format": "int64" + }, + "dataSourceUID": { + "type": "string" + }, + "defaultTTLMs": { + "description": "These are returned by the HTTP API, but are managed internally - read-only\nNote: 'created' and 'updated' are special properties managed automatically by xorm, but we are setting them manually", + "type": "integer", + "format": "int64" + }, + "enabled": { + "type": "boolean" + }, + "ttlQueriesMs": { + "description": "TTL MS, or \"time to live\", is how long a cached item will stay in the cache before it is removed (in milliseconds)", + "type": "integer", + "format": "int64" + }, + "ttlResourcesMs": { + "type": "integer", + "format": "int64" + }, + "updated": { + "type": "string", + "format": "date-time" + }, + "useDefaultTTL": { + "description": "If UseDefaultTTL is enabled, then the TTLQueriesMS and TTLResourcesMS in this object is always sent as the default TTL located in grafana.ini", + "type": "boolean" + } + } + }, + "CacheConfigResponse": { + "type": "object", + "properties": { + "created": { + "type": "string", + "format": "date-time" + }, + "dataSourceID": { + "description": "Fields that can be set by the API caller - read/write", + "type": "integer", + "format": "int64" + }, + "dataSourceUID": { + "type": "string" + }, + "defaultTTLMs": { + "description": "These are returned by the HTTP API, but are managed internally - read-only\nNote: 'created' and 'updated' are special properties managed automatically by xorm, but we are setting them manually", + "type": "integer", + "format": "int64" + }, + "enabled": { + "type": "boolean" + }, + "message": { + "type": "string" + }, + "ttlQueriesMs": { + "description": "TTL MS, or \"time to live\", is how long a cached item will stay in the cache before it is removed (in milliseconds)", + "type": "integer", + "format": "int64" + }, + "ttlResourcesMs": { + "type": "integer", + "format": "int64" + }, + "updated": { + "type": "string", + "format": "date-time" + }, + "useDefaultTTL": { + "description": "If UseDefaultTTL is enabled, then the TTLQueriesMS and TTLResourcesMS in this object is always sent as the default TTL located in grafana.ini", + "type": "boolean" + } + } + }, + "CacheConfigSetter": { + "description": "ConfigSetter defines the cache parameters that users can configure per datasource\nThis is only intended to be consumed by the SetCache HTTP Handler", + "type": "object", + "properties": { + "dataSourceID": { + "type": "integer", + "format": "int64" + }, + "dataSourceUID": { + "type": "string" + }, + "enabled": { + "type": "boolean" + }, + "ttlQueriesMs": { + "description": "TTL MS, or \"time to live\", is how long a cached item will stay in the cache before it is removed (in milliseconds)", + "type": "integer", + "format": "int64" + }, + "ttlResourcesMs": { + "type": "integer", + "format": "int64" + }, + "useDefaultTTL": { + "description": "If UseDefaultTTL is enabled, then the TTLQueriesMS and TTLResourcesMS in this object is always sent as the default TTL located in grafana.ini", + "type": "boolean" + } + } + }, "CalculateDiffTarget": { "type": "object", "properties": { @@ -12638,7 +12904,15 @@ "type": "string" } }, + "Policies": { + "description": "Policies contains all policy identifiers included in the certificate.\nIn Go 1.22, encoding/gob cannot handle and ignores this field.", + "type": "array", + "items": { + "$ref": "#/definitions/OID" + } + }, "PolicyIdentifiers": { + "description": "PolicyIdentifiers contains asn1.ObjectIdentifiers, the components\nof which are limited to int32. If a certificate contains a policy which\ncannot be represented by asn1.ObjectIdentifier, it will not be included in\nPolicyIdentifiers, but will be present in Policies, which contains all parsed\npolicy OIDs.", "type": "array", "items": { "$ref": "#/definitions/ObjectIdentifier" @@ -12864,15 +13138,15 @@ "properties": { "analytics": { "type": "object", - "additionalProperties": false + "additionalProperties": {} }, "functional": { "type": "object", - "additionalProperties": false + "additionalProperties": {} }, "performance": { "type": "object", - "additionalProperties": false + "additionalProperties": {} } } }, @@ -12939,7 +13213,7 @@ "target": { "description": "Target data query", "type": "object", - "additionalProperties": false, + "additionalProperties": {}, "example": { "prop1": "value1", "prop2": "value" @@ -12967,7 +13241,7 @@ "target": { "description": "Target data query", "type": "object", - "additionalProperties": false, + "additionalProperties": {}, "example": { "prop1": "value1", "prop2": "value" @@ -14268,12 +14542,12 @@ "color": { "description": "Map values to a display color\nNOTE: this interface is under development in the frontend... so simple map for now", "type": "object", - "additionalProperties": false + "additionalProperties": {} }, "custom": { "description": "Panel Specific Values", "type": "object", - "additionalProperties": false + "additionalProperties": {} }, "decimals": { "type": "integer", @@ -15306,7 +15580,7 @@ } }, "IPMask": { - "description": "See type IPNet and func ParseCIDR for details.", + "description": "See type [IPNet] and func [ParseCIDR] for details.", "type": "array", "title": "An IPMask is a bitmask that can be used to manipulate\nIP addresses for IP addressing and routing.", "items": { @@ -16029,7 +16303,7 @@ } }, "Name": { - "description": "Name represents an X.509 distinguished name. This only includes the common\nelements of a DN. Note that Name is only an approximation of the X.509\nstructure. If an accurate representation is needed, asn1.Unmarshal the raw\nsubject or issuer as an RDNSequence.", + "description": "Name represents an X.509 distinguished name. This only includes the common\nelements of a DN. Note that Name is only an approximation of the X.509\nstructure. If an accurate representation is needed, asn1.Unmarshal the raw\nsubject or issuer as an [RDNSequence].", "type": "object", "properties": { "Country": { @@ -16267,6 +16541,10 @@ } } }, + "OID": { + "type": "object", + "title": "An OID represents an ASN.1 OBJECT IDENTIFIER." + }, "ObjectIdentifier": { "type": "array", "title": "An ObjectIdentifier represents an ASN.1 OBJECT IDENTIFIER.", @@ -17557,7 +17835,7 @@ "properties": { "extra": { "type": "object", - "additionalProperties": false + "additionalProperties": {} }, "message": { "type": "string" @@ -17740,12 +18018,12 @@ "color": { "description": "Map values to a display color\nNOTE: this interface is under development in the frontend... so simple map for now", "type": "object", - "additionalProperties": false + "additionalProperties": {} }, "custom": { "description": "Panel Specific Values", "type": "object", - "additionalProperties": false + "additionalProperties": {} }, "decimals": { "type": "integer", @@ -17982,7 +18260,7 @@ "type": "array", "items": { "type": "object", - "additionalProperties": false + "additionalProperties": {} } }, "range": { @@ -20061,7 +20339,7 @@ } }, "URL": { - "description": "The general form represented is:\n\n[scheme:][//[userinfo@]host][/]path[?query][#fragment]\n\nURLs that do not start with a slash after the scheme are interpreted as:\n\nscheme:opaque[?query][#fragment]\n\nNote that the Path field is stored in decoded form: /%47%6f%2f becomes /Go/.\nA consequence is that it is impossible to tell which slashes in the Path were\nslashes in the raw URL and which were %2f. This distinction is rarely important,\nbut when it is, the code should use the EscapedPath method, which preserves\nthe original encoding of Path.\n\nThe RawPath field is an optional field which is only set when the default\nencoding of Path is different from the escaped path. See the EscapedPath method\nfor more details.\n\nURL's String method uses the EscapedPath method to obtain the path.", + "description": "The general form represented is:\n\n[scheme:][//[userinfo@]host][/]path[?query][#fragment]\n\nURLs that do not start with a slash after the scheme are interpreted as:\n\nscheme:opaque[?query][#fragment]\n\nThe Host field contains the host and port subcomponents of the URL.\nWhen the port is present, it is separated from the host with a colon.\nWhen the host is an IPv6 address, it must be enclosed in square brackets:\n\"[fe80::1]:80\". The [net.JoinHostPort] function combines a host and port\ninto a string suitable for the Host field, adding square brackets to\nthe host when necessary.\n\nNote that the Path field is stored in decoded form: /%47%6f%2f becomes /Go/.\nA consequence is that it is impossible to tell which slashes in the Path were\nslashes in the raw URL and which were %2f. This distinction is rarely important,\nbut when it is, the code should use the [URL.EscapedPath] method, which preserves\nthe original encoding of Path.\n\nThe RawPath field is an optional field which is only set when the default\nencoding of Path is different from the escaped path. See the EscapedPath method\nfor more details.\n\nURL's String method uses the EscapedPath method to obtain the path.", "type": "object", "title": "A URL represents a parsed URL (technically, a URI reference).", "properties": { @@ -20107,7 +20385,7 @@ "Object": { "description": "Object is a JSON compatible map with string, float, int, bool, []interface{},\nor map[string]interface{} children.", "type": "object", - "additionalProperties": false + "additionalProperties": {} } } }, @@ -20699,7 +20977,7 @@ } }, "Userinfo": { - "description": "The Userinfo type is an immutable encapsulation of username and\npassword details for a URL. An existing Userinfo value is guaranteed\nto have a username set (potentially empty, as allowed by RFC 2396),\nand optionally a password.", + "description": "The Userinfo type is an immutable encapsulation of username and\npassword details for a [URL]. An existing Userinfo value is guaranteed\nto have a username set (potentially empty, as allowed by RFC 2396),\nand optionally a password.", "type": "object" }, "ValidationError": { @@ -21341,7 +21619,7 @@ "extra": { "description": "Extra Additional information about the error", "type": "object", - "additionalProperties": false + "additionalProperties": {} }, "message": { "description": "Message A human readable message", @@ -22356,7 +22634,7 @@ }, "settings": { "type": "object", - "additionalProperties": false + "additionalProperties": {} }, "source": { "type": "string" @@ -22557,7 +22835,7 @@ }, "settings": { "type": "object", - "additionalProperties": false + "additionalProperties": {} }, "source": { "type": "string" diff --git a/public/app/app.ts b/public/app/app.ts index 5796b18c21e..de0f8814d68 100644 --- a/public/app/app.ts +++ b/public/app/app.ts @@ -36,7 +36,7 @@ import { setEmbeddedDashboard, setAppEvents, setReturnToPreviousHook, - type GetPluginExtensions, + setPluginExtensionsHook, } from '@grafana/runtime'; import { setPanelDataErrorView } from '@grafana/runtime/src/components/PanelDataErrorView'; import { setPanelRenderer } from '@grafana/runtime/src/components/PanelRenderer'; @@ -80,11 +80,12 @@ import { initGrafanaLive } from './features/live'; import { PanelDataErrorView } from './features/panel/components/PanelDataErrorView'; import { PanelRenderer } from './features/panel/components/PanelRenderer'; import { DatasourceSrv } from './features/plugins/datasource_srv'; -import { createPluginExtensionRegistry } from './features/plugins/extensions/createPluginExtensionRegistry'; import { getCoreExtensionConfigurations } from './features/plugins/extensions/getCoreExtensionConfigurations'; -import { getPluginExtensions } from './features/plugins/extensions/getPluginExtensions'; +import { createPluginExtensionsGetter } from './features/plugins/extensions/getPluginExtensions'; +import { ReactivePluginExtensionsRegistry } from './features/plugins/extensions/reactivePluginExtensionRegistry'; +import { createPluginExtensionsHook } from './features/plugins/extensions/usePluginExtensions'; import { importPanelPlugin, syncGetPanelPlugin } from './features/plugins/importPanelPlugin'; -import { PluginPreloadResult, preloadPlugins } from './features/plugins/pluginPreloader'; +import { preloadPlugins } from './features/plugins/pluginPreloader'; import { QueryRunner } from './features/query/state/QueryRunner'; import { runRequest } from './features/query/state/runRequest'; import { initWindowRuntime } from './features/runtime/init'; @@ -206,24 +207,26 @@ export class GrafanaApp { setDataSourceSrv(dataSourceSrv); initWindowRuntime(); - let preloadResults: PluginPreloadResult[] = []; + // Initialize plugin extensions + const extensionsRegistry = new ReactivePluginExtensionsRegistry(); + extensionsRegistry.register({ + pluginId: 'grafana', + extensionConfigs: getCoreExtensionConfigurations(), + }); if (contextSrv.user.orgRole !== '') { - // Preload selected app plugins - preloadResults = await preloadPlugins(config.apps); + // The "cloud-home-app" is registering banners once it's loaded, and this can cause a rerender in the AppChrome if it's loaded after the Grafana app init. + // TODO: remove the following exception once the issue mentioned above is fixed. + const awaitedAppPluginIds = ['cloud-home-app']; + const awaitedAppPlugins = Object.values(config.apps).filter((app) => awaitedAppPluginIds.includes(app.id)); + const appPlugins = Object.values(config.apps).filter((app) => !awaitedAppPluginIds.includes(app.id)); + + preloadPlugins(appPlugins, extensionsRegistry); + await preloadPlugins(awaitedAppPlugins, extensionsRegistry, 'frontend_awaited_plugins_preload'); } - // Create extension registry out of preloaded plugins and core extensions - const extensionRegistry = createPluginExtensionRegistry([ - { pluginId: 'grafana', extensionConfigs: getCoreExtensionConfigurations() }, - ...preloadResults, - ]); - - // Expose the getPluginExtension function via grafana-runtime - const pluginExtensionGetter: GetPluginExtensions = (options) => - getPluginExtensions({ ...options, registry: extensionRegistry }); - - setPluginExtensionGetter(pluginExtensionGetter); + setPluginExtensionGetter(createPluginExtensionsGetter(extensionsRegistry)); + setPluginExtensionsHook(createPluginExtensionsHook(extensionsRegistry)); // initialize chrome service const queryParams = locationService.getSearchObject(); diff --git a/public/app/core/components/AppChrome/AppChrome.test.tsx b/public/app/core/components/AppChrome/AppChrome.test.tsx index 870030ab187..7285224be50 100644 --- a/public/app/core/components/AppChrome/AppChrome.test.tsx +++ b/public/app/core/components/AppChrome/AppChrome.test.tsx @@ -16,7 +16,7 @@ import { AppChrome } from './AppChrome'; jest.mock('@grafana/runtime', () => ({ ...jest.requireActual('@grafana/runtime'), - getPluginLinkExtensions: jest.fn().mockReturnValue({ extensions: [] }), + usePluginLinkExtensions: jest.fn().mockReturnValue({ extensions: [] }), })); const searchData: DataFrame = { diff --git a/public/app/core/components/AppChrome/AppChromeMenu.tsx b/public/app/core/components/AppChrome/AppChromeMenu.tsx index 88b8db031ce..ee58fb39bc6 100644 --- a/public/app/core/components/AppChrome/AppChromeMenu.tsx +++ b/public/app/core/components/AppChrome/AppChromeMenu.tsx @@ -6,7 +6,7 @@ import React, { useRef } from 'react'; import CSSTransition from 'react-transition-group/CSSTransition'; import { GrafanaTheme2 } from '@grafana/data'; -import { handleReducedMotion, useStyles2, useTheme2 } from '@grafana/ui'; +import { useStyles2, useTheme2 } from '@grafana/ui'; import { useGrafana } from 'app/core/context/GrafanaContext'; import { KioskMode } from 'app/types'; @@ -125,10 +125,10 @@ const getStyles = (theme: GrafanaTheme2, searchBarHidden?: boolean) => { const getAnimStyles = (theme: GrafanaTheme2, animationDuration: number) => { const commonTransition = { - ...handleReducedMotion({ + [theme.transitions.handleMotion('no-preference')]: { transitionDuration: `${animationDuration}ms`, transitionTimingFunction: theme.transitions.easing.easeInOut, - }), + }, [theme.breakpoints.down('md')]: { overflow: 'hidden', }, diff --git a/public/app/core/components/AppChrome/MegaMenu/MegaMenu.test.tsx b/public/app/core/components/AppChrome/MegaMenu/MegaMenu.test.tsx index 43f9bc20180..68c4519423e 100644 --- a/public/app/core/components/AppChrome/MegaMenu/MegaMenu.test.tsx +++ b/public/app/core/components/AppChrome/MegaMenu/MegaMenu.test.tsx @@ -2,14 +2,13 @@ import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import React from 'react'; import { Router } from 'react-router-dom'; +import { TestProvider } from 'test/helpers/TestProvider'; import { getGrafanaContextMock } from 'test/mocks/getGrafanaContextMock'; import { NavModelItem } from '@grafana/data'; import { selectors } from '@grafana/e2e-selectors'; import { locationService } from '@grafana/runtime'; -import { TestProvider } from '../../../../../test/helpers/TestProvider'; - import { MegaMenu } from './MegaMenu'; const setup = () => { diff --git a/public/app/core/components/AppChrome/MegaMenu/MegaMenu.tsx b/public/app/core/components/AppChrome/MegaMenu/MegaMenu.tsx index 4f55f840990..7cb0e65dd83 100644 --- a/public/app/core/components/AppChrome/MegaMenu/MegaMenu.tsx +++ b/public/app/core/components/AppChrome/MegaMenu/MegaMenu.tsx @@ -32,7 +32,7 @@ export const MegaMenu = React.memo( .filter((item) => item.id !== 'profile' && item.id !== 'help') .map((item) => enrichWithInteractionTracking(item, state.megaMenuDocked)); - const activeItem = getActiveItem(navItems, location.pathname); + const activeItem = getActiveItem(navItems, state.sectionNav.node, location.pathname); const handleDockedMenu = () => { chrome.setMegaMenuDocked(!state.megaMenuDocked); diff --git a/public/app/core/components/AppChrome/MegaMenu/MegaMenuItemText.tsx b/public/app/core/components/AppChrome/MegaMenu/MegaMenuItemText.tsx index 6510fda9a27..f05cbd54ee9 100644 --- a/public/app/core/components/AppChrome/MegaMenu/MegaMenuItemText.tsx +++ b/public/app/core/components/AppChrome/MegaMenu/MegaMenuItemText.tsx @@ -38,6 +38,7 @@ export function MegaMenuItemText({ children, isActive, onClick, target, url }: P href={url} target={target} onClick={onClick} + {...(isActive && { 'aria-current': 'page' })} > {linkContent} diff --git a/public/app/core/components/AppChrome/MegaMenu/utils.test.ts b/public/app/core/components/AppChrome/MegaMenu/utils.test.ts index eacea3cfdf9..e03c46022e7 100644 --- a/public/app/core/components/AppChrome/MegaMenu/utils.test.ts +++ b/public/app/core/components/AppChrome/MegaMenu/utils.test.ts @@ -1,7 +1,7 @@ -import { GrafanaConfig, locationUtil, NavModelItem } from '@grafana/data'; +import { NavModelItem } from '@grafana/data'; import { ContextSrv, setContextSrv } from 'app/core/services/context_srv'; -import { enrichHelpItem, getActiveItem, isMatchOrChildMatch } from './utils'; +import { enrichHelpItem, getActiveItem } from './utils'; jest.mock('../../../app_events', () => ({ publish: jest.fn(), @@ -44,143 +44,83 @@ describe('enrichConfigItems', () => { }); }); -describe('isMatchOrChildMatch', () => { - const mockChild: NavModelItem = { - text: 'Child', - url: '/dashboards/child', - }; - const mockItemToCheck: NavModelItem = { - text: 'Dashboards', - url: '/dashboards', - children: [mockChild], - }; - - it('returns true if the itemToCheck is an exact match with the searchItem', () => { - const searchItem = mockItemToCheck; - expect(isMatchOrChildMatch(mockItemToCheck, searchItem)).toBe(true); - }); - - it('returns true if the itemToCheck has a child that matches the searchItem', () => { - const searchItem = mockChild; - expect(isMatchOrChildMatch(mockItemToCheck, searchItem)).toBe(true); - }); - - it('returns false otherwise', () => { - const searchItem: NavModelItem = { - text: 'No match', - url: '/noMatch', - }; - expect(isMatchOrChildMatch(mockItemToCheck, searchItem)).toBe(false); - }); -}); - describe('getActiveItem', () => { + const starredDashboardUid = 'foo'; const mockNavTree: NavModelItem[] = [ { text: 'Item', url: '/item', - }, - { - text: 'Item with query param', - url: '/itemWithQueryParam?foo=bar', - }, - { - text: 'Item after subpath', - url: '/subUrl/itemAfterSubpath', + id: 'item', }, { text: 'Item with children', url: '/itemWithChildren', + id: 'item-with-children', children: [ { text: 'Child', url: '/child', + id: 'child', }, ], }, - { - text: 'Alerting item', - url: '/alerting/list', - }, { text: 'Base', url: '/', + id: 'home', }, { text: 'Starred', url: '/dashboards?starred', id: 'starred', + children: [ + { + id: `starred/${starredDashboardUid}`, + text: 'Lazy Loading', + url: `/d/${starredDashboardUid}/some-name`, + }, + ], }, { text: 'Dashboards', url: '/dashboards', - }, - { - text: 'More specific dashboard', - url: '/d/moreSpecificDashboard', + id: 'dashboards', }, ]; - beforeEach(() => { - locationUtil.initialize({ - config: { appSubUrl: '/subUrl' } as GrafanaConfig, - getVariablesUrlParams: () => ({}), - getTimeRangeForUrl: () => ({ from: 'now-7d', to: 'now' }), - }); - }); it('returns an exact match at the top level', () => { - const mockPathName = '/item'; - expect(getActiveItem(mockNavTree, mockPathName)).toEqual({ - text: 'Item', - url: '/item', - }); + const mockPage: NavModelItem = { + text: 'Some current page', + id: 'item', + }; + expect(getActiveItem(mockNavTree, mockPage)?.id).toEqual('item'); }); - it('returns an exact match ignoring root subpath', () => { - const mockPathName = '/itemAfterSubpath'; - expect(getActiveItem(mockNavTree, mockPathName)).toEqual({ - text: 'Item after subpath', - url: '/subUrl/itemAfterSubpath', - }); - }); - - it('returns an exact match ignoring query params', () => { - const mockPathName = '/itemWithQueryParam?bar=baz'; - expect(getActiveItem(mockNavTree, mockPathName)).toEqual({ - text: 'Item with query param', - url: '/itemWithQueryParam?foo=bar', - }); + it('returns parent item if no other matches in nav tree', () => { + const mockPage: NavModelItem = { + text: 'Some child page', + id: 'something-that-doesnt-exist', + parentItem: { + text: 'Some home page', + id: 'home', + }, + }; + expect(getActiveItem(mockNavTree, mockPage)?.id).toEqual('home'); }); it('returns an exact child match', () => { - const mockPathName = '/child'; - expect(getActiveItem(mockNavTree, mockPathName)).toEqual({ - text: 'Child', - url: '/child', - }); + const mockPage: NavModelItem = { + text: 'Some child page', + id: 'child', + }; + expect(getActiveItem(mockNavTree, mockPage)?.id).toEqual('child'); }); - it('returns the alerting link if the pathname is an alert notification', () => { - const mockPathName = '/alerting/notification/foo'; - expect(getActiveItem(mockNavTree, mockPathName)).toEqual({ - text: 'Alerting item', - url: '/alerting/list', - }); - }); - - it('returns the dashboards route link if the pathname starts with /d/', () => { - const mockPathName = '/d/foo'; - expect(getActiveItem(mockNavTree, mockPathName)).toEqual({ - text: 'Dashboards', - url: '/dashboards', - }); - }); - - it('returns a more specific link if one exists', () => { - const mockPathName = '/d/moreSpecificDashboard'; - expect(getActiveItem(mockNavTree, mockPathName)).toEqual({ - text: 'More specific dashboard', - url: '/d/moreSpecificDashboard', - }); + it('handles home page', () => { + const mockPage: NavModelItem = { + text: 'Something else', + id: 'not-home', + }; + expect(getActiveItem(mockNavTree, mockPage, '/')?.id).toEqual('home'); }); }); diff --git a/public/app/core/components/AppChrome/MegaMenu/utils.ts b/public/app/core/components/AppChrome/MegaMenu/utils.ts index 9486c3ee5ce..90950cc25cd 100644 --- a/public/app/core/components/AppChrome/MegaMenu/utils.ts +++ b/public/app/core/components/AppChrome/MegaMenu/utils.ts @@ -1,6 +1,7 @@ -import { locationUtil, NavModelItem } from '@grafana/data'; +import { NavModelItem } from '@grafana/data'; import { config, reportInteraction } from '@grafana/runtime'; import { t } from 'app/core/internationalization'; +import { HOME_NAV_ID } from 'app/core/reducers/navModel'; import { ShowModalReactEvent } from '../../../../types/events'; import appEvents from '../../../app_events'; @@ -46,10 +47,6 @@ export const enrichWithInteractionTracking = (item: NavModelItem, megaMenuDocked return newItem; }; -export const isMatchOrChildMatch = (itemToCheck: NavModelItem, searchItem?: NavModelItem) => { - return Boolean(itemToCheck === searchItem || hasChildMatch(itemToCheck, searchItem)); -}; - export const hasChildMatch = (itemToCheck: NavModelItem, searchItem?: NavModelItem): boolean => { return Boolean( itemToCheck.children?.some((child) => { @@ -62,57 +59,48 @@ export const hasChildMatch = (itemToCheck: NavModelItem, searchItem?: NavModelIt ); }; -const stripQueryParams = (url?: string) => { - return url?.split('?')[0] ?? ''; -}; - -const isBetterMatch = (newMatch: NavModelItem, currentMatch?: NavModelItem) => { - const currentMatchUrl = stripQueryParams(currentMatch?.url); - const newMatchUrl = stripQueryParams(newMatch.url); - return newMatchUrl && newMatchUrl.length > currentMatchUrl?.length; -}; - export const getActiveItem = ( navTree: NavModelItem[], - pathname: string, - currentBestMatch?: NavModelItem + currentPage: NavModelItem, + url?: string ): NavModelItem | undefined => { - const dashboardLinkMatch = '/dashboards'; + const { id, parentItem } = currentPage; - for (const link of navTree) { - const linkWithoutParams = stripQueryParams(link.url); - const linkPathname = locationUtil.stripBaseFromUrl(linkWithoutParams); - if (linkPathname && link.id !== 'starred') { - if (linkPathname === pathname) { - // exact match - currentBestMatch = link; - break; - } else if (linkPathname !== '/' && pathname.startsWith(linkPathname)) { - // partial match - if (isBetterMatch(link, currentBestMatch)) { - currentBestMatch = link; - } - } else if (linkPathname === '/alerting/list' && pathname.startsWith('/alerting/notification/')) { - // alert channel match - // TODO refactor routes such that we don't need this custom logic - currentBestMatch = link; - break; - } else if (linkPathname === dashboardLinkMatch && pathname.startsWith('/d/')) { - // dashboard match - // TODO refactor routes such that we don't need this custom logic - if (isBetterMatch(link, currentBestMatch)) { - currentBestMatch = link; - } + // special case for the home page + if (url === '/') { + return navTree.find((item) => item.id === HOME_NAV_ID); + } + + // special case for profile as it's not part of the mega menu + if (currentPage.id === 'profile') { + return undefined; + } + + for (const navItem of navTree) { + const isIdMatch = Boolean(navItem.id && navItem.id === id); + const isTextUrlMatch = navItem.text === currentPage.text && navItem.url === currentPage.url; + + // ideally, we should only match on id + // unfortunately it's not a required property of the interface, and there are some cases + // where it's not set, particularly with child pages of plugins + // in those cases, we fall back to a text + url match + if (isIdMatch || isTextUrlMatch) { + return navItem; + } + + if (navItem.children) { + const childrenMatch = getActiveItem(navItem.children, currentPage); + if (childrenMatch) { + return childrenMatch; } } - if (link.children) { - currentBestMatch = getActiveItem(link.children, pathname, currentBestMatch); - } - if (stripQueryParams(currentBestMatch?.url) === pathname) { - return currentBestMatch; - } } - return currentBestMatch; + + if (parentItem) { + return getActiveItem(navTree, parentItem); + } + + return undefined; }; export function getEditionAndUpdateLinks(): NavModelItem[] { diff --git a/public/app/core/components/BouncingLoader/BouncingLoader.tsx b/public/app/core/components/BouncingLoader/BouncingLoader.tsx index 21e76010532..e3273663af4 100644 --- a/public/app/core/components/BouncingLoader/BouncingLoader.tsx +++ b/public/app/core/components/BouncingLoader/BouncingLoader.tsx @@ -33,6 +33,18 @@ const fadeIn = keyframes({ }, }); +const pulse = keyframes({ + '0%': { + opacity: 0, + }, + '50%': { + opacity: 1, + }, + '100%': { + opacity: 0, + }, +}); + const bounce = keyframes({ 'from, to': { transform: 'translateY(0px)', @@ -70,25 +82,37 @@ const squash = keyframes({ const getStyles = (theme: GrafanaTheme2) => ({ container: css({ opacity: 0, - animationName: fadeIn, - animationIterationCount: 1, - animationDuration: '0.9s', - animationDelay: '0.5s', - animationFillMode: 'forwards', + [theme.transitions.handleMotion('no-preference')]: { + animationName: fadeIn, + animationIterationCount: 1, + animationDuration: '0.9s', + animationDelay: '0.5s', + animationFillMode: 'forwards', + }, + [theme.transitions.handleMotion('reduce')]: { + animationName: pulse, + animationIterationCount: 'infinite', + animationDuration: '4s', + animationDelay: '0.5s', + }, }), bounce: css({ textAlign: 'center', - animationName: bounce, - animationDuration: '0.9s', - animationIterationCount: 'infinite', + [theme.transitions.handleMotion('no-preference')]: { + animationName: bounce, + animationDuration: '0.9s', + animationIterationCount: 'infinite', + }, }), logo: css({ display: 'inline-block', - animationName: squash, - animationDuration: '0.9s', - animationIterationCount: 'infinite', + [theme.transitions.handleMotion('no-preference')]: { + animationName: squash, + animationDuration: '0.9s', + animationIterationCount: 'infinite', + }, width: '60px', height: '60px', }), diff --git a/public/app/core/components/Login/LoginLayout.tsx b/public/app/core/components/Login/LoginLayout.tsx index cb391662213..c7ef7649be5 100644 --- a/public/app/core/components/Login/LoginLayout.tsx +++ b/public/app/core/components/Login/LoginLayout.tsx @@ -2,7 +2,7 @@ import { cx, css, keyframes } from '@emotion/css'; import React, { useEffect, useState } from 'react'; import { GrafanaTheme2 } from '@grafana/data'; -import { handleReducedMotion, useStyles2 } from '@grafana/ui'; +import { useStyles2 } from '@grafana/ui'; import { Branding } from '../Branding/Branding'; import { BrandingSettings } from '../Branding/types'; @@ -148,7 +148,9 @@ export const getLoginStyles = (theme: GrafanaTheme2) => { borderRadius: theme.shape.radius.default, padding: theme.spacing(2, 0), opacity: 0, - transition: 'opacity 0.5s ease-in-out', + [theme.transitions.handleMotion('no-preference', 'reduce')]: { + transition: 'opacity 0.5s ease-in-out', + }, [theme.breakpoints.up('sm')]: { minHeight: theme.spacing(40), @@ -171,12 +173,14 @@ export const getLoginStyles = (theme: GrafanaTheme2) => { maxWidth: 415, width: '100%', transform: 'translate(0px, 0px)', - transition: '0.25s ease', + [theme.transitions.handleMotion('no-preference')]: { + transition: '0.25s ease', + }, }), enterAnimation: css({ - ...handleReducedMotion({ + [theme.transitions.handleMotion('no-preference')]: { animation: `${flyInAnimation} ease-out 0.2s`, - }), + }, }), }; }; diff --git a/public/app/core/components/Login/LoginServiceButtons.tsx b/public/app/core/components/Login/LoginServiceButtons.tsx index a2d67b05749..157fe2e7e62 100644 --- a/public/app/core/components/Login/LoginServiceButtons.tsx +++ b/public/app/core/components/Login/LoginServiceButtons.tsx @@ -149,7 +149,7 @@ export const LoginServiceButtons = () => { if (hasServices) { return ( - + {Object.entries(enabledServices).map(([key, service]) => { const serviceName = service.name; diff --git a/public/app/core/components/connectWithCleanUp.tsx b/public/app/core/components/connectWithCleanUp.tsx deleted file mode 100644 index 135e692bd49..00000000000 --- a/public/app/core/components/connectWithCleanUp.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import hoistNonReactStatics from 'hoist-non-react-statics'; -import React, { ComponentType, FunctionComponent, useEffect } from 'react'; -import { connect, MapDispatchToPropsParam, MapStateToPropsParam } from 'react-redux'; - -import { useDispatch } from 'app/types'; - -import { cleanUpAction, CleanUpAction } from '../actions/cleanUp'; - -export const connectWithCleanUp = - ( - mapStateToProps: MapStateToPropsParam, - mapDispatchToProps: MapDispatchToPropsParam, - cleanupAction: CleanUpAction - ) => - (Component: ComponentType) => { - const ConnectedComponent = connect( - mapStateToProps, - mapDispatchToProps - // @ts-ignore - )(Component); - - const ConnectedComponentWithCleanUp: FunctionComponent = (props) => { - const dispatch = useDispatch(); - useEffect(() => { - return function cleanUp() { - dispatch(cleanUpAction({ cleanupAction: cleanupAction })); - }; - }, [dispatch]); - // @ts-ignore - return ; - }; - - ConnectedComponentWithCleanUp.displayName = `ConnectWithCleanUp(${ConnectedComponent.displayName})`; - hoistNonReactStatics(ConnectedComponentWithCleanUp, Component); - type Hoisted = typeof ConnectedComponentWithCleanUp & Statics; - - return ConnectedComponentWithCleanUp as Hoisted; - }; diff --git a/public/app/core/history/RichHistoryLocalStorage.ts b/public/app/core/history/RichHistoryLocalStorage.ts index 4e5a1039d85..eee2d841584 100644 --- a/public/app/core/history/RichHistoryLocalStorage.ts +++ b/public/app/core/history/RichHistoryLocalStorage.ts @@ -185,7 +185,7 @@ function cleanUp(richHistory: RichHistoryLocalStorageDTO[]): RichHistoryLocalSto * Ensures the entry can be added. Throws an error if current limit has been hit. * Returns queries that should be saved back giving space for one extra query. */ -function checkLimits(queriesToKeep: RichHistoryLocalStorageDTO[]): { +export function checkLimits(queriesToKeep: RichHistoryLocalStorageDTO[]): { queriesToKeep: RichHistoryLocalStorageDTO[]; limitExceeded: boolean; } { diff --git a/public/app/core/reducers/navModel.ts b/public/app/core/reducers/navModel.ts index d4968f54684..6717c66b350 100644 --- a/public/app/core/reducers/navModel.ts +++ b/public/app/core/reducers/navModel.ts @@ -73,6 +73,8 @@ export const updateNavIndex = createAction('navIndex/updateNavInde // Since the configuration subtitle includes the organization name, we include this action to update the org name if it changes. export const updateConfigurationSubtitle = createAction('navIndex/updateConfigurationSubtitle'); +export const removeNavIndex = createAction('navIndex/removeNavIndex'); + export const getItemWithNewSubTitle = (item: NavModelItem, subTitle: string): NavModelItem => ({ ...item, parentItem: { @@ -122,6 +124,8 @@ export const navIndexReducer = (state: NavIndex = initialState, action: AnyActio 'org-settings': getItemWithNewSubTitle(state['org-settings'], subTitle), apikeys: getItemWithNewSubTitle(state.apikeys, subTitle), }; + } else if (removeNavIndex.match(action)) { + delete state[action.payload]; } return state; diff --git a/public/app/core/services/withFocusedPanelId.ts b/public/app/core/services/withFocusedPanelId.ts index 857144ee706..9c67c0c71ca 100644 --- a/public/app/core/services/withFocusedPanelId.ts +++ b/public/app/core/services/withFocusedPanelId.ts @@ -2,6 +2,14 @@ export function withFocusedPanel(fn: (panelId: number) => void) { return () => { const elements = document.querySelectorAll(':hover'); + // Handle keyboard focus first + const focusedGridElement = document.activeElement?.closest('[data-panelid]'); + + if (focusedGridElement instanceof HTMLElement && focusedGridElement.dataset?.panelid) { + fn(parseInt(focusedGridElement.dataset?.panelid, 10)); + return; + } + for (let i = elements.length - 1; i > 0; i--) { const element = elements[i]; if (element instanceof HTMLElement && element.dataset?.panelid) { diff --git a/public/app/core/utils/metrics.ts b/public/app/core/utils/metrics.ts index 97551a721cc..e244851aa74 100644 --- a/public/app/core/utils/metrics.ts +++ b/public/app/core/utils/metrics.ts @@ -1,7 +1,7 @@ import { reportPerformance } from '../services/echo/EchoSrv'; export function startMeasure(eventName: string) { - if (!performance) { + if (!performance || !performance.mark) { return; } @@ -13,7 +13,7 @@ export function startMeasure(eventName: string) { } export function stopMeasure(eventName: string) { - if (!performance) { + if (!performance || !performance.mark) { return; } @@ -29,7 +29,9 @@ export function stopMeasure(eventName: string) { performance.clearMarks(started); performance.clearMarks(completed); performance.clearMeasures(measured); + return measure; } catch (error) { console.error(`[Metrics] Failed to stopMeasure ${eventName}`, error); + return; } } diff --git a/public/app/features/admin/UserListPublicDashboardPage/UserListPublicDashboardPage.tsx b/public/app/features/admin/UserListPublicDashboardPage/UserListPublicDashboardPage.tsx index 234cdca3f5d..655b80fcced 100644 --- a/public/app/features/admin/UserListPublicDashboardPage/UserListPublicDashboardPage.tsx +++ b/public/app/features/admin/UserListPublicDashboardPage/UserListPublicDashboardPage.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { selectors as e2eSelectors } from '@grafana/e2e-selectors/src'; -import { HorizontalGroup, Icon, Tag, Tooltip } from '@grafana/ui/src'; +import { Icon, Stack, Tag, Tooltip } from '@grafana/ui/src'; import { Page } from 'app/core/components/Page/Page'; import { Trans, t } from 'app/core/internationalization'; @@ -60,10 +60,10 @@ export const UserListPublicDashboardPage = () => { {user.firstSeenAtAge} {user.lastSeenAtAge} - + {user.totalDashboards} dashboard(s) - + diff --git a/public/app/features/admin/UserOrgs.tsx b/public/app/features/admin/UserOrgs.tsx index 5583380263a..644d42d7eb7 100644 --- a/public/app/features/admin/UserOrgs.tsx +++ b/public/app/features/admin/UserOrgs.tsx @@ -6,7 +6,6 @@ import { Button, ConfirmButton, Field, - HorizontalGroup, Icon, Modal, stylesFactory, @@ -386,14 +385,14 @@ export class AddToOrgModal extends PureComponent - + - + ); diff --git a/public/app/features/admin/ldap/LdapUserGroups.tsx b/public/app/features/admin/ldap/LdapUserGroups.tsx index d8a628acf95..ced2acdab68 100644 --- a/public/app/features/admin/ldap/LdapUserGroups.tsx +++ b/public/app/features/admin/ldap/LdapUserGroups.tsx @@ -46,7 +46,7 @@ export const LdapUserGroups = ({ groups }: Props) => { }} columns={columns} data={items} - getRowId={(row) => row.orgId + row.orgRole} + getRowId={(row) => row.orgId + row.orgRole + row.groupDN} /> ); }; diff --git a/public/app/features/alerting/unified/AlertGroups.test.tsx b/public/app/features/alerting/unified/AlertGroups.test.tsx index 5771a296785..23a848d50f0 100644 --- a/public/app/features/alerting/unified/AlertGroups.test.tsx +++ b/public/app/features/alerting/unified/AlertGroups.test.tsx @@ -93,7 +93,9 @@ describe('AlertGroups', () => { expect(groups).toHaveLength(2); expect(groups[0]).toHaveTextContent('No grouping'); - expect(groups[1]).toHaveTextContent('severitywarning regionUS-Central'); + const labels = byTestId('label-value').getAll(); + expect(labels[0]).toHaveTextContent('severitywarning'); + expect(labels[1]).toHaveTextContent('regionUS-Central'); await userEvent.click(ui.groupCollapseToggle.get(groups[0])); expect(ui.groupTable.get()).toBeDefined(); diff --git a/public/app/features/alerting/unified/CloneRuleEditor.test.tsx b/public/app/features/alerting/unified/CloneRuleEditor.test.tsx index c2f77b60a46..61fef735f65 100644 --- a/public/app/features/alerting/unified/CloneRuleEditor.test.tsx +++ b/public/app/features/alerting/unified/CloneRuleEditor.test.tsx @@ -21,8 +21,9 @@ import { import { cloneRuleDefinition, CloneRuleEditor } from './CloneRuleEditor'; import { ExpressionEditorProps } from './components/rule-editor/ExpressionEditor'; -import { mockSearchApi } from './mockApi'; +import { mockApi, mockSearchApi } from './mockApi'; import { + labelsPluginMetaMock, mockDataSource, MockDataSourceSrv, mockRulerAlertingRule, @@ -138,6 +139,7 @@ const amConfig: AlertManagerCortexConfig = { template_files: {}, }; +mockApi(server).plugins.getPluginSettings({ ...labelsPluginMetaMock, enabled: false }); describe('CloneRuleEditor', function () { describe('Grafana-managed rules', function () { it('should populate form values from the existing alert rule', async function () { @@ -174,8 +176,16 @@ describe('CloneRuleEditor', function () { expect(ui.inputs.name.get()).toHaveValue('First Grafana Rule (copy)'); expect(ui.inputs.folderContainer.get()).toHaveTextContent('folder-one'); expect(ui.inputs.group.get()).toHaveTextContent('group1'); - expect(ui.inputs.labelValue(0).get()).toHaveTextContent('critical'); - expect(ui.inputs.labelValue(1).get()).toHaveTextContent('nasa'); + expect( + byRole('listitem', { + name: 'severity: critical', + }).get() + ).toBeInTheDocument(); + expect( + byRole('listitem', { + name: 'region: nasa', + }).get() + ).toBeInTheDocument(); expect(ui.inputs.annotationValue(0).get()).toHaveTextContent('This is a very important alert rule'); }); }); @@ -244,8 +254,16 @@ describe('CloneRuleEditor', function () { expect(ui.inputs.expr.get()).toHaveValue('vector(1) > 0'); expect(ui.inputs.namespace.get()).toHaveTextContent('namespace-one'); expect(ui.inputs.group.get()).toHaveTextContent('group1'); - expect(ui.inputs.labelValue(0).get()).toHaveTextContent('critical'); - expect(ui.inputs.labelValue(1).get()).toHaveTextContent('nasa'); + expect( + byRole('listitem', { + name: 'severity: critical', + }).get() + ).toBeInTheDocument(); + expect( + byRole('listitem', { + name: 'region: nasa', + }).get() + ).toBeInTheDocument(); expect(ui.inputs.annotationValue(0).get()).toHaveTextContent('This is a very important alert rule'); }); }); diff --git a/public/app/features/alerting/unified/PanelAlertTabContent.test.tsx b/public/app/features/alerting/unified/PanelAlertTabContent.test.tsx index 9c70f018da8..bf2364f7e96 100644 --- a/public/app/features/alerting/unified/PanelAlertTabContent.test.tsx +++ b/public/app/features/alerting/unified/PanelAlertTabContent.test.tsx @@ -286,7 +286,7 @@ describe('PanelAlertTabContent', () => { }); }); - it('Will render alerts belonging to panel and a button to create alert from panel queries', async () => { + it.skip('Will render alerts belonging to panel and a button to create alert from panel queries', async () => { mocks.api.fetchRules.mockResolvedValue(rules); mocks.api.fetchRulerRules.mockResolvedValue(rulerRules); diff --git a/public/app/features/alerting/unified/RuleEditorCloudRules.test.tsx b/public/app/features/alerting/unified/RuleEditorCloudRules.test.tsx index 061b5dcbdb4..e17a7c7a2bf 100644 --- a/public/app/features/alerting/unified/RuleEditorCloudRules.test.tsx +++ b/public/app/features/alerting/unified/RuleEditorCloudRules.test.tsx @@ -1,4 +1,4 @@ -import { screen, waitFor, waitForElementToBeRemoved, within } from '@testing-library/react'; +import { screen, waitFor, waitForElementToBeRemoved } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import React from 'react'; import { renderRuleEditor, ui } from 'test/helpers/alertingRuleEditor'; @@ -13,7 +13,7 @@ import { searchFolders } from '../../manage-dashboards/state/actions'; import { fetchRulerRules, fetchRulerRulesGroup, fetchRulerRulesNamespace, setRulerRuleGroup } from './api/ruler'; import { ExpressionEditorProps } from './components/rule-editor/ExpressionEditor'; import { mockApi, mockFeatureDiscoveryApi, setupMswServer } from './mockApi'; -import { grantUserPermissions, mockDataSource } from './mocks'; +import { grantUserPermissions, labelsPluginMetaMock, mockDataSource } from './mocks'; import { defaultAlertmanagerChoiceResponse, emptyExternalAlertmanagersResponse, @@ -58,6 +58,7 @@ mockFeatureDiscoveryApi(server).discoverDsFeatures(dataSources.default, buildInf mockAlertmanagerChoiceResponse(server, defaultAlertmanagerChoiceResponse); mockAlertmanagersResponse(server, emptyExternalAlertmanagersResponse); mockApi(server).eval({ results: {} }); +mockApi(server).plugins.getPluginSettings({ ...labelsPluginMetaMock, enabled: false }); // these tests are rather slow because we have to wait for various API calls and mocks to be called // and wait for the UI to be in particular states, drone seems to time out quite often so @@ -76,8 +77,6 @@ const mocks = { }, }; -const getLabelInput = (selector: HTMLElement) => within(selector).getByRole('combobox'); - describe('RuleEditor cloud', () => { beforeEach(() => { jest.clearAllMocks(); @@ -158,9 +157,6 @@ describe('RuleEditor cloud', () => { // TODO remove skipPointerEventsCheck once https://github.com/jsdom/jsdom/issues/3232 is fixed await user.click(ui.buttons.addLabel.get()); - await user.type(getLabelInput(ui.inputs.labelKey(0).get()), 'severity{enter}'); - await user.type(getLabelInput(ui.inputs.labelValue(0).get()), 'warn{enter}'); - // save and check what was sent to backend await user.click(ui.buttons.saveAndExit.get()); await waitFor(() => expect(mocks.api.setRulerRuleGroup).toHaveBeenCalled()); @@ -173,9 +169,10 @@ describe('RuleEditor cloud', () => { { alert: 'my great new rule', annotations: { description: 'some description', summary: 'some summary' }, - labels: { severity: 'warn' }, expr: 'up == 1', for: '1m', + labels: {}, + keep_firing_for: undefined, }, ], } diff --git a/public/app/features/alerting/unified/RuleEditorExisting.test.tsx b/public/app/features/alerting/unified/RuleEditorExisting.test.tsx index ee8a14a749f..ec49c212dfa 100644 --- a/public/app/features/alerting/unified/RuleEditorExisting.test.tsx +++ b/public/app/features/alerting/unified/RuleEditorExisting.test.tsx @@ -1,4 +1,4 @@ -import { render, screen, waitFor, within } from '@testing-library/react'; +import { render, screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import React from 'react'; import { Route } from 'react-router-dom'; @@ -18,7 +18,7 @@ import RuleEditor from './RuleEditor'; import { discoverFeatures } from './api/buildInfo'; import { fetchRulerRules, fetchRulerRulesGroup, fetchRulerRulesNamespace, setRulerRuleGroup } from './api/ruler'; import { ExpressionEditorProps } from './components/rule-editor/ExpressionEditor'; -import { grantUserPermissions, mockDataSource, MockDataSourceSrv, mockFolder } from './mocks'; +import { MockDataSourceSrv, grantUserPermissions, mockDataSource, mockFolder } from './mocks'; import { fetchRulerRulesIfNotFetchedYet } from './state/actions'; import * as config from './utils/config'; import { GRAFANA_RULES_SOURCE_NAME } from './utils/datasource'; @@ -73,7 +73,6 @@ function renderRuleEditor(identifier?: string) { ); } -const getLabelInput = (selector: HTMLElement) => within(selector).getByRole('combobox'); describe('RuleEditor grafana managed rules', () => { beforeEach(() => { jest.clearAllMocks(); @@ -187,10 +186,6 @@ describe('RuleEditor grafana managed rules', () => { await userEvent.type(screen.getByPlaceholderText('Enter custom annotation name...'), 'custom'); await userEvent.type(screen.getByPlaceholderText('Enter custom annotation content...'), 'value'); - //add a label - await userEvent.type(getLabelInput(ui.inputs.labelKey(2).get()), 'custom{enter}'); - await userEvent.type(getLabelInput(ui.inputs.labelValue(2).get()), 'value{enter}'); - // save and check what was sent to backend await userEvent.click(ui.buttons.save.get()); await waitFor(() => expect(mocks.api.setRulerRuleGroup).toHaveBeenCalled()); @@ -207,13 +202,14 @@ describe('RuleEditor grafana managed rules', () => { rules: [ { annotations: { description: 'some description', summary: 'some summary', custom: 'value' }, - labels: { severity: 'warn', team: 'the a-team', custom: 'value' }, + labels: { severity: 'warn', team: 'the a-team' }, for: '1m', grafana_alert: { uid, condition: 'B', data: getDefaultQueries(), exec_err_state: GrafanaAlertStateDecision.Error, + notification_settings: undefined, is_paused: false, no_data_state: 'NoData', title: 'my great new rule', diff --git a/public/app/features/alerting/unified/RuleEditorGrafanaRules.test.tsx b/public/app/features/alerting/unified/RuleEditorGrafanaRules.test.tsx index cdd52a78046..ebfdcb91742 100644 --- a/public/app/features/alerting/unified/RuleEditorGrafanaRules.test.tsx +++ b/public/app/features/alerting/unified/RuleEditorGrafanaRules.test.tsx @@ -1,9 +1,10 @@ -import { screen, waitFor, waitForElementToBeRemoved, within } from '@testing-library/react'; -import userEvent, { PointerEventsCheckLevel } from '@testing-library/user-event'; +import { screen, waitFor, waitForElementToBeRemoved } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; import React from 'react'; import { renderRuleEditor, ui } from 'test/helpers/alertingRuleEditor'; import { clickSelectOption } from 'test/helpers/selectOptionInTest'; import { byRole } from 'testing-library-selector'; +import 'whatwg-fetch'; import { setDataSourceSrv } from '@grafana/runtime'; import { contextSrv } from 'app/core/services/context_srv'; @@ -68,7 +69,6 @@ const mocks = { const server = setupMswServer(); -const getLabelInput = (selector: HTMLElement) => within(selector).getByRole('combobox'); describe('RuleEditor grafana managed rules', () => { beforeEach(() => { mockApi(server).eval({ results: {} }); @@ -196,13 +196,6 @@ describe('RuleEditor grafana managed rules', () => { await clickSelectOption(groupInput, 'group1'); await userEvent.type(ui.inputs.annotationValue(1).get(), 'some description'); - // TODO remove skipPointerEventsCheck once https://github.com/jsdom/jsdom/issues/3232 is fixed - await userEvent.click(ui.buttons.addLabel.get(), { pointerEventsCheck: PointerEventsCheckLevel.Never }); - - await userEvent.type(getLabelInput(ui.inputs.labelKey(0).get()), 'severity{enter}'); - await userEvent.type(getLabelInput(ui.inputs.labelValue(0).get()), 'warn{enter}'); - //8 segons - // save and check what was sent to backend await userEvent.click(ui.buttons.saveAndExit.get()); // 9seg @@ -217,7 +210,7 @@ describe('RuleEditor grafana managed rules', () => { rules: [ { annotations: { description: 'some description' }, - labels: { severity: 'warn' }, + labels: {}, for: '1m', grafana_alert: { condition: 'B', @@ -226,6 +219,7 @@ describe('RuleEditor grafana managed rules', () => { is_paused: false, no_data_state: 'NoData', title: 'my great new rule', + notification_settings: undefined, }, }, ], diff --git a/public/app/features/alerting/unified/RuleEditorRecordingRule.test.tsx b/public/app/features/alerting/unified/RuleEditorRecordingRule.test.tsx index 63f6ea53268..bbb0aead53e 100644 --- a/public/app/features/alerting/unified/RuleEditorRecordingRule.test.tsx +++ b/public/app/features/alerting/unified/RuleEditorRecordingRule.test.tsx @@ -1,9 +1,10 @@ -import { screen, waitFor, waitForElementToBeRemoved, within } from '@testing-library/react'; -import userEvent, { PointerEventsCheckLevel } from '@testing-library/user-event'; +import { screen, waitFor, waitForElementToBeRemoved } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; import React from 'react'; import { renderRuleEditor, ui } from 'test/helpers/alertingRuleEditor'; import { clickSelectOption } from 'test/helpers/selectOptionInTest'; import { byText } from 'testing-library-selector'; +import 'whatwg-fetch'; import { setDataSourceSrv } from '@grafana/runtime'; import { contextSrv } from 'app/core/services/context_srv'; @@ -16,7 +17,7 @@ import { searchFolders } from '../../manage-dashboards/state/actions'; import { discoverFeatures } from './api/buildInfo'; import { fetchRulerRules, fetchRulerRulesGroup, fetchRulerRulesNamespace, setRulerRuleGroup } from './api/ruler'; import { RecordingRuleEditorProps } from './components/rule-editor/RecordingRuleEditor'; -import { grantUserPermissions, mockDataSource, MockDataSourceSrv } from './mocks'; +import { MockDataSourceSrv, grantUserPermissions, labelsPluginMetaMock, mockDataSource } from './mocks'; import { fetchRulerRulesIfNotFetchedYet } from './state/actions'; import * as config from './utils/config'; @@ -92,9 +93,9 @@ const mocks = { }, }; -const getLabelInput = (selector: HTMLElement) => within(selector).getByRole('combobox'); - const server = setupMswServer(); +mockApi(server).plugins.getPluginSettings({ ...labelsPluginMetaMock, enabled: false }); +mockApi(server).eval({ results: { A: { frames: [] } } }); describe('RuleEditor recording rules', () => { beforeEach(() => { @@ -162,12 +163,6 @@ describe('RuleEditor recording rules', () => { await userEvent.type(await ui.inputs.expr.find(), 'up == 1'); - // TODO remove skipPointerEventsCheck once https://github.com/jsdom/jsdom/issues/3232 is fixed - await userEvent.click(ui.buttons.addLabel.get(), { pointerEventsCheck: PointerEventsCheckLevel.Never }); - - await userEvent.type(getLabelInput(ui.inputs.labelKey(1).get()), 'team{enter}'); - await userEvent.type(getLabelInput(ui.inputs.labelValue(1).get()), 'the a-team{enter}'); - // try to save, find out that recording rule name is invalid await userEvent.click(ui.buttons.saveAndExit.get()); await waitFor(() => @@ -194,7 +189,7 @@ describe('RuleEditor recording rules', () => { rules: [ { record: 'my:great:new:recording:rule', - labels: { team: 'the a-team' }, + labels: {}, expr: 'up == 1', }, ], diff --git a/public/app/features/alerting/unified/RuleList.test.tsx b/public/app/features/alerting/unified/RuleList.test.tsx index 06d1e9c8256..85f00b549ff 100644 --- a/public/app/features/alerting/unified/RuleList.test.tsx +++ b/public/app/features/alerting/unified/RuleList.test.tsx @@ -14,6 +14,7 @@ import { locationService, setBackendSrv, setDataSourceSrv, + usePluginLinkExtensions, } from '@grafana/runtime'; import { backendSrv } from 'app/core/services/backend_srv'; import * as ruleActionButtons from 'app/features/alerting/unified/components/rules/RuleActionsButtons'; @@ -57,6 +58,7 @@ import { DataSourceType, GRAFANA_RULES_SOURCE_NAME } from './utils/datasource'; jest.mock('@grafana/runtime', () => ({ ...jest.requireActual('@grafana/runtime'), getPluginLinkExtensions: jest.fn(), + usePluginLinkExtensions: jest.fn(), useReturnToPrevious: jest.fn(), })); jest.mock('./api/buildInfo'); @@ -81,6 +83,7 @@ jest.spyOn(actions, 'rulesInSameGroupHaveInvalidFor').mockReturnValue([]); const mocks = { getAllDataSourcesMock: jest.mocked(config.getAllDataSources), getPluginLinkExtensionsMock: jest.mocked(getPluginLinkExtensions), + usePluginLinkExtensionsMock: jest.mocked(usePluginLinkExtensions), rulesInSameGroupHaveInvalidForMock: jest.mocked(actions.rulesInSameGroupHaveInvalidFor), api: { @@ -152,7 +155,7 @@ const ui = { paused: byText(/^Paused/), }, actionButtons: { - more: byRole('button', { name: 'More' }), + more: byRole('button', { name: /more-actions/ }), }, moreActionItems: { pause: byRole('menuitem', { name: /pause evaluation/i }), @@ -201,7 +204,7 @@ describe('RuleList', () => { AccessControlAction.AlertingRuleExternalWrite, ]); mocks.rulesInSameGroupHaveInvalidForMock.mockReturnValue([]); - mocks.getPluginLinkExtensionsMock.mockReturnValue({ + mocks.usePluginLinkExtensionsMock.mockReturnValue({ extensions: [ { pluginId: 'grafana-ml-app', @@ -213,6 +216,7 @@ describe('RuleList', () => { onClick: jest.fn(), }, ], + isLoading: false, }); }); @@ -440,8 +444,10 @@ describe('RuleList', () => { await userEvent.click(ui.ruleCollapseToggle.get(ruleRows[1])); const ruleDetails = ui.expandedContent.get(ruleRows[1]); + const labels = byTestId('label-value').getAll(ruleDetails); + expect(labels[0]).toHaveTextContent('severitywarning'); + expect(labels[1]).toHaveTextContent('foobar'); - expect(ruleDetails).toHaveTextContent('Labels severitywarning foobar'); expect(ruleDetails).toHaveTextContent('Expressiontopk ( 5 , foo ) [ 5m ]'); expect(ruleDetails).toHaveTextContent('messagegreat alert'); expect(ruleDetails).toHaveTextContent('Matching instances'); @@ -452,8 +458,8 @@ describe('RuleList', () => { const instanceRows = byTestId('row').getAll(instancesTable); expect(instanceRows).toHaveLength(2); - expect(instanceRows![0]).toHaveTextContent('Firing foobar severitywarning2021-03-18 08:47:05'); - expect(instanceRows![1]).toHaveTextContent('Firing foobaz severityerror2021-03-18 08:47:05'); + expect(instanceRows![0]).toHaveTextContent('Firingfoobarseveritywarning2021-03-18 08:47:05'); + expect(instanceRows![1]).toHaveTextContent('Firingfoobazseverityerror2021-03-18 08:47:05'); // expand details of an instance await userEvent.click(ui.ruleCollapseToggle.get(instanceRows![0])); @@ -593,8 +599,9 @@ describe('RuleList', () => { await userEvent.click(ui.ruleCollapseToggle.get(ruleRows[0])); const ruleDetails = ui.expandedContent.get(ruleRows[0]); - - expect(ruleDetails).toHaveTextContent('Labels severitywarning foobar'); + const labels = byTestId('label-value').getAll(ruleDetails); + expect(labels[0]).toHaveTextContent('severitywarning'); + expect(labels[1]).toHaveTextContent('foobar'); // Check for different label matchers await userEvent.clear(filterInput); diff --git a/public/app/features/alerting/unified/Silences.test.tsx b/public/app/features/alerting/unified/Silences.test.tsx index 905ed88d476..e80fca33996 100644 --- a/public/app/features/alerting/unified/Silences.test.tsx +++ b/public/app/features/alerting/unified/Silences.test.tsx @@ -1,49 +1,41 @@ -import { render, waitFor } from '@testing-library/react'; -import userEvent, { PointerEventsCheckLevel } from '@testing-library/user-event'; import React from 'react'; -import { TestProvider } from 'test/helpers/TestProvider'; +import { render, waitFor, userEvent, screen } from 'test/test-utils'; import { byLabelText, byPlaceholderText, byRole, byTestId, byText } from 'testing-library-selector'; import { dateTime } from '@grafana/data'; import { selectors } from '@grafana/e2e-selectors'; import { config, locationService, setDataSourceSrv } from '@grafana/runtime'; -import { contextSrv } from 'app/core/services/context_srv'; -import { AlertState, MatcherOperator } from 'app/plugins/datasource/alertmanager/types'; +import { setupMswServer } from 'app/features/alerting/unified/mockApi'; +import { + MOCK_DATASOURCE_UID_BROKEN_ALERTMANAGER, + MOCK_DATASOURCE_NAME_BROKEN_ALERTMANAGER, +} from 'app/features/alerting/unified/mocks/datasources'; +import { waitForServerRequest } from 'app/features/alerting/unified/mocks/server/events'; +import { silenceCreateHandler } from 'app/features/alerting/unified/mocks/silences'; +import { MatcherOperator } from 'app/plugins/datasource/alertmanager/types'; import { AccessControlAction } from 'app/types'; -import { SilenceState } from '../../../plugins/datasource/alertmanager/types'; - import Silences from './Silences'; -import { createOrUpdateSilence, fetchAlerts, fetchSilences } from './api/alertmanager'; -import { grantUserPermissions, mockAlertmanagerAlert, mockDataSource, MockDataSourceSrv, mockSilence } from './mocks'; +import { grantUserPermissions, MOCK_SILENCE_ID_EXISTING, mockDataSource, MockDataSourceSrv } from './mocks'; import { AlertmanagerProvider } from './state/AlertmanagerContext'; import { setupDataSources } from './testSetup/datasources'; -import { parseMatchers } from './utils/alertmanager'; import { DataSourceType } from './utils/datasource'; -jest.mock('./api/alertmanager'); jest.mock('app/core/services/context_srv'); const TEST_TIMEOUT = 60000; -const mocks = { - api: { - fetchSilences: jest.mocked(fetchSilences), - fetchAlerts: jest.mocked(fetchAlerts), - createOrUpdateSilence: jest.mocked(createOrUpdateSilence), - }, - contextSrv: jest.mocked(contextSrv), -}; - const renderSilences = (location = '/alerting/silences/') => { locationService.push(location); - return render( - - - - - + + + , + { + historyOptions: { + initialEntries: [location], + }, + } ); }; @@ -52,16 +44,23 @@ const dataSources = { name: 'Alertmanager', type: DataSourceType.Alertmanager, }), + [MOCK_DATASOURCE_NAME_BROKEN_ALERTMANAGER]: mockDataSource({ + uid: MOCK_DATASOURCE_UID_BROKEN_ALERTMANAGER, + name: MOCK_DATASOURCE_NAME_BROKEN_ALERTMANAGER, + type: DataSourceType.Alertmanager, + }), }; const ui = { notExpiredTable: byTestId('not-expired-table'), expiredTable: byTestId('expired-table'), - expiredCaret: byText(/expired/i), + expiredCaret: byText(/expired silences \(/i), + silencesTags: byLabelText(/tags/i), silenceRow: byTestId('row'), silencedAlertCell: byTestId('alerts'), addSilenceButton: byRole('link', { name: /add silence/i }), queryBar: byPlaceholderText('Search'), + existingSilenceNotFound: byRole('alert', { name: /existing silence .* not found/i }), editor: { timeRange: byTestId(selectors.components.TimePicker.openButton), durationField: byLabelText('Duration'), @@ -80,28 +79,6 @@ const ui = { const resetMocks = () => { jest.resetAllMocks(); - mocks.api.fetchSilences.mockImplementation(() => { - return Promise.resolve([ - mockSilence({ id: '12345' }), - mockSilence({ id: '67890', matchers: parseMatchers('foo!=bar'), comment: 'Catch all' }), - mockSilence({ id: '1111', status: { state: SilenceState.Expired } }), - ]); - }); - - mocks.api.fetchAlerts.mockImplementation(() => { - return Promise.resolve([ - mockAlertmanagerAlert({ - labels: { foo: 'bar' }, - status: { state: AlertState.Suppressed, silencedBy: ['12345'], inhibitedBy: [] }, - }), - mockAlertmanagerAlert({ - labels: { foo: 'buzz' }, - status: { state: AlertState.Suppressed, silencedBy: ['67890'], inhibitedBy: [] }, - }), - ]); - }); - - mocks.api.createOrUpdateSilence.mockResolvedValue(mockSilence()); grantUserPermissions([ AccessControlAction.AlertingInstanceRead, @@ -117,6 +94,25 @@ const setUserLogged = (isLogged: boolean) => { config.bootData.user.name = isLogged ? 'admin' : ''; }; +const enterSilenceLabel = async (index: number, name: string, matcher: MatcherOperator, value: string) => { + const user = userEvent.setup(); + await user.type(ui.editor.matcherName.getAll()[index], name); + await user.type(ui.editor.matcherOperatorSelect.getAll()[index], matcher); + await user.tab(); + await user.type(ui.editor.matcherValue.getAll()[index], value); +}; + +const addAdditionalMatcher = async () => { + const user = userEvent.setup(); + await user.click(ui.editor.addMatcherButton.get()); +}; + +setupMswServer(); + +beforeEach(() => { + setupDataSources(dataSources.am, dataSources[MOCK_DATASOURCE_NAME_BROKEN_ALERTMANAGER]); +}); + describe('Silences', () => { beforeAll(resetMocks); afterEach(resetMocks); @@ -128,26 +124,29 @@ describe('Silences', () => { it( 'loads and shows silences', async () => { + const user = userEvent.setup(); renderSilences(); - await waitFor(() => expect(mocks.api.fetchSilences).toHaveBeenCalled()); - await waitFor(() => expect(mocks.api.fetchAlerts).toHaveBeenCalled()); - await userEvent.click(ui.expiredCaret.get()); - expect(ui.notExpiredTable.get()).not.toBeNull(); - expect(ui.expiredTable.get()).not.toBeNull(); - let silences = ui.silenceRow.queryAll(); - expect(silences).toHaveLength(3); - expect(silences[0]).toHaveTextContent('foo=bar'); - expect(silences[1]).toHaveTextContent('foo!=bar'); - expect(silences[2]).toHaveTextContent('foo=bar'); + expect(await ui.notExpiredTable.find()).toBeInTheDocument(); - await userEvent.click(ui.expiredCaret.getAll()[0]); - expect(ui.notExpiredTable.get()).not.toBeNull(); - expect(ui.expiredTable.query()).toBeNull(); - silences = ui.silenceRow.queryAll(); - expect(silences).toHaveLength(2); - expect(silences[0]).toHaveTextContent('foo=bar'); - expect(silences[1]).toHaveTextContent('foo!=bar'); + await user.click(ui.expiredCaret.get()); + expect(ui.expiredTable.get()).toBeInTheDocument(); + + const allSilences = ui.silenceRow.queryAll(); + expect(allSilences).toHaveLength(3); + expect(allSilences[0]).toHaveTextContent('foo=bar'); + expect(allSilences[1]).toHaveTextContent('foo!=bar'); + expect(allSilences[2]).toHaveTextContent('foo=bar'); + + await user.click(ui.expiredCaret.get()); + + expect(ui.notExpiredTable.get()).toBeInTheDocument(); + expect(ui.expiredTable.query()).not.toBeInTheDocument(); + + const activeSilences = ui.silenceRow.queryAll(); + expect(activeSilences).toHaveLength(2); + expect(activeSilences[0]).toHaveTextContent('foo=bar'); + expect(activeSilences[1]).toHaveTextContent('foo!=bar'); }, TEST_TIMEOUT ); @@ -155,25 +154,13 @@ describe('Silences', () => { it( 'shows the correct number of silenced alerts', async () => { - mocks.api.fetchAlerts.mockImplementation(() => { - return Promise.resolve([ - mockAlertmanagerAlert({ - labels: { foo: 'bar', buzz: 'bazz' }, - status: { state: AlertState.Suppressed, silencedBy: ['12345'], inhibitedBy: [] }, - }), - mockAlertmanagerAlert({ - labels: { foo: 'bar', buzz: 'bazz' }, - status: { state: AlertState.Suppressed, silencedBy: ['12345'], inhibitedBy: [] }, - }), - ]); - }); - renderSilences(); - await waitFor(() => expect(mocks.api.fetchSilences).toHaveBeenCalled()); - await waitFor(() => expect(mocks.api.fetchAlerts).toHaveBeenCalled()); - const silencedAlertRows = ui.silencedAlertCell.getAll(ui.notExpiredTable.get()); - expect(silencedAlertRows).toHaveLength(2); + const notExpiredTable = await ui.notExpiredTable.find(); + + expect(notExpiredTable).toBeInTheDocument(); + + const silencedAlertRows = await ui.silencedAlertCell.findAll(notExpiredTable); expect(silencedAlertRows[0]).toHaveTextContent('2'); expect(silencedAlertRows[1]).toHaveTextContent('0'); }, @@ -183,13 +170,11 @@ describe('Silences', () => { it( 'filters silences by matchers', async () => { + const user = userEvent.setup(); renderSilences(); - await waitFor(() => expect(mocks.api.fetchSilences).toHaveBeenCalled()); - await waitFor(() => expect(mocks.api.fetchAlerts).toHaveBeenCalled()); - const queryBar = ui.queryBar.get(); - await userEvent.click(queryBar); - await userEvent.paste('foo=bar'); + const queryBar = await ui.queryBar.find(); + await user.type(queryBar, 'foo=bar'); await waitFor(() => expect(ui.silenceRow.getAll()).toHaveLength(2)); }, @@ -199,31 +184,34 @@ describe('Silences', () => { it('shows creating a silence button for users with access', async () => { renderSilences(); - await waitFor(() => expect(mocks.api.fetchSilences).toHaveBeenCalled()); - await waitFor(() => expect(mocks.api.fetchAlerts).toHaveBeenCalled()); - - expect(ui.addSilenceButton.get()).toBeInTheDocument(); + expect(await ui.addSilenceButton.find()).toBeInTheDocument(); }); it('hides actions for creating a silence for users without access', async () => { grantUserPermissions([AccessControlAction.AlertingInstanceRead, AccessControlAction.AlertingInstancesExternalRead]); renderSilences(); - await waitFor(() => expect(mocks.api.fetchSilences).toHaveBeenCalled()); - await waitFor(() => expect(mocks.api.fetchAlerts).toHaveBeenCalled()); + + const notExpiredTable = await ui.notExpiredTable.find(); + + expect(notExpiredTable).toBeInTheDocument(); expect(ui.addSilenceButton.query()).not.toBeInTheDocument(); }); + + it('handles error case when broken alertmanager is used', async () => { + renderSilences(`/alerting/silences?alertmanager=${encodeURIComponent(MOCK_DATASOURCE_NAME_BROKEN_ALERTMANAGER)}`); + expect(await screen.findByText(/error loading silences/i)).toBeInTheDocument(); + }); }); -describe('Silence edit', () => { +describe('Silence create/edit', () => { const baseUrlPath = '/alerting/silence/new'; beforeAll(resetMocks); afterEach(resetMocks); beforeEach(() => { setUserLogged(true); - setupDataSources(dataSources.am); }); it('Should not render createdBy if user is logged in and has a name', async () => { @@ -242,7 +230,7 @@ describe('Silence edit', () => { const matchersQueryString = matchersParams.map((matcher) => `matcher=${encodeURIComponent(matcher)}`).join('&'); renderSilences(`${baseUrlPath}?${matchersQueryString}`); - await waitFor(() => expect(ui.editor.durationField.query()).not.toBeNull()); + expect(await ui.editor.durationField.find()).toBeInTheDocument(); const matchers = ui.editor.matchersField.queryAll(); expect(matchers).toHaveLength(4); @@ -269,8 +257,11 @@ describe('Silence edit', () => { it( 'creates a new silence', async () => { - renderSilences(baseUrlPath); - await waitFor(() => expect(ui.editor.durationField.query()).not.toBeNull()); + const user = userEvent.setup(); + renderSilences(`${baseUrlPath}?alertmanager=Alertmanager`); + expect(await ui.editor.durationField.find()).toBeInTheDocument(); + + const postRequest = waitForServerRequest(silenceCreateHandler()); const start = new Date(); const end = new Date(start.getTime() + 24 * 60 * 60 * 1000); @@ -278,83 +269,86 @@ describe('Silence edit', () => { const startDateString = dateTime(start).format('YYYY-MM-DD'); const endDateString = dateTime(end).format('YYYY-MM-DD'); - await userEvent.clear(ui.editor.durationInput.get()); - await userEvent.type(ui.editor.durationInput.get(), '1d'); + await user.clear(ui.editor.durationInput.get()); + await user.type(ui.editor.durationInput.get(), '1d'); await waitFor(() => expect(ui.editor.durationInput.query()).toHaveValue('1d')); await waitFor(() => expect(ui.editor.timeRange.get()).toHaveTextContent(startDateString)); await waitFor(() => expect(ui.editor.timeRange.get()).toHaveTextContent(endDateString)); - await userEvent.type(ui.editor.matcherName.get(), 'foo'); - await userEvent.type(ui.editor.matcherOperatorSelect.get(), '='); - await userEvent.tab(); - await userEvent.type(ui.editor.matcherValue.get(), 'bar'); + await enterSilenceLabel(0, 'foo', MatcherOperator.equal, 'bar'); - // TODO remove skipPointerEventsCheck once https://github.com/jsdom/jsdom/issues/3232 is fixed - await userEvent.click(ui.editor.addMatcherButton.get(), { pointerEventsCheck: PointerEventsCheckLevel.Never }); - await userEvent.type(ui.editor.matcherName.getAll()[1], 'bar'); - await userEvent.type(ui.editor.matcherOperatorSelect.getAll()[1], '!='); - await userEvent.tab(); - await userEvent.type(ui.editor.matcherValue.getAll()[1], 'buzz'); + await addAdditionalMatcher(); + await enterSilenceLabel(1, 'bar', MatcherOperator.notEqual, 'buzz'); - // TODO remove skipPointerEventsCheck once https://github.com/jsdom/jsdom/issues/3232 is fixed - await userEvent.click(ui.editor.addMatcherButton.get(), { pointerEventsCheck: PointerEventsCheckLevel.Never }); - await userEvent.type(ui.editor.matcherName.getAll()[2], 'region'); - await userEvent.type(ui.editor.matcherOperatorSelect.getAll()[2], '=~'); - await userEvent.tab(); - await userEvent.type(ui.editor.matcherValue.getAll()[2], 'us-west-.*'); + await addAdditionalMatcher(); + await enterSilenceLabel(2, 'region', MatcherOperator.regex, 'us-west-.*'); - // TODO remove skipPointerEventsCheck once https://github.com/jsdom/jsdom/issues/3232 is fixed - await userEvent.click(ui.editor.addMatcherButton.get(), { pointerEventsCheck: PointerEventsCheckLevel.Never }); - await userEvent.type(ui.editor.matcherName.getAll()[3], 'env'); - await userEvent.type(ui.editor.matcherOperatorSelect.getAll()[3], '!~'); - await userEvent.tab(); - await userEvent.type(ui.editor.matcherValue.getAll()[3], 'dev|staging'); + await addAdditionalMatcher(); + await enterSilenceLabel(3, 'env', MatcherOperator.notRegex, 'dev|staging'); - await userEvent.click(ui.editor.submit.get()); + await user.click(ui.editor.submit.get()); - await waitFor(() => - expect(mocks.api.createOrUpdateSilence).toHaveBeenCalledWith( - 'grafana', - expect.objectContaining({ - comment: expect.stringMatching(/created (\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2})/), - matchers: [ - { isEqual: true, isRegex: false, name: 'foo', value: 'bar' }, - { isEqual: false, isRegex: false, name: 'bar', value: 'buzz' }, - { isEqual: true, isRegex: true, name: 'region', value: 'us-west-.*' }, - { isEqual: false, isRegex: true, name: 'env', value: 'dev|staging' }, - ], - }) - ) + expect(await ui.notExpiredTable.find()).toBeInTheDocument(); + + const createSilenceRequest = await postRequest; + const requestBody = await createSilenceRequest.clone().json(); + expect(requestBody).toMatchObject( + expect.objectContaining({ + comment: expect.stringMatching(/created (\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2})/), + matchers: [ + { isEqual: true, isRegex: false, name: 'foo', value: 'bar' }, + { isEqual: false, isRegex: false, name: 'bar', value: 'buzz' }, + { isEqual: true, isRegex: true, name: 'region', value: 'us-west-.*' }, + { isEqual: false, isRegex: true, name: 'env', value: 'dev|staging' }, + ], + }) ); }, TEST_TIMEOUT ); + it('shows an error when existing silence cannot be found', async () => { + renderSilences('/alerting/silence/foo-bar/edit'); + + expect(await ui.existingSilenceNotFound.find()).toBeInTheDocument(); + }); + + it('populates form with existing silence information', async () => { + renderSilences(`/alerting/silence/${MOCK_SILENCE_ID_EXISTING}/edit`); + + // Await the first value to be populated, after which we can expect that all of the other + // existing fields have been filled out as well + await waitFor(() => expect(ui.editor.matcherName.get()).toHaveValue('foo')); + expect(ui.editor.matcherValue.get()).toHaveValue('bar'); + expect(ui.editor.comment.get()).toHaveValue('Silence noisy alerts'); + }); + it( 'silences page should contain alertmanager parameter after creating a silence', async () => { const user = userEvent.setup(); + const postRequest = waitForServerRequest(silenceCreateHandler()); + renderSilences(`${baseUrlPath}?alertmanager=Alertmanager`); await waitFor(() => expect(ui.editor.durationField.query()).not.toBeNull()); - await user.type(ui.editor.matcherName.getAll()[0], 'foo'); - await user.type(ui.editor.matcherOperatorSelect.getAll()[0], '='); - await user.type(ui.editor.matcherValue.getAll()[0], 'bar'); + await enterSilenceLabel(0, 'foo', MatcherOperator.equal, 'bar'); await user.click(ui.editor.submit.get()); - await waitFor(() => - expect(mocks.api.createOrUpdateSilence).toHaveBeenCalledWith( - 'Alertmanager', - expect.objectContaining({ - matchers: [{ isEqual: true, isRegex: false, name: 'foo', value: 'bar' }], - }) - ) - ); + expect(await ui.notExpiredTable.find()).toBeInTheDocument(); expect(locationService.getSearch().get('alertmanager')).toBe('Alertmanager'); + + const createSilenceRequest = await postRequest; + const requestBody = await createSilenceRequest.clone().json(); + expect(requestBody).toMatchObject( + expect.objectContaining({ + matchers: [{ isEqual: true, isRegex: false, name: 'foo', value: 'bar' }], + }) + ); }, TEST_TIMEOUT ); diff --git a/public/app/features/alerting/unified/Silences.tsx b/public/app/features/alerting/unified/Silences.tsx index ec4addf133e..0663017b29e 100644 --- a/public/app/features/alerting/unified/Silences.tsx +++ b/public/app/features/alerting/unified/Silences.tsx @@ -1,59 +1,18 @@ -import React, { useCallback, useEffect } from 'react'; +import React from 'react'; import { Route, RouteChildrenProps, Switch } from 'react-router-dom'; -import { Alert, withErrorBoundary } from '@grafana/ui'; -import { Silence } from 'app/plugins/datasource/alertmanager/types'; -import { useDispatch } from 'app/types'; +import { withErrorBoundary } from '@grafana/ui'; -import { featureDiscoveryApi } from './api/featureDiscoveryApi'; import { AlertmanagerPageWrapper } from './components/AlertingPageWrapper'; import { GrafanaAlertmanagerDeliveryWarning } from './components/GrafanaAlertmanagerDeliveryWarning'; import SilencesEditor from './components/silences/SilencesEditor'; import SilencesTable from './components/silences/SilencesTable'; import { useSilenceNavData } from './hooks/useSilenceNavData'; -import { useUnifiedAlertingSelector } from './hooks/useUnifiedAlertingSelector'; import { useAlertmanager } from './state/AlertmanagerContext'; -import { fetchAmAlertsAction, fetchSilencesAction } from './state/actions'; -import { SILENCES_POLL_INTERVAL_MS } from './utils/constants'; -import { AsyncRequestState, initialAsyncRequestState } from './utils/redux'; const Silences = () => { const { selectedAlertmanager } = useAlertmanager(); - const dispatch = useDispatch(); - const silences = useUnifiedAlertingSelector((state) => state.silences); - const alertsRequests = useUnifiedAlertingSelector((state) => state.amAlerts); - const alertsRequest = selectedAlertmanager - ? alertsRequests[selectedAlertmanager] || initialAsyncRequestState - : undefined; - - const { currentData: amFeatures } = featureDiscoveryApi.useDiscoverAmFeaturesQuery( - { amSourceName: selectedAlertmanager ?? '' }, - { skip: !selectedAlertmanager } - ); - - useEffect(() => { - function fetchAll() { - if (selectedAlertmanager) { - dispatch(fetchSilencesAction(selectedAlertmanager)); - dispatch(fetchAmAlertsAction(selectedAlertmanager)); - } - } - fetchAll(); - const interval = setInterval(() => fetchAll, SILENCES_POLL_INTERVAL_MS); - return () => { - clearInterval(interval); - }; - }, [selectedAlertmanager, dispatch]); - - const { result, loading, error }: AsyncRequestState = - (selectedAlertmanager && silences[selectedAlertmanager]) || initialAsyncRequestState; - - const getSilenceById = useCallback((id: string) => result && result.find((silence) => silence.id === id), [result]); - - const mimirLazyInitError = - error?.message?.includes('the Alertmanager is not configured') && amFeatures?.lazyConfigInit; - if (!selectedAlertmanager) { return null; } @@ -62,48 +21,23 @@ const Silences = () => { <> - {mimirLazyInitError && ( - - Create a new contact point to create a configuration using the default values or contact your administrator to - set up the Alertmanager. - - )} - {error && !loading && !mimirLazyInitError && ( - - {error.message || 'Unknown error.'} - - )} - {alertsRequest?.error && !alertsRequest?.loading && !mimirLazyInitError && ( - - {alertsRequest.error?.message || 'Unknown error.'} - - )} - {result && !error && ( - - - - - - - - - {({ match }: RouteChildrenProps<{ id: string }>) => { - return ( - match?.params.id && ( - - ) - ); - }} - - - )} + + + + + + + + + {({ match }: RouteChildrenProps<{ id: string }>) => { + return ( + match?.params.id && ( + + ) + ); + }} + + ); }; diff --git a/public/app/features/alerting/unified/TODO.md b/public/app/features/alerting/unified/TODO.md index 86e60811548..749ed2e388b 100644 --- a/public/app/features/alerting/unified/TODO.md +++ b/public/app/features/alerting/unified/TODO.md @@ -17,7 +17,6 @@ If the item needs more rationale and you feel like a single sentence is inedequa ## Refactoring - Get rid of "+ Add new" in drop-downs : Let's see if is there a way we can make it work with ` New folder @@ -258,6 +260,7 @@ export function FolderAndGroup({ fill="outline" variant="secondary" disabled={!folder} + data-testid={selectors.components.AlertRules.newEvaluationGroupButton} > New evaluation group @@ -309,6 +312,7 @@ function FolderCreationModal({ invalid={error} > Cancel - @@ -388,6 +396,7 @@ function EvaluationGroupCreationModal({ invalid={Boolean(formState.errors.group)} > Cancel - diff --git a/public/app/features/alerting/unified/components/rule-editor/LabelsField.test.tsx b/public/app/features/alerting/unified/components/rule-editor/LabelsField.test.tsx deleted file mode 100644 index 2dd636d6187..00000000000 --- a/public/app/features/alerting/unified/components/rule-editor/LabelsField.test.tsx +++ /dev/null @@ -1,130 +0,0 @@ -import { render, screen, waitFor, within } from '@testing-library/react'; -import userEvent from '@testing-library/user-event'; -import React from 'react'; -import { FormProvider, useForm } from 'react-hook-form'; -import { Provider } from 'react-redux'; - -import { configureStore } from 'app/store/configureStore'; - -import LabelsField from './LabelsField'; - -const labels = [ - { key: 'key1', value: 'value1' }, - { key: 'key2', value: 'value2' }, -]; - -const FormProviderWrapper = ({ children }: React.PropsWithChildren<{}>) => { - const methods = useForm({ defaultValues: { labels } }); - return {children}; -}; - -function renderAlertLabels(dataSourceName?: string) { - const store = configureStore({}); - - render( - - {dataSourceName ? : } - , - { wrapper: FormProviderWrapper } - ); -} - -describe('LabelsField with suggestions', () => { - it('Should display two dropdowns with the existing labels', async () => { - renderAlertLabels('grafana'); - - await waitFor(() => expect(screen.getAllByTestId('alertlabel-key-picker')).toHaveLength(2)); - - expect(screen.getByTestId('label-key-0').textContent).toBe('key1'); - expect(screen.getByTestId('label-key-1').textContent).toBe('key2'); - - expect(screen.getAllByTestId('alertlabel-value-picker')).toHaveLength(2); - - expect(screen.getByTestId('label-value-0').textContent).toBe('value1'); - expect(screen.getByTestId('label-value-1').textContent).toBe('value2'); - }); - - it('Should delete a key-value combination', async () => { - renderAlertLabels('grafana'); - - await waitFor(() => expect(screen.getAllByTestId('alertlabel-key-picker')).toHaveLength(2)); - - expect(screen.getAllByTestId('alertlabel-key-picker')).toHaveLength(2); - expect(screen.getAllByTestId('alertlabel-value-picker')).toHaveLength(2); - - await userEvent.click(screen.getByTestId('delete-label-1')); - - expect(screen.getAllByTestId('alertlabel-key-picker')).toHaveLength(1); - expect(screen.getAllByTestId('alertlabel-value-picker')).toHaveLength(1); - }); - - it('Should add new key-value dropdowns', async () => { - renderAlertLabels('grafana'); - - await waitFor(() => expect(screen.getByText('Add label')).toBeVisible()); - await userEvent.click(screen.getByText('Add label')); - - expect(screen.getAllByTestId('alertlabel-key-picker')).toHaveLength(3); - - expect(screen.getByTestId('label-key-0').textContent).toBe('key1'); - expect(screen.getByTestId('label-key-1').textContent).toBe('key2'); - expect(screen.getByTestId('label-key-2').textContent).toBe('Choose key'); - - expect(screen.getAllByTestId('alertlabel-value-picker')).toHaveLength(3); - - expect(screen.getByTestId('label-value-0').textContent).toBe('value1'); - expect(screen.getByTestId('label-value-1').textContent).toBe('value2'); - expect(screen.getByTestId('label-value-2').textContent).toBe('Choose value'); - }); - - it('Should be able to write new keys and values using the dropdowns', async () => { - renderAlertLabels('grafana'); - - await waitFor(() => expect(screen.getByText('Add label')).toBeVisible()); - await userEvent.click(screen.getByText('Add label')); - - const LastKeyDropdown = within(screen.getByTestId('label-key-2')); - const LastValueDropdown = within(screen.getByTestId('label-value-2')); - - await userEvent.type(LastKeyDropdown.getByRole('combobox'), 'key3{enter}'); - await userEvent.type(LastValueDropdown.getByRole('combobox'), 'value3{enter}'); - - expect(screen.getByTestId('label-key-2').textContent).toBe('key3'); - expect(screen.getByTestId('label-value-2').textContent).toBe('value3'); - }); - it('Should be able to write new keys and values using the dropdowns, case sensitive', async () => { - renderAlertLabels('grafana'); - - await waitFor(() => expect(screen.getAllByTestId('alertlabel-key-picker')).toHaveLength(2)); - expect(screen.getByTestId('label-key-0').textContent).toBe('key1'); - expect(screen.getByTestId('label-key-1').textContent).toBe('key2'); - expect(screen.getByTestId('label-value-0').textContent).toBe('value1'); - expect(screen.getByTestId('label-value-1').textContent).toBe('value2'); - - const LastKeyDropdown = within(screen.getByTestId('label-key-1')); - const LastValueDropdown = within(screen.getByTestId('label-value-1')); - - await userEvent.type(LastKeyDropdown.getByRole('combobox'), 'KEY2{enter}'); - expect(screen.getByTestId('label-key-0').textContent).toBe('key1'); - expect(screen.getByTestId('label-key-1').textContent).toBe('KEY2'); - - await userEvent.type(LastValueDropdown.getByRole('combobox'), 'VALUE2{enter}'); - expect(screen.getByTestId('label-value-0').textContent).toBe('value1'); - expect(screen.getByTestId('label-value-1').textContent).toBe('VALUE2'); - }); -}); - -describe('LabelsField without suggestions', () => { - it('Should display two inputs without label suggestions', async () => { - renderAlertLabels(); - - await waitFor(() => expect(screen.getAllByTestId('alertlabel-input-wrapper')).toHaveLength(2)); - expect(screen.queryAllByTestId('alertlabel-key-picker')).toHaveLength(0); - - expect(screen.getByTestId('label-key-0')).toHaveValue('key1'); - expect(screen.getByTestId('label-key-1')).toHaveValue('key2'); - - expect(screen.getByTestId('label-value-0')).toHaveValue('value1'); - expect(screen.getByTestId('label-value-1')).toHaveValue('value2'); - }); -}); diff --git a/public/app/features/alerting/unified/components/rule-editor/LabelsField.tsx b/public/app/features/alerting/unified/components/rule-editor/LabelsField.tsx deleted file mode 100644 index 69f14d36315..00000000000 --- a/public/app/features/alerting/unified/components/rule-editor/LabelsField.tsx +++ /dev/null @@ -1,339 +0,0 @@ -import { css, cx } from '@emotion/css'; -import React, { FC, useCallback, useEffect, useMemo, useState } from 'react'; -import { useFieldArray, UseFieldArrayAppend, useFormContext, Controller } from 'react-hook-form'; - -import { GrafanaTheme2, SelectableValue } from '@grafana/data'; -import { Button, Field, InlineLabel, Input, LoadingPlaceholder, Stack, Text, useStyles2 } from '@grafana/ui'; -import { useDispatch } from 'app/types'; - -import { useUnifiedAlertingSelector } from '../../hooks/useUnifiedAlertingSelector'; -import { fetchRulerRulesIfNotFetchedYet } from '../../state/actions'; -import { RuleFormValues } from '../../types/rule-form'; -import AlertLabelDropdown from '../AlertLabelDropdown'; - -import { NeedHelpInfo } from './NeedHelpInfo'; - -interface Props { - className?: string; - dataSourceName?: string | null; -} - -const useGetCustomLabels = (dataSourceName: string): { loading: boolean; labelsByKey: Record> } => { - const dispatch = useDispatch(); - - useEffect(() => { - dispatch(fetchRulerRulesIfNotFetchedYet(dataSourceName)); - }, [dispatch, dataSourceName]); - - const rulerRuleRequests = useUnifiedAlertingSelector((state) => state.rulerRules); - const rulerRequest = rulerRuleRequests[dataSourceName]; - - const labelsByKeyResult = useMemo>>(() => { - const labelsByKey: Record> = {}; - - const rulerRulesConfig = rulerRequest?.result; - if (!rulerRulesConfig) { - return labelsByKey; - } - - const allRules = Object.values(rulerRulesConfig) - .flatMap((groups) => groups) - .flatMap((group) => group.rules); - - allRules.forEach((rule) => { - if (rule.labels) { - Object.entries(rule.labels).forEach(([key, value]) => { - if (!value) { - return; - } - - const labelEntry = labelsByKey[key]; - if (labelEntry) { - labelEntry.add(value); - } else { - labelsByKey[key] = new Set([value]); - } - }); - } - }); - - return labelsByKey; - }, [rulerRequest]); - - return { loading: rulerRequest?.loading, labelsByKey: labelsByKeyResult }; -}; - -function mapLabelsToOptions(items: Iterable = []): Array> { - return Array.from(items, (item) => ({ label: item, value: item })); -} - -const RemoveButton: FC<{ - remove: (index?: number | number[] | undefined) => void; - className: string; - index: number; -}> = ({ remove, className, index }) => ( - -); - -const LabelsWithSuggestions: FC<{ dataSourceName: string }> = ({ dataSourceName }) => { - const styles = useStyles2(getStyles); - const { - control, - watch, - formState: { errors }, - } = useFormContext(); - - const labels = watch('labels'); - const { fields, remove, append } = useFieldArray({ control, name: 'labels' }); - - const { loading, labelsByKey } = useGetCustomLabels(dataSourceName); - - const [selectedKey, setSelectedKey] = useState(''); - - const keys = useMemo(() => { - return mapLabelsToOptions(Object.keys(labelsByKey)); - }, [labelsByKey]); - - const getValuesForLabel = useCallback( - (key: string) => { - return mapLabelsToOptions(labelsByKey[key]); - }, - [labelsByKey] - ); - - const values = useMemo(() => { - return getValuesForLabel(selectedKey); - }, [selectedKey, getValuesForLabel]); - - return ( - <> - {loading && } - {!loading && ( - - {fields.map((field, index) => { - return ( -
-
- - { - return ( - { - onChange(newValue.value); - setSelectedKey(newValue.value); - }} - type="key" - /> - ); - }} - /> - - = - - { - return ( - { - onChange(newValue.value); - }} - onOpenMenu={() => { - setSelectedKey(labels[index].key); - }} - type="value" - /> - ); - }} - /> - - - -
-
- ); - })} - -
- )} - - ); -}; - -const LabelsWithoutSuggestions: FC = () => { - const styles = useStyles2(getStyles); - const { - register, - control, - watch, - formState: { errors }, - } = useFormContext(); - - const labels = watch('labels'); - const { fields, remove, append } = useFieldArray({ control, name: 'labels' }); - - return ( - <> - {fields.map((field, index) => { - return ( -
-
- - - - = - - - - -
-
- ); - })} - - - ); -}; - -const LabelsField: FC = ({ dataSourceName }) => { - const styles = useStyles2(getStyles); - - return ( -
- - Labels - - - Add labels to your rule for searching, silencing, or routing to a notification policy. - - - - -
- {dataSourceName ? : } -
- ); -}; - -const getStyles = (theme: GrafanaTheme2) => { - return { - icon: css({ - marginRight: theme.spacing(0.5), - }), - flexColumn: css({ - display: 'flex', - flexDirection: 'column', - }), - flexRow: css({ - display: 'flex', - flexDirection: 'row', - justifyContent: 'flex-start', - '& + button': { - marginLeft: theme.spacing(0.5), - }, - }), - deleteLabelButton: css({ - marginLeft: theme.spacing(0.5), - alignSelf: 'flex-start', - }), - addLabelButton: css({ - flexGrow: 0, - alignSelf: 'flex-start', - }), - centerAlignRow: css({ - alignItems: 'baseline', - }), - equalSign: css({ - alignSelf: 'flex-start', - width: '28px', - justifyContent: 'center', - marginLeft: theme.spacing(0.5), - }), - labelInput: css({ - width: '175px', - marginBottom: `-${theme.spacing(1)}`, - '& + &': { - marginLeft: theme.spacing(1), - }, - }), - labelsContainer: css({ - marginBottom: theme.spacing(3), - }), - }; -}; - -export default LabelsField; diff --git a/public/app/features/alerting/unified/components/rule-editor/NotificationsStep.tsx b/public/app/features/alerting/unified/components/rule-editor/NotificationsStep.tsx index d4c9a8af1ee..2fbfdcc7f2d 100644 --- a/public/app/features/alerting/unified/components/rule-editor/NotificationsStep.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/NotificationsStep.tsx @@ -1,5 +1,5 @@ import { css } from '@emotion/css'; -import React from 'react'; +import React, { useState } from 'react'; import { useFormContext } from 'react-hook-form'; import { GrafanaTheme2 } from '@grafana/data'; @@ -9,10 +9,11 @@ import { Icon, RadioButtonGroup, Stack, Text, useStyles2 } from '@grafana/ui'; import { RuleFormType, RuleFormValues } from '../../types/rule-form'; import { GRAFANA_RULES_SOURCE_NAME } from '../../utils/datasource'; -import LabelsField from './LabelsField'; import { NeedHelpInfo } from './NeedHelpInfo'; import { RuleEditorSection } from './RuleEditorSection'; import { SimplifiedRouting } from './alert-rule-form/simplifiedRouting/SimplifiedRouting'; +import { LabelsEditorModal } from './labels/LabelsEditorModal'; +import { LabelsFieldInForm } from './labels/LabelsFieldInForm'; import { NotificationPreview } from './notificaton-preview/NotificationPreview'; type NotificationsStepProps = { @@ -25,16 +26,29 @@ enum RoutingOptions { } export const NotificationsStep = ({ alertUid }: NotificationsStepProps) => { - const { watch } = useFormContext(); + const { watch, getValues, setValue } = useFormContext(); const styles = useStyles2(getStyles); const [type] = watch(['type', 'labels', 'queries', 'condition', 'folder', 'name', 'manualRouting']); + const [showLabelsEditor, setShowLabelsEditor] = useState(false); const dataSourceName = watch('dataSourceName') ?? GRAFANA_RULES_SOURCE_NAME; const simplifiedRoutingToggleEnabled = config.featureToggles.alertingSimplifiedRouting ?? false; const shouldRenderpreview = type === RuleFormType.grafana; const shouldAllowSimplifiedRouting = type === RuleFormType.grafana && simplifiedRoutingToggleEnabled; + function onCloseLabelsEditor( + labelsToUpdate?: Array<{ + key: string; + value: string; + }> + ) { + if (labelsToUpdate) { + setValue('labels', labelsToUpdate); + } + setShowLabelsEditor(false); + } + return ( { } fullWidth > - + setShowLabelsEditor(true)} /> + {shouldAllowSimplifiedRouting && (
Notifications diff --git a/public/app/features/alerting/unified/components/rule-editor/PreviewRule.tsx b/public/app/features/alerting/unified/components/rule-editor/PreviewRule.tsx index dcb979db88c..aef426bc1e6 100644 --- a/public/app/features/alerting/unified/components/rule-editor/PreviewRule.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/PreviewRule.tsx @@ -6,7 +6,7 @@ import { takeWhile } from 'rxjs/operators'; import { dateTimeFormatISO, GrafanaTheme2, LoadingState } from '@grafana/data'; import { getDataSourceSrv } from '@grafana/runtime'; -import { Alert, Button, HorizontalGroup, useStyles2 } from '@grafana/ui'; +import { Alert, Button, Stack, useStyles2 } from '@grafana/ui'; import { previewAlertRule } from '../../api/preview'; import { useAlertQueriesStatus } from '../../hooks/useAlertQueriesStatus'; @@ -32,7 +32,7 @@ export function PreviewRule(): React.ReactElement | null { return (
- + {allDataSourcesAvailable && (
); diff --git a/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/AlertRuleForm.tsx b/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/AlertRuleForm.tsx index 6c1a1434f02..1753714d312 100644 --- a/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/AlertRuleForm.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/AlertRuleForm.tsx @@ -5,7 +5,7 @@ import { Link, useParams } from 'react-router-dom'; import { GrafanaTheme2 } from '@grafana/data'; import { config } from '@grafana/runtime'; -import { Button, ConfirmModal, CustomScrollbar, HorizontalGroup, Spinner, Stack, useStyles2 } from '@grafana/ui'; +import { Button, ConfirmModal, CustomScrollbar, Spinner, Stack, useStyles2 } from '@grafana/ui'; import { AppChromeUpdate } from 'app/core/components/AppChrome/AppChromeUpdate'; import { useAppNotification } from 'app/core/copy/appNotification'; import { contextSrv } from 'app/core/core'; @@ -184,7 +184,7 @@ export const AlertRuleForm = ({ existing, prefill }: Props) => { useEffect(() => setEvaluateEvery(evaluateEveryInForm), [evaluateEveryInForm]); const actionButtons = ( - + {existing && ( ) : null} - {existing && isCortexLokiOrRecordingRule(watch) && ( )} - + ); const isPaused = existing && isGrafanaRulerRule(existing.rule) && isGrafanaRulerRulePaused(existing.rule); diff --git a/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/simplifiedRouting/SimplifiedRuleEditor.test.tsx b/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/simplifiedRouting/SimplifiedRuleEditor.test.tsx index e449a25f8ea..6535e123522 100644 --- a/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/simplifiedRouting/SimplifiedRuleEditor.test.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/simplifiedRouting/SimplifiedRuleEditor.test.tsx @@ -263,6 +263,7 @@ describe('Can create a new grafana managed alert unsing simplified routing', () expect(mocks.api.setRulerRuleGroup).not.toHaveBeenCalled(); }); }); + it('can create new grafana managed alert when using simplified routing and selecting a contact point', async () => { const contactPointsAvailable: ContactPointWithMetadata[] = [ { @@ -279,7 +280,7 @@ describe('Can create a new grafana managed alert unsing simplified routing', () settings: {}, }, ], - numberOfPolicies: 0, + policies: [], }, ]; mocks.useContactPointsWithStatus.mockReturnValue({ diff --git a/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/simplifiedRouting/contactPoint/ContactPointSelector.tsx b/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/simplifiedRouting/contactPoint/ContactPointSelector.tsx index 7914bd495b0..8b5abe2c7c4 100644 --- a/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/simplifiedRouting/contactPoint/ContactPointSelector.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/simplifiedRouting/contactPoint/ContactPointSelector.tsx @@ -1,4 +1,4 @@ -import { css, cx } from '@emotion/css'; +import { css, cx, keyframes } from '@emotion/css'; import React, { useCallback, useEffect, useState } from 'react'; import { useFormContext, Controller } from 'react-hook-form'; @@ -149,6 +149,15 @@ function LinkToContactPoints() { ); } +const rotation = keyframes({ + from: { + transform: 'rotate(720deg)', + }, + to: { + transform: 'rotate(0deg)', + }, +}); + const getStyles = (theme: GrafanaTheme2) => ({ contactPointsSelector: css({ display: 'flex', @@ -172,14 +181,11 @@ const getStyles = (theme: GrafanaTheme2) => ({ }), loading: css({ pointerEvents: 'none', - animation: 'rotation 2s infinite linear', - '@keyframes rotation': { - from: { - transform: 'rotate(720deg)', - }, - to: { - transform: 'rotate(0deg)', - }, + [theme.transitions.handleMotion('no-preference')]: { + animation: `${rotation} 2s infinite linear`, + }, + [theme.transitions.handleMotion('reduce')]: { + animation: `${rotation} 6s infinite linear`, }, }), warn: css({ diff --git a/public/app/features/alerting/unified/components/rule-editor/labels/LabelsButtons.tsx b/public/app/features/alerting/unified/components/rule-editor/labels/LabelsButtons.tsx new file mode 100644 index 00000000000..c7f9683bec6 --- /dev/null +++ b/public/app/features/alerting/unified/components/rule-editor/labels/LabelsButtons.tsx @@ -0,0 +1,33 @@ +import React from 'react'; +import { UseFieldArrayRemove } from 'react-hook-form'; + +import { Button } from '@grafana/ui'; + +interface RemoveButtonProps { + remove: UseFieldArrayRemove; + index: number; +} +export function RemoveButton({ remove, index }: RemoveButtonProps) { + return ( + + ); +} diff --git a/public/app/features/alerting/unified/components/rule-editor/labels/LabelsEditorModal.tsx b/public/app/features/alerting/unified/components/rule-editor/labels/LabelsEditorModal.tsx new file mode 100644 index 00000000000..3b6d5675fb5 --- /dev/null +++ b/public/app/features/alerting/unified/components/rule-editor/labels/LabelsEditorModal.tsx @@ -0,0 +1,27 @@ +import React from 'react'; + +import { Modal } from '@grafana/ui'; + +import { LabelsSubForm } from './LabelsField'; + +export interface LabelsEditorModalProps { + isOpen: boolean; + initialLabels: Array<{ + key: string; + value: string; + }>; + onClose: ( + labelsToUodate?: Array<{ + key: string; + value: string; + }> + ) => void; + dataSourceName: string; +} +export function LabelsEditorModal({ isOpen, onClose, dataSourceName, initialLabels }: LabelsEditorModalProps) { + return ( + onClose()}> + + + ); +} diff --git a/public/app/features/alerting/unified/components/rule-editor/labels/LabelsField.test.tsx b/public/app/features/alerting/unified/components/rule-editor/labels/LabelsField.test.tsx new file mode 100644 index 00000000000..3f279087e5b --- /dev/null +++ b/public/app/features/alerting/unified/components/rule-editor/labels/LabelsField.test.tsx @@ -0,0 +1,176 @@ +import { render, screen, waitFor, within } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import React from 'react'; +import { FormProvider, useForm } from 'react-hook-form'; +import { TestProvider } from 'test/helpers/TestProvider'; + +import { clearPluginSettingsCache } from 'app/features/plugins/pluginSettings'; + +import { mockAlertRuleApi, mockApi, setupMswServer } from '../../../mockApi'; +import { getGrafanaRule, labelsPluginMetaMock } from '../../../mocks'; +import { GRAFANA_RULES_SOURCE_NAME } from '../../../utils/datasource'; + +import LabelsField, { LabelsWithSuggestions } from './LabelsField'; + +const labels = [ + { key: 'key1', value: 'value1' }, + { key: 'key2', value: 'value2' }, +]; + +const FormProviderWrapper = ({ children }: React.PropsWithChildren<{}>) => { + const methods = useForm({ defaultValues: { labels } }); + return {children}; +}; +const SubFormProviderWrapper = ({ children }: React.PropsWithChildren<{}>) => { + const methods = useForm({ defaultValues: { labelsInSubform: labels } }); + return {children}; +}; + +function renderAlertLabels() { + render( + + + , + { wrapper: TestProvider } + ); +} + +function renderLabelsWithSuggestions() { + render( + + + , + { wrapper: TestProvider } + ); +} + +const grafanaRule = getGrafanaRule(undefined, { + uid: 'test-rule-uid', + title: 'cpu-usage', + namespace_uid: 'folderUID1', + data: [ + { + refId: 'A', + datasourceUid: 'uid1', + queryType: 'alerting', + relativeTimeRange: { from: 1000, to: 2000 }, + model: { + refId: 'A', + expression: 'vector(1)', + queryType: 'alerting', + datasource: { uid: 'uid1', type: 'prometheus' }, + }, + }, + ], +}); +const server = setupMswServer(); +describe('LabelsField with suggestions', () => { + afterEach(() => { + server.resetHandlers(); + clearPluginSettingsCache(); + }); + beforeEach(() => { + mockApi(server).plugins.getPluginSettings({ ...labelsPluginMetaMock, enabled: false }); + mockAlertRuleApi(server).rulerRules(GRAFANA_RULES_SOURCE_NAME, { + [grafanaRule.namespace.name]: [{ name: grafanaRule.group.name, interval: '1m', rules: [grafanaRule.rulerRule!] }], + }); + }); + + it('Should display two dropdowns with the existing labels', async () => { + renderLabelsWithSuggestions(); + + await waitFor(() => expect(screen.getAllByTestId('alertlabel-key-picker')).toHaveLength(2)); + + expect(screen.getByTestId('labelsInSubform-key-0').textContent).toBe('key1'); + expect(screen.getByTestId('labelsInSubform-key-1').textContent).toBe('key2'); + + expect(screen.getAllByTestId('alertlabel-value-picker')).toHaveLength(2); + + expect(screen.getByTestId('labelsInSubform-value-0').textContent).toBe('value1'); + expect(screen.getByTestId('labelsInSubform-value-1').textContent).toBe('value2'); + }); + + it('Should delete a key-value combination', async () => { + renderLabelsWithSuggestions(); + + await waitFor(() => expect(screen.getAllByTestId('alertlabel-key-picker')).toHaveLength(2)); + + expect(screen.getAllByTestId('alertlabel-key-picker')).toHaveLength(2); + expect(screen.getAllByTestId('alertlabel-value-picker')).toHaveLength(2); + + await userEvent.click(screen.getByTestId('delete-label-1')); + + expect(screen.getAllByTestId('alertlabel-key-picker')).toHaveLength(1); + expect(screen.getAllByTestId('alertlabel-value-picker')).toHaveLength(1); + }); + + it('Should add new key-value dropdowns', async () => { + renderLabelsWithSuggestions(); + + await waitFor(() => expect(screen.getByText('Add more')).toBeVisible()); + await userEvent.click(screen.getByText('Add more')); + + expect(screen.getAllByTestId('alertlabel-key-picker')).toHaveLength(3); + + expect(screen.getByTestId('labelsInSubform-key-0').textContent).toBe('key1'); + expect(screen.getByTestId('labelsInSubform-key-1').textContent).toBe('key2'); + expect(screen.getByTestId('labelsInSubform-key-2').textContent).toBe('Choose key'); + + expect(screen.getAllByTestId('alertlabel-value-picker')).toHaveLength(3); + + expect(screen.getByTestId('labelsInSubform-value-0').textContent).toBe('value1'); + expect(screen.getByTestId('labelsInSubform-value-1').textContent).toBe('value2'); + expect(screen.getByTestId('labelsInSubform-value-2').textContent).toBe('Choose value'); + }); + + it('Should be able to write new keys and values using the dropdowns', async () => { + renderLabelsWithSuggestions(); + + await waitFor(() => expect(screen.getByText('Add more')).toBeVisible()); + await userEvent.click(screen.getByText('Add more')); + + const LastKeyDropdown = within(screen.getByTestId('labelsInSubform-key-2')); + const LastValueDropdown = within(screen.getByTestId('labelsInSubform-value-2')); + + await userEvent.type(LastKeyDropdown.getByRole('combobox'), 'key3{enter}'); + await userEvent.type(LastValueDropdown.getByRole('combobox'), 'value3{enter}'); + + expect(screen.getByTestId('labelsInSubform-key-2').textContent).toBe('key3'); + expect(screen.getByTestId('labelsInSubform-value-2').textContent).toBe('value3'); + }); + it('Should be able to write new keys and values using the dropdowns, case sensitive', async () => { + renderLabelsWithSuggestions(); + + await waitFor(() => expect(screen.getAllByTestId('alertlabel-key-picker')).toHaveLength(2)); + expect(screen.getByTestId('labelsInSubform-key-0').textContent).toBe('key1'); + expect(screen.getByTestId('labelsInSubform-key-1').textContent).toBe('key2'); + expect(screen.getByTestId('labelsInSubform-value-0').textContent).toBe('value1'); + expect(screen.getByTestId('labelsInSubform-value-1').textContent).toBe('value2'); + + const LastKeyDropdown = within(screen.getByTestId('labelsInSubform-key-1')); + const LastValueDropdown = within(screen.getByTestId('labelsInSubform-value-1')); + + await userEvent.type(LastKeyDropdown.getByRole('combobox'), 'KEY2{enter}'); + expect(screen.getByTestId('labelsInSubform-key-0').textContent).toBe('key1'); + expect(screen.getByTestId('labelsInSubform-key-1').textContent).toBe('KEY2'); + + await userEvent.type(LastValueDropdown.getByRole('combobox'), 'VALUE2{enter}'); + expect(screen.getByTestId('labelsInSubform-value-0').textContent).toBe('value1'); + expect(screen.getByTestId('labelsInSubform-value-1').textContent).toBe('VALUE2'); + }); +}); + +describe('LabelsField without suggestions', () => { + it('Should display two inputs without label suggestions', async () => { + renderAlertLabels(); + + await waitFor(() => expect(screen.getAllByTestId('alertlabel-input-wrapper')).toHaveLength(2)); + expect(screen.queryAllByTestId('alertlabel-key-picker')).toHaveLength(0); + + expect(screen.getByTestId('label-key-0')).toHaveValue('key1'); + expect(screen.getByTestId('label-key-1')).toHaveValue('key2'); + + expect(screen.getByTestId('label-value-0')).toHaveValue('value1'); + expect(screen.getByTestId('label-value-1')).toHaveValue('value2'); + }); +}); diff --git a/public/app/features/alerting/unified/components/rule-editor/labels/LabelsField.tsx b/public/app/features/alerting/unified/components/rule-editor/labels/LabelsField.tsx new file mode 100644 index 00000000000..3573f37ef6c --- /dev/null +++ b/public/app/features/alerting/unified/components/rule-editor/labels/LabelsField.tsx @@ -0,0 +1,499 @@ +import { css, cx } from '@emotion/css'; +import React, { FC, useCallback, useEffect, useMemo, useState } from 'react'; +import { Controller, FormProvider, useFieldArray, useForm, useFormContext } from 'react-hook-form'; + +import { GrafanaTheme2, SelectableValue } from '@grafana/data'; +import { Button, Field, InlineLabel, Input, LoadingPlaceholder, Space, Stack, Text, useStyles2 } from '@grafana/ui'; +import { useDispatch } from 'app/types'; + +import { labelsApi } from '../../../api/labelsApi'; +import { usePluginBridge } from '../../../hooks/usePluginBridge'; +import { useUnifiedAlertingSelector } from '../../../hooks/useUnifiedAlertingSelector'; +import { fetchRulerRulesIfNotFetchedYet } from '../../../state/actions'; +import { SupportedPlugin } from '../../../types/pluginBridges'; +import { RuleFormValues } from '../../../types/rule-form'; +import { isPrivateLabelKey } from '../../../utils/labels'; +import AlertLabelDropdown from '../../AlertLabelDropdown'; +import { AlertLabels } from '../../AlertLabels'; +import { NeedHelpInfo } from '../NeedHelpInfo'; + +import { AddButton, RemoveButton } from './LabelsButtons'; + +const useGetOpsLabelsKeys = (skip: boolean) => { + const { currentData, isLoading: isloadingLabels } = labelsApi.endpoints.getLabels.useQuery(undefined, { + skip, + }); + return { loading: isloadingLabels, labelsOpsKeys: currentData }; +}; +const useGetAlertRulesLabels = ( + dataSourceName: string +): { loading: boolean; labelsByKey: Record> } => { + const dispatch = useDispatch(); + + useEffect(() => { + dispatch(fetchRulerRulesIfNotFetchedYet(dataSourceName)); + }, [dispatch, dataSourceName]); + + const rulerRuleRequests = useUnifiedAlertingSelector((state) => state.rulerRules); + const rulerRequest = rulerRuleRequests[dataSourceName]; + + const labelsByKeyResult = useMemo>>(() => { + const labelsByKey: Record> = {}; + + const rulerRulesConfig = rulerRequest?.result; + if (!rulerRulesConfig) { + return labelsByKey; + } + + const allRules = Object.values(rulerRulesConfig) + .flatMap((groups) => groups) + .flatMap((group) => group.rules); + + allRules.forEach((rule) => { + if (rule.labels) { + Object.entries(rule.labels).forEach(([key, value]) => { + if (!value) { + return; + } + + const labelEntry = labelsByKey[key]; + if (labelEntry) { + labelEntry.add(value); + } else { + labelsByKey[key] = new Set([value]); + } + }); + } + }); + + return labelsByKey; + }, [rulerRequest]); + + return { loading: rulerRequest?.loading, labelsByKey: labelsByKeyResult }; +}; + +function mapLabelsToOptions( + items: Iterable = [], + labelsInSubForm?: Array<{ key: string; value: string }> +): Array> { + const existingKeys = new Set(labelsInSubForm ? labelsInSubForm.map((label) => label.key) : []); + return Array.from(items, (item) => ({ label: item, value: item, isDisabled: existingKeys.has(item) })); +} + +export interface LabelsInRuleProps { + labels: Array<{ key: string; value: string }>; +} + +export const LabelsInRule = ({ labels }: LabelsInRuleProps) => { + const labelsObj: Record = labels.reduce((acc: Record, label) => { + if (label.key) { + acc[label.key] = label.value; + } + return acc; + }, {}); + + return ; +}; + +export type LabelsSubformValues = { + labelsInSubform: Array<{ key: string; value: string }>; +}; + +export interface LabelsSubFormProps { + dataSourceName: string; + initialLabels: Array<{ key: string; value: string }>; + onClose: ( + labelsToUodate?: Array<{ + key: string; + value: string; + }> + ) => void; +} + +export function LabelsSubForm({ dataSourceName, onClose, initialLabels }: LabelsSubFormProps) { + const styles = useStyles2(getStyles); + + const onSave = (labels: LabelsSubformValues) => { + onClose(labels.labelsInSubform); + }; + const onCancel = () => { + onClose(); + }; + // default values for the subform are the initial labels + const defaultValues: LabelsSubformValues = useMemo(() => { + return { labelsInSubform: initialLabels }; + }, [initialLabels]); + + const formAPI = useForm({ defaultValues }); + return ( + +
+ + Add labels to your rule for searching, silencing, or routing to a notification policy. + + + + + +
+ + +
+
+
+
+
+ ); +} + +const isKeyAllowed = (labelKey: string) => !isPrivateLabelKey(labelKey); + +export function useCombinedLabels( + dataSourceName: string, + labelsPluginInstalled: boolean, + loadingLabelsPlugin: boolean, + labelsInSubform: Array<{ key: string; value: string }>, + selectedKey: string +) { + // ------- Get labels keys and their values from existing alerts + const { loading, labelsByKey: labelsByKeyFromExisingAlerts } = useGetAlertRulesLabels(dataSourceName); + // ------- Get only the keys from the ops labels, as we will fetch the values for the keys once the key is selected. + const { loading: isLoadingLabels, labelsOpsKeys = [] } = useGetOpsLabelsKeys( + !labelsPluginInstalled || loadingLabelsPlugin + ); + //------ Convert the labelsOpsKeys to the same format as the labelsByKeyFromExisingAlerts + const labelsByKeyOps = useMemo(() => { + return labelsOpsKeys.reduce((acc: Record>, label) => { + acc[label.name] = new Set(); + return acc; + }, {}); + }, [labelsOpsKeys]); + + //------- Convert the keys from the ops labels to options for the dropdown + const keysFromGopsLabels = useMemo(() => { + return mapLabelsToOptions(Object.keys(labelsByKeyOps).filter(isKeyAllowed), labelsInSubform); + }, [labelsByKeyOps, labelsInSubform]); + + //------- Convert the keys from the existing alerts to options for the dropdown + const keysFromExistingAlerts = useMemo(() => { + return mapLabelsToOptions(Object.keys(labelsByKeyFromExisingAlerts).filter(isKeyAllowed), labelsInSubform); + }, [labelsByKeyFromExisingAlerts, labelsInSubform]); + + // create two groups of labels, one for ops and one for custom + const groupedOptions = [ + { + label: 'From alerts', + options: keysFromExistingAlerts, + expanded: true, + }, + { + label: 'From system', + options: keysFromGopsLabels, + expanded: true, + }, + ]; + + const selectedKeyIsFromAlerts = + labelsByKeyFromExisingAlerts[selectedKey] !== undefined && labelsByKeyFromExisingAlerts[selectedKey]?.size > 0; + const selectedKeyIsFromOps = labelsByKeyOps[selectedKey] !== undefined && labelsByKeyOps[selectedKey]?.size > 0; + const selectedKeyDoesNotExist = !selectedKeyIsFromAlerts && !selectedKeyIsFromOps; + + const valuesAlreadyFetched = !selectedKeyIsFromAlerts && labelsByKeyOps[selectedKey]?.size > 0; + + // Only fetch the values for the selected key if it is from ops and the values are not already fetched (the selected key is not in the labelsByKeyOps object) + const { + currentData: valuesData, + isLoading: isLoadingValues = false, + error, + } = labelsApi.endpoints.getLabelValues.useQuery( + { key: selectedKey }, + { + skip: + !labelsPluginInstalled || + !selectedKey || + selectedKeyIsFromAlerts || + valuesAlreadyFetched || + selectedKeyDoesNotExist, + } + ); + + // these are the values for the selected key in case it is from ops + const valuesFromSelectedGopsKey = useMemo(() => { + // if it is from alerts, we need to fetch the values from the existing alerts + if (selectedKeyIsFromAlerts) { + return []; + } + // in case of a label from ops, we need to fetch the values from the plugin + // fetch values from ops only if there is no value for the key + const valuesForSelectedKey = labelsByKeyOps[selectedKey]; + const valuesAlreadyFetched = valuesForSelectedKey?.size > 0; + if (valuesAlreadyFetched) { + return mapLabelsToOptions(valuesForSelectedKey); + } + if (!isLoadingValues && valuesData?.values?.length && !error) { + const values = valuesData?.values.map((value) => value.name); + labelsByKeyOps[selectedKey] = new Set(values); + return mapLabelsToOptions(values); + } + return []; + }, [selectedKeyIsFromAlerts, labelsByKeyOps, selectedKey, isLoadingValues, valuesData, error]); + + const getValuesForLabel = useCallback( + (key: string) => { + if (!isKeyAllowed(key)) { + return []; + } + + // values from existing alerts will take precedence over values from ops + if (selectedKeyIsFromAlerts || !labelsPluginInstalled) { + return mapLabelsToOptions(labelsByKeyFromExisingAlerts[key]); + } + return valuesFromSelectedGopsKey; + }, + [labelsByKeyFromExisingAlerts, labelsPluginInstalled, valuesFromSelectedGopsKey, selectedKeyIsFromAlerts] + ); + + return { + loading: loading || isLoadingLabels, + keysFromExistingAlerts, + groupedOptions, + getValuesForLabel, + }; +} + +/* + We will suggest labels from two sources: existing alerts and ops labels. + We only will suggest labels from ops if the grafana-labels-app plugin is installed + This component is only used by the alert rule form. + */ +export interface LabelsWithSuggestionsProps { + dataSourceName: string; +} + +export function LabelsWithSuggestions({ dataSourceName }: LabelsWithSuggestionsProps) { + const styles = useStyles2(getStyles); + const { + control, + watch, + formState: { errors }, + } = useFormContext(); + + const labelsInSubform = watch('labelsInSubform'); + const { fields, remove, append } = useFieldArray({ control, name: 'labelsInSubform' }); + const appendLabel = useCallback(() => { + append({ key: '', value: '' }); + }, [append]); + + // check if the labels plugin is installed + const { installed: labelsPluginInstalled = false, loading: loadingLabelsPlugin } = usePluginBridge( + SupportedPlugin.Labels + ); + const [selectedKey, setSelectedKey] = useState(''); + + const { loading, keysFromExistingAlerts, groupedOptions, getValuesForLabel } = useCombinedLabels( + dataSourceName, + labelsPluginInstalled, + loadingLabelsPlugin, + labelsInSubform, + selectedKey + ); + + const values = useMemo(() => { + return getValuesForLabel(selectedKey); + }, [selectedKey, getValuesForLabel]); + + const isLoading = loading || loadingLabelsPlugin; + + return ( + <> + {isLoading && } + {!isLoading && ( + + {fields.map((field, index) => { + return ( +
+ + { + return ( + { + onChange(newValue.value); + setSelectedKey(newValue.value); + }} + type="key" + /> + ); + }} + /> + + = + + { + return ( + { + onChange(newValue.value); + }} + onOpenMenu={() => { + setSelectedKey(labelsInSubform[index].key); + }} + type="value" + /> + ); + }} + /> + + + +
+ ); + })} + +
+ )} + + ); +} + +export const LabelsWithoutSuggestions: FC = () => { + const styles = useStyles2(getStyles); + const { + register, + control, + watch, + formState: { errors }, + } = useFormContext(); + + const labels = watch('labels'); + const { fields, remove, append } = useFieldArray({ control, name: 'labels' }); + const appendLabel = useCallback(() => { + append({ key: '', value: '' }); + }, [append]); + + return ( + <> + {fields.map((field, index) => { + return ( +
+
+ + + + = + + + + +
+
+ ); + })} + + + ); +}; + +function LabelsField() { + return ( +
+ + Labels + + + Add labels to your rule for searching, silencing, or routing to a notification policy. + + + + + +
+ ); +} + +const getStyles = (theme: GrafanaTheme2) => { + return { + flexColumn: css({ + display: 'flex', + flexDirection: 'column', + }), + flexRow: css({ + display: 'flex', + flexDirection: 'row', + justifyContent: 'flex-start', + }), + centerAlignRow: css({ + alignItems: 'center', + gap: theme.spacing(0.5), + }), + equalSign: css({ + alignSelf: 'flex-start', + width: '28px', + justifyContent: 'center', + margin: 0, + }), + labelInput: css({ + width: '175px', + margin: 0, + }), + confirmButton: css({ + display: 'flex', + flexDirection: 'row', + gap: theme.spacing(1), + marginLeft: 'auto', + }), + }; +}; + +export default LabelsField; diff --git a/public/app/features/alerting/unified/components/rule-editor/labels/LabelsFieldInForm.tsx b/public/app/features/alerting/unified/components/rule-editor/labels/LabelsFieldInForm.tsx new file mode 100644 index 00000000000..14d7319b088 --- /dev/null +++ b/public/app/features/alerting/unified/components/rule-editor/labels/LabelsFieldInForm.tsx @@ -0,0 +1,51 @@ +import React from 'react'; +import { useFormContext } from 'react-hook-form'; + +import { Button, Stack, Text } from '@grafana/ui'; + +import { RuleFormValues } from '../../../types/rule-form'; +import { NeedHelpInfo } from '../NeedHelpInfo'; + +import { LabelsInRule } from './LabelsField'; + +interface LabelsFieldInFormProps { + onEditClick: () => void; +} +export function LabelsFieldInForm({ onEditClick }: LabelsFieldInFormProps) { + const { watch } = useFormContext(); + const labels = watch('labels'); + const hasLabels = Object.keys(labels).length > 0 && labels.some((label) => label.key || label.value); + + return ( + + + Labels + + + Add labels to your rule for searching, silencing, or routing to a notification policy. + + + + + + + {hasLabels ? ( + + ) : ( + + No labels selected + + + )} + + + ); +} diff --git a/public/app/features/alerting/unified/components/rule-editor/query-and-alert-condition/QueryAndExpressionsStep.tsx b/public/app/features/alerting/unified/components/rule-editor/query-and-alert-condition/QueryAndExpressionsStep.tsx index 9a40a1a3d55..ef70b82adc0 100644 --- a/public/app/features/alerting/unified/components/rule-editor/query-and-alert-condition/QueryAndExpressionsStep.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/query-and-alert-condition/QueryAndExpressionsStep.tsx @@ -497,7 +497,13 @@ export const QueryAndExpressionsStep = ({ editingExistingRule, onDataChange }: P )} {!isPreviewLoading && ( - )} diff --git a/public/app/features/alerting/unified/components/rule-viewer/Actions.tsx b/public/app/features/alerting/unified/components/rule-viewer/Actions.tsx deleted file mode 100644 index ee57acb14b2..00000000000 --- a/public/app/features/alerting/unified/components/rule-viewer/Actions.tsx +++ /dev/null @@ -1,115 +0,0 @@ -import React from 'react'; - -import { AppEvents } from '@grafana/data'; -import { Dropdown, LinkButton, Menu } from '@grafana/ui'; -import appEvents from 'app/core/app_events'; -import MenuItemPauseRule from 'app/features/alerting/unified/components/MenuItemPauseRule'; -import { CombinedRule, RuleIdentifier } from 'app/types/unified-alerting'; - -import { AlertRuleAction, useAlertRuleAbility } from '../../hooks/useAbilities'; -import { createShareLink, isLocalDevEnv, isOpenSourceEdition, makeRuleBasedSilenceLink } from '../../utils/misc'; -import * as ruleId from '../../utils/rule-id'; -import { createUrl } from '../../utils/url'; -import MoreButton from '../MoreButton'; -import { DeclareIncidentMenuItem } from '../bridges/DeclareIncidentButton'; - -import { useAlertRule } from './RuleContext'; - -interface Props { - handleDelete: (rule: CombinedRule) => void; - handleDuplicateRule: (identifier: RuleIdentifier) => void; -} - -export const useAlertRulePageActions = ({ handleDelete, handleDuplicateRule }: Props) => { - const { rule, identifier } = useAlertRule(); - - // check all abilities and permissions - const [editSupported, editAllowed] = useAlertRuleAbility(rule, AlertRuleAction.Update); - const canEdit = editSupported && editAllowed; - - const [deleteSupported, deleteAllowed] = useAlertRuleAbility(rule, AlertRuleAction.Delete); - const canDelete = deleteSupported && deleteAllowed; - - const [duplicateSupported, duplicateAllowed] = useAlertRuleAbility(rule, AlertRuleAction.Duplicate); - const canDuplicate = duplicateSupported && duplicateAllowed; - - const [silenceSupported, silenceAllowed] = useAlertRuleAbility(rule, AlertRuleAction.Silence); - const canSilence = silenceSupported && silenceAllowed; - - const [exportSupported, exportAllowed] = useAlertRuleAbility(rule, AlertRuleAction.ModifyExport); - const canExport = exportSupported && exportAllowed; - - /** - * Since Incident isn't available as an open-source product we shouldn't show it for Open-Source licenced editions of Grafana. - * We should show it in development mode - */ - const shouldShowDeclareIncidentButton = !isOpenSourceEdition() || isLocalDevEnv(); - const shareUrl = createShareLink(rule.namespace.rulesSource, rule); - - return [ - canEdit && , - - {canEdit && } - {canSilence && ( - - )} - {shouldShowDeclareIncidentButton && } - {canDuplicate && handleDuplicateRule(identifier)} />} - - copyToClipboard(shareUrl)} /> - {canExport && ( - ]} - /> - )} - {canDelete && ( - <> - - handleDelete(rule)} /> - - )} - - } - > - - , - ]; -}; - -function copyToClipboard(text: string) { - navigator.clipboard?.writeText(text).then(() => { - appEvents.emit(AppEvents.alertSuccess, ['URL copied to clipboard']); - }); -} - -type PropsWithIdentifier = { identifier: RuleIdentifier }; - -const ExportMenuItem = ({ identifier }: PropsWithIdentifier) => { - const returnTo = location.pathname + location.search; - const url = createUrl(`/alerting/${encodeURIComponent(ruleId.stringifyIdentifier(identifier))}/modify-export`, { - returnTo, - }); - - return ; -}; - -const EditButton = ({ identifier }: PropsWithIdentifier) => { - const returnTo = location.pathname + location.search; - const ruleIdentifier = ruleId.stringifyIdentifier(identifier); - const editURL = createUrl(`/alerting/${encodeURIComponent(ruleIdentifier)}/edit`, { returnTo }); - - return ( - - Edit - - ); -}; diff --git a/public/app/features/alerting/unified/components/rule-viewer/AlertRuleMenu.tsx b/public/app/features/alerting/unified/components/rule-viewer/AlertRuleMenu.tsx new file mode 100644 index 00000000000..b08c731d6f9 --- /dev/null +++ b/public/app/features/alerting/unified/components/rule-viewer/AlertRuleMenu.tsx @@ -0,0 +1,139 @@ +import React from 'react'; + +import { AppEvents } from '@grafana/data'; +import { ComponentSize, Dropdown, Menu } from '@grafana/ui'; +import appEvents from 'app/core/app_events'; +import MenuItemPauseRule from 'app/features/alerting/unified/components/MenuItemPauseRule'; +import MoreButton from 'app/features/alerting/unified/components/MoreButton'; +import { useRulePluginLinkExtension } from 'app/features/alerting/unified/plugins/useRulePluginLinkExtensions'; +import { isAlertingRule } from 'app/features/alerting/unified/utils/rules'; +import { CombinedRule, RuleIdentifier } from 'app/types/unified-alerting'; +import { PromAlertingRuleState } from 'app/types/unified-alerting-dto'; + +import { AlertRuleAction, useAlertRuleAbility } from '../../hooks/useAbilities'; +import { createShareLink, isLocalDevEnv, isOpenSourceEdition, makeRuleBasedSilenceLink } from '../../utils/misc'; +import * as ruleId from '../../utils/rule-id'; +import { createUrl } from '../../utils/url'; +import { DeclareIncidentMenuItem } from '../bridges/DeclareIncidentButton'; + +interface Props { + rule: CombinedRule; + identifier: RuleIdentifier; + showCopyLinkButton?: boolean; + handleDelete: (rule: CombinedRule) => void; + handleDuplicateRule: (identifier: RuleIdentifier) => void; + onPauseChange?: () => void; + buttonSize?: ComponentSize; + hideLabels?: boolean; +} + +/** + * Get a list of menu items + divider elements for rendering in an alert rule's + * dropdown menu + */ +const AlertRuleMenu = ({ + rule, + identifier, + showCopyLinkButton, + handleDelete, + handleDuplicateRule, + onPauseChange, + buttonSize, +}: Props) => { + // check all abilities and permissions + const [pauseSupported, pauseAllowed] = useAlertRuleAbility(rule, AlertRuleAction.Pause); + const canPause = pauseSupported && pauseAllowed; + + const [deleteSupported, deleteAllowed] = useAlertRuleAbility(rule, AlertRuleAction.Delete); + const canDelete = deleteSupported && deleteAllowed; + + const [duplicateSupported, duplicateAllowed] = useAlertRuleAbility(rule, AlertRuleAction.Duplicate); + const canDuplicate = duplicateSupported && duplicateAllowed; + + const [silenceSupported, silenceAllowed] = useAlertRuleAbility(rule, AlertRuleAction.Silence); + const canSilence = silenceSupported && silenceAllowed; + + const [exportSupported, exportAllowed] = useAlertRuleAbility(rule, AlertRuleAction.ModifyExport); + const canExport = exportSupported && exportAllowed; + + const ruleExtensionLinks = useRulePluginLinkExtension(rule); + + const extensionsAvailable = ruleExtensionLinks.length > 0; + + /** + * Since Incident isn't available as an open-source product we shouldn't show it for Open-Source licenced editions of Grafana. + * We should show it in development mode + */ + const shouldShowDeclareIncidentButton = + (!isOpenSourceEdition() || isLocalDevEnv()) && + isAlertingRule(rule.promRule) && + rule.promRule.state === PromAlertingRuleState.Firing; + const shareUrl = createShareLink(rule.namespace.rulesSource, rule); + + const showDivider = + [canPause, canSilence, shouldShowDeclareIncidentButton, canDuplicate].some(Boolean) && + [showCopyLinkButton, canExport].some(Boolean); + + const menuItems = ( + <> + {canPause && } + {canSilence && ( + + )} + {shouldShowDeclareIncidentButton && } + {canDuplicate && handleDuplicateRule(identifier)} />} + {showDivider && } + {shareUrl && copyToClipboard(shareUrl)} />} + {canExport && ( + ]} + /> + )} + {extensionsAvailable && ( + <> + + {ruleExtensionLinks.map((extension) => ( + + ))} + + )} + {canDelete && ( + <> + + handleDelete(rule)} /> + + )} + + ); + + return ( + {menuItems}}> + + + ); +}; + +function copyToClipboard(text: string) { + navigator.clipboard?.writeText(text).then(() => { + appEvents.emit(AppEvents.alertSuccess, ['URL copied to clipboard']); + }); +} + +type PropsWithIdentifier = { identifier: RuleIdentifier }; + +const ExportMenuItem = ({ identifier }: PropsWithIdentifier) => { + const returnTo = location.pathname + location.search; + const url = createUrl(`/alerting/${encodeURIComponent(ruleId.stringifyIdentifier(identifier))}/modify-export`, { + returnTo, + }); + + return ; +}; + +export default AlertRuleMenu; diff --git a/public/app/features/alerting/unified/components/rule-viewer/RuleViewer.test.tsx b/public/app/features/alerting/unified/components/rule-viewer/RuleViewer.test.tsx index 1a82e056194..3f660cc17a2 100644 --- a/public/app/features/alerting/unified/components/rule-viewer/RuleViewer.test.tsx +++ b/public/app/features/alerting/unified/components/rule-viewer/RuleViewer.test.tsx @@ -1,16 +1,23 @@ -import { render, waitFor, screen } from '@testing-library/react'; -import userEvent from '@testing-library/user-event'; import React from 'react'; -import { TestProvider } from 'test/helpers/TestProvider'; +import { render, waitFor, screen, userEvent } from 'test/test-utils'; import { byText, byRole } from 'testing-library-selector'; -import { setBackendSrv } from '@grafana/runtime'; +import { setBackendSrv, setPluginExtensionsHook } from '@grafana/runtime'; import { backendSrv } from 'app/core/services/backend_srv'; import { AccessControlAction } from 'app/types'; import { CombinedRule, RuleIdentifier } from 'app/types/unified-alerting'; -import { getCloudRule, getGrafanaRule, grantUserPermissions } from '../../mocks'; +import { + getCloudRule, + getGrafanaRule, + grantUserPermissions, + mockDataSource, + mockPluginLinkExtension, +} from '../../mocks'; +import { setupDataSources } from '../../testSetup/datasources'; +import { plugins, setupPlugins } from '../../testSetup/plugins'; import { Annotation } from '../../utils/constants'; +import { DataSourceType } from '../../utils/datasource'; import * as ruleId from '../../utils/rule-id'; import { AlertRuleProvider } from './RuleContext'; @@ -33,20 +40,58 @@ const ELEMENTS = { button: byRole('button', { name: /More/i }), actions: { silence: byRole('link', { name: /Silence/i }), - declareIncident: byRole('menuitem', { name: /Declare incident/i }), duplicate: byRole('menuitem', { name: /Duplicate/i }), copyLink: byRole('menuitem', { name: /Copy link/i }), export: byRole('menuitem', { name: /Export/i }), delete: byRole('menuitem', { name: /Delete/i }), }, + pluginActions: { + sloDashboard: byRole('menuitem', { name: /SLO dashboard/i }), + declareIncident: byRole('link', { name: /Declare incident/i }), + assertsWorkbench: byRole('menuitem', { name: /Open workbench/i }), + }, }, }, }; +const { apiHandlers: pluginApiHandlers } = setupPlugins(plugins); + +const server = createMockGrafanaServer(...pluginApiHandlers); + +setupDataSources(mockDataSource({ type: DataSourceType.Prometheus, name: 'mimir-1' })); +setPluginExtensionsHook(() => ({ + extensions: [ + mockPluginLinkExtension({ pluginId: 'grafana-slo-app', title: 'SLO dashboard', path: '/a/grafana-slo-app' }), + mockPluginLinkExtension({ + pluginId: 'grafana-asserts-app', + title: 'Open workbench', + path: '/a/grafana-asserts-app', + }), + ], + isLoading: false, +})); + +beforeAll(() => { + grantUserPermissions([ + AccessControlAction.AlertingRuleCreate, + AccessControlAction.AlertingRuleRead, + AccessControlAction.AlertingRuleUpdate, + AccessControlAction.AlertingRuleDelete, + AccessControlAction.AlertingInstanceCreate, + ]); + setBackendSrv(backendSrv); +}); + +beforeEach(() => { + server.listen(); +}); + +afterAll(() => { + server.close(); +}); + describe('RuleViewer', () => { describe('Grafana managed alert rule', () => { - const server = createMockGrafanaServer(); - const mockRule = getGrafanaRule( { name: 'Test alert', @@ -71,29 +116,6 @@ describe('RuleViewer', () => { ); const mockRuleIdentifier = ruleId.fromCombinedRule('grafana', mockRule); - beforeAll(() => { - grantUserPermissions([ - AccessControlAction.AlertingRuleCreate, - AccessControlAction.AlertingRuleRead, - AccessControlAction.AlertingRuleUpdate, - AccessControlAction.AlertingRuleDelete, - AccessControlAction.AlertingInstanceCreate, - ]); - setBackendSrv(backendSrv); - }); - - beforeEach(() => { - server.listen(); - }); - - afterAll(() => { - server.close(); - }); - - afterEach(() => { - server.resetHandlers(); - }); - it('should render a Grafana managed alert rule', async () => { await renderRuleViewer(mockRule, mockRuleIdentifier); @@ -131,8 +153,12 @@ describe('RuleViewer', () => { }); }); - describe.skip('Data source managed alert rule', () => { - const mockRule = getCloudRule({ name: 'cloud test alert' }); + describe('Data source managed alert rule', () => { + const mockRule = getCloudRule({ + name: 'cloud test alert', + annotations: { [Annotation.summary]: 'cloud summary', [Annotation.runbookURL]: 'https://runbook.example.com' }, + group: { name: 'Cloud group', interval: '15m', rules: [], totals: { alerting: 1 } }, + }); const mockRuleIdentifier = ruleId.fromCombinedRule('mimir-1', mockRule); beforeAll(() => { @@ -146,14 +172,53 @@ describe('RuleViewer', () => { renderRuleViewer(mockRule, mockRuleIdentifier); // assert on basic info to be vissible - expect(screen.getByText('Test alert')).toBeInTheDocument(); + expect(screen.getByText('cloud test alert')).toBeInTheDocument(); expect(screen.getByText('Firing')).toBeInTheDocument(); expect(screen.getByText(mockRule.annotations[Annotation.summary])).toBeInTheDocument(); - expect(screen.getByRole('link', { name: 'View panel' })).toBeInTheDocument(); expect(screen.getByRole('link', { name: mockRule.annotations[Annotation.runbookURL] })).toBeInTheDocument(); expect(screen.getByText(`Every ${mockRule.group.interval}`)).toBeInTheDocument(); }); + + it('should render custom plugin actions for a plugin-provided rule', async () => { + const sloRule = getCloudRule({ + name: 'slo test alert', + labels: { __grafana_origin: 'plugin/grafana-slo-app' }, + }); + const sloRuleIdentifier = ruleId.fromCombinedRule('mimir-1', sloRule); + + const user = userEvent.setup(); + + renderRuleViewer(sloRule, sloRuleIdentifier); + + expect(ELEMENTS.actions.more.button.get()).toBeInTheDocument(); + + await user.click(ELEMENTS.actions.more.button.get()); + + expect(ELEMENTS.actions.more.pluginActions.sloDashboard.get()).toBeInTheDocument(); + expect(ELEMENTS.actions.more.pluginActions.assertsWorkbench.query()).not.toBeInTheDocument(); + + await waitFor(() => expect(ELEMENTS.actions.more.pluginActions.declareIncident.get()).toBeEnabled()); + }); + + it('should render different custom plugin actions for a different plugin-provided rule', async () => { + const assertsRule = getCloudRule({ + name: 'asserts test alert', + labels: { __grafana_origin: 'plugin/grafana-asserts-app' }, + }); + const assertsRuleIdentifier = ruleId.fromCombinedRule('mimir-1', assertsRule); + + renderRuleViewer(assertsRule, assertsRuleIdentifier); + + expect(ELEMENTS.actions.more.button.get()).toBeInTheDocument(); + + await userEvent.click(ELEMENTS.actions.more.button.get()); + + expect(ELEMENTS.actions.more.pluginActions.assertsWorkbench.get()).toBeInTheDocument(); + expect(ELEMENTS.actions.more.pluginActions.sloDashboard.query()).not.toBeInTheDocument(); + + await waitFor(() => expect(ELEMENTS.actions.more.pluginActions.declareIncident.get()).toBeEnabled()); + }); }); }); @@ -161,8 +226,7 @@ const renderRuleViewer = async (rule: CombinedRule, identifier: RuleIdentifier) render( - , - { wrapper: TestProvider } + ); await waitFor(() => expect(ELEMENTS.loading.query()).not.toBeInTheDocument()); diff --git a/public/app/features/alerting/unified/components/rule-viewer/RuleViewer.tsx b/public/app/features/alerting/unified/components/rule-viewer/RuleViewer.tsx index d6899597755..be5acf4370e 100644 --- a/public/app/features/alerting/unified/components/rule-viewer/RuleViewer.tsx +++ b/public/app/features/alerting/unified/components/rule-viewer/RuleViewer.tsx @@ -7,13 +7,17 @@ import { Alert, LinkButton, Stack, TabContent, Text, TextLink, useStyles2 } from import { PageInfoItem } from 'app/core/components/Page/types'; import { useQueryParams } from 'app/core/hooks/useQueryParams'; import InfoPausedRule from 'app/features/alerting/unified/components/InfoPausedRule'; +import { RuleActionsButtons } from 'app/features/alerting/unified/components/rules/RuleActionsButtons'; import { CombinedRule, RuleHealth, RuleIdentifier } from 'app/types/unified-alerting'; import { PromAlertingRuleState, PromRuleType } from 'app/types/unified-alerting-dto'; import { defaultPageNav } from '../../RuleViewer'; +import { PluginOriginBadge } from '../../plugins/PluginOriginBadge'; import { Annotation } from '../../utils/constants'; import { makeDashboardLink, makePanelLink } from '../../utils/misc'; import { + RulePluginOrigin, + getRulePluginOrigin, isAlertingRule, isFederatedRuleGroup, isGrafanaRulerRule, @@ -28,8 +32,6 @@ import { WithReturnButton } from '../WithReturnButton'; import { decodeGrafanaNamespace } from '../expressions/util'; import { RedirectToCloneRule } from '../rules/CloneRule'; -import { useAlertRulePageActions } from './Actions'; -import { useDeleteModal } from './DeleteModal'; import { FederatedRuleWarning } from './FederatedRuleWarning'; import PausedBadge from './PausedBadge'; import { useAlertRule } from './RuleContext'; @@ -57,12 +59,6 @@ const RuleViewer = () => { // of duplicating provisioned alert rules const [duplicateRuleIdentifier, setDuplicateRuleIdentifier] = useState(); - const [deleteModal, showDeleteModal] = useDeleteModal(); - const actions = useAlertRulePageActions({ - handleDuplicateRule: setDuplicateRuleIdentifier, - handleDelete: showDeleteModal, - }); - const { annotations, promRule } = rule; const hasError = isErrorHealth(rule.promRule?.health); @@ -73,6 +69,7 @@ const RuleViewer = () => { const isPaused = isGrafanaRulerRule(rule.rulerRule) && isGrafanaRulerRulePaused(rule.rulerRule); const showError = hasError && !isPaused; + const ruleOrigin = getRulePluginOrigin(rule); const summary = annotations[Annotation.summary]; @@ -88,9 +85,10 @@ const RuleViewer = () => { state={isAlertType ? promRule.state : undefined} health={rule.promRule?.health} ruleType={rule.promRule?.type} + ruleOrigin={ruleOrigin} /> )} - actions={actions} + actions={} info={createMetadata(rule)} subTitle={ @@ -123,7 +121,6 @@ const RuleViewer = () => { {activeTab === ActiveTab.Details &&
} - {deleteModal} {duplicateRuleIdentifier && ( { +export const Title = ({ name, paused = false, state, health, ruleType, ruleOrigin }: TitleProps) => { const styles = useStyles2(getStyles); const isRecordingRule = ruleType === PromRuleType.Recording; return (
+ {ruleOrigin && } {name} diff --git a/public/app/features/alerting/unified/components/rule-viewer/__mocks__/server.ts b/public/app/features/alerting/unified/components/rule-viewer/__mocks__/server.ts index de4dcdcf261..4bfdda20e06 100644 --- a/public/app/features/alerting/unified/components/rule-viewer/__mocks__/server.ts +++ b/public/app/features/alerting/unified/components/rule-viewer/__mocks__/server.ts @@ -1,11 +1,12 @@ -import { http, HttpResponse } from 'msw'; -import { SetupServer, setupServer } from 'msw/node'; +import { http, HttpResponse, RequestHandler } from 'msw'; +import { setupServer } from 'msw/node'; import { AlertmanagersChoiceResponse } from 'app/features/alerting/unified/api/alertmanagerApi'; -import { mockAlertmanagerChoiceResponse } from 'app/features/alerting/unified/mocks/alertmanagerApi'; import { AlertmanagerChoice } from 'app/plugins/datasource/alertmanager/types'; import { AccessControlAction } from 'app/types'; +import { alertmanagerChoiceHandler } from '../../../mocks/alertmanagerApi'; + const alertmanagerChoiceMockedResponse: AlertmanagersChoiceResponse = { alertmanagersChoice: AlertmanagerChoice.Internal, numExternalAlertmanagers: 0, @@ -18,39 +19,24 @@ const folderAccess = { [AccessControlAction.AlertingRuleDelete]: true, }; -export function createMockGrafanaServer() { - const server = setupServer(); +export function createMockGrafanaServer(...handlers: RequestHandler[]) { + const folderHandler = mockFolderAccess(folderAccess); + const amChoiceHandler = alertmanagerChoiceHandler(alertmanagerChoiceMockedResponse); - mockFolderAccess(server, folderAccess); - mockAlertmanagerChoiceResponse(server, alertmanagerChoiceMockedResponse); - mockGrafanaIncidentPluginSettings(server); - - return server; + return setupServer(folderHandler, amChoiceHandler, ...handlers); } // this endpoint is used to determine of we have edit / delete permissions for the Grafana managed alert rule // a user must alsso have permissions for the folder (namespace) in which the alert rule is stored -function mockFolderAccess(server: SetupServer, accessControl: Partial>) { - server.use( - http.get('/api/folders/:uid', ({ request }) => { - const url = new URL(request.url); - const uid = url.searchParams.get('uid'); +function mockFolderAccess(accessControl: Partial>) { + return http.get('/api/folders/:uid', ({ request }) => { + const url = new URL(request.url); + const uid = url.searchParams.get('uid'); - return HttpResponse.json({ - title: 'My Folder', - uid, - accessControl, - }); - }) - ); - - return server; -} - -function mockGrafanaIncidentPluginSettings(server: SetupServer) { - server.use( - http.get('/api/plugins/grafana-incident-app/settings', () => { - return HttpResponse.json({}); - }) - ); + return HttpResponse.json({ + title: 'My Folder', + uid, + accessControl, + }); + }); } diff --git a/public/app/features/alerting/unified/components/rules/MultipleDataSourcePicker.tsx b/public/app/features/alerting/unified/components/rules/MultipleDataSourcePicker.tsx index 119d0fc8ec0..72124081a5a 100644 --- a/public/app/features/alerting/unified/components/rules/MultipleDataSourcePicker.tsx +++ b/public/app/features/alerting/unified/components/rules/MultipleDataSourcePicker.tsx @@ -10,7 +10,7 @@ import { import { selectors } from '@grafana/e2e-selectors'; import { getDataSourceSrv, DataSourcePickerState, DataSourcePickerProps } from '@grafana/runtime'; import { ExpressionDatasourceRef } from '@grafana/runtime/src/utils/DataSourceWithBackend'; -import { ActionMeta, HorizontalGroup, PluginSignatureBadge, MultiSelect } from '@grafana/ui'; +import { ActionMeta, Stack, PluginSignatureBadge, MultiSelect } from '@grafana/ui'; import { isDataSourceManagingAlerts } from '../../utils/datasource'; @@ -168,9 +168,9 @@ export const MultipleDataSourcePicker = (props: MultipleDataSourcePickerProps) = getOptionLabel={(o) => { if (o.meta && isUnsignedPluginSignature(o.meta.signature) && o !== value) { return ( - + {o.label} - + ); } return o.label || ''; diff --git a/public/app/features/alerting/unified/components/rules/RuleActionsButtons.test.tsx b/public/app/features/alerting/unified/components/rules/RuleActionsButtons.test.tsx new file mode 100644 index 00000000000..7fa0fca52ce --- /dev/null +++ b/public/app/features/alerting/unified/components/rules/RuleActionsButtons.test.tsx @@ -0,0 +1,126 @@ +import { produce } from 'immer'; +import React from 'react'; +import { render, screen, userEvent } from 'test/test-utils'; +import { byLabelText } from 'testing-library-selector'; + +import { setPluginExtensionsHook } from '@grafana/runtime'; +import { contextSrv } from 'app/core/services/context_srv'; +import { RuleActionsButtons } from 'app/features/alerting/unified/components/rules/RuleActionsButtons'; +import { setupMswServer } from 'app/features/alerting/unified/mockApi'; +import { + getCloudRule, + getGrafanaRule, + grantUserPermissions, + mockDataSource, + mockGrafanaRulerRule, + mockPromAlertingRule, +} from 'app/features/alerting/unified/mocks'; +import { configureStore } from 'app/store/configureStore'; +import { AccessControlAction } from 'app/types'; +import { PromAlertingRuleState } from 'app/types/unified-alerting-dto'; + +setupMswServer(); +jest.mock('app/core/services/context_srv'); +const mockContextSrv = jest.mocked(contextSrv); + +const ui = { + moreButton: byLabelText('more-actions'), +}; + +const grantAllPermissions = () => { + grantUserPermissions([ + AccessControlAction.AlertingRuleCreate, + AccessControlAction.AlertingRuleRead, + AccessControlAction.AlertingRuleUpdate, + AccessControlAction.AlertingRuleDelete, + AccessControlAction.AlertingInstanceCreate, + ]); + mockContextSrv.hasPermissionInMetadata.mockImplementation(() => true); + mockContextSrv.hasPermission.mockImplementation(() => true); +}; +const grantNoPermissions = () => { + grantUserPermissions([]); + mockContextSrv.hasPermissionInMetadata.mockImplementation(() => false); + mockContextSrv.hasPermission.mockImplementation(() => false); +}; + +const getMenuContents = async () => { + await screen.findByRole('menu'); + const allMenuItems = screen.queryAllByRole('menuitem').map((el) => el.textContent); + const allLinkItems = screen.queryAllByRole('link').map((el) => el.textContent); + + return [...allMenuItems, ...allLinkItems]; +}; + +setPluginExtensionsHook(() => ({ + extensions: [], + isLoading: false, +})); + +describe('RuleActionsButtons', () => { + it('renders correct options for grafana managed rule', async () => { + const user = userEvent.setup(); + grantAllPermissions(); + const mockRule = getGrafanaRule(); + + render(); + + await user.click(await ui.moreButton.find()); + + expect(await getMenuContents()).toMatchSnapshot(); + }); + + it('renders correct options for Cloud rule', async () => { + const user = userEvent.setup(); + grantAllPermissions(); + const mockRule = getCloudRule(); + const dataSource = mockDataSource({ id: 1 }); + + const defaultState = configureStore().getState(); + render(, { + preloadedState: produce(defaultState, (store) => { + store.unifiedAlerting.dataSources[dataSource.name] = { + loading: false, + dispatched: true, + result: { + id: 'test-ds', + name: dataSource.name, + rulerConfig: { + dataSourceName: dataSource.name, + apiVersion: 'config', + }, + }, + }; + }), + }); + + await user.click(await ui.moreButton.find()); + + expect(await getMenuContents()).toMatchSnapshot(); + }); + + it('renders minimal "More" menu when appropriate', async () => { + const user = userEvent.setup(); + grantNoPermissions(); + + const mockRule = getGrafanaRule({ promRule: mockPromAlertingRule({ state: PromAlertingRuleState.Inactive }) }); + + render(); + + await user.click(await ui.moreButton.find()); + + expect(await getMenuContents()).toMatchSnapshot(); + }); + + it('does not allow deletion when rule is provisioned', async () => { + const user = userEvent.setup(); + grantAllPermissions(); + const mockRule = getGrafanaRule({ rulerRule: mockGrafanaRulerRule({ provenance: 'file' }) }); + + render(); + + await user.click(await ui.moreButton.find()); + + expect(screen.queryByText(/delete/i)).not.toBeInTheDocument(); + }); +}); diff --git a/public/app/features/alerting/unified/components/rules/RuleActionsButtons.tsx b/public/app/features/alerting/unified/components/rules/RuleActionsButtons.tsx index cf66b4b76b7..b16add8fd93 100644 --- a/public/app/features/alerting/unified/components/rules/RuleActionsButtons.tsx +++ b/public/app/features/alerting/unified/components/rules/RuleActionsButtons.tsx @@ -1,32 +1,20 @@ -import { css } from '@emotion/css'; -import { uniqueId } from 'lodash'; +import { css, cx } from '@emotion/css'; import React, { useState } from 'react'; import { useLocation } from 'react-router-dom'; import { GrafanaTheme2 } from '@grafana/data'; -import { - Button, - ClipboardButton, - ConfirmModal, - Dropdown, - Icon, - LinkButton, - Menu, - Tooltip, - useStyles2, - Stack, -} from '@grafana/ui'; -import { useAppNotification } from 'app/core/copy/appNotification'; -import MenuItemPauseRule from 'app/features/alerting/unified/components/MenuItemPauseRule'; +import { LinkButton, useStyles2, Stack } from '@grafana/ui'; +import AlertRuleMenu from 'app/features/alerting/unified/components/rule-viewer/AlertRuleMenu'; +import { useDeleteModal } from 'app/features/alerting/unified/components/rule-viewer/DeleteModal'; import { INSTANCES_DISPLAY_LIMIT } from 'app/features/alerting/unified/components/rules/RuleDetails'; import { useRulesFilter } from 'app/features/alerting/unified/hooks/useFilteredRules'; import { useDispatch } from 'app/types'; import { CombinedRule, RuleIdentifier, RulesSource } from 'app/types/unified-alerting'; import { AlertRuleAction, useAlertRuleAbility } from '../../hooks/useAbilities'; -import { deleteRuleAction, fetchAllPromAndRulerRulesAction } from '../../state/actions'; -import { getRulesSourceName } from '../../utils/datasource'; -import { createShareLink, createViewLink } from '../../utils/misc'; +import { fetchPromAndRulerRulesAction } from '../../state/actions'; +import { GRAFANA_RULES_SOURCE_NAME, getRulesSourceName } from '../../utils/datasource'; +import { createViewLink } from '../../utils/misc'; import * as ruleId from '../../utils/rule-id'; import { isGrafanaRulerRule } from '../../utils/rules'; import { createUrl } from '../../utils/url'; @@ -38,216 +26,124 @@ export const matchesWidth = (width: number) => window.matchMedia(`(max-width: ${ interface Props { rule: CombinedRule; rulesSource: RulesSource; + /** + * Should we show the buttons in a "compact" state? + * i.e. without text and using smaller button sizes + */ + compact?: boolean; + showViewButton?: boolean; + showCopyLinkButton?: boolean; } -export const RuleActionsButtons = ({ rule, rulesSource }: Props) => { +/** + * **Action** buttons to show for an alert rule - e.g. "View", "Edit", "More..." + */ +export const RuleActionsButtons = ({ compact, showViewButton, showCopyLinkButton, rule, rulesSource }: Props) => { const dispatch = useDispatch(); const location = useLocation(); - const notifyApp = useAppNotification(); const style = useStyles2(getStyles); + const [deleteModal, showDeleteModal] = useDeleteModal(); const [redirectToClone, setRedirectToClone] = useState< { identifier: RuleIdentifier; isProvisioned: boolean } | undefined >(undefined); const { namespace, group, rulerRule } = rule; - const [ruleToDelete, setRuleToDelete] = useState(); const { hasActiveFilters } = useRulesFilter(); const returnTo = location.pathname + location.search; - const isViewMode = inViewMode(location.pathname); const isProvisioned = isGrafanaRulerRule(rule.rulerRule) && Boolean(rule.rulerRule.grafana_alert.provenance); const [editRuleSupported, editRuleAllowed] = useAlertRuleAbility(rule, AlertRuleAction.Update); - const [deleteRuleSupported, deleteRuleAllowed] = useAlertRuleAbility(rule, AlertRuleAction.Delete); - const [duplicateRuleSupported, duplicateRuleAllowed] = useAlertRuleAbility(rule, AlertRuleAction.Duplicate); - const [modifyExportSupported, modifyExportAllowed] = useAlertRuleAbility(rule, AlertRuleAction.ModifyExport); const canEditRule = editRuleSupported && editRuleAllowed; - const canDeleteRule = deleteRuleSupported && deleteRuleAllowed; - const canDuplicateRule = duplicateRuleSupported && duplicateRuleAllowed; - const canModifyExport = modifyExportSupported && modifyExportAllowed; const buttons: JSX.Element[] = []; - const moreActions: JSX.Element[] = []; - const deleteRule = () => { - if (ruleToDelete && ruleToDelete.rulerRule) { - const identifier = ruleId.fromRulerRule( - getRulesSourceName(ruleToDelete.namespace.rulesSource), - ruleToDelete.namespace.name, - ruleToDelete.group.name, - ruleToDelete.rulerRule - ); - - dispatch(deleteRuleAction(identifier, { navigateTo: isViewMode ? '/alerting/list' : undefined })); - setRuleToDelete(undefined); - } - }; - - const buildShareUrl = () => createShareLink(rulesSource, rule); + const buttonClasses = cx({ [style.compactButton]: compact }); + const buttonSize = compact ? 'sm' : 'md'; const sourceName = getRulesSourceName(rulesSource); - if (!isViewMode) { + const identifier = ruleId.fromCombinedRule(sourceName, rule); + + if (showViewButton) { buttons.push( - - - + + {!compact && 'View'} + ); } - if (rulerRule) { + if (rulerRule && canEditRule) { const identifier = ruleId.fromRulerRule(sourceName, namespace.name, group.name, rulerRule); - if (canEditRule) { - const editURL = createUrl(`/alerting/${encodeURIComponent(ruleId.stringifyIdentifier(identifier))}/edit`, { - returnTo, - }); + const editURL = createUrl(`/alerting/${encodeURIComponent(ruleId.stringifyIdentifier(identifier))}/edit`, { + returnTo, + }); - buttons.push( - - - - ); - - moreActions.push( - { - // Uses INSTANCES_DISPLAY_LIMIT + 1 here as exporting LIMIT_ALERTS from RuleList has the side effect - // of breaking some unrelated tests in Policy.test.tsx due to mocking approach - const limitAlerts = hasActiveFilters ? undefined : INSTANCES_DISPLAY_LIMIT + 1; - // Trigger a re-fetch of the rules table - // TODO: Migrate rules table functionality to RTK Query, so we instead rely - // on tag invalidation (or optimistic cache updates) for this - dispatch(fetchAllPromAndRulerRulesAction(false, { limitAlerts })); - }} - /> - ); - } - - if (isViewMode) { - buttons.push( - { - notifyApp.error('Error while copying URL', copiedText); - }} - className={style.button} - size="sm" - getText={buildShareUrl} - > - Copy link to rule - - ); - } - - if (canDuplicateRule) { - moreActions.push( - setRedirectToClone({ identifier, isProvisioned })} /> - ); - } - - if (canModifyExport) { - moreActions.push( - - ); - } - - if (canDeleteRule) { - moreActions.push( setRuleToDelete(rule)} />); - } - } - - if (buttons.length || moreActions.length) { - return ( - <> - - {buttons.map((button, index) => ( - {button} - ))} - {moreActions.length > 0 && ( - - {moreActions.map((action) => ( - {action} - ))} - - } - > - - - )} - - {!!ruleToDelete && ( - -

- Deleting "{ruleToDelete.name}" will permanently remove it from your alert - rule list. -

-

Are you sure you want to delete this rule?

-
- } - confirmText="Yes, delete" - icon="exclamation-triangle" - onConfirm={deleteRule} - onDismiss={() => setRuleToDelete(undefined)} - /> - )} - - {redirectToClone && ( - setRedirectToClone(undefined)} - /> - )} - + buttons.push( + + {!compact && 'Edit'} + ); } - return null; + return ( + + {buttons} + showDeleteModal(rule)} + handleDuplicateRule={() => setRedirectToClone({ identifier, isProvisioned })} + onPauseChange={() => { + // Uses INSTANCES_DISPLAY_LIMIT + 1 here as exporting LIMIT_ALERTS from RuleList has the side effect + // of breaking some unrelated tests in Policy.test.tsx due to mocking approach + const limitAlerts = hasActiveFilters ? undefined : INSTANCES_DISPLAY_LIMIT + 1; + // Trigger a re-fetch of the rules table + // TODO: Migrate rules table functionality to RTK Query, so we instead rely + // on tag invalidation (or optimistic cache updates) for this + dispatch(fetchPromAndRulerRulesAction({ rulesSourceName: GRAFANA_RULES_SOURCE_NAME, limitAlerts })); + }} + /> + {deleteModal} + {redirectToClone?.identifier && ( + setRedirectToClone(undefined)} + /> + )} + + ); }; -function inViewMode(pathname: string): boolean { - return pathname.endsWith('/view'); -} - -export const getStyles = (theme: GrafanaTheme2) => ({ - button: css` - padding: 0 ${theme.spacing(2)}; - `, +const getStyles = (theme: GrafanaTheme2) => ({ + compactButton: css({ + padding: `0 ${theme.spacing(2)}`, + }), }); diff --git a/public/app/features/alerting/unified/components/rules/RuleDetails.test.tsx b/public/app/features/alerting/unified/components/rules/RuleDetails.test.tsx index e5220ac4295..596e63b6bc3 100644 --- a/public/app/features/alerting/unified/components/rules/RuleDetails.test.tsx +++ b/public/app/features/alerting/unified/components/rules/RuleDetails.test.tsx @@ -7,12 +7,10 @@ import { MemoryRouter } from 'react-router-dom'; import { byRole } from 'testing-library-selector'; import { PluginExtensionTypes } from '@grafana/data'; -import { getPluginLinkExtensions, setBackendSrv } from '@grafana/runtime'; +import { usePluginLinkExtensions, setBackendSrv } from '@grafana/runtime'; import { backendSrv } from 'app/core/services/backend_srv'; -import { contextSrv } from 'app/core/services/context_srv'; import { AlertmanagerChoice } from 'app/plugins/datasource/alertmanager/types'; import { configureStore } from 'app/store/configureStore'; -import { AccessControlAction } from 'app/types'; import { CombinedRule } from 'app/types/unified-alerting'; import { AlertmanagersChoiceResponse } from '../../api/alertmanagerApi'; @@ -25,14 +23,14 @@ import { RuleDetails } from './RuleDetails'; jest.mock('@grafana/runtime', () => ({ ...jest.requireActual('@grafana/runtime'), - getPluginLinkExtensions: jest.fn(), + usePluginLinkExtensions: jest.fn(), useReturnToPrevious: jest.fn(), })); jest.mock('../../hooks/useIsRuleEditable'); const mocks = { - getPluginLinkExtensionsMock: jest.mocked(getPluginLinkExtensions), + usePluginLinkExtensionsMock: jest.mocked(usePluginLinkExtensions), useIsRuleEditable: jest.mocked(useIsRuleEditable), }; @@ -68,7 +66,7 @@ afterAll(() => { }); beforeEach(() => { - mocks.getPluginLinkExtensionsMock.mockReturnValue({ + mocks.usePluginLinkExtensionsMock.mockReturnValue({ extensions: [ { pluginId: 'grafana-ml-app', @@ -80,6 +78,7 @@ beforeEach(() => { onClick: jest.fn(), }, ], + isLoading: false, }); server.resetHandlers(); mockAlertmanagerChoiceResponse(server, alertmanagerChoiceMockedResponse); @@ -112,32 +111,6 @@ describe('RuleDetails RBAC', () => { expect(ui.actionButtons.delete.query()).not.toBeInTheDocument(); await waitFor(() => screen.queryByRole('button', { name: 'Declare incident' })); }); - - it('Should not render Silence button for users wihout the instance create permission', async () => { - // Arrange - jest.spyOn(contextSrv, 'hasPermission').mockReturnValue(false); - - // Act - renderRuleDetails(grafanaRule); - - // Assert - expect(ui.actionButtons.silence.query()).not.toBeInTheDocument(); - await waitFor(() => screen.queryByRole('button', { name: 'Declare incident' })); - }); - - it('Should render Silence button for users with the instance create permissions', async () => { - // Arrange - jest - .spyOn(contextSrv, 'hasPermission') - .mockImplementation((action) => action === AccessControlAction.AlertingInstanceCreate); - - // Act - renderRuleDetails(grafanaRule); - - // Assert - expect(await ui.actionButtons.silence.find()).toBeInTheDocument(); - await waitFor(() => screen.queryByRole('button', { name: 'Declare incident' })); - }); }); describe('Cloud rules action buttons', () => { diff --git a/public/app/features/alerting/unified/components/rules/RuleDetails.tsx b/public/app/features/alerting/unified/components/rules/RuleDetails.tsx index 3e42e25f146..5023e5d0853 100644 --- a/public/app/features/alerting/unified/components/rules/RuleDetails.tsx +++ b/public/app/features/alerting/unified/components/rules/RuleDetails.tsx @@ -12,8 +12,8 @@ import { isNullDate } from '../../utils/time'; import { AlertLabels } from '../AlertLabels'; import { DetailsField } from '../DetailsField'; -import { RuleDetailsActionButtons } from './RuleDetailsActionButtons'; import { RuleDetailsAnnotations } from './RuleDetailsAnnotations'; +import RuleDetailsButtons from './RuleDetailsButtons'; import { RuleDetailsDataSources } from './RuleDetailsDataSources'; import { RuleDetailsExpression } from './RuleDetailsExpression'; import { RuleDetailsMatchingInstances } from './RuleDetailsMatchingInstances'; @@ -37,7 +37,7 @@ export const RuleDetails = ({ rule }: Props) => { return (
- +
{} @@ -111,21 +111,21 @@ const EvaluationBehaviorSummary = ({ rule }: EvaluationBehaviorSummaryProps) => }; export const getStyles = (theme: GrafanaTheme2) => ({ - wrapper: css` - display: flex; - flex-direction: row; + wrapper: css({ + display: 'flex', + flexDirection: 'row', - ${theme.breakpoints.down('md')} { - flex-direction: column; - } - `, - leftSide: css` - flex: 1; - `, - rightSide: css` - ${theme.breakpoints.up('md')} { - padding-left: 90px; - width: 300px; - } - `, + [theme.breakpoints.down('md')]: { + flexDirection: 'column', + }, + }), + leftSide: css({ + flex: '1', + }), + rightSide: css({ + [theme.breakpoints.up('md')]: { + paddingLeft: '90px', + width: '300px', + }, + }), }); diff --git a/public/app/features/alerting/unified/components/rules/RuleDetailsActionButtons.tsx b/public/app/features/alerting/unified/components/rules/RuleDetailsActionButtons.tsx deleted file mode 100644 index a9a6014380f..00000000000 --- a/public/app/features/alerting/unified/components/rules/RuleDetailsActionButtons.tsx +++ /dev/null @@ -1,257 +0,0 @@ -import { css } from '@emotion/css'; -import { uniqueId } from 'lodash'; -import React, { Fragment, useState } from 'react'; - -import { GrafanaTheme2, textUtil } from '@grafana/data'; -import { config, useReturnToPrevious } from '@grafana/runtime'; -import { Button, ConfirmModal, Dropdown, HorizontalGroup, Icon, LinkButton, Menu, useStyles2 } from '@grafana/ui'; -import { useDispatch } from 'app/types'; -import { CombinedRule, RuleIdentifier, RulesSource } from 'app/types/unified-alerting'; -import { PromAlertingRuleState } from 'app/types/unified-alerting-dto'; - -import { AlertRuleAction, useAlertRuleAbility } from '../../hooks/useAbilities'; -import { useStateHistoryModal } from '../../hooks/useStateHistoryModal'; -import { deleteRuleAction } from '../../state/actions'; -import { getAlertmanagerByUid } from '../../utils/alertmanager'; -import { Annotation } from '../../utils/constants'; -import { getRulesSourceName, isCloudRulesSource, isGrafanaRulesSource } from '../../utils/datasource'; -import { - createExploreLink, - createShareLink, - isLocalDevEnv, - isOpenSourceEdition, - makeRuleBasedSilenceLink, -} from '../../utils/misc'; -import * as ruleId from '../../utils/rule-id'; -import { isAlertingRule, isFederatedRuleGroup, isGrafanaRulerRule } from '../../utils/rules'; -import { DeclareIncidentButton } from '../bridges/DeclareIncidentButton'; - -import { RedirectToCloneRule } from './CloneRule'; - -interface Props { - rule: CombinedRule; - rulesSource: RulesSource; -} - -export const RuleDetailsActionButtons = ({ rule, rulesSource }: Props) => { - const style = useStyles2(getStyles); - const { group } = rule; - const { StateHistoryModal, showStateHistoryModal } = useStateHistoryModal(); - const dispatch = useDispatch(); - - const setReturnToPrevious = useReturnToPrevious(); - - const [ruleToDelete, setRuleToDelete] = useState(); - const [redirectToClone, setRedirectToClone] = useState< - { identifier: RuleIdentifier; isProvisioned: boolean } | undefined - >(undefined); - - const alertmanagerSourceName = isGrafanaRulesSource(rulesSource) - ? rulesSource - : getAlertmanagerByUid(rulesSource.jsonData.alertmanagerUid)?.name; - - const [silenceSupported, silenceAllowed] = useAlertRuleAbility(rule, AlertRuleAction.Silence); - const [exploreSupported, exploreAllowed] = useAlertRuleAbility(rule, AlertRuleAction.Explore); - - const buttons: JSX.Element[] = []; - const rightButtons: JSX.Element[] = []; - const moreActionsButtons: React.ReactElement[] = []; - - const deleteRule = () => { - if (ruleToDelete && ruleToDelete.rulerRule) { - const identifier = ruleId.fromRulerRule( - getRulesSourceName(ruleToDelete.namespace.rulesSource), - ruleToDelete.namespace.name, - ruleToDelete.group.name, - ruleToDelete.rulerRule - ); - - dispatch(deleteRuleAction(identifier, { navigateTo: undefined })); - setRuleToDelete(undefined); - } - }; - - const isFederated = isFederatedRuleGroup(group); - - const isFiringRule = isAlertingRule(rule.promRule) && rule.promRule.state === PromAlertingRuleState.Firing; - - const canSilence = silenceSupported && silenceAllowed && alertmanagerSourceName; - - const buildShareUrl = () => createShareLink(rulesSource, rule); - - // explore does not support grafana rule queries atm - // neither do "federated rules" - if (isCloudRulesSource(rulesSource) && exploreSupported && exploreAllowed && !isFederated) { - buttons.push( - - See graph - - ); - } - if (rule.annotations[Annotation.runbookURL]) { - buttons.push( - - View runbook - - ); - } - if (rule.annotations[Annotation.dashboardUID]) { - const dashboardUID = rule.annotations[Annotation.dashboardUID]; - const isReturnToPreviousEnabled = config.featureToggles.returnToPrevious; - if (dashboardUID) { - buttons.push( - { - setReturnToPrevious(rule.name); - }} - > - Go to dashboard - - ); - const panelId = rule.annotations[Annotation.panelID]; - if (panelId) { - buttons.push( - { - setReturnToPrevious(rule.name); - }} - > - Go to panel - - ); - } - } - } - - if (canSilence) { - buttons.push( - - Silence - - ); - } - - if (isGrafanaRulerRule(rule.rulerRule)) { - buttons.push( - - - {StateHistoryModal} - - ); - } - - if (isFiringRule && shouldShowDeclareIncidentButton()) { - buttons.push( - - - - ); - } - - if (buttons.length || rightButtons.length || moreActionsButtons.length) { - return ( - <> -
- {buttons.length ? buttons :
} - - {rightButtons.length && rightButtons} - {moreActionsButtons.length && ( - - {moreActionsButtons.map((action) => ( - {action} - ))} - - } - > - - - )} - -
- {!!ruleToDelete && ( - setRuleToDelete(undefined)} - /> - )} - {redirectToClone && ( - setRedirectToClone(undefined)} - /> - )} - - ); - } - - return null; -}; - -/** - * Since Incident isn't available as an open-source product we shouldn't show it for Open-Source licenced editions of Grafana. - * We should show it in development mode - */ -function shouldShowDeclareIncidentButton() { - return !isOpenSourceEdition() || isLocalDevEnv(); -} - -export const getStyles = (theme: GrafanaTheme2) => ({ - wrapper: css` - padding: 0 0 ${theme.spacing(2)} 0; - gap: ${theme.spacing(1)}; - display: flex; - flex-direction: row; - justify-content: space-between; - flex-wrap: wrap; - border-bottom: solid 1px ${theme.colors.border.medium}; - `, -}); diff --git a/public/app/features/alerting/unified/components/rules/RuleDetailsButtons.tsx b/public/app/features/alerting/unified/components/rules/RuleDetailsButtons.tsx new file mode 100644 index 00000000000..2008d401a77 --- /dev/null +++ b/public/app/features/alerting/unified/components/rules/RuleDetailsButtons.tsx @@ -0,0 +1,127 @@ +import React, { Fragment } from 'react'; + +import { textUtil } from '@grafana/data'; +import { config, useReturnToPrevious } from '@grafana/runtime'; +import { Button, LinkButton, Stack } from '@grafana/ui'; +import { CombinedRule, RulesSource } from 'app/types/unified-alerting'; + +import { AlertRuleAction, useAlertRuleAbility } from '../../hooks/useAbilities'; +import { useStateHistoryModal } from '../../hooks/useStateHistoryModal'; +import { Annotation } from '../../utils/constants'; +import { isCloudRulesSource } from '../../utils/datasource'; +import { createExploreLink } from '../../utils/misc'; +import { isFederatedRuleGroup, isGrafanaRulerRule } from '../../utils/rules'; + +interface Props { + rule: CombinedRule; + rulesSource: RulesSource; +} + +/** + * Buttons to display on an expanded alert rule in the list view + * + * e.g. "Show state history", "Go to dashboard" + * + * Shouldn't include *actions* for the alert rule, just navigation items + */ +const RuleDetailsButtons = ({ rule, rulesSource }: Props) => { + const { group } = rule; + const { StateHistoryModal, showStateHistoryModal } = useStateHistoryModal(); + + const setReturnToPrevious = useReturnToPrevious(); + + const [exploreSupported, exploreAllowed] = useAlertRuleAbility(rule, AlertRuleAction.Explore); + + const buttons: JSX.Element[] = []; + + const isFederated = isFederatedRuleGroup(group); + + // explore does not support grafana rule queries atm + // neither do "federated rules" + if (isCloudRulesSource(rulesSource) && exploreSupported && exploreAllowed && !isFederated) { + buttons.push( + + See graph + + ); + } + if (rule.annotations[Annotation.runbookURL]) { + buttons.push( + + View runbook + + ); + } + if (rule.annotations[Annotation.dashboardUID]) { + const dashboardUID = rule.annotations[Annotation.dashboardUID]; + const isReturnToPreviousEnabled = config.featureToggles.returnToPrevious; + if (dashboardUID) { + buttons.push( + { + setReturnToPrevious(rule.name); + }} + > + Go to dashboard + + ); + const panelId = rule.annotations[Annotation.panelID]; + if (panelId) { + buttons.push( + { + setReturnToPrevious(rule.name); + }} + > + Go to panel + + ); + } + } + } + + if (isGrafanaRulerRule(rule.rulerRule)) { + buttons.push( + + + {StateHistoryModal} + + ); + } + + return buttons.length ? {buttons} : null; +}; + +export default RuleDetailsButtons; diff --git a/public/app/features/alerting/unified/components/rules/RuleDetailsMatchingInstances.test.tsx b/public/app/features/alerting/unified/components/rules/RuleDetailsMatchingInstances.test.tsx index d9bfcb6b6e2..0770a0e184a 100644 --- a/public/app/features/alerting/unified/components/rules/RuleDetailsMatchingInstances.test.tsx +++ b/public/app/features/alerting/unified/components/rules/RuleDetailsMatchingInstances.test.tsx @@ -5,7 +5,7 @@ import React from 'react'; import { byLabelText, byRole, byTestId } from 'testing-library-selector'; import { PluginExtensionTypes } from '@grafana/data'; -import { getPluginLinkExtensions } from '@grafana/runtime'; +import { usePluginLinkExtensions } from '@grafana/runtime'; import { CombinedRuleNamespace } from '../../../../../types/unified-alerting'; import { GrafanaAlertState, PromAlertingRuleState } from '../../../../../types/unified-alerting-dto'; @@ -17,10 +17,11 @@ import { RuleDetailsMatchingInstances } from './RuleDetailsMatchingInstances'; jest.mock('@grafana/runtime', () => ({ ...jest.requireActual('@grafana/runtime'), getPluginLinkExtensions: jest.fn(), + usePluginLinkExtensions: jest.fn(), })); const mocks = { - getPluginLinkExtensionsMock: jest.mocked(getPluginLinkExtensions), + usePluginLinkExtensionsMock: jest.mocked(usePluginLinkExtensions), }; const ui = { @@ -43,7 +44,7 @@ const ui = { describe('RuleDetailsMatchingInstances', () => { beforeEach(() => { - mocks.getPluginLinkExtensionsMock.mockReturnValue({ + mocks.usePluginLinkExtensionsMock.mockReturnValue({ extensions: [ { pluginId: 'grafana-ml-app', @@ -55,6 +56,7 @@ describe('RuleDetailsMatchingInstances', () => { onClick: jest.fn(), }, ], + isLoading: false, }); }); diff --git a/public/app/features/alerting/unified/components/rules/RuleListGroupView.test.tsx b/public/app/features/alerting/unified/components/rules/RuleListGroupView.test.tsx index 5e35dbdda97..0c51d4ccbb9 100644 --- a/public/app/features/alerting/unified/components/rules/RuleListGroupView.test.tsx +++ b/public/app/features/alerting/unified/components/rules/RuleListGroupView.test.tsx @@ -4,7 +4,7 @@ import { Provider } from 'react-redux'; import { Router } from 'react-router-dom'; import { byRole } from 'testing-library-selector'; -import { locationService } from '@grafana/runtime'; +import { locationService, setPluginExtensionsHook } from '@grafana/runtime'; import { contextSrv } from 'app/core/services/context_srv'; import { configureStore } from 'app/store/configureStore'; import { AccessControlAction } from 'app/types'; @@ -23,6 +23,11 @@ const ui = { cloudRulesHeading: byRole('heading', { name: 'Mimir / Cortex / Loki' }), }; +setPluginExtensionsHook(() => ({ + extensions: [], + isLoading: false, +})); + describe('RuleListGroupView', () => { describe('RBAC', () => { it('Should display Grafana rules when the user has the alert rule read permission', async () => { diff --git a/public/app/features/alerting/unified/components/rules/RulesGroup.tsx b/public/app/features/alerting/unified/components/rules/RulesGroup.tsx index dab832d4298..2ca8d1ca3f9 100644 --- a/public/app/features/alerting/unified/components/rules/RulesGroup.tsx +++ b/public/app/features/alerting/unified/components/rules/RulesGroup.tsx @@ -4,7 +4,7 @@ import React, { useEffect, useState } from 'react'; import { GrafanaTheme2 } from '@grafana/data'; import { selectors } from '@grafana/e2e-selectors'; -import { Badge, ConfirmModal, HorizontalGroup, Icon, Spinner, Stack, Tooltip, useStyles2 } from '@grafana/ui'; +import { Badge, ConfirmModal, Icon, Spinner, Stack, Tooltip, useStyles2 } from '@grafana/ui'; import { useDispatch } from 'app/types'; import { CombinedRuleGroup, CombinedRuleNamespace } from 'app/types/unified-alerting'; @@ -83,10 +83,10 @@ export const RulesGroup = React.memo(({ group, namespace, expandAll, viewMode }: // for grafana, link to folder views if (isDeleting) { actionIcons.push( - + deleting - + ); } else if (rulesSource === GRAFANA_RULES_SOURCE_NAME) { if (folderUID) { diff --git a/public/app/features/alerting/unified/components/rules/RulesTable.test.tsx b/public/app/features/alerting/unified/components/rules/RulesTable.test.tsx index 3065719d8c0..d6489633255 100644 --- a/public/app/features/alerting/unified/components/rules/RulesTable.test.tsx +++ b/public/app/features/alerting/unified/components/rules/RulesTable.test.tsx @@ -5,11 +5,13 @@ import { Provider } from 'react-redux'; import { MemoryRouter } from 'react-router-dom'; import { byRole } from 'testing-library-selector'; +import { setPluginExtensionsHook } from '@grafana/runtime'; +import { mockApi, setupMswServer } from 'app/features/alerting/unified/mockApi'; import { configureStore } from 'app/store/configureStore'; import { CombinedRule } from 'app/types/unified-alerting'; import { AlertRuleAction, useAlertRuleAbility } from '../../hooks/useAbilities'; -import { getCloudRule, getGrafanaRule } from '../../mocks'; +import { getCloudRule, getGrafanaRule, getMockPluginMeta } from '../../mocks'; import { RulesTable } from './RulesTable'; @@ -19,11 +21,16 @@ const mocks = { useAlertRuleAbility: jest.mocked(useAlertRuleAbility), }; +setPluginExtensionsHook(() => ({ + extensions: [], + isLoading: false, +})); + const ui = { actionButtons: { edit: byRole('link', { name: 'Edit' }), view: byRole('link', { name: 'View' }), - more: byRole('button', { name: 'More' }), + more: byRole('button', { name: /more-actions/i }), }, moreActionItems: { delete: byRole('menuitem', { name: 'Delete' }), @@ -43,8 +50,14 @@ function renderRulesTable(rule: CombinedRule) { } const user = userEvent.setup(); +const server = setupMswServer(); describe('RulesTable RBAC', () => { + beforeEach(() => { + mockApi(server).plugins.getPluginSettings({ + ...getMockPluginMeta('grafana-incident-app', 'Grafana Incident'), + }); + }); describe('Grafana rules action buttons', () => { const grafanaRule = getGrafanaRule({ name: 'Grafana' }); diff --git a/public/app/features/alerting/unified/components/rules/RulesTable.tsx b/public/app/features/alerting/unified/components/rules/RulesTable.tsx index 56985d845ef..0a40744e898 100644 --- a/public/app/features/alerting/unified/components/rules/RulesTable.tsx +++ b/public/app/features/alerting/unified/components/rules/RulesTable.tsx @@ -16,8 +16,9 @@ import { CombinedRule } from 'app/types/unified-alerting'; import { DEFAULT_PER_PAGE_PAGINATION } from '../../../../../core/constants'; import { useHasRuler } from '../../hooks/useHasRuler'; +import { PluginOriginBadge } from '../../plugins/PluginOriginBadge'; import { Annotation } from '../../utils/constants'; -import { isGrafanaRulerRule, isGrafanaRulerRulePaused } from '../../utils/rules'; +import { getRulePluginOrigin, isGrafanaRulerRule, isGrafanaRulerRulePaused } from '../../utils/rules'; import { DynamicTable, DynamicTableColumnProps, DynamicTableItemProps } from '../DynamicTable'; import { DynamicTableWithGuidelines } from '../DynamicTableWithGuidelines'; import { ProvisioningBadge } from '../Provisioning'; @@ -175,13 +176,18 @@ function useColumns(showSummaryColumn: boolean, showGroupColumn: boolean, showNe size: showNextEvaluationColumn ? 4 : 5, }, { - id: 'provisioned', + id: 'metadata', label: '', // eslint-disable-next-line react/display-name renderCell: ({ data: rule }) => { const rulerRule = rule.rulerRule; - const isGrafanaManagedRule = isGrafanaRulerRule(rulerRule); + const originMeta = getRulePluginOrigin(rule); + if (originMeta) { + return ; + } + + const isGrafanaManagedRule = isGrafanaRulerRule(rulerRule); if (!isGrafanaManagedRule) { return null; } @@ -260,7 +266,7 @@ function useColumns(showSummaryColumn: boolean, showGroupColumn: boolean, showNe label: 'Actions', // eslint-disable-next-line react/display-name renderCell: ({ data: rule }) => { - return ; + return ; }, size: '200px', }); diff --git a/public/app/features/alerting/unified/components/rules/__snapshots__/RuleActionsButtons.test.tsx.snap b/public/app/features/alerting/unified/components/rules/__snapshots__/RuleActionsButtons.test.tsx.snap new file mode 100644 index 00000000000..7e8ec05632e --- /dev/null +++ b/public/app/features/alerting/unified/components/rules/__snapshots__/RuleActionsButtons.test.tsx.snap @@ -0,0 +1,28 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`RuleActionsButtons renders correct options for Cloud rule 1`] = ` +[ + "Duplicate", + "Copy link", + "Delete", + "Declare incident", +] +`; + +exports[`RuleActionsButtons renders correct options for grafana managed rule 1`] = ` +[ + "Pause evaluation", + "Duplicate", + "Copy link", + "Export", + "Delete", + "Silence notifications", + "Declare incident", +] +`; + +exports[`RuleActionsButtons renders minimal "More" menu when appropriate 1`] = ` +[ + "Copy link", +] +`; diff --git a/public/app/features/alerting/unified/components/silences/NoSilencesCTA.tsx b/public/app/features/alerting/unified/components/silences/NoSilencesCTA.tsx index b754f5b4fdb..fd42325863a 100644 --- a/public/app/features/alerting/unified/components/silences/NoSilencesCTA.tsx +++ b/public/app/features/alerting/unified/components/silences/NoSilencesCTA.tsx @@ -1,7 +1,7 @@ import React from 'react'; -import { CallToActionCard } from '@grafana/ui'; -import EmptyListCTA from 'app/core/components/EmptyListCTA/EmptyListCTA'; +import { CallToActionCard, EmptyState, LinkButton } from '@grafana/ui'; +import { Trans, t } from 'app/core/internationalization'; import { contextSrv } from 'app/core/services/context_srv'; import { getInstancesPermissions } from '../../utils/access-control'; @@ -16,11 +16,14 @@ export const NoSilencesSplash = ({ alertManagerSourceName }: Props) => { if (contextSrv.hasPermission(permissions.create)) { return ( - + Create silence + + } + message={t('silences.empty-state.title', "You haven't created any silences yet")} /> ); } diff --git a/public/app/features/alerting/unified/components/silences/SilencesEditor.tsx b/public/app/features/alerting/unified/components/silences/SilencesEditor.tsx index ebcefaced79..1bad2e84fa6 100644 --- a/public/app/features/alerting/unified/components/silences/SilencesEditor.tsx +++ b/public/app/features/alerting/unified/components/silences/SilencesEditor.tsx @@ -1,6 +1,6 @@ import { css, cx } from '@emotion/css'; import { isEqual, pickBy } from 'lodash'; -import React, { useMemo, useState } from 'react'; +import React, { useEffect, useMemo, useState } from 'react'; import { FormProvider, useForm } from 'react-hook-form'; import { useDebounce } from 'react-use'; @@ -13,27 +13,34 @@ import { isValidDate, parseDuration, } from '@grafana/data'; -import { config } from '@grafana/runtime'; -import { Button, Field, FieldSet, Input, LinkButton, TextArea, useStyles2 } from '@grafana/ui'; -import { useCleanup } from 'app/core/hooks/useCleanup'; +import { config, isFetchError, locationService } from '@grafana/runtime'; +import { + Alert, + Button, + Field, + FieldSet, + Input, + LinkButton, + LoadingPlaceholder, + TextArea, + useStyles2, +} from '@grafana/ui'; +import { alertSilencesApi } from 'app/features/alerting/unified/api/alertSilencesApi'; +import { getDatasourceAPIUid } from 'app/features/alerting/unified/utils/datasource'; import { Matcher, MatcherOperator, Silence, SilenceCreatePayload } from 'app/plugins/datasource/alertmanager/types'; -import { useDispatch } from 'app/types'; import { useURLSearchParams } from '../../hooks/useURLSearchParams'; -import { useUnifiedAlertingSelector } from '../../hooks/useUnifiedAlertingSelector'; -import { createOrUpdateSilenceAction } from '../../state/actions'; import { SilenceFormFields } from '../../types/silence-form'; import { matcherFieldToMatcher, matcherToMatcherField } from '../../utils/alertmanager'; import { parseQueryParamMatchers } from '../../utils/matchers'; import { makeAMLink } from '../../utils/misc'; -import { initialAsyncRequestState } from '../../utils/redux'; import MatchersField from './MatchersField'; import { SilencePeriod } from './SilencePeriod'; import { SilencedInstancesPreview } from './SilencedInstancesPreview'; interface Props { - silence?: Silence; + silenceId?: string; alertManagerSourceName: string; } @@ -97,24 +104,25 @@ const getDefaultFormValues = (searchParams: URLSearchParams, silence?: Silence): } }; -export const SilencesEditor = ({ silence, alertManagerSourceName }: Props) => { +export const SilencesEditor = ({ silenceId, alertManagerSourceName }: Props) => { + // Use a lazy query to fetch the Silence info, as we may not always require this + // (e.g. if creating a new one from scratch, we don't need to fetch anything) + const [getSilence, { data: silence, isLoading: getSilenceIsLoading, error: errorGettingExistingSilence }] = + alertSilencesApi.endpoints.getSilence.useLazyQuery(); + const [createSilence, { isLoading }] = alertSilencesApi.endpoints.createSilence.useMutation(); const [urlSearchParams] = useURLSearchParams(); const defaultValues = useMemo(() => getDefaultFormValues(urlSearchParams, silence), [silence, urlSearchParams]); const formAPI = useForm({ defaultValues }); - const dispatch = useDispatch(); + const styles = useStyles2(getStyles); const [matchersForPreview, setMatchersForPreview] = useState( defaultValues.matchers.map(matcherFieldToMatcher) ); - const { loading } = useUnifiedAlertingSelector((state) => state.updateSilence); + const { register, handleSubmit, formState, watch, setValue, clearErrors, reset } = formAPI; - useCleanup((state) => (state.unifiedAlerting.updateSilence = initialAsyncRequestState)); - - const { register, handleSubmit, formState, watch, setValue, clearErrors } = formAPI; - - const onSubmit = (data: SilenceFormFields) => { + const onSubmit = async (data: SilenceFormFields) => { const { id, startsAt, endsAt, comment, createdBy, matchers: matchersFields } = data; const matchers = matchersFields.map(matcherFieldToMatcher); const payload = pickBy( @@ -128,14 +136,11 @@ export const SilencesEditor = ({ silence, alertManagerSourceName }: Props) => { }, (value) => !!value ) as SilenceCreatePayload; - dispatch( - createOrUpdateSilenceAction({ - alertManagerSourceName, - payload, - exitOnSave: true, - successMessage: `Silence ${payload.id ? 'updated' : 'created'}`, - }) - ); + await createSilence({ datasourceUid: getDatasourceAPIUid(alertManagerSourceName), payload }) + .unwrap() + .then(() => { + locationService.push(makeAMLink('/alerting/silences', alertManagerSourceName)); + }); }; const duration = watch('duration'); @@ -143,6 +148,19 @@ export const SilencesEditor = ({ silence, alertManagerSourceName }: Props) => { const endsAt = watch('endsAt'); const matcherFields = watch('matchers'); + useEffect(() => { + if (silence) { + // Allows the form to correctly initialise when an existing silence is fetch from the backend + reset(getDefaultFormValues(urlSearchParams, silence)); + } + }, [reset, silence, urlSearchParams]); + + useEffect(() => { + if (silenceId) { + getSilence({ id: silenceId, datasourceUid: getDatasourceAPIUid(alertManagerSourceName) }); + } + }, [alertManagerSourceName, getSilence, silenceId]); + // Keep duration and endsAt in sync const [prevDuration, setPrevDuration] = useState(duration); useDebounce( @@ -183,10 +201,21 @@ export const SilencesEditor = ({ silence, alertManagerSourceName }: Props) => { const userLogged = Boolean(config.bootData.user.isSignedIn && config.bootData.user.name); + if (getSilenceIsLoading) { + return ; + } + + const existingSilenceNotFound = + isFetchError(errorGettingExistingSilence) && errorGettingExistingSilence.status === 404; + + if (existingSilenceNotFound) { + return ; + } + return (
-
+
{
- {loading && ( + {isLoading && ( )} - {!loading && } + {!isLoading && } Cancel diff --git a/public/app/features/alerting/unified/components/silences/SilencesTable.tsx b/public/app/features/alerting/unified/components/silences/SilencesTable.tsx index 3d8c21c4b83..bf03af22087 100644 --- a/public/app/features/alerting/unified/components/silences/SilencesTable.tsx +++ b/public/app/features/alerting/unified/components/silences/SilencesTable.tsx @@ -2,15 +2,18 @@ import { css } from '@emotion/css'; import React, { useMemo } from 'react'; import { dateMath, GrafanaTheme2 } from '@grafana/data'; -import { CollapsableSection, Icon, Link, LinkButton, useStyles2, Stack } from '@grafana/ui'; +import { CollapsableSection, Icon, Link, LinkButton, useStyles2, Stack, Alert, LoadingPlaceholder } from '@grafana/ui'; import { useQueryParams } from 'app/core/hooks/useQueryParams'; +import { alertSilencesApi } from 'app/features/alerting/unified/api/alertSilencesApi'; +import { alertmanagerApi } from 'app/features/alerting/unified/api/alertmanagerApi'; +import { featureDiscoveryApi } from 'app/features/alerting/unified/api/featureDiscoveryApi'; +import { SILENCES_POLL_INTERVAL_MS } from 'app/features/alerting/unified/utils/constants'; +import { getDatasourceAPIUid } from 'app/features/alerting/unified/utils/datasource'; import { AlertmanagerAlert, Silence, SilenceState } from 'app/plugins/datasource/alertmanager/types'; -import { useDispatch } from 'app/types'; import { AlertmanagerAction, useAlertmanagerAbility } from '../../hooks/useAbilities'; -import { expireSilenceAction } from '../../state/actions'; import { parseMatchers } from '../../utils/alertmanager'; -import { getSilenceFiltersFromUrlParams, makeAMLink } from '../../utils/misc'; +import { getSilenceFiltersFromUrlParams, makeAMLink, stringifyErrorLike } from '../../utils/misc'; import { Authorize } from '../Authorize'; import { DynamicTable, DynamicTableColumnProps, DynamicTableItemProps } from '../DynamicTable'; import { ActionButton } from '../rules/ActionButton'; @@ -29,12 +32,35 @@ export interface SilenceTableItem extends Silence { type SilenceTableColumnProps = DynamicTableColumnProps; type SilenceTableItemProps = DynamicTableItemProps; interface Props { - silences: Silence[]; - alertManagerAlerts: AlertmanagerAlert[]; alertManagerSourceName: string; } -const SilencesTable = ({ silences, alertManagerAlerts, alertManagerSourceName }: Props) => { +const API_QUERY_OPTIONS = { pollingInterval: SILENCES_POLL_INTERVAL_MS, refetchOnFocus: true }; + +const SilencesTable = ({ alertManagerSourceName }: Props) => { + const { data: alertManagerAlerts = [], isLoading: amAlertsIsLoading } = + alertmanagerApi.endpoints.getAlertmanagerAlerts.useQuery( + { amSourceName: alertManagerSourceName, filter: { silenced: true, active: true, inhibited: true } }, + API_QUERY_OPTIONS + ); + + const { + data: silences = [], + isLoading, + error, + } = alertSilencesApi.endpoints.getSilences.useQuery( + { datasourceUid: getDatasourceAPIUid(alertManagerSourceName) }, + API_QUERY_OPTIONS + ); + + const { currentData: amFeatures } = featureDiscoveryApi.useDiscoverAmFeaturesQuery( + { amSourceName: alertManagerSourceName ?? '' }, + { skip: !alertManagerSourceName } + ); + + const mimirLazyInitError = + stringifyErrorLike(error).includes('the Alertmanager is not configured') && amFeatures?.lazyConfigInit; + const styles = useStyles2(getStyles); const [queryParams] = useQueryParams(); const filteredSilencesNotExpired = useFilteredSilences(silences, false); @@ -69,17 +95,39 @@ const SilencesTable = ({ silences, alertManagerAlerts, alertManagerSourceName }: }); }, [filteredSilencesExpired, alertManagerAlerts]); + if (isLoading || amAlertsIsLoading) { + return ; + } + + if (mimirLazyInitError) { + return ( + + Create a new contact point to create a configuration using the default values or contact your administrator to + set up the Alertmanager. + + ); + } + + if (error) { + const errMessage = stringifyErrorLike(error) || 'Unknown error.'; + return ( + + {errMessage} + + ); + } + return (
{!!silences.length && ( -
+ Add Silence -
+
{ }; const getStyles = (theme: GrafanaTheme2) => ({ - topButtonContainer: css` - display: flex; - flex-direction: row; - justify-content: flex-end; - `, - addNewSilence: css` - margin: ${theme.spacing(2, 0)}; - `, - callout: css` - background-color: ${theme.colors.background.secondary}; - border-top: 3px solid ${theme.colors.info.border}; - border-radius: ${theme.shape.radius.default}; - height: 62px; - display: flex; - flex-direction: row; - align-items: center; + callout: css({ + backgroundColor: theme.colors.background.secondary, + borderTop: `3px solid ${theme.colors.info.border}`, + borderRadius: theme.shape.radius.default, + height: '62px', + display: 'flex', + flexDirection: 'row', + alignItems: 'center', - & > * { - margin-left: ${theme.spacing(1)}; - } - `, - calloutIcon: css` - color: ${theme.colors.info.text}; - `, - editButton: css` - margin-left: ${theme.spacing(0.5)}; - `, + '& > *': { + marginLeft: theme.spacing(1), + }, + }), + calloutIcon: css({ + color: theme.colors.info.text, + }), }); function useColumns(alertManagerSourceName: string) { - const dispatch = useDispatch(); - const styles = useStyles2(getStyles); const [updateSupported, updateAllowed] = useAlertmanagerAbility(AlertmanagerAction.UpdateSilence); + const [expireSilence] = alertSilencesApi.endpoints.expireSilence.useMutation(); return useMemo((): SilenceTableColumnProps[] => { - const handleExpireSilenceClick = (id: string) => { - dispatch(expireSilenceAction(alertManagerSourceName, id)); + const handleExpireSilenceClick = (silenceId: string) => { + expireSilence({ datasourceUid: getDatasourceAPIUid(alertManagerSourceName), silenceId }); }; const columns: SilenceTableColumnProps[] = [ { @@ -268,10 +304,9 @@ function useColumns(alertManagerSourceName: string) { )} {silence.status.state !== 'expired' && ( )} @@ -281,6 +316,6 @@ function useColumns(alertManagerSourceName: string) { }); } return columns; - }, [alertManagerSourceName, dispatch, styles.editButton, updateAllowed, updateSupported]); + }, [alertManagerSourceName, expireSilence, updateAllowed, updateSupported]); } export default SilencesTable; diff --git a/public/app/features/alerting/unified/home/GettingStarted.tsx b/public/app/features/alerting/unified/home/GettingStarted.tsx index 6415801a058..1087fda355c 100644 --- a/public/app/features/alerting/unified/home/GettingStarted.tsx +++ b/public/app/features/alerting/unified/home/GettingStarted.tsx @@ -3,23 +3,8 @@ import React from 'react'; import SVG from 'react-inlinesvg'; import { GrafanaTheme2 } from '@grafana/data'; -import { EmbeddedScene, SceneFlexLayout, SceneFlexItem, SceneReactObject } from '@grafana/scenes'; import { useStyles2, useTheme2, Stack, Text, TextLink } from '@grafana/ui'; -export const getOverviewScene = () => { - return new EmbeddedScene({ - body: new SceneFlexLayout({ - children: [ - new SceneFlexItem({ - body: new SceneReactObject({ - component: GettingStarted, - }), - }), - ], - }), - }); -}; - export default function GettingStarted() { const theme = useTheme2(); const styles = useStyles2(getWelcomePageStyles); @@ -110,9 +95,7 @@ export function WelcomeHeader({ className }: { className?: string }) { const styles = useStyles2(getWelcomeHeaderStyles); return ( -
-
Learn about problems in your systems moments after they occur
- + -
+ ); } const getWelcomeHeaderStyles = (theme: GrafanaTheme2) => ({ - welcomeHeaderWrapper: css({ - color: theme.colors.text.primary, - }), - subtitle: css({ - color: theme.colors.text.secondary, - paddingBottom: theme.spacing(2), - }), ctaContainer: css({ padding: theme.spacing(2), display: 'flex', @@ -195,6 +171,7 @@ function WelcomeCTABox({ title, description, href, hrefText }: WelcomeCTABoxProp const getWelcomeCTAButtonStyles = (theme: GrafanaTheme2) => ({ container: css({ + color: theme.colors.text.primary, flex: 1, minWidth: '240px', display: 'grid', diff --git a/public/app/features/alerting/unified/home/Home.tsx b/public/app/features/alerting/unified/home/Home.tsx index c5656587529..d036a8e9f3a 100644 --- a/public/app/features/alerting/unified/home/Home.tsx +++ b/public/app/features/alerting/unified/home/Home.tsx @@ -1,74 +1,54 @@ import React, { useState } from 'react'; import { config } from '@grafana/runtime'; -import { SceneApp, SceneAppPage } from '@grafana/scenes'; -import { usePageNav } from 'app/core/components/Page/usePageNav'; -import { PluginPageContext, PluginPageContextType } from 'app/features/plugins/components/PluginPageContext'; +import { Box, Stack, Tab, TabContent, TabsBar } from '@grafana/ui'; +import { AlertingPageWrapper } from '../components/AlertingPageWrapper'; import { isLocalDevEnv, isOpenSourceEdition } from '../utils/misc'; -import { getOverviewScene, WelcomeHeader } from './GettingStarted'; +import GettingStarted, { WelcomeHeader } from './GettingStarted'; import { getInsightsScenes } from './Insights'; - -let homeApp: SceneApp | undefined; - -export function getHomeApp(insightsEnabled: boolean) { - if (homeApp) { - return homeApp; - } - - if (insightsEnabled) { - homeApp = new SceneApp({ - pages: [ - new SceneAppPage({ - title: 'Alerting', - subTitle: , - url: '/alerting', - hideFromBreadcrumbs: true, - tabs: [ - new SceneAppPage({ - title: 'Insights', - url: '/alerting/home/insights', - getScene: getInsightsScenes, - }), - new SceneAppPage({ - title: 'Get started', - url: '/alerting/home/overview', - getScene: getOverviewScene, - }), - ], - }), - ], - }); - } else { - homeApp = new SceneApp({ - pages: [ - new SceneAppPage({ - title: 'Alerting', - subTitle: , - url: '/alerting', - hideFromBreadcrumbs: true, - getScene: getOverviewScene, - }), - ], - }); - } - - return homeApp; -} +import { PluginIntegrations } from './PluginIntegrations'; export default function Home() { const insightsEnabled = (!isOpenSourceEdition() || isLocalDevEnv()) && Boolean(config.featureToggles.alertingInsights); - const appScene = getHomeApp(insightsEnabled); - - const sectionNav = usePageNav('alerting')!; - const [pluginContext] = useState({ sectionNav }); + const [activeTab, setActiveTab] = useState<'insights' | 'overview'>(insightsEnabled ? 'insights' : 'overview'); + const insightsScene = getInsightsScenes(); return ( - - - + + + + + + + + {insightsEnabled && ( + setActiveTab('insights')} + /> + )} + setActiveTab('overview')} + /> + + + {activeTab === 'insights' && } + {activeTab === 'overview' && } + + + ); } diff --git a/public/app/features/alerting/unified/home/PluginIntegrations.tsx b/public/app/features/alerting/unified/home/PluginIntegrations.tsx new file mode 100644 index 00000000000..1e9baf298b6 --- /dev/null +++ b/public/app/features/alerting/unified/home/PluginIntegrations.tsx @@ -0,0 +1,45 @@ +import { css } from '@emotion/css'; +import React from 'react'; + +import { PluginExtensionPoints } from '@grafana/data'; +import { GrafanaTheme2 } from '@grafana/data/'; +import { usePluginComponentExtensions } from '@grafana/runtime'; +import { Stack, Text } from '@grafana/ui'; +import { useStyles2 } from '@grafana/ui/'; + +export function PluginIntegrations() { + const styles = useStyles2(getStyles); + + const { extensions } = usePluginComponentExtensions({ + extensionPointId: PluginExtensionPoints.AlertingHomePage, + limitPerPlugin: 1, + }); + + if (extensions.length === 0) { + return null; + } + + return ( + + + Speed up your alerts creation now by using one of our tailored apps + + + {extensions.map((extension) => ( +
+ +
+ ))} +
+
+ ); +} + +const getStyles = (theme: GrafanaTheme2) => ({ + box: css({ + padding: theme.spacing(2), + flex: 1, + backgroundColor: theme.colors.background.secondary, + maxWidth: '460px', + }), +}); diff --git a/public/app/features/alerting/unified/hooks/__snapshots__/useAbilities.test.tsx.snap b/public/app/features/alerting/unified/hooks/__snapshots__/useAbilities.test.tsx.snap index 2b0417df565..02137bf67eb 100644 --- a/public/app/features/alerting/unified/hooks/__snapshots__/useAbilities.test.tsx.snap +++ b/public/app/features/alerting/unified/hooks/__snapshots__/useAbilities.test.tsx.snap @@ -18,6 +18,10 @@ exports[`AlertRule abilities should report no permissions while we are loading d false, false, ], + "pause-alert-rule": [ + false, + false, + ], "silence-alert-rule": [ false, false, @@ -51,6 +55,10 @@ exports[`AlertRule abilities should report that all actions are supported for a true, false, ], + "pause-alert-rule": [ + true, + false, + ], "silence-alert-rule": [ true, false, diff --git a/public/app/features/alerting/unified/hooks/useAbilities.ts b/public/app/features/alerting/unified/hooks/useAbilities.ts index 9906553bf7b..9ba6999239c 100644 --- a/public/app/features/alerting/unified/hooks/useAbilities.ts +++ b/public/app/features/alerting/unified/hooks/useAbilities.ts @@ -9,7 +9,7 @@ import { alertmanagerApi } from '../api/alertmanagerApi'; import { useAlertmanager } from '../state/AlertmanagerContext'; import { getInstancesPermissions, getNotificationsPermissions, getRulesPermissions } from '../utils/access-control'; import { GRAFANA_RULES_SOURCE_NAME } from '../utils/datasource'; -import { isFederatedRuleGroup, isGrafanaRulerRule } from '../utils/rules'; +import { isFederatedRuleGroup, isGrafanaRulerRule, isPluginProvidedRule } from '../utils/rules'; import { useIsRuleEditable } from './useIsRuleEditable'; @@ -69,6 +69,7 @@ export enum AlertRuleAction { Explore = 'explore-alert-rule', Silence = 'silence-alert-rule', ModifyExport = 'modify-export-rule', + Pause = 'pause-alert-rule', } // this enum lists all of the actions we can perform within alerting in general, not linked to a specific @@ -147,9 +148,10 @@ export function useAllAlertRuleAbilities(rule: CombinedRule): Abilities = { - [AlertRuleAction.Duplicate]: toAbility(MaybeSupported, rulesPermissions.create), + [AlertRuleAction.Duplicate]: toAbility(duplicateSupported, rulesPermissions.create), [AlertRuleAction.View]: toAbility(AlwaysSupported, rulesPermissions.read), [AlertRuleAction.Update]: [MaybeSupportedUnlessImmutable, isEditable ?? false], [AlertRuleAction.Delete]: [MaybeSupportedUnlessImmutable, isRemovable ?? false], [AlertRuleAction.Explore]: toAbility(AlwaysSupported, AccessControlAction.DataSourcesExplore), [AlertRuleAction.Silence]: canSilence, [AlertRuleAction.ModifyExport]: [isGrafanaManagedAlertRule, exportAllowed], + [AlertRuleAction.Pause]: [MaybeSupportedUnlessImmutable && isGrafanaManagedAlertRule, isEditable ?? false], }; return abilities; diff --git a/public/app/features/alerting/unified/mockApi.ts b/public/app/features/alerting/unified/mockApi.ts index fe4403d3f97..77bf8b968fe 100644 --- a/public/app/features/alerting/unified/mockApi.ts +++ b/public/app/features/alerting/unified/mockApi.ts @@ -5,6 +5,7 @@ import { setupServer, SetupServer } from 'msw/node'; import { DataSourceInstanceSettings, PluginMeta } from '@grafana/data'; import { setBackendSrv } from '@grafana/runtime'; import { AlertRuleUpdated } from 'app/features/alerting/unified/api/alertRuleApi'; +import allHandlers from 'app/features/alerting/unified/mocks/server/handlers'; import { DashboardDTO, FolderDTO, NotifierDTO, OrgUser } from 'app/types'; import { PromBuildInfoResponse, @@ -424,10 +425,10 @@ export function mockDashboardApi(server: SetupServer) { }; } +const server = setupServer(...allHandlers); + // Creates a MSW server and sets up beforeAll, afterAll and beforeEach handlers for it export function setupMswServer() { - const server = setupServer(); - beforeAll(() => { setBackendSrv(backendSrv); server.listen({ onUnhandledRequest: 'error' }); @@ -443,3 +444,5 @@ export function setupMswServer() { return server; } + +export default server; diff --git a/public/app/features/alerting/unified/mocks.ts b/public/app/features/alerting/unified/mocks.ts index 458d97df016..22b239e2bd1 100644 --- a/public/app/features/alerting/unified/mocks.ts +++ b/public/app/features/alerting/unified/mocks.ts @@ -10,6 +10,8 @@ import { DataSourceJsonData, DataSourcePluginMeta, DataSourceRef, + PluginExtensionLink, + PluginExtensionTypes, PluginMeta, PluginType, ScopedVars, @@ -18,6 +20,7 @@ import { import { DataSourceSrv, GetDataSourceListFilters, config } from '@grafana/runtime'; import { defaultDashboard } from '@grafana/schema'; import { contextSrv } from 'app/core/services/context_srv'; +import { parseMatchers } from 'app/features/alerting/unified/utils/alertmanager'; import { DatasourceSrv } from 'app/features/plugins/datasource_srv'; import { AlertManagerCortexConfig, @@ -306,6 +309,18 @@ export const mockSilence = (partial: Partial = {}): Silence => { }; }; +export const MOCK_SILENCE_ID_EXISTING = 'f209e273-0e4e-434f-9f66-e72f092025a2'; + +export const mockSilences = [ + mockSilence({ id: MOCK_SILENCE_ID_EXISTING }), + mockSilence({ + id: 'ce031625-61c7-47cd-9beb-8760bccf0ed7', + matchers: parseMatchers('foo!=bar'), + comment: 'Catch all', + }), + mockSilence({ id: '145884a8-ee20-4864-9f84-661305fb7d82', status: { state: SilenceState.Expired } }), +]; + export const mockNotifiersState = (partial: Partial = {}): NotifiersState => { return { email: [ @@ -694,6 +709,18 @@ export function getCloudRule(override?: Partial) { }); } +export function mockPluginLinkExtension(extension: Partial): PluginExtensionLink { + return { + type: PluginExtensionTypes.link, + id: 'plugin-id', + pluginId: 'grafana-test-app', + title: 'Test plugin link', + description: 'Test plugin link', + path: '/test', + ...extension, + }; +} + export function mockAlertWithState(state: GrafanaAlertState, labels?: {}): Alert { return { activeAt: '', annotations: {}, labels: labels || {}, state: state, value: '' }; } @@ -728,22 +755,27 @@ export function mockDashboardDto( }; } -export const onCallPluginMetaMock: PluginMeta = { - name: 'Grafana OnCall', - id: 'grafana-oncall-app', - type: PluginType.app, - module: 'plugins/grafana-oncall-app/module', - baseUrl: 'public/plugins/grafana-oncall-app', - info: { - author: { name: 'Grafana Labs' }, - description: 'Grafana OnCall', - updated: '', - version: '', - links: [], - logos: { - small: '', - large: '', +export const getMockPluginMeta: (id: string, name: string) => PluginMeta = (id, name) => { + return { + name, + id, + type: PluginType.app, + module: `plugins/${id}/module`, + baseUrl: `public/plugins/${id}`, + info: { + author: { name: 'Grafana Labs' }, + description: name, + updated: '', + version: '', + links: [], + logos: { + small: '', + large: '', + }, + screenshots: [], }, - screenshots: [], - }, + }; }; + +export const labelsPluginMetaMock = getMockPluginMeta('grafana-labels-app', 'Grafana IRM Labels'); +export const onCallPluginMetaMock = getMockPluginMeta('grafana-oncall-app', 'Grafana OnCall'); diff --git a/public/app/features/alerting/unified/mocks/alertmanagerApi.ts b/public/app/features/alerting/unified/mocks/alertmanagerApi.ts index 1db4862f414..9083fd30a89 100644 --- a/public/app/features/alerting/unified/mocks/alertmanagerApi.ts +++ b/public/app/features/alerting/unified/mocks/alertmanagerApi.ts @@ -1,9 +1,13 @@ import { http, HttpResponse } from 'msw'; import { SetupServer } from 'msw/node'; +import { MOCK_SILENCE_ID_EXISTING, mockAlertmanagerAlert } from 'app/features/alerting/unified/mocks'; +import { MOCK_DATASOURCE_UID_BROKEN_ALERTMANAGER } from 'app/features/alerting/unified/mocks/datasources'; + import { AlertmanagerChoice, AlertManagerCortexConfig, + AlertState, ExternalAlertmanagersResponse, } from '../../../../plugins/datasource/alertmanager/types'; import { AlertmanagersChoiceResponse } from '../api/alertmanagerApi'; @@ -13,8 +17,12 @@ export const defaultAlertmanagerChoiceResponse: AlertmanagersChoiceResponse = { alertmanagersChoice: AlertmanagerChoice.Internal, numExternalAlertmanagers: 0, }; + +export const alertmanagerChoiceHandler = (response = defaultAlertmanagerChoiceResponse) => + http.get('/api/v1/ngalert', () => HttpResponse.json(response)); + export function mockAlertmanagerChoiceResponse(server: SetupServer, response: AlertmanagersChoiceResponse) { - server.use(http.get('/api/v1/ngalert', () => HttpResponse.json(response))); + server.use(alertmanagerChoiceHandler(response)); } export const emptyExternalAlertmanagersResponse: ExternalAlertmanagersResponse = { @@ -38,3 +46,20 @@ export function mockAlertmanagerConfigResponse( ) ); } + +export const alertmanagerAlertsListHandler = () => + http.get<{ datasourceUid: string }>('/api/alertmanager/:datasourceUid/api/v2/alerts', ({ params }) => { + if (params.datasourceUid === MOCK_DATASOURCE_UID_BROKEN_ALERTMANAGER) { + return HttpResponse.json({ traceId: '' }, { status: 502 }); + } + return HttpResponse.json([ + mockAlertmanagerAlert({ + labels: { foo: 'bar', buzz: 'bazz' }, + status: { state: AlertState.Suppressed, silencedBy: [MOCK_SILENCE_ID_EXISTING], inhibitedBy: [] }, + }), + mockAlertmanagerAlert({ + labels: { foo: 'bar', buzz: 'bazz' }, + status: { state: AlertState.Suppressed, silencedBy: [MOCK_SILENCE_ID_EXISTING], inhibitedBy: [] }, + }), + ]); + }); diff --git a/public/app/features/alerting/unified/mocks/datasources.ts b/public/app/features/alerting/unified/mocks/datasources.ts new file mode 100644 index 00000000000..04f579a7c24 --- /dev/null +++ b/public/app/features/alerting/unified/mocks/datasources.ts @@ -0,0 +1,10 @@ +import { HttpResponse, http } from 'msw'; + +// TODO: Add more accurate endpoint responses as tests require +export const datasourceBuildInfoHandler = () => + http.get('/api/datasources/proxy/uid/:datasourceUid/api/v1/status/buildinfo', () => HttpResponse.json({})); + +/** UID of the alertmanager that is expected to be broken in tests */ +export const MOCK_DATASOURCE_UID_BROKEN_ALERTMANAGER = 'FwkfQfEmYlAthB'; +/** Display name of the alertmanager that is expected to be broken in tests */ +export const MOCK_DATASOURCE_NAME_BROKEN_ALERTMANAGER = 'broken alertmanager'; diff --git a/public/app/features/alerting/unified/mocks/folders.ts b/public/app/features/alerting/unified/mocks/folders.ts new file mode 100644 index 00000000000..9b20e2d826f --- /dev/null +++ b/public/app/features/alerting/unified/mocks/folders.ts @@ -0,0 +1,6 @@ +import { HttpResponse, http } from 'msw'; + +import { mockFolder } from 'app/features/alerting/unified/mocks'; + +export const folderHandler = (response = mockFolder()) => + http.get(`/api/folders/:folderUid`, () => HttpResponse.json(response)); diff --git a/public/app/features/alerting/unified/mocks/plugins.ts b/public/app/features/alerting/unified/mocks/plugins.ts index 48c98edb33b..54d0038f757 100644 --- a/public/app/features/alerting/unified/mocks/plugins.ts +++ b/public/app/features/alerting/unified/mocks/plugins.ts @@ -1,17 +1,12 @@ import { http, HttpResponse } from 'msw'; -import { SetupServer } from 'msw/lib/node'; import { PluginMeta } from '@grafana/data'; +import { plugins } from 'app/features/alerting/unified/testSetup/plugins'; -import { SupportedPlugin } from '../types/pluginBridges'; - -export function mockPluginSettings(server: SetupServer, plugin: SupportedPlugin, response?: PluginMeta) { - server.use( - http.get(`/api/plugins/${plugin}/settings`, () => { - if (response) { - return HttpResponse.json(response); - } - return HttpResponse.json({}, { status: 404 }); - }) - ); -} +export const pluginsHandler = (pluginsArray: PluginMeta[] = plugins) => + http.get<{ pluginId: string }>(`/api/plugins/:pluginId/settings`, ({ params: { pluginId } }) => { + const matchingPlugin = pluginsArray.find((plugin) => plugin.id === pluginId); + return matchingPlugin + ? HttpResponse.json(matchingPlugin) + : HttpResponse.json({ message: 'Plugin not found, no installed plugin with that id' }, { status: 404 }); + }); diff --git a/public/app/features/alerting/unified/mocks/server/configure.ts b/public/app/features/alerting/unified/mocks/server/configure.ts new file mode 100644 index 00000000000..04b17f571ac --- /dev/null +++ b/public/app/features/alerting/unified/mocks/server/configure.ts @@ -0,0 +1,15 @@ +import server from 'app/features/alerting/unified/mockApi'; +import { alertmanagerChoiceHandler } from 'app/features/alerting/unified/mocks/alertmanagerApi'; +import { AlertmanagerChoice } from 'app/plugins/datasource/alertmanager/types'; + +/** + * Makes the mock server respond in a way that matches the different behaviour associated with + * Alertmanager choices and the number of configured external alertmanagers + */ +export const setAlertmanagerChoices = (alertmanagersChoice: AlertmanagerChoice, numExternalAlertmanagers: number) => { + const response = { + alertmanagersChoice, + numExternalAlertmanagers, + }; + server.use(alertmanagerChoiceHandler(response)); +}; diff --git a/public/app/features/alerting/unified/mocks/server/events.ts b/public/app/features/alerting/unified/mocks/server/events.ts new file mode 100644 index 00000000000..1feb57d5a89 --- /dev/null +++ b/public/app/features/alerting/unified/mocks/server/events.ts @@ -0,0 +1,23 @@ +import { HttpHandler, matchRequestUrl } from 'msw'; + +import server from 'app/features/alerting/unified/mockApi'; + +/** + * Wait for the mock server to receive a request for the given method + url combination, + * and resolve with information about the request that was made + * + * @deprecated Try not to use this 🙏 instead aim to assert against UI side effects + */ +export function waitForServerRequest(handler: HttpHandler) { + const { method, path } = handler.info; + return new Promise((resolve) => { + server.events.on('request:match', ({ request }) => { + const matchesMethod = request.method.toLowerCase() === String(method).toLowerCase(); + const matchesUrl = matchRequestUrl(new URL(request.url), path); + + if (matchesMethod && matchesUrl) { + resolve(request); + } + }); + }); +} diff --git a/public/app/features/alerting/unified/mocks/server/handlers.ts b/public/app/features/alerting/unified/mocks/server/handlers.ts new file mode 100644 index 00000000000..99871367855 --- /dev/null +++ b/public/app/features/alerting/unified/mocks/server/handlers.ts @@ -0,0 +1,36 @@ +/** + * Contains all handlers that are required for test rendering of components within Alerting + */ + +import { + alertmanagerAlertsListHandler, + alertmanagerChoiceHandler, +} from 'app/features/alerting/unified/mocks/alertmanagerApi'; +import { datasourceBuildInfoHandler } from 'app/features/alerting/unified/mocks/datasources'; +import { folderHandler } from 'app/features/alerting/unified/mocks/folders'; +import { pluginsHandler } from 'app/features/alerting/unified/mocks/plugins'; +import { + silenceCreateHandler, + silenceGetHandler, + silencesListHandler, +} from 'app/features/alerting/unified/mocks/silences'; + +/** + * All mock handlers that are required across Alerting tests + */ +const allHandlers = [ + alertmanagerChoiceHandler(), + alertmanagerAlertsListHandler(), + + folderHandler(), + + pluginsHandler(), + + silencesListHandler(), + silenceGetHandler(), + silenceCreateHandler(), + + datasourceBuildInfoHandler(), +]; + +export default allHandlers; diff --git a/public/app/features/alerting/unified/mocks/silences.ts b/public/app/features/alerting/unified/mocks/silences.ts new file mode 100644 index 00000000000..8ef9944d83f --- /dev/null +++ b/public/app/features/alerting/unified/mocks/silences.ts @@ -0,0 +1,32 @@ +import { HttpResponse, http } from 'msw'; + +import { mockSilences } from 'app/features/alerting/unified/mocks'; +import { MOCK_DATASOURCE_UID_BROKEN_ALERTMANAGER } from 'app/features/alerting/unified/mocks/datasources'; + +////////////// +// Silences // +////////////// + +export const silencesListHandler = (silences = mockSilences) => + http.get<{ datasourceUid: string }>('/api/alertmanager/:datasourceUid/api/v2/silences', ({ params }) => { + if (params.datasourceUid === MOCK_DATASOURCE_UID_BROKEN_ALERTMANAGER) { + return HttpResponse.json({ traceId: '' }, { status: 502 }); + } + return HttpResponse.json(silences); + }); + +export const silenceGetHandler = () => + http.get<{ uuid: string }>('/api/alertmanager/:datasourceUid/api/v2/silence/:uuid', ({ params }) => { + const { uuid } = params; + const matchingMockSilence = mockSilences.find((silence) => silence.id === uuid); + if (matchingMockSilence) { + return HttpResponse.json(matchingMockSilence); + } + + return HttpResponse.json({ message: 'silence not found' }, { status: 404 }); + }); + +export const silenceCreateHandler = () => + http.post('/api/alertmanager/:datasourceUid/api/v2/silences', () => + HttpResponse.json({ silenceId: '4bda5b38-7939-4887-9ec2-16323b8e3b4e' }) + ); diff --git a/public/app/features/alerting/unified/plugins/PluginOriginBadge.tsx b/public/app/features/alerting/unified/plugins/PluginOriginBadge.tsx new file mode 100644 index 00000000000..437d56fd192 --- /dev/null +++ b/public/app/features/alerting/unified/plugins/PluginOriginBadge.tsx @@ -0,0 +1,32 @@ +import React from 'react'; +import { useAsync } from 'react-use'; + +import { Badge, Tooltip } from '@grafana/ui'; + +import { getPluginSettings } from '../../../plugins/pluginSettings'; + +interface PluginOriginBadgeProps { + pluginId: string; +} + +export function PluginOriginBadge({ pluginId }: PluginOriginBadgeProps) { + const { value: pluginMeta } = useAsync(() => getPluginSettings(pluginId)); + + const logo = pluginMeta?.info.logos?.small; + + const badgeIcon = logo ? ( + {pluginMeta?.name} + ) : ( + + ); + + const tooltipContent = pluginMeta + ? `This rule is managed by the ${pluginMeta?.name} plugin` + : `This rule is managed by a plugin`; + + return ( + +
{badgeIcon}
+
+ ); +} diff --git a/public/app/features/alerting/unified/plugins/useRulePluginLinkExtensions.ts b/public/app/features/alerting/unified/plugins/useRulePluginLinkExtensions.ts new file mode 100644 index 00000000000..237dbbe8154 --- /dev/null +++ b/public/app/features/alerting/unified/plugins/useRulePluginLinkExtensions.ts @@ -0,0 +1,92 @@ +import { useMemo } from 'react'; + +import { PluginExtensionPoints } from '@grafana/data'; +import { usePluginLinkExtensions } from '@grafana/runtime'; +import { CombinedRule } from 'app/types/unified-alerting'; +import { PromRuleType } from 'app/types/unified-alerting-dto'; + +import { getRulePluginOrigin } from '../utils/rules'; + +interface BaseRuleExtensionContext { + name: string; + namespace: string; + group: string; + expression: string; + labels: Record; +} + +export interface AlertingRuleExtensionContext extends BaseRuleExtensionContext { + annotations: Record; +} + +export interface RecordingRuleExtensionContext extends BaseRuleExtensionContext {} + +export function useRulePluginLinkExtension(rule: CombinedRule) { + const ruleExtensionPoint = useRuleExtensionPoint(rule); + const { extensions } = usePluginLinkExtensions(ruleExtensionPoint); + + const ruleOrigin = getRulePluginOrigin(rule); + const ruleType = rule.promRule?.type; + if (!ruleOrigin || !ruleType) { + return []; + } + + const { pluginId } = ruleOrigin; + + return extensions.filter((extension) => extension.pluginId === pluginId); +} + +export interface PluginRuleExtensionParam { + pluginId: string; + rule: CombinedRule; +} + +interface AlertingRuleExtensionPoint { + extensionPointId: PluginExtensionPoints.AlertingAlertingRuleAction; + context: AlertingRuleExtensionContext; +} + +interface RecordingRuleExtensionPoint { + extensionPointId: PluginExtensionPoints.AlertingRecordingRuleAction; + context: RecordingRuleExtensionContext; +} + +interface EmptyExtensionPoint { + extensionPointId: ''; +} + +type RuleExtensionPoint = AlertingRuleExtensionPoint | RecordingRuleExtensionPoint | EmptyExtensionPoint; + +function useRuleExtensionPoint(rule: CombinedRule): RuleExtensionPoint { + return useMemo(() => { + const ruleType = rule.promRule?.type; + + switch (ruleType) { + case PromRuleType.Alerting: + return { + extensionPointId: PluginExtensionPoints.AlertingAlertingRuleAction, + context: { + name: rule.name, + namespace: rule.namespace.name, + group: rule.group.name, + expression: rule.query, + labels: rule.labels, + annotations: rule.annotations, + }, + }; + case PromRuleType.Recording: + return { + extensionPointId: PluginExtensionPoints.AlertingRecordingRuleAction, + context: { + name: rule.name, + namespace: rule.namespace.name, + group: rule.group.name, + expression: rule.query, + labels: rule.labels, + }, + }; + default: + return { extensionPointId: '' }; + } + }, [rule]); +} diff --git a/public/app/features/alerting/unified/state/actions.ts b/public/app/features/alerting/unified/state/actions.ts index caf8d9db0a0..d362b7282b9 100644 --- a/public/app/features/alerting/unified/state/actions.ts +++ b/public/app/features/alerting/unified/state/actions.ts @@ -4,15 +4,12 @@ import { isEmpty } from 'lodash'; import { locationService } from '@grafana/runtime'; import { logMeasurement } from '@grafana/runtime/src/utils/logging'; import { - AlertmanagerAlert, AlertManagerCortexConfig, AlertmanagerGroup, ExternalAlertmanagerConfig, ExternalAlertmanagersResponse, Matcher, Receiver, - Silence, - SilenceCreatePayload, TestReceiversAlert, } from 'app/plugins/datasource/alertmanager/types'; import { FolderDTO, NotifierDTO, StoreState, ThunkResult } from 'app/types'; @@ -45,14 +42,10 @@ import { } from '../Analytics'; import { addAlertManagers, - createOrUpdateSilence, deleteAlertManagerConfig, - expireSilence, fetchAlertGroups, - fetchAlerts, fetchExternalAlertmanagerConfig, fetchExternalAlertmanagers, - fetchSilences, testReceivers, updateAlertManagerConfig, } from '../api/alertmanager'; @@ -204,17 +197,6 @@ export function fetchPromAndRulerRulesAction({ }; } -export const fetchSilencesAction = createAsyncThunk( - 'unifiedalerting/fetchSilences', - (alertManagerSourceName: string): Promise => { - const fetchSilencesWithLogging = withPerformanceLogging('unifiedalerting/fetchSilences', fetchSilences, { - dataSourceName: alertManagerSourceName, - }); - - return withSerializedError(fetchSilencesWithLogging(alertManagerSourceName)); - } -); - // this will only trigger ruler rules fetch if rules are not loaded yet and request is not in flight export function fetchRulerRulesIfNotFetchedYet(rulesSourceName: string): ThunkResult { return (dispatch, getStore) => { @@ -561,47 +543,6 @@ export const updateAlertManagerConfigAction = createAsyncThunk => - withSerializedError(fetchAlerts(alertManagerSourceName, [], true, true, true)) -); - -export const expireSilenceAction = (alertManagerSourceName: string, silenceId: string): ThunkResult => { - return async (dispatch) => { - await withAppEvents(expireSilence(alertManagerSourceName, silenceId), { - successMessage: 'Silence expired.', - }); - dispatch(fetchSilencesAction(alertManagerSourceName)); - dispatch(fetchAmAlertsAction(alertManagerSourceName)); - }; -}; - -type UpdateSilenceActionOptions = { - alertManagerSourceName: string; - payload: SilenceCreatePayload; - exitOnSave: boolean; - successMessage?: string; -}; - -export const createOrUpdateSilenceAction = createAsyncThunk( - 'unifiedalerting/updateSilence', - ({ alertManagerSourceName, payload, exitOnSave, successMessage }): Promise => - withAppEvents( - withSerializedError( - (async () => { - await createOrUpdateSilence(alertManagerSourceName, payload); - if (exitOnSave) { - locationService.push(makeAMLink('/alerting/silences', alertManagerSourceName)); - } - })() - ), - { - successMessage, - } - ) -); - export const deleteReceiverAction = (receiverName: string, alertManagerSourceName: string): ThunkResult => { return async (dispatch) => { const config = await dispatch( diff --git a/public/app/features/alerting/unified/state/reducers.ts b/public/app/features/alerting/unified/state/reducers.ts index c6e2c1c531b..8783f479877 100644 --- a/public/app/features/alerting/unified/state/reducers.ts +++ b/public/app/features/alerting/unified/state/reducers.ts @@ -3,10 +3,8 @@ import { combineReducers } from 'redux'; import { createAsyncMapSlice, createAsyncSlice } from '../utils/redux'; import { - createOrUpdateSilenceAction, deleteAlertManagerConfigAction, fetchAlertGroupsAction, - fetchAmAlertsAction, fetchEditableRuleAction, fetchExternalAlertmanagersAction, fetchExternalAlertmanagersConfigAction, @@ -16,7 +14,6 @@ import { fetchPromRulesAction, fetchRulerRulesAction, fetchRulesSourceBuildInfoAction, - fetchSilencesAction, saveRuleFormAction, testReceiversAction, updateAlertManagerConfigAction, @@ -32,8 +29,6 @@ export const reducer = combineReducers({ promRules: createAsyncMapSlice('promRules', fetchPromRulesAction, ({ rulesSourceName }) => rulesSourceName).reducer, rulerRules: createAsyncMapSlice('rulerRules', fetchRulerRulesAction, ({ rulesSourceName }) => rulesSourceName) .reducer, - silences: createAsyncMapSlice('silences', fetchSilencesAction, (alertManagerSourceName) => alertManagerSourceName) - .reducer, ruleForm: combineReducers({ saveRule: createAsyncSlice('saveRule', saveRuleFormAction).reducer, existingRule: createAsyncSlice('existingRule', fetchEditableRuleAction).reducer, @@ -41,9 +36,6 @@ export const reducer = combineReducers({ grafanaNotifiers: createAsyncSlice('grafanaNotifiers', fetchGrafanaNotifiersAction).reducer, saveAMConfig: createAsyncSlice('saveAMConfig', updateAlertManagerConfigAction).reducer, deleteAMConfig: createAsyncSlice('deleteAMConfig', deleteAlertManagerConfigAction).reducer, - updateSilence: createAsyncSlice('updateSilence', createOrUpdateSilenceAction).reducer, - amAlerts: createAsyncMapSlice('amAlerts', fetchAmAlertsAction, (alertManagerSourceName) => alertManagerSourceName) - .reducer, folders: createAsyncMapSlice('folders', fetchFolderAction, (uid) => uid).reducer, amAlertGroups: createAsyncMapSlice( 'amAlertGroups', diff --git a/public/app/features/alerting/unified/testSetup/plugins.ts b/public/app/features/alerting/unified/testSetup/plugins.ts new file mode 100644 index 00000000000..9cd8041afdf --- /dev/null +++ b/public/app/features/alerting/unified/testSetup/plugins.ts @@ -0,0 +1,94 @@ +import { RequestHandler } from 'msw'; + +import { PluginMeta, PluginType } from '@grafana/data'; +import { config } from '@grafana/runtime'; + +import { pluginsHandler } from '../mocks/plugins'; + +export function setupPlugins(plugins: PluginMeta[]): { apiHandlers: RequestHandler[] } { + plugins.forEach((plugin) => { + config.apps[plugin.id] = { + id: plugin.id, + path: plugin.baseUrl, + preload: true, + version: plugin.info.version, + angular: plugin.angular ?? { detected: false, hideDeprecation: false }, + }; + }); + + return { + apiHandlers: [pluginsHandler(plugins)], + }; +} + +export const plugins: PluginMeta[] = [ + { + id: 'grafana-slo-app', + name: 'SLO dashboard', + type: PluginType.app, + enabled: true, + info: { + author: { + name: 'Grafana Labs', + url: '', + }, + description: 'Create and manage Service Level Objectives', + links: [], + logos: { + small: 'public/plugins/grafana-slo-app/img/logo.svg', + large: 'public/plugins/grafana-slo-app/img/logo.svg', + }, + screenshots: [], + version: 'local-dev', + updated: '2024-04-09', + }, + module: 'public/plugins/grafana-slo-app/module.js', + baseUrl: 'public/plugins/grafana-slo-app', + }, + { + id: 'grafana-incident-app', + name: 'Incident management', + type: PluginType.app, + enabled: true, + info: { + author: { + name: 'Grafana Labs', + url: '', + }, + description: 'Incident management', + links: [], + logos: { + small: 'public/plugins/grafana-incident-app/img/logo.svg', + large: 'public/plugins/grafana-incident-app/img/logo.svg', + }, + screenshots: [], + version: 'local-dev', + updated: '2024-04-09', + }, + module: 'public/plugins/grafana-incident-app/module.js', + baseUrl: 'public/plugins/grafana-incident-app', + }, + { + id: 'grafana-asserts-app', + name: 'Asserts', + type: PluginType.app, + enabled: true, + info: { + author: { + name: 'Grafana Labs', + url: '', + }, + description: 'Asserts', + links: [], + logos: { + small: 'public/plugins/grafana-asserts-app/img/logo.svg', + large: 'public/plugins/grafana-asserts-app/img/logo.svg', + }, + screenshots: [], + version: 'local-dev', + updated: '2024-04-09', + }, + module: 'public/plugins/grafana-asserts-app/module.js', + baseUrl: 'public/plugins/grafana-asserts-app', + }, +]; diff --git a/public/app/features/alerting/unified/types/pluginBridges.ts b/public/app/features/alerting/unified/types/pluginBridges.ts index bb761008393..fbe809743b6 100644 --- a/public/app/features/alerting/unified/types/pluginBridges.ts +++ b/public/app/features/alerting/unified/types/pluginBridges.ts @@ -2,4 +2,5 @@ export enum SupportedPlugin { Incident = 'grafana-incident-app', OnCall = 'grafana-oncall-app', MachineLearning = 'grafana-ml-app', + Labels = 'grafana-labels-app', } diff --git a/public/app/features/alerting/unified/utils/cloud-alertmanager-notifier-types.ts b/public/app/features/alerting/unified/utils/cloud-alertmanager-notifier-types.ts index 494e9ef231c..e8100e9aba9 100644 --- a/public/app/features/alerting/unified/utils/cloud-alertmanager-notifier-types.ts +++ b/public/app/features/alerting/unified/utils/cloud-alertmanager-notifier-types.ts @@ -320,7 +320,11 @@ export const cloudNotifierTypes: Array> = [ 'max_alerts', 'Max alerts', 'The maximum number of alerts to include in a single webhook message. Alerts above this threshold are truncated. When leaving this at its default value of 0, all alerts are included.', - { placeholder: '0', validationRule: '(^\\d+$|^$)' } + { + placeholder: '0', + validationRule: '(^\\d+$|^$)', + setValueAs: (value) => (typeof value === 'string' ? parseInt(value, 10) : 0), + } ), httpConfigOption, ], diff --git a/public/app/features/alerting/unified/utils/labels.ts b/public/app/features/alerting/unified/utils/labels.ts index 49da17ab97a..7c89af0ea13 100644 --- a/public/app/features/alerting/unified/utils/labels.ts +++ b/public/app/features/alerting/unified/utils/labels.ts @@ -32,3 +32,11 @@ export function arrayKeyValuesToObject( return labelsObject; } + +export const GRAFANA_ORIGIN_LABEL = '__grafana_origin'; + +export function isPrivateLabelKey(labelKey: string) { + return (labelKey.startsWith('__') && labelKey.endsWith('__')) || labelKey === GRAFANA_ORIGIN_LABEL; +} + +export const isPrivateLabel = ([key, _]: [string, string]) => isPrivateLabelKey(key); diff --git a/public/app/features/alerting/unified/utils/matchers.ts b/public/app/features/alerting/unified/utils/matchers.ts index 2cac197f8d9..a596a318da0 100644 --- a/public/app/features/alerting/unified/utils/matchers.ts +++ b/public/app/features/alerting/unified/utils/matchers.ts @@ -11,6 +11,8 @@ import { Matcher, MatcherOperator, ObjectMatcher, Route } from 'app/plugins/data import { Labels } from '../../../../types/unified-alerting-dto'; +import { isPrivateLabelKey } from './labels'; + const matcherOperators = [ MatcherOperator.regex, MatcherOperator.notRegex, @@ -91,9 +93,7 @@ export function parseQueryParamMatchers(matcherPairs: string[]): Matcher[] { } export const getMatcherQueryParams = (labels: Labels) => { - const validMatcherLabels = Object.entries(labels).filter( - ([labelKey]) => !(labelKey.startsWith('__') && labelKey.endsWith('__')) - ); + const validMatcherLabels = Object.entries(labels).filter(([labelKey]) => !isPrivateLabelKey(labelKey)); const matcherUrlParams = new URLSearchParams(); validMatcherLabels.forEach(([labelKey, labelValue]) => diff --git a/public/app/features/alerting/unified/utils/misc.ts b/public/app/features/alerting/unified/utils/misc.ts index 32fc700f0ae..9106eb4a022 100644 --- a/public/app/features/alerting/unified/utils/misc.ts +++ b/public/app/features/alerting/unified/utils/misc.ts @@ -5,7 +5,7 @@ import { GrafanaEdition } from '@grafana/data/src/types/config'; import { config, isFetchError } from '@grafana/runtime'; import { DataSourceRef } from '@grafana/schema'; import { escapePathSeparators } from 'app/features/alerting/unified/utils/rule-id'; -import { alertInstanceKey } from 'app/features/alerting/unified/utils/rules'; +import { alertInstanceKey, isGrafanaRulerRule } from 'app/features/alerting/unified/utils/rules'; import { SortOrder } from 'app/plugins/panel/alertlist/types'; import { Alert, CombinedRule, FilterState, RulesSource, SilenceFilterState } from 'app/types/unified-alerting'; import { @@ -54,14 +54,16 @@ export function createMuteTimingLink(muteTimingName: string, alertManagerSourceN }); } -export function createShareLink(ruleSource: RulesSource, rule: CombinedRule): string { +export function createShareLink(ruleSource: RulesSource, rule: CombinedRule): string | undefined { if (isCloudRulesSource(ruleSource)) { return createAbsoluteUrl( `/alerting/${encodeURIComponent(ruleSource.name)}/${encodeURIComponent(escapePathSeparators(rule.name))}/find` ); + } else if (isGrafanaRulerRule(rule.rulerRule)) { + return createUrl(`/alerting/grafana/${rule.rulerRule.grafana_alert.uid}/view`); } - return window.location.href.split('?')[0]; + return; } export function arrayToRecord(items: Array<{ key: string; value: string }>): Record { @@ -232,7 +234,17 @@ export function isErrorLike(error: unknown): error is Error { export function stringifyErrorLike(error: unknown): string { const fetchError = isFetchError(error); if (fetchError) { - return error.data.message; + if (error.message) { + return error.message; + } + if ('message' in error.data && typeof error.data.message === 'string') { + return error.data.message; + } + if (error.statusText) { + return error.statusText; + } + + return String(error.status) || 'Unknown error'; } return isErrorLike(error) ? error.message : String(error); diff --git a/public/app/features/alerting/unified/utils/rules.test.ts b/public/app/features/alerting/unified/utils/rules.test.ts new file mode 100644 index 00000000000..7ec2349f298 --- /dev/null +++ b/public/app/features/alerting/unified/utils/rules.test.ts @@ -0,0 +1,45 @@ +import { config } from '@grafana/runtime'; + +import { mockCombinedRule } from '../mocks'; + +import { GRAFANA_ORIGIN_LABEL } from './labels'; +import { getRulePluginOrigin } from './rules'; + +describe('getRuleOrigin', () => { + it('returns undefined when no origin label is present', () => { + const rule = mockCombinedRule({ + labels: {}, + }); + expect(getRulePluginOrigin(rule)).toBeUndefined(); + }); + + it('returns undefined when origin label does not match expected format', () => { + const rule = mockCombinedRule({ + labels: { [GRAFANA_ORIGIN_LABEL]: 'invalid_format' }, + }); + expect(getRulePluginOrigin(rule)).toBeUndefined(); + }); + + it('returns undefined when plugin is not installed', () => { + const rule = mockCombinedRule({ + labels: { [GRAFANA_ORIGIN_LABEL]: 'plugin/uninstalled_plugin' }, + }); + expect(getRulePluginOrigin(rule)).toBeUndefined(); + }); + + it('returns pluginId when origin label matches expected format and plugin is installed', () => { + config.apps = { + installed_plugin: { + id: 'installed_plugin', + version: '', + path: '', + preload: true, + angular: { detected: false, hideDeprecation: false }, + }, + }; + const rule = mockCombinedRule({ + labels: { [GRAFANA_ORIGIN_LABEL]: 'plugin/installed_plugin' }, + }); + expect(getRulePluginOrigin(rule)).toEqual({ pluginId: 'installed_plugin' }); + }); +}); diff --git a/public/app/features/alerting/unified/utils/rules.ts b/public/app/features/alerting/unified/utils/rules.ts index 49481acb971..248774818e2 100644 --- a/public/app/features/alerting/unified/utils/rules.ts +++ b/public/app/features/alerting/unified/utils/rules.ts @@ -1,10 +1,12 @@ import { capitalize } from 'lodash'; import { AlertState } from '@grafana/data'; +import { config } from '@grafana/runtime'; import { Alert, AlertingRule, CloudRuleIdentifier, + CombinedRule, CombinedRuleGroup, CombinedRuleWithLocation, GrafanaRuleIdentifier, @@ -33,6 +35,7 @@ import { RuleHealth } from '../search/rulesSearchParser'; import { RULER_NOT_SUPPORTED_MSG } from './constants'; import { getRulesSourceName } from './datasource'; +import { GRAFANA_ORIGIN_LABEL } from './labels'; import { AsyncRequestState } from './redux'; import { safeParsePrometheusDuration } from './time'; @@ -100,6 +103,41 @@ export function getRuleHealth(health: string): RuleHealth | undefined { } } +export interface RulePluginOrigin { + pluginId: string; +} + +export function getRulePluginOrigin(rule: CombinedRule): RulePluginOrigin | undefined { + // com.grafana.origin=plugin/ + // Prom and Mimir do not support dots in label names 😔 + const origin = rule.labels[GRAFANA_ORIGIN_LABEL]; + if (!origin) { + return undefined; + } + + const match = origin.match(/^plugin\/(?.+)$/); + if (!match?.groups) { + return undefined; + } + + const pluginId = match.groups['pluginId']; + const pluginInstalled = isPluginInstalled(pluginId); + + if (!pluginInstalled) { + return undefined; + } + + return { pluginId }; +} + +function isPluginInstalled(pluginId: string) { + return Boolean(config.apps[pluginId]); +} + +export function isPluginProvidedRule(rule: CombinedRule): boolean { + return Boolean(getRulePluginOrigin(rule)); +} + export function alertStateToReadable(state: PromAlertingRuleState | GrafanaAlertStateWithReason | AlertState): string { if (state === PromAlertingRuleState.Inactive) { return 'Normal'; diff --git a/public/app/features/api-keys/ApiKeysTable.tsx b/public/app/features/api-keys/ApiKeysTable.tsx index cd5946fd67f..452aa370e94 100644 --- a/public/app/features/api-keys/ApiKeysTable.tsx +++ b/public/app/features/api-keys/ApiKeysTable.tsx @@ -2,7 +2,7 @@ import { css } from '@emotion/css'; import React from 'react'; import { dateTimeFormat, GrafanaTheme2, TimeZone } from '@grafana/data'; -import { Button, DeleteButton, HorizontalGroup, Icon, Tooltip, useTheme2 } from '@grafana/ui'; +import { Button, DeleteButton, Icon, Stack, Tooltip, useTheme2 } from '@grafana/ui'; import { contextSrv } from 'app/core/core'; import { AccessControlAction } from 'app/types'; @@ -50,7 +50,7 @@ export const ApiKeysTable = ({ apiKeys, timeZone, onDelete, onMigrate }: Props) {formatLastUsedAtDate(timeZone, key.lastUsedAt)} - + @@ -60,7 +60,7 @@ export const ApiKeysTable = ({ apiKeys, timeZone, onDelete, onMigrate }: Props) onConfirm={() => onDelete(key)} disabled={!contextSrv.hasPermissionInMetadata(AccessControlAction.ActionAPIKeysDelete, key)} /> - + ); diff --git a/public/app/features/auth-config/AuthProvidersListPage.tsx b/public/app/features/auth-config/AuthProvidersListPage.tsx index 1bf44fbf8bb..ceece9d32ea 100644 --- a/public/app/features/auth-config/AuthProvidersListPage.tsx +++ b/public/app/features/auth-config/AuthProvidersListPage.tsx @@ -52,6 +52,9 @@ export const AuthConfigPageUnconnected = ({ reportInteraction('authentication_ui_provider_clicked', { provider: providerType, enabled }); }; + // filter out saml from sso providers because it is already included in availableProviders + providers = providers.filter((p) => p.provider !== 'saml'); + const providerList = availableProviders.length ? [ ...availableProviders.map((p) => ({ diff --git a/public/app/features/browse-dashboards/components/BrowseView.test.tsx b/public/app/features/browse-dashboards/components/BrowseView.test.tsx index bdca23a807d..a083f8374c5 100644 --- a/public/app/features/browse-dashboards/components/BrowseView.test.tsx +++ b/public/app/features/browse-dashboards/components/BrowseView.test.tsx @@ -149,7 +149,7 @@ describe('browse-dashboards BrowseView', () => { describe('when there is no item in the folder', () => { it('shows a CTA for creating a dashboard if the user has editor rights', async () => { render(); - expect(await screen.findByText('Create Dashboard')).toBeInTheDocument(); + expect(await screen.findByText('Create dashboard')).toBeInTheDocument(); }); it('shows a simple message if the user has viewer rights', async () => { diff --git a/public/app/features/browse-dashboards/components/BrowseView.tsx b/public/app/features/browse-dashboards/components/BrowseView.tsx index 4c657da02db..b9cc1ea5a2d 100644 --- a/public/app/features/browse-dashboards/components/BrowseView.tsx +++ b/public/app/features/browse-dashboards/components/BrowseView.tsx @@ -1,7 +1,7 @@ import React, { useCallback } from 'react'; -import { CallToActionCard } from '@grafana/ui'; -import EmptyListCTA from 'app/core/components/EmptyListCTA/EmptyListCTA'; +import { CallToActionCard, EmptyState, LinkButton, TextLink } from '@grafana/ui'; +import { Trans, t } from 'app/core/internationalization'; import { DashboardViewItem } from 'app/features/search/types'; import { useDispatch } from 'app/types'; @@ -117,16 +117,32 @@ export function BrowseView({ folderUID, width, height, canSelect }: BrowseViewPr return (
{canSelect ? ( - '} - proTipLink={folderUID && 'dashboards'} - proTipLinkTitle={folderUID && 'Browse dashboards'} - proTipTarget="" - /> + + Create dashboard + + } + message={ + folderUID + ? t('browse-dashboards.empty-state.title-folder', "This folder doesn't have any dashboards yet") + : t('browse-dashboards.empty-state.title', "You haven't created any dashboards yet") + } + > + {folderUID && ( + + Add/move dashboards to your folder at{' '} + + Browse dashboards + + + )} + ) : ( This folder is empty} /> )} diff --git a/public/app/features/canvas/element.ts b/public/app/features/canvas/element.ts index 371cc46ec43..61d7061eaaf 100644 --- a/public/app/features/canvas/element.ts +++ b/public/app/features/canvas/element.ts @@ -101,6 +101,12 @@ export interface CanvasElementItem extends RegistryI /** Optional config to customize what standard element editor options are available for the item */ standardEditorConfig?: StandardEditorConfig; + + /** Custom connection anchor coordinates, like for svg elements such as triangle, cloud, etc */ + customConnectionAnchors?: Array<{ + x: number; + y: number; + }>; } export const defaultBgColor = '#D9D9D9'; diff --git a/public/app/features/canvas/elements/cloud.tsx b/public/app/features/canvas/elements/cloud.tsx index 4f1bd04648c..1d48f43a35c 100644 --- a/public/app/features/canvas/elements/cloud.tsx +++ b/public/app/features/canvas/elements/cloud.tsx @@ -184,6 +184,24 @@ export const cloudItem: CanvasElementItem = { }, }); }, + + customConnectionAnchors: [ + { x: -0.58, y: 0.63 }, // Top Left + { x: -0.22, y: 0.99 }, // Top Middle + { x: 0.235, y: 0.75 }, // Top Right + + { x: 0.8, y: 0.6 }, // Right Top + { x: 0.785, y: 0.06 }, // Right Middle + { x: 0.91, y: -0.51 }, // Right Bottom + + { x: 0.62, y: -0.635 }, // Bottom Right + { x: 0.05, y: -0.98 }, // Bottom Middle + { x: -0.45, y: -0.635 }, // Bottom Left + + { x: -0.8, y: -0.58 }, // Left Bottom + { x: -0.78, y: -0.06 }, // Left Middle + { x: -0.9, y: 0.48 }, // Left Top + ], }; const getStyles = (theme: GrafanaTheme2, data: CanvasElementData | undefined) => { diff --git a/public/app/features/canvas/elements/droneFront.tsx b/public/app/features/canvas/elements/droneFront.tsx index 29921fced33..031df8af28b 100644 --- a/public/app/features/canvas/elements/droneFront.tsx +++ b/public/app/features/canvas/elements/droneFront.tsx @@ -122,6 +122,8 @@ export const droneFrontItem: CanvasElementItem = { const getStyles = (theme: GrafanaTheme2) => ({ droneFront: css({ + // TODO: figure out what styles to apply when prefers-reduced-motion is set + // eslint-disable-next-line @grafana/no-unreduced-motion transition: 'transform 0.4s', }), }); diff --git a/public/app/features/canvas/elements/droneSide.tsx b/public/app/features/canvas/elements/droneSide.tsx index aa9ad4b9561..a82b11d3ff1 100644 --- a/public/app/features/canvas/elements/droneSide.tsx +++ b/public/app/features/canvas/elements/droneSide.tsx @@ -121,6 +121,8 @@ export const droneSideItem: CanvasElementItem = { const getStyles = (theme: GrafanaTheme2) => ({ droneSide: css({ + // TODO: figure out what styles to apply when prefers-reduced-motion is set + // eslint-disable-next-line @grafana/no-unreduced-motion transition: 'transform 0.4s', }), }); diff --git a/public/app/features/canvas/elements/droneTop.tsx b/public/app/features/canvas/elements/droneTop.tsx index f4c2bff38f7..31584b00583 100644 --- a/public/app/features/canvas/elements/droneTop.tsx +++ b/public/app/features/canvas/elements/droneTop.tsx @@ -180,9 +180,13 @@ const getStyles = (theme: GrafanaTheme2) => ({ }, }), propellerCW: css({ + // TODO: figure out what styles to apply when prefers-reduced-motion is set + // eslint-disable-next-line @grafana/no-unreduced-motion animationDirection: 'normal', }), propellerCCW: css({ + // TODO: figure out what styles to apply when prefers-reduced-motion is set + // eslint-disable-next-line @grafana/no-unreduced-motion animationDirection: 'reverse', }), }); diff --git a/public/app/features/canvas/elements/ellipse.tsx b/public/app/features/canvas/elements/ellipse.tsx index 8cab935e0ec..b15b5908610 100644 --- a/public/app/features/canvas/elements/ellipse.tsx +++ b/public/app/features/canvas/elements/ellipse.tsx @@ -190,6 +190,24 @@ export const ellipseItem: CanvasElementItem { diff --git a/public/app/features/canvas/elements/parallelogram.tsx b/public/app/features/canvas/elements/parallelogram.tsx index 4caec1e776a..61c4b8531fa 100644 --- a/public/app/features/canvas/elements/parallelogram.tsx +++ b/public/app/features/canvas/elements/parallelogram.tsx @@ -184,6 +184,25 @@ export const parallelogramItem: CanvasElementItem = { }, }); }, + + customConnectionAnchors: [ + { x: -0.6, y: 1 }, // Angled Top Left + { x: -0.1, y: 1 }, // Top Middle + { x: 0.5, y: 1 }, // Angled Top Right + { x: 1, y: 1 }, // Top Right + { x: 0.925, y: 0.6 }, // Angled Right Top + { x: 0.84, y: 0.2 }, // Right Middle + { x: 0.76, y: -0.2 }, // Angled Right Bottom + { x: 0.675, y: -0.6 }, // Bottom Right + { x: -0.5, y: -1 }, // Angled Bottom Right + { x: 0.1, y: -1 }, // Bottom Middle + { x: 0.6, y: -1 }, // Angled Bottom Left + { x: -1, y: -1 }, // Bottom Left + { x: -0.925, y: -0.6 }, // Angled Left Bottom + { x: -0.84, y: -0.2 }, // Left Middle + { x: -0.76, y: 0.2 }, // Angled Left Top + { x: -0.675, y: 0.6 }, // Top Left 2 + ], }; const getStyles = (theme: GrafanaTheme2, data: CanvasElementData | undefined) => { diff --git a/public/app/features/canvas/elements/server/server.tsx b/public/app/features/canvas/elements/server/server.tsx index 633959b929c..cdb69ea7fbd 100644 --- a/public/app/features/canvas/elements/server/server.tsx +++ b/public/app/features/canvas/elements/server/server.tsx @@ -173,7 +173,9 @@ export const getServerStyles = (data: ServerData | undefined) => (theme: Grafana fill: data?.statusColor ?? 'transparent', }), circle: css({ - animation: `blink ${data?.blinkRate ? 1 / data.blinkRate : 0}s infinite step-end`, + [theme.transitions.handleMotion('no-preference', 'reduce')]: { + animation: `blink ${data?.blinkRate ? 1 / data.blinkRate : 0}s infinite step-end`, + }, fill: data?.bulbColor, stroke: 'none', }), diff --git a/public/app/features/canvas/elements/triangle.tsx b/public/app/features/canvas/elements/triangle.tsx index 6af74d68775..9a46e7f3dd5 100644 --- a/public/app/features/canvas/elements/triangle.tsx +++ b/public/app/features/canvas/elements/triangle.tsx @@ -185,6 +185,29 @@ export const triangleItem: CanvasElementItem = { }, }); }, + + customConnectionAnchors: [ + // points along the left edge + { x: -1, y: -1 }, // bottom left + { x: -0.8, y: -0.6 }, + { x: -0.6, y: -0.2 }, + { x: -0.4, y: 0.2 }, + { x: -0.2, y: 0.6 }, + { x: 0, y: 1 }, // top + + // points along the right edge + { x: 0.2, y: 0.6 }, + { x: 0.4, y: 0.2 }, + { x: 0.6, y: -0.2 }, + { x: 0.8, y: -0.6 }, + { x: 1, y: -1 }, // bottom right + + // points along the bottom edge + { x: 0.6, y: -1 }, + { x: 0.2, y: -1 }, + { x: -0.2, y: -1 }, + { x: -0.6, y: -1 }, + ], }; const getStyles = (theme: GrafanaTheme2, data: CanvasElementData | undefined) => { diff --git a/public/app/features/commandPalette/actions/extensionActions.ts b/public/app/features/commandPalette/actions/extensionActions.ts deleted file mode 100644 index c3df191e021..00000000000 --- a/public/app/features/commandPalette/actions/extensionActions.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { PluginExtensionCommandPaletteContext, PluginExtensionPoints } from '@grafana/data'; -import { getPluginLinkExtensions } from '@grafana/runtime'; - -import { CommandPaletteAction } from '../types'; -import { EXTENSIONS_PRIORITY } from '../values'; - -export default function getExtensionActions(): CommandPaletteAction[] { - const context: PluginExtensionCommandPaletteContext = {}; - const { extensions } = getPluginLinkExtensions({ - extensionPointId: PluginExtensionPoints.CommandPalette, - context, - limitPerPlugin: 3, - }); - return extensions.map((extension) => ({ - section: extension.category ?? 'Extensions', - priority: EXTENSIONS_PRIORITY, - id: extension.id, - name: extension.title, - target: extension.path, - perform: () => extension.onClick && extension.onClick(), - })); -} diff --git a/public/app/features/commandPalette/actions/staticActions.ts b/public/app/features/commandPalette/actions/staticActions.ts index b9bc77118b8..ed82e1cf994 100644 --- a/public/app/features/commandPalette/actions/staticActions.ts +++ b/public/app/features/commandPalette/actions/staticActions.ts @@ -6,8 +6,6 @@ import { changeTheme } from 'app/core/services/theme'; import { CommandPaletteAction } from '../types'; import { ACTIONS_PRIORITY, DEFAULT_PRIORITY, PREFERENCES_PRIORITY } from '../values'; -import getExtensionActions from './extensionActions'; - // TODO: Clean this once ID is mandatory on nav items function idForNavItem(navItem: NavModelItem) { return 'navModel.' + navItem.id ?? navItem.url ?? navItem.text ?? navItem.subTitle; @@ -72,7 +70,7 @@ function navTreeToActions(navTree: NavModelItem[], parents: NavModelItem[] = []) return navActions; } -export default (navBarTree: NavModelItem[]): CommandPaletteAction[] => { +export default (navBarTree: NavModelItem[], extensionActions: CommandPaletteAction[]): CommandPaletteAction[] => { const globalActions: CommandPaletteAction[] = [ { id: 'preferences/theme', @@ -99,7 +97,6 @@ export default (navBarTree: NavModelItem[]): CommandPaletteAction[] => { }, ]; - const extensionActions = getExtensionActions(); const navBarActions = navTreeToActions(navBarTree); return [...globalActions, ...extensionActions, ...navBarActions]; diff --git a/public/app/features/commandPalette/actions/useActions.ts b/public/app/features/commandPalette/actions/useActions.ts index 19e83721171..44200cba273 100644 --- a/public/app/features/commandPalette/actions/useActions.ts +++ b/public/app/features/commandPalette/actions/useActions.ts @@ -6,18 +6,20 @@ import { CommandPaletteAction } from '../types'; import { getRecentDashboardActions } from './dashboardActions'; import getStaticActions from './staticActions'; +import useExtensionActions from './useExtensionActions'; export default function useActions(searchQuery: string) { const [navTreeActions, setNavTreeActions] = useState([]); const [recentDashboardActions, setRecentDashboardActions] = useState([]); + const extensionActions = useExtensionActions(); const navBarTree = useSelector((state) => state.navBarTree); // Load standard static actions useEffect(() => { - const staticActionsResp = getStaticActions(navBarTree); + const staticActionsResp = getStaticActions(navBarTree, extensionActions); setNavTreeActions(staticActionsResp); - }, [navBarTree]); + }, [navBarTree, extensionActions]); // Load recent dashboards - we don't want them to reload when the nav tree changes useEffect(() => { diff --git a/public/app/features/commandPalette/actions/useExtensionActions.ts b/public/app/features/commandPalette/actions/useExtensionActions.ts new file mode 100644 index 00000000000..3861b685793 --- /dev/null +++ b/public/app/features/commandPalette/actions/useExtensionActions.ts @@ -0,0 +1,29 @@ +import { useMemo } from 'react'; + +import { PluginExtensionCommandPaletteContext, PluginExtensionPoints } from '@grafana/data'; +import { usePluginLinkExtensions } from '@grafana/runtime'; + +import { CommandPaletteAction } from '../types'; +import { EXTENSIONS_PRIORITY } from '../values'; + +// NOTE: we are defining this here, as if we would define it in the hook, it would be recreated on every render, which would cause unnecessary re-renders. +const context: PluginExtensionCommandPaletteContext = {}; + +export default function useExtensionActions(): CommandPaletteAction[] { + const { extensions } = usePluginLinkExtensions({ + extensionPointId: PluginExtensionPoints.CommandPalette, + context, + limitPerPlugin: 3, + }); + + return useMemo(() => { + return extensions.map((extension) => ({ + section: extension.category ?? 'Extensions', + priority: EXTENSIONS_PRIORITY, + id: extension.id, + name: extension.title, + target: extension.path, + perform: () => extension.onClick && extension.onClick(), + })); + }, [extensions]); +} diff --git a/public/app/features/correlations/Forms/CorrelationFormNavigation.tsx b/public/app/features/correlations/Forms/CorrelationFormNavigation.tsx index 831e3a61260..95d6d56fc24 100644 --- a/public/app/features/correlations/Forms/CorrelationFormNavigation.tsx +++ b/public/app/features/correlations/Forms/CorrelationFormNavigation.tsx @@ -1,6 +1,6 @@ import React from 'react'; -import { Button, HorizontalGroup } from '@grafana/ui'; +import { Button, Stack } from '@grafana/ui'; import { Trans, t } from 'app/core/internationalization'; import { useWizardContext } from '../components/Wizard/wizardContext'; @@ -26,7 +26,7 @@ export const CorrelationFormNavigation = () => { ); return ( - + {currentPage > 0 ? ( + } + message={t('correlations.empty-state.title', "You haven't defined any correlations yet")} + > + + You can also define correlations via datasource provisioning + + ) : ( There are no correlations configured yet. diff --git a/public/app/features/dashboard-scene/inspect/InspectJsonTab.tsx b/public/app/features/dashboard-scene/inspect/InspectJsonTab.tsx index ce9e21371f0..e43ac6f5d0f 100644 --- a/public/app/features/dashboard-scene/inspect/InspectJsonTab.tsx +++ b/public/app/features/dashboard-scene/inspect/InspectJsonTab.tsx @@ -163,7 +163,7 @@ export class InspectJsonTab extends SceneObjectBase { return (
-
+
{ - + <> setWaiting(true), [panelName]); useDebounce(() => setDebouncedPanelName(panelName), 350, [panelName]); diff --git a/public/app/features/library-panels/components/LibraryPanelsSearch/LibraryPanelsSearch.test.tsx b/public/app/features/library-panels/components/LibraryPanelsSearch/LibraryPanelsSearch.test.tsx index 862f34739d9..15d0a9699fa 100644 --- a/public/app/features/library-panels/components/LibraryPanelsSearch/LibraryPanelsSearch.test.tsx +++ b/public/app/features/library-panels/components/LibraryPanelsSearch/LibraryPanelsSearch.test.tsx @@ -105,7 +105,7 @@ describe('LibraryPanelsSearch', () => { await getTestContext(); expect(screen.getByPlaceholderText(/search by name/i)).toBeInTheDocument(); - expect(screen.getByText(/no library panels found/i)).toBeInTheDocument(); + expect(screen.getByText(/you haven\'t created any library panels yet/i)).toBeInTheDocument(); }); describe('and user searches for library panel by name or description', () => { @@ -132,7 +132,7 @@ describe('LibraryPanelsSearch', () => { await getTestContext({ showSort: true }); expect(screen.getByPlaceholderText(/search by name/i)).toBeInTheDocument(); - expect(screen.getByText(/no library panels found/i)).toBeInTheDocument(); + expect(screen.getByText(/you haven\'t created any library panels yet/i)).toBeInTheDocument(); expect(screen.getByText(/sort \(default a–z\)/i)).toBeInTheDocument(); }); @@ -160,7 +160,7 @@ describe('LibraryPanelsSearch', () => { await getTestContext({ showPanelFilter: true }); expect(screen.getByPlaceholderText(/search by name/i)).toBeInTheDocument(); - expect(screen.getByText(/no library panels found/i)).toBeInTheDocument(); + expect(screen.getByText(/you haven\'t created any library panels yet/i)).toBeInTheDocument(); expect(screen.getByRole('combobox', { name: /panel type filter/i })).toBeInTheDocument(); }); @@ -188,7 +188,7 @@ describe('LibraryPanelsSearch', () => { await getTestContext({ showFolderFilter: true }); expect(screen.getByPlaceholderText(/search by name/i)).toBeInTheDocument(); - expect(screen.getByText(/no library panels found/i)).toBeInTheDocument(); + expect(screen.getByText(/you haven\'t created any library panels yet/i)).toBeInTheDocument(); expect(screen.getByRole('combobox', { name: /folder filter/i })).toBeInTheDocument(); }); @@ -274,7 +274,7 @@ describe('LibraryPanelsSearch', () => { const card = () => screen.getByLabelText(/plugin visualization item time series/i); - expect(screen.queryByText(/no library panels found/i)).not.toBeInTheDocument(); + expect(screen.queryByText(/you haven\'t created any library panels yet/i)).not.toBeInTheDocument(); expect(card()).toBeInTheDocument(); expect(within(card()).getByText(/library panel name/i)).toBeInTheDocument(); expect(within(card()).getByText(/library panel description/i)).toBeInTheDocument(); @@ -315,7 +315,7 @@ describe('LibraryPanelsSearch', () => { const card = () => screen.getByLabelText(/plugin visualization item time series/i); - expect(screen.queryByText(/no library panels found/i)).not.toBeInTheDocument(); + expect(screen.queryByText(/you haven\'t created any library panels yet/i)).not.toBeInTheDocument(); expect(card()).toBeInTheDocument(); expect(within(card()).getByText(/library panel name/i)).toBeInTheDocument(); expect(within(card()).getByText(/library panel description/i)).toBeInTheDocument(); diff --git a/public/app/features/library-panels/components/LibraryPanelsView/LibraryPanelsView.tsx b/public/app/features/library-panels/components/LibraryPanelsView/LibraryPanelsView.tsx index a401ba5bf72..22b7ae79026 100644 --- a/public/app/features/library-panels/components/LibraryPanelsView/LibraryPanelsView.tsx +++ b/public/app/features/library-panels/components/LibraryPanelsView/LibraryPanelsView.tsx @@ -3,8 +3,8 @@ import React, { useMemo, useReducer } from 'react'; import { useDebounce } from 'react-use'; import { GrafanaTheme2, LoadingState } from '@grafana/data'; -import { EmptyState, Pagination, Stack, useStyles2 } from '@grafana/ui'; -import { t } from 'app/core/internationalization'; +import { EmptyState, Pagination, Stack, TextLink, useStyles2 } from '@grafana/ui'; +import { Trans, t } from 'app/core/internationalization'; import { LibraryElementDTO } from '../../types'; import { LibraryPanelCard } from '../LibraryPanelCard/LibraryPanelCard'; @@ -74,6 +74,26 @@ export const LibraryPanelsView = ({ }) ); const onPageChange = (page: number) => asyncDispatch(changePage({ page })); + const hasFilter = searchString || panelFilter?.length || folderFilter?.length; + + if (!hasFilter && loadingState === LoadingState.Done && libraryPanels.length < 1) { + return ( + + + Create a library panel from any existing dashboard panel through the panel context menu.{' '} + + Learn more + + + + ); + } return ( diff --git a/public/app/features/manage-dashboards/components/PublicDashboardListTable/PublicDashboardListTable.test.tsx b/public/app/features/manage-dashboards/components/PublicDashboardListTable/PublicDashboardListTable.test.tsx index 6792db27a35..0e395959ad3 100644 --- a/public/app/features/manage-dashboards/components/PublicDashboardListTable/PublicDashboardListTable.test.tsx +++ b/public/app/features/manage-dashboards/components/PublicDashboardListTable/PublicDashboardListTable.test.tsx @@ -99,13 +99,13 @@ const renderPublicDashboardTable = async (waitForListRendering?: boolean) => { ); - waitForListRendering && (await waitForElementToBeRemoved(screen.getAllByTestId('Spinner')[1], { timeout: 3000 })); + waitForListRendering && (await waitForElementToBeRemoved(screen.getAllByTestId('Spinner')[0], { timeout: 3000 })); }; describe('Show table', () => { it('renders loader spinner while loading', async () => { await renderPublicDashboardTable(); - const spinner = screen.getAllByTestId('Spinner')[1]; + const spinner = screen.getAllByTestId('Spinner')[0]; expect(spinner).toBeInTheDocument(); await waitForElementToBeRemoved(spinner); diff --git a/public/app/features/manage-dashboards/components/PublicDashboardListTable/PublicDashboardListTable.tsx b/public/app/features/manage-dashboards/components/PublicDashboardListTable/PublicDashboardListTable.tsx index 5486f77cf75..c0a6d4e4d1f 100644 --- a/public/app/features/manage-dashboards/components/PublicDashboardListTable/PublicDashboardListTable.tsx +++ b/public/app/features/manage-dashboards/components/PublicDashboardListTable/PublicDashboardListTable.tsx @@ -2,7 +2,7 @@ import { css } from '@emotion/css'; import React, { useMemo, useState } from 'react'; import { useMedia } from 'react-use'; -import { GrafanaTheme2 } from '@grafana/data/src'; +import { GrafanaTheme2 } from '@grafana/data'; import { selectors as e2eSelectors } from '@grafana/e2e-selectors/src'; import { reportInteraction } from '@grafana/runtime'; import { @@ -16,7 +16,9 @@ import { Switch, Pagination, HorizontalGroup, -} from '@grafana/ui/src'; + EmptyState, + TextLink, +} from '@grafana/ui'; import { Page } from 'app/core/components/Page/Page'; import { Trans, t } from 'app/core/internationalization'; import { contextSrv } from 'app/core/services/context_srv'; @@ -141,28 +143,50 @@ export const PublicDashboardListTable = () => { const [page, setPage] = useState(1); const styles = useStyles2(getStyles); - const { data: paginatedPublicDashboards, isLoading, isFetching, isError } = useListPublicDashboardsQuery(page); + const { data: paginatedPublicDashboards, isLoading, isError } = useListPublicDashboardsQuery(page); return ( - }> + {!isLoading && !isError && !!paginatedPublicDashboards && (
-
    - {paginatedPublicDashboards.publicDashboards.map((pd: PublicDashboardListResponse) => ( -
  • - -
  • - ))} -
- - - + {paginatedPublicDashboards.publicDashboards.length === 0 ? ( + + + Create a public dashboard from any existing dashboard through the Share modal.{' '} + + Learn more + + + + ) : ( + <> +
    + {paginatedPublicDashboards.publicDashboards.map((pd: PublicDashboardListResponse) => ( +
  • + +
  • + ))} +
+ + + + + )}
)}
diff --git a/public/app/features/manage-dashboards/components/SnapshotListTable.tsx b/public/app/features/manage-dashboards/components/SnapshotListTable.tsx index 37e1d7d03c3..aeac45764c1 100644 --- a/public/app/features/manage-dashboards/components/SnapshotListTable.tsx +++ b/public/app/features/manage-dashboards/components/SnapshotListTable.tsx @@ -2,13 +2,13 @@ import React, { useState, useCallback } from 'react'; import useAsync from 'react-use/lib/useAsync'; import { config } from '@grafana/runtime'; -import { ConfirmModal } from '@grafana/ui'; -import { Trans } from 'app/core/internationalization'; +import { ConfirmModal, EmptyState, TextLink } from '@grafana/ui'; +import { Trans, t } from 'app/core/internationalization'; import { getDashboardSnapshotSrv, Snapshot } from 'app/features/dashboard/services/SnapshotSrv'; import { SnapshotListTableRow } from './SnapshotListTableRow'; -export function getSnapshots() { +export async function getSnapshots() { return getDashboardSnapshotSrv() .getSnapshots() .then((result: Snapshot[]) => { @@ -42,6 +42,25 @@ export const SnapshotListTable = () => { [snapshots] ); + if (!isFetching && snapshots.length === 0) { + return ( + + + You can create a snapshot of any dashboard through the Share modal.{' '} + + Learn more + + + + ); + } + return (
diff --git a/public/app/features/migrate-to-cloud/api/index.ts b/public/app/features/migrate-to-cloud/api/index.ts index ac7a0b07d78..6ec39eaae3b 100644 --- a/public/app/features/migrate-to-cloud/api/index.ts +++ b/public/app/features/migrate-to-cloud/api/index.ts @@ -6,6 +6,9 @@ import { generatedAPI } from './endpoints.gen'; export const cloudMigrationAPI = generatedAPI.enhanceEndpoints({ addTagTypes: ['cloud-migration-config', 'cloud-migration-run', 'cloud-migration-run-list'], endpoints: { + // Cloud-side - create token + createCloudMigrationToken: suppressErrorsOnQuery, + // List Cloud Configs getMigrationList: { providesTags: ['cloud-migration-config'] /* should this be a -list? */, @@ -39,9 +42,7 @@ export const cloudMigrationAPI = generatedAPI.enhanceEndpoints({ invalidatesTags: ['cloud-migration-run-list'], }, - getDashboardByUid(endpoint) { - suppressErrorsOnQuery(endpoint); - }, + getDashboardByUid: suppressErrorsOnQuery, }, }); diff --git a/public/app/features/migrate-to-cloud/cloud/MigrationTokenPane/MigrationTokenModal.tsx b/public/app/features/migrate-to-cloud/cloud/MigrationTokenPane/MigrationTokenModal.tsx index d0b17c6a29d..c52cd7dcfcf 100644 --- a/public/app/features/migrate-to-cloud/cloud/MigrationTokenPane/MigrationTokenModal.tsx +++ b/public/app/features/migrate-to-cloud/cloud/MigrationTokenPane/MigrationTokenModal.tsx @@ -6,14 +6,15 @@ import { Trans, t } from 'app/core/internationalization'; import { TokenErrorAlert } from '../TokenErrorAlert'; interface Props { + isOpen: boolean; hideModal: () => void; migrationToken?: string; } -export const MigrationTokenModal = ({ hideModal, migrationToken }: Props) => { +export const MigrationTokenModal = ({ isOpen, hideModal, migrationToken }: Props) => { return ( diff --git a/public/app/features/migrate-to-cloud/cloud/MigrationTokenPane/MigrationTokenPane.tsx b/public/app/features/migrate-to-cloud/cloud/MigrationTokenPane/MigrationTokenPane.tsx index e580b40ec50..59b8123dced 100644 --- a/public/app/features/migrate-to-cloud/cloud/MigrationTokenPane/MigrationTokenPane.tsx +++ b/public/app/features/migrate-to-cloud/cloud/MigrationTokenPane/MigrationTokenPane.tsx @@ -1,6 +1,6 @@ -import React from 'react'; +import React, { useCallback, useState } from 'react'; -import { Box, Button, ModalsController, Text } from '@grafana/ui'; +import { Box, Button, Text } from '@grafana/ui'; import { t, Trans } from 'app/core/internationalization'; import { useCreateCloudMigrationTokenMutation } from '../../api'; @@ -11,52 +11,53 @@ import { MigrationTokenModal } from './MigrationTokenModal'; import { TokenStatus } from './TokenStatus'; export const MigrationTokenPane = () => { + const [showModal, setShowModal] = useState(false); const isFetchingStatus = false; // TODO: No API for this yet - const [createToken, createTokenResponse] = useCreateCloudMigrationTokenMutation(); + const [createTokenMutation, createTokenResponse] = useCreateCloudMigrationTokenMutation(); const hasToken = Boolean(createTokenResponse.data?.token); const isLoading = isFetchingStatus || createTokenResponse.isLoading; /* || deleteTokenResponse.isLoading */ + const handleGenerateToken = useCallback(async () => { + const resp = await createTokenMutation(); + if (!('error' in resp)) { + setShowModal(true); + } + }, [createTokenMutation]); + return ( - - {({ showModal, hideModal }) => ( - - - - Your self-managed Grafana instance will require a special authentication token to securely connect to this - cloud stack. + <> + + + + Your self-managed Grafana instance will require a special authentication token to securely connect to this + cloud stack. + + + + {createTokenResponse?.isError ? ( + + ) : ( + + + Current status: - + + )} - {createTokenResponse?.isError ? ( - - ) : ( - - - Current status: - - - )} + + - - - )} - + setShowModal(false)} + migrationToken={createTokenResponse.data?.token} + /> + ); }; diff --git a/public/app/features/panel/components/VizTypePicker/PanelTypeCard.tsx b/public/app/features/panel/components/VizTypePicker/PanelTypeCard.tsx index ba6bad69f35..8b651759f6e 100644 --- a/public/app/features/panel/components/VizTypePicker/PanelTypeCard.tsx +++ b/public/app/features/panel/components/VizTypePicker/PanelTypeCard.tsx @@ -138,9 +138,11 @@ const getStyles = (theme: GrafanaTheme2) => { padding: theme.spacing(1), width: '100%', overflow: 'hidden', - transition: theme.transitions.create(['background'], { - duration: theme.transitions.duration.short, - }), + [theme.transitions.handleMotion('no-preference', 'reduce')]: { + transition: theme.transitions.create(['background'], { + duration: theme.transitions.duration.short, + }), + }, '&:hover': { background: theme.colors.emphasize(theme.colors.background.secondary, 0.03), diff --git a/public/app/features/playlist/PlaylistPage.tsx b/public/app/features/playlist/PlaylistPage.tsx index bebb5f3c906..f8cb99c6c77 100644 --- a/public/app/features/playlist/PlaylistPage.tsx +++ b/public/app/features/playlist/PlaylistPage.tsx @@ -1,8 +1,7 @@ import React, { useMemo, useState } from 'react'; import { useAsync } from 'react-use'; -import { ConfirmModal, EmptyState, LinkButton } from '@grafana/ui'; -import EmptyListCTA from 'app/core/components/EmptyListCTA/EmptyListCTA'; +import { ConfirmModal, EmptyState, LinkButton, TextLink } from '@grafana/ui'; import { Page } from 'app/core/components/Page/Page'; import PageActionBar from 'app/core/components/PageActionBar/PageActionBar'; import { Trans, t } from 'app/core/internationalization'; @@ -35,20 +34,6 @@ export const PlaylistPage = () => { }); }; - const emptyListBanner = ( - - ); - const showSearch = allPlaylists.loading || playlists.length > 0 || searchQuery.length > 0; return ( @@ -78,7 +63,24 @@ export const PlaylistPage = () => { setPlaylistToDelete={setPlaylistToDelete} /> )} - {!showSearch && emptyListBanner} + {!showSearch && ( + + Create playlist + + } + message={t('playlist-page.empty.title', 'There are no playlists created yet')} + > + + You can use playlists to cycle dashboards on TVs without user control.{' '} + + Learn more + + + + )} {playlistToDelete && ( { background: theme.colors.background.secondary, borderRadius: theme.shape.radius.default, padding: theme.spacing(3), - transition: theme.transitions.create(['background-color', 'box-shadow', 'border-color', 'color'], { - duration: theme.transitions.duration.short, - }), + [theme.transitions.handleMotion('no-preference', 'reduce')]: { + transition: theme.transitions.create(['background-color', 'box-shadow', 'border-color', 'color'], { + duration: theme.transitions.duration.short, + }), + }, '&:hover': { background: theme.colors.emphasize(theme.colors.background.secondary, 0.03), diff --git a/public/app/features/plugins/built_in_plugins.ts b/public/app/features/plugins/built_in_plugins.ts index 03dd4d2a603..5a0bf8ab763 100644 --- a/public/app/features/plugins/built_in_plugins.ts +++ b/public/app/features/plugins/built_in_plugins.ts @@ -15,8 +15,6 @@ const influxdbPlugin = async () => const lokiPlugin = async () => await import(/* webpackChunkName: "lokiPlugin" */ 'app/plugins/datasource/loki/module'); const mixedPlugin = async () => await import(/* webpackChunkName: "mixedPlugin" */ 'app/plugins/datasource/mixed/module'); -const mysqlPlugin = async () => - await import(/* webpackChunkName: "mysqlPlugin" */ 'app/plugins/datasource/mysql/module'); const postgresPlugin = async () => await import(/* webpackChunkName: "postgresPlugin" */ 'app/plugins/datasource/grafana-postgresql-datasource/module'); const prometheusPlugin = async () => @@ -83,7 +81,6 @@ const builtInPlugins: Record Promise { - const placement1 = 'grafana/dashboard/panel/menu'; - const placement2 = 'plugins/myorg-basic-app/start'; - const pluginId = 'grafana-basic-app'; - let link1: PluginExtensionLinkConfig, link2: PluginExtensionLinkConfig; - - beforeEach(() => { - link1 = { - type: PluginExtensionTypes.link, - title: 'Link 1', - description: 'Link 1 description', - path: `/a/${pluginId}/declare-incident`, - extensionPointId: placement1, - configure: jest.fn().mockReturnValue({}), - }; - link2 = { - type: PluginExtensionTypes.link, - title: 'Link 2', - description: 'Link 2 description', - path: `/a/${pluginId}/declare-incident`, - extensionPointId: placement2, - configure: jest.fn().mockImplementation((context) => ({ title: context?.title })), - }; - - global.console.warn = jest.fn(); - }); - - it('should be possible to register extensions', () => { - const registry = createPluginExtensionRegistry([{ pluginId, extensionConfigs: [link1, link2] }]); - - expect(Object.getOwnPropertyNames(registry)).toEqual([placement1, placement2]); - - // Placement 1 - expect(registry[placement1]).toHaveLength(1); - expect(registry[placement1]).toEqual( - expect.arrayContaining([ - expect.objectContaining({ - pluginId, - config: { - ...link1, - configure: expect.any(Function), - }, - }), - ]) - ); - - // Placement 2 - expect(registry[placement2]).toHaveLength(1); - expect(registry[placement2]).toEqual( - expect.arrayContaining([ - expect.objectContaining({ - pluginId, - config: { - ...link2, - configure: expect.any(Function), - }, - }), - ]) - ); - }); - - it('should not register link extensions with invalid path configured', () => { - const registry = createPluginExtensionRegistry([ - { pluginId, extensionConfigs: [{ ...link1, path: 'invalid-path' }, link2] }, - ]); - - expect(Object.getOwnPropertyNames(registry)).toEqual([placement2]); - - // Placement 2 - expect(registry[placement2]).toHaveLength(1); - expect(registry[placement2]).toEqual( - expect.arrayContaining([ - expect.objectContaining({ - pluginId, - config: { - ...link2, - configure: expect.any(Function), - }, - }), - ]) - ); - }); - - it('should not register extensions for a plugin that had errors', () => { - const registry = createPluginExtensionRegistry([ - { pluginId, extensionConfigs: [link1, link2], error: new Error('Plugin failed to load') }, - ]); - - expect(Object.getOwnPropertyNames(registry)).toEqual([]); - }); - - it('should not register an extension if it has an invalid configure() function', () => { - const registry = createPluginExtensionRegistry([ - // @ts-ignore (We would like to provide an invalid configure function on purpose) - { pluginId, extensionConfigs: [{ ...link1, configure: '...' }, link2] }, - ]); - - expect(Object.getOwnPropertyNames(registry)).toEqual([placement2]); - - // Placement 2 (checking if it still registers the extension with a valid configuration) - expect(registry[placement2]).toHaveLength(1); - expect(registry[placement2]).toEqual( - expect.arrayContaining([ - expect.objectContaining({ - pluginId, - config: { - ...link2, - configure: expect.any(Function), - }, - }), - ]) - ); - }); - - it('should not register an extension if it has invalid properties (empty title / description)', () => { - const registry = createPluginExtensionRegistry([ - { pluginId, extensionConfigs: [{ ...link1, title: '', description: '' }, link2] }, - ]); - - expect(Object.getOwnPropertyNames(registry)).toEqual([placement2]); - - // Placement 2 (checking if it still registers the extension with a valid configuration) - expect(registry[placement2]).toHaveLength(1); - expect(registry[placement2]).toEqual( - expect.arrayContaining([ - expect.objectContaining({ - pluginId, - config: { - ...link2, - configure: expect.any(Function), - }, - }), - ]) - ); - }); -}); diff --git a/public/app/features/plugins/extensions/createPluginExtensionRegistry.ts b/public/app/features/plugins/extensions/createPluginExtensionRegistry.ts deleted file mode 100644 index 37090c8a736..00000000000 --- a/public/app/features/plugins/extensions/createPluginExtensionRegistry.ts +++ /dev/null @@ -1,39 +0,0 @@ -import type { PluginPreloadResult } from '../pluginPreloader'; - -import type { PluginExtensionRegistryItem, PluginExtensionRegistry } from './types'; -import { deepFreeze, logWarning } from './utils'; -import { isPluginExtensionConfigValid } from './validators'; - -export function createPluginExtensionRegistry(pluginPreloadResults: PluginPreloadResult[]): PluginExtensionRegistry { - const registry: PluginExtensionRegistry = {}; - - for (const { pluginId, extensionConfigs, error } of pluginPreloadResults) { - if (error) { - logWarning(`"${pluginId}" plugin failed to load, skip registering its extensions.`); - continue; - } - - for (const extensionConfig of extensionConfigs) { - const { extensionPointId } = extensionConfig; - - if (!extensionConfig || !isPluginExtensionConfigValid(pluginId, extensionConfig)) { - continue; - } - - let registryItem: PluginExtensionRegistryItem = { - config: extensionConfig, - - // Additional meta information about the extension - pluginId, - }; - - if (!Array.isArray(registry[extensionPointId])) { - registry[extensionPointId] = [registryItem]; - } else { - registry[extensionPointId].push(registryItem); - } - } - } - - return deepFreeze(registry); -} diff --git a/public/app/features/plugins/extensions/getPluginExtensions.test.tsx b/public/app/features/plugins/extensions/getPluginExtensions.test.tsx index 85c6304154e..c98b1e32772 100644 --- a/public/app/features/plugins/extensions/getPluginExtensions.test.tsx +++ b/public/app/features/plugins/extensions/getPluginExtensions.test.tsx @@ -3,8 +3,8 @@ import React from 'react'; import { PluginExtensionComponentConfig, PluginExtensionLinkConfig, PluginExtensionTypes } from '@grafana/data'; import { reportInteraction } from '@grafana/runtime'; -import { createPluginExtensionRegistry } from './createPluginExtensionRegistry'; import { getPluginExtensions } from './getPluginExtensions'; +import { ReactivePluginExtensionsRegistry } from './reactivePluginExtensionRegistry'; import { isReadOnlyProxy } from './utils'; import { assertPluginExtensionLink } from './validators'; @@ -15,6 +15,19 @@ jest.mock('@grafana/runtime', () => { }; }); +function createPluginExtensionRegistry(preloadResults: Array<{ pluginId: string; extensionConfigs: any[] }>) { + const registry = new ReactivePluginExtensionsRegistry(); + + for (const { pluginId, extensionConfigs } of preloadResults) { + registry.register({ + pluginId, + extensionConfigs, + }); + } + + return registry.getRegistry(); +} + describe('getPluginExtensions()', () => { const extensionPoint1 = 'grafana/dashboard/panel/menu'; const extensionPoint2 = 'plugins/myorg-basic-app/start'; @@ -54,8 +67,8 @@ describe('getPluginExtensions()', () => { jest.mocked(reportInteraction).mockReset(); }); - test('should return the extensions for the given placement', () => { - const registry = createPluginExtensionRegistry([{ pluginId, extensionConfigs: [link1, link2] }]); + test('should return the extensions for the given placement', async () => { + const registry = await createPluginExtensionRegistry([{ pluginId, extensionConfigs: [link1, link2] }]); const { extensions } = getPluginExtensions({ registry, extensionPointId: extensionPoint1 }); expect(extensions).toHaveLength(1); @@ -70,9 +83,11 @@ describe('getPluginExtensions()', () => { ); }); - test('should not limit the number of extensions per plugin by default', () => { + test('should not limit the number of extensions per plugin by default', async () => { // Registering 3 extensions for the same plugin for the same placement - const registry = createPluginExtensionRegistry([{ pluginId, extensionConfigs: [link1, link1, link1, link2] }]); + const registry = await createPluginExtensionRegistry([ + { pluginId, extensionConfigs: [link1, link1, link1, link2] }, + ]); const { extensions } = getPluginExtensions({ registry, extensionPointId: extensionPoint1 }); expect(extensions).toHaveLength(3); @@ -87,8 +102,8 @@ describe('getPluginExtensions()', () => { ); }); - test('should be possible to limit the number of extensions per plugin for a given placement', () => { - const registry = createPluginExtensionRegistry([ + test('should be possible to limit the number of extensions per plugin for a given placement', async () => { + const registry = await createPluginExtensionRegistry([ { pluginId, extensionConfigs: [link1, link1, link1, link2] }, { pluginId: 'my-plugin', @@ -116,16 +131,16 @@ describe('getPluginExtensions()', () => { ); }); - test('should return with an empty list if there are no extensions registered for a placement yet', () => { - const registry = createPluginExtensionRegistry([{ pluginId, extensionConfigs: [link1, link2] }]); + test('should return with an empty list if there are no extensions registered for a placement yet', async () => { + const registry = await createPluginExtensionRegistry([{ pluginId, extensionConfigs: [link1, link2] }]); const { extensions } = getPluginExtensions({ registry, extensionPointId: 'placement-with-no-extensions' }); expect(extensions).toEqual([]); }); - test('should pass the context to the configure() function', () => { + test('should pass the context to the configure() function', async () => { const context = { title: 'New title from the context!' }; - const registry = createPluginExtensionRegistry([{ pluginId, extensionConfigs: [link2] }]); + const registry = await createPluginExtensionRegistry([{ pluginId, extensionConfigs: [link2] }]); getPluginExtensions({ registry, context, extensionPointId: extensionPoint2 }); @@ -133,7 +148,7 @@ describe('getPluginExtensions()', () => { expect(link2.configure).toHaveBeenCalledWith(context); }); - test('should be possible to update the basic properties with the configure() function', () => { + test('should be possible to update the basic properties with the configure() function', async () => { link2.configure = jest.fn().mockImplementation(() => ({ title: 'Updated title', description: 'Updated description', @@ -142,7 +157,7 @@ describe('getPluginExtensions()', () => { category: 'Machine Learning', })); - const registry = createPluginExtensionRegistry([{ pluginId, extensionConfigs: [link2] }]); + const registry = await createPluginExtensionRegistry([{ pluginId, extensionConfigs: [link2] }]); const { extensions } = getPluginExtensions({ registry, extensionPointId: extensionPoint2 }); const [extension] = extensions; @@ -156,7 +171,7 @@ describe('getPluginExtensions()', () => { expect(extension.category).toBe('Machine Learning'); }); - test('should append link tracking to path when running configure() function', () => { + test('should append link tracking to path when running configure() function', async () => { link2.configure = jest.fn().mockImplementation(() => ({ title: 'Updated title', description: 'Updated description', @@ -165,7 +180,7 @@ describe('getPluginExtensions()', () => { category: 'Machine Learning', })); - const registry = createPluginExtensionRegistry([{ pluginId, extensionConfigs: [link2] }]); + const registry = await createPluginExtensionRegistry([{ pluginId, extensionConfigs: [link2] }]); const { extensions } = getPluginExtensions({ registry, extensionPointId: extensionPoint2 }); const [extension] = extensions; @@ -177,7 +192,7 @@ describe('getPluginExtensions()', () => { ); }); - test('should ignore restricted properties passed via the configure() function', () => { + test('should ignore restricted properties passed via the configure() function', async () => { link2.configure = jest.fn().mockImplementation(() => ({ // The following props are not allowed to override type: 'unknown-type', @@ -190,7 +205,7 @@ describe('getPluginExtensions()', () => { title: 'test', })); - const registry = createPluginExtensionRegistry([{ pluginId, extensionConfigs: [link2] }]); + const registry = await createPluginExtensionRegistry([{ pluginId, extensionConfigs: [link2] }]); const { extensions } = getPluginExtensions({ registry, extensionPointId: extensionPoint2 }); const [extension] = extensions; @@ -202,9 +217,9 @@ describe('getPluginExtensions()', () => { //@ts-ignore expect(extension.testing).toBeUndefined(); }); - test('should pass a read only context to the configure() function', () => { + test('should pass a read only context to the configure() function', async () => { const context = { title: 'New title from the context!' }; - const registry = createPluginExtensionRegistry([{ pluginId, extensionConfigs: [link2] }]); + const registry = await createPluginExtensionRegistry([{ pluginId, extensionConfigs: [link2] }]); const { extensions } = getPluginExtensions({ registry, context, extensionPointId: extensionPoint2 }); const [extension] = extensions; const readOnlyContext = (link2.configure as jest.Mock).mock.calls[0][0]; @@ -219,12 +234,12 @@ describe('getPluginExtensions()', () => { expect(context.title).toBe('New title from the context!'); }); - test('should catch errors in the configure() function and log them as warnings', () => { + test('should catch errors in the configure() function and log them as warnings', async () => { link2.configure = jest.fn().mockImplementation(() => { throw new Error('Something went wrong!'); }); - const registry = createPluginExtensionRegistry([{ pluginId, extensionConfigs: [link2] }]); + const registry = await createPluginExtensionRegistry([{ pluginId, extensionConfigs: [link2] }]); expect(() => { getPluginExtensions({ registry, extensionPointId: extensionPoint2 }); @@ -235,7 +250,7 @@ describe('getPluginExtensions()', () => { expect(global.console.warn).toHaveBeenCalledWith('[Plugin Extensions] Something went wrong!'); }); - test('should skip the link extension if the configure() function returns with an invalid path', () => { + test('should skip the link extension if the configure() function returns with an invalid path', async () => { link1.configure = jest.fn().mockImplementation(() => ({ path: '/a/another-plugin/page-a', })); @@ -243,7 +258,7 @@ describe('getPluginExtensions()', () => { path: 'invalid-path', })); - const registry = createPluginExtensionRegistry([{ pluginId, extensionConfigs: [link1, link2] }]); + const registry = await createPluginExtensionRegistry([{ pluginId, extensionConfigs: [link1, link2] }]); const { extensions: extensionsAtPlacement1 } = getPluginExtensions({ registry, extensionPointId: extensionPoint1 }); const { extensions: extensionsAtPlacement2 } = getPluginExtensions({ registry, extensionPointId: extensionPoint2 }); @@ -255,7 +270,7 @@ describe('getPluginExtensions()', () => { expect(global.console.warn).toHaveBeenCalledTimes(2); }); - test('should skip the extension if any of the updated props returned by the configure() function are invalid', () => { + test('should skip the extension if any of the updated props returned by the configure() function are invalid', async () => { const overrides = { title: '', // Invalid empty string for title - should be ignored description: 'A valid description.', // This should be updated @@ -263,7 +278,7 @@ describe('getPluginExtensions()', () => { link2.configure = jest.fn().mockImplementation(() => overrides); - const registry = createPluginExtensionRegistry([{ pluginId, extensionConfigs: [link2] }]); + const registry = await createPluginExtensionRegistry([{ pluginId, extensionConfigs: [link2] }]); const { extensions } = getPluginExtensions({ registry, extensionPointId: extensionPoint2 }); expect(extensions).toHaveLength(0); @@ -271,10 +286,10 @@ describe('getPluginExtensions()', () => { expect(global.console.warn).toHaveBeenCalledTimes(1); }); - test('should skip the extension if the configure() function returns a promise', () => { + test('should skip the extension if the configure() function returns a promise', async () => { link2.configure = jest.fn().mockImplementation(() => Promise.resolve({})); - const registry = createPluginExtensionRegistry([{ pluginId, extensionConfigs: [link2] }]); + const registry = await createPluginExtensionRegistry([{ pluginId, extensionConfigs: [link2] }]); const { extensions } = getPluginExtensions({ registry, extensionPointId: extensionPoint2 }); expect(extensions).toHaveLength(0); @@ -282,24 +297,24 @@ describe('getPluginExtensions()', () => { expect(global.console.warn).toHaveBeenCalledTimes(1); }); - test('should skip (hide) the extension if the configure() function returns undefined', () => { + test('should skip (hide) the extension if the configure() function returns undefined', async () => { link2.configure = jest.fn().mockImplementation(() => undefined); - const registry = createPluginExtensionRegistry([{ pluginId, extensionConfigs: [link2] }]); + const registry = await createPluginExtensionRegistry([{ pluginId, extensionConfigs: [link2] }]); const { extensions } = getPluginExtensions({ registry, extensionPointId: extensionPoint2 }); expect(extensions).toHaveLength(0); expect(global.console.warn).toHaveBeenCalledTimes(0); // As this is intentional, no warning should be logged }); - test('should pass event, context and helper to extension onClick()', () => { + test('should pass event, context and helper to extension onClick()', async () => { link2.path = undefined; link2.onClick = jest.fn().mockImplementation(() => { throw new Error('Something went wrong!'); }); const context = {}; - const registry = createPluginExtensionRegistry([{ pluginId, extensionConfigs: [link2] }]); + const registry = await createPluginExtensionRegistry([{ pluginId, extensionConfigs: [link2] }]); const { extensions } = getPluginExtensions({ registry, extensionPointId: extensionPoint2 }); const [extension] = extensions; @@ -322,7 +337,7 @@ describe('getPluginExtensions()', () => { link2.path = undefined; link2.onClick = jest.fn().mockRejectedValue(new Error('testing')); - const registry = createPluginExtensionRegistry([{ pluginId, extensionConfigs: [link2] }]); + const registry = await createPluginExtensionRegistry([{ pluginId, extensionConfigs: [link2] }]); const { extensions } = getPluginExtensions({ registry, extensionPointId: extensionPoint2 }); const [extension] = extensions; @@ -335,13 +350,13 @@ describe('getPluginExtensions()', () => { expect(global.console.warn).toHaveBeenCalledTimes(1); }); - test('should catch errors in the onClick() function and log them as warnings', () => { + test('should catch errors in the onClick() function and log them as warnings', async () => { link2.path = undefined; link2.onClick = jest.fn().mockImplementation(() => { throw new Error('Something went wrong!'); }); - const registry = createPluginExtensionRegistry([{ pluginId, extensionConfigs: [link2] }]); + const registry = await createPluginExtensionRegistry([{ pluginId, extensionConfigs: [link2] }]); const { extensions } = getPluginExtensions({ registry, extensionPointId: extensionPoint2 }); const [extension] = extensions; @@ -353,13 +368,13 @@ describe('getPluginExtensions()', () => { expect(global.console.warn).toHaveBeenCalledWith('[Plugin Extensions] Something went wrong!'); }); - test('should pass a read only context to the onClick() function', () => { + test('should pass a read only context to the onClick() function', async () => { const context = { title: 'New title from the context!' }; link2.path = undefined; link2.onClick = jest.fn(); - const registry = createPluginExtensionRegistry([{ pluginId, extensionConfigs: [link2] }]); + const registry = await createPluginExtensionRegistry([{ pluginId, extensionConfigs: [link2] }]); const { extensions } = getPluginExtensions({ registry, context, extensionPointId: extensionPoint2 }); const [extension] = extensions; @@ -375,14 +390,14 @@ describe('getPluginExtensions()', () => { }).toThrow(); }); - test('should not make original context read only', () => { + test('should not make original context read only', async () => { const context = { title: 'New title from the context!', nested: { title: 'title' }, array: ['a'], }; - const registry = createPluginExtensionRegistry([{ pluginId, extensionConfigs: [link2] }]); + const registry = await createPluginExtensionRegistry([{ pluginId, extensionConfigs: [link2] }]); getPluginExtensions({ registry, context, extensionPointId: extensionPoint2 }); expect(() => { @@ -392,10 +407,10 @@ describe('getPluginExtensions()', () => { }).not.toThrow(); }); - test('should report interaction when onClick is triggered', () => { + test('should report interaction when onClick is triggered', async () => { const reportInteractionMock = jest.mocked(reportInteraction); - const registry = createPluginExtensionRegistry([ + const registry = await createPluginExtensionRegistry([ { pluginId, extensionConfigs: [ @@ -423,9 +438,9 @@ describe('getPluginExtensions()', () => { }); }); - test('should be possible to register and get component type extensions', () => { + test('should be possible to register and get component type extensions', async () => { const extension = component1; - const registry = createPluginExtensionRegistry([{ pluginId, extensionConfigs: [extension] }]); + const registry = await createPluginExtensionRegistry([{ pluginId, extensionConfigs: [extension] }]); const { extensions } = getPluginExtensions({ registry, extensionPointId: extension.extensionPointId }); expect(extensions).toHaveLength(1); diff --git a/public/app/features/plugins/extensions/getPluginExtensions.ts b/public/app/features/plugins/extensions/getPluginExtensions.ts index 2db726b5e2a..17f5ec77633 100644 --- a/public/app/features/plugins/extensions/getPluginExtensions.ts +++ b/public/app/features/plugins/extensions/getPluginExtensions.ts @@ -8,8 +8,9 @@ import { type PluginExtensionComponent, urlUtil, } from '@grafana/data'; -import { reportInteraction } from '@grafana/runtime'; +import { GetPluginExtensions, reportInteraction } from '@grafana/runtime'; +import { ReactivePluginExtensionsRegistry } from './reactivePluginExtensionRegistry'; import type { PluginExtensionRegistry } from './types'; import { isPluginExtensionLinkConfig, @@ -40,10 +41,22 @@ type GetExtensions = ({ registry: PluginExtensionRegistry; }) => { extensions: PluginExtension[] }; +let registry: PluginExtensionRegistry = { id: '', extensions: {} }; + +export function createPluginExtensionsGetter(extensionRegistry: ReactivePluginExtensionsRegistry): GetPluginExtensions { + // Create a subscription to keep an copy of the registry state for use in the non-async + // plugin extensions getter. + extensionRegistry.asObservable().subscribe((r) => { + registry = r; + }); + + return (options) => getPluginExtensions({ ...options, registry }); +} + // Returns with a list of plugin extensions for the given extension point export const getPluginExtensions: GetExtensions = ({ context, extensionPointId, limitPerPlugin, registry }) => { const frozenContext = context ? getReadOnlyProxy(context) : {}; - const registryItems = registry[extensionPointId] ?? []; + const registryItems = registry.extensions[extensionPointId] ?? []; // We don't return the extensions separated by type, because in that case it would be much harder to define a sort-order for them. const extensions: PluginExtension[] = []; const extensionsByPlugin: Record = {}; diff --git a/public/app/features/plugins/extensions/reactivePluginExtensionRegistry.test.ts b/public/app/features/plugins/extensions/reactivePluginExtensionRegistry.test.ts new file mode 100644 index 00000000000..b958016764a --- /dev/null +++ b/public/app/features/plugins/extensions/reactivePluginExtensionRegistry.test.ts @@ -0,0 +1,682 @@ +import { firstValueFrom } from 'rxjs'; + +import { PluginExtensionTypes } from '@grafana/data'; + +import { ReactivePluginExtensionsRegistry } from './reactivePluginExtensionRegistry'; + +describe('createPluginExtensionsRegistry', () => { + const consoleWarn = jest.fn(); + + beforeEach(() => { + global.console.warn = consoleWarn; + consoleWarn.mockReset(); + }); + + it('should return empty registry when no extensions registered', async () => { + const reactiveRegistry = new ReactivePluginExtensionsRegistry(); + const observable = reactiveRegistry.asObservable(); + const registry = await firstValueFrom(observable); + expect(registry).toEqual({ + id: '', + extensions: {}, + }); + }); + + it('should generate an id for the registry once we register an extension to it', async () => { + const pluginId = 'grafana-basic-app'; + const extensionPointId = 'grafana/dashboard/panel/menu'; + const reactiveRegistry = new ReactivePluginExtensionsRegistry(); + + reactiveRegistry.register({ + pluginId, + extensionConfigs: [ + { + type: PluginExtensionTypes.link, + title: 'Link 1', + description: 'Link 1 description', + path: `/a/${pluginId}/declare-incident`, + extensionPointId, + configure: jest.fn().mockReturnValue({}), + }, + ], + }); + + const registry = await reactiveRegistry.getRegistry(); + + expect(registry.id).toBeDefined(); + expect(registry.extensions[extensionPointId]).toHaveLength(1); + }); + + it('should generate an a new id every time the registry changes', async () => { + const pluginId = 'grafana-basic-app'; + const extensionPointId = 'grafana/dashboard/panel/menu'; + const reactiveRegistry = new ReactivePluginExtensionsRegistry(); + + reactiveRegistry.register({ + pluginId, + extensionConfigs: [ + { + type: PluginExtensionTypes.link, + title: 'Link 1', + description: 'Link 1 description', + path: `/a/${pluginId}/declare-incident`, + extensionPointId, + configure: jest.fn().mockReturnValue({}), + }, + ], + }); + + const registry1 = await reactiveRegistry.getRegistry(); + const id1 = registry1.id; + + expect(id1).toBeDefined(); + + reactiveRegistry.register({ + pluginId, + extensionConfigs: [ + { + type: PluginExtensionTypes.link, + title: 'Link 2', + description: 'Link 2 description', + path: `/a/${pluginId}/declare-incident`, + extensionPointId, + configure: jest.fn().mockReturnValue({}), + }, + ], + }); + + const registry2 = await reactiveRegistry.getRegistry(); + const id2 = registry2.id; + + expect(id2).toBeDefined(); + expect(id2).not.toEqual(id1); + }); + + it('should be possible to register extensions in the registry', async () => { + const pluginId = 'grafana-basic-app'; + const reactiveRegistry = new ReactivePluginExtensionsRegistry(); + + reactiveRegistry.register({ + pluginId, + extensionConfigs: [ + { + type: PluginExtensionTypes.link, + title: 'Link 1', + description: 'Link 1 description', + path: `/a/${pluginId}/declare-incident`, + extensionPointId: 'grafana/dashboard/panel/menu', + configure: jest.fn().mockReturnValue({}), + }, + { + type: PluginExtensionTypes.link, + title: 'Link 2', + description: 'Link 2 description', + path: `/a/${pluginId}/declare-incident`, + extensionPointId: 'plugins/myorg-basic-app/start', + configure: jest.fn().mockImplementation((context) => ({ title: context?.title })), + }, + ], + }); + + const registry = await reactiveRegistry.getRegistry(); + + expect(registry.extensions).toEqual({ + 'grafana/dashboard/panel/menu': [ + { + pluginId: pluginId, + config: { + type: PluginExtensionTypes.link, + title: 'Link 1', + description: 'Link 1 description', + path: `/a/${pluginId}/declare-incident`, + extensionPointId: 'grafana/dashboard/panel/menu', + configure: expect.any(Function), + }, + }, + ], + 'plugins/myorg-basic-app/start': [ + { + pluginId: pluginId, + config: { + type: PluginExtensionTypes.link, + title: 'Link 2', + description: 'Link 2 description', + path: `/a/${pluginId}/declare-incident`, + extensionPointId: 'plugins/myorg-basic-app/start', + configure: expect.any(Function), + }, + }, + ], + }); + }); + + it('should be possible to asynchronously register extensions for the same placement (different plugins)', async () => { + const pluginId1 = 'grafana-basic-app'; + const pluginId2 = 'grafana-basic-app2'; + const reactiveRegistry = new ReactivePluginExtensionsRegistry(); + + // Register extensions for the first plugin + reactiveRegistry.register({ + pluginId: pluginId1, + extensionConfigs: [ + { + type: PluginExtensionTypes.link, + title: 'Link 1', + description: 'Link 1 description', + path: `/a/${pluginId1}/declare-incident`, + extensionPointId: 'grafana/dashboard/panel/menu', + configure: jest.fn().mockReturnValue({}), + }, + ], + }); + + const registry1 = await reactiveRegistry.getRegistry(); + + expect(registry1.extensions).toEqual({ + 'grafana/dashboard/panel/menu': [ + { + pluginId: pluginId1, + config: { + type: PluginExtensionTypes.link, + title: 'Link 1', + description: 'Link 1 description', + path: `/a/${pluginId1}/declare-incident`, + extensionPointId: 'grafana/dashboard/panel/menu', + configure: expect.any(Function), + }, + }, + ], + }); + + // Register extensions for the second plugin to a different placement + reactiveRegistry.register({ + pluginId: pluginId2, + extensionConfigs: [ + { + type: PluginExtensionTypes.link, + title: 'Link 2', + description: 'Link 2 description', + path: `/a/${pluginId2}/declare-incident`, + extensionPointId: 'grafana/dashboard/panel/menu', + configure: jest.fn().mockReturnValue({}), + }, + ], + }); + + const registry2 = await reactiveRegistry.getRegistry(); + + expect(registry2.extensions).toEqual({ + 'grafana/dashboard/panel/menu': [ + { + pluginId: pluginId1, + config: { + type: PluginExtensionTypes.link, + title: 'Link 1', + description: 'Link 1 description', + path: `/a/${pluginId1}/declare-incident`, + extensionPointId: 'grafana/dashboard/panel/menu', + configure: expect.any(Function), + }, + }, + { + pluginId: pluginId2, + config: { + type: PluginExtensionTypes.link, + title: 'Link 2', + description: 'Link 2 description', + path: `/a/${pluginId2}/declare-incident`, + extensionPointId: 'grafana/dashboard/panel/menu', + configure: expect.any(Function), + }, + }, + ], + }); + }); + + it('should be possible to asynchronously register extensions for a different placement (different plugin)', async () => { + const pluginId1 = 'grafana-basic-app'; + const pluginId2 = 'grafana-basic-app2'; + const reactiveRegistry = new ReactivePluginExtensionsRegistry(); + + // Register extensions for the first plugin + reactiveRegistry.register({ + pluginId: pluginId1, + extensionConfigs: [ + { + type: PluginExtensionTypes.link, + title: 'Link 1', + description: 'Link 1 description', + path: `/a/${pluginId1}/declare-incident`, + extensionPointId: 'grafana/dashboard/panel/menu', + configure: jest.fn().mockReturnValue({}), + }, + ], + }); + + const registry1 = await reactiveRegistry.getRegistry(); + + expect(registry1.extensions).toEqual({ + 'grafana/dashboard/panel/menu': [ + { + pluginId: pluginId1, + config: { + type: PluginExtensionTypes.link, + title: 'Link 1', + description: 'Link 1 description', + path: `/a/${pluginId1}/declare-incident`, + extensionPointId: 'grafana/dashboard/panel/menu', + configure: expect.any(Function), + }, + }, + ], + }); + + // Register extensions for the second plugin to a different placement + reactiveRegistry.register({ + pluginId: pluginId2, + extensionConfigs: [ + { + type: PluginExtensionTypes.link, + title: 'Link 2', + description: 'Link 2 description', + path: `/a/${pluginId2}/declare-incident`, + extensionPointId: 'plugins/myorg-basic-app/start', + configure: jest.fn().mockReturnValue({}), + }, + ], + }); + + const registry2 = await reactiveRegistry.getRegistry(); + + expect(registry2.extensions).toEqual({ + 'grafana/dashboard/panel/menu': [ + { + pluginId: pluginId1, + config: { + type: PluginExtensionTypes.link, + title: 'Link 1', + description: 'Link 1 description', + path: `/a/${pluginId1}/declare-incident`, + extensionPointId: 'grafana/dashboard/panel/menu', + configure: expect.any(Function), + }, + }, + ], + 'plugins/myorg-basic-app/start': [ + { + pluginId: pluginId2, + config: { + type: PluginExtensionTypes.link, + title: 'Link 2', + description: 'Link 2 description', + path: `/a/${pluginId2}/declare-incident`, + extensionPointId: 'plugins/myorg-basic-app/start', + configure: expect.any(Function), + }, + }, + ], + }); + }); + + it('should be possible to asynchronously register extensions for the same placement (same plugin)', async () => { + const pluginId = 'grafana-basic-app'; + const reactiveRegistry = new ReactivePluginExtensionsRegistry(); + + // Register extensions for the first extension point + reactiveRegistry.register({ + pluginId: pluginId, + extensionConfigs: [ + { + type: PluginExtensionTypes.link, + title: 'Link 1', + description: 'Link 1 description', + path: `/a/${pluginId}/declare-incident-1`, + extensionPointId: 'grafana/dashboard/panel/menu', + configure: jest.fn().mockReturnValue({}), + }, + ], + }); + + // Register extensions to a different extension point + reactiveRegistry.register({ + pluginId: pluginId, + extensionConfigs: [ + { + type: PluginExtensionTypes.link, + title: 'Link 2', + description: 'Link 2 description', + path: `/a/${pluginId}/declare-incident-2`, + extensionPointId: 'grafana/dashboard/panel/menu', + configure: jest.fn().mockReturnValue({}), + }, + ], + }); + + const registry2 = await reactiveRegistry.getRegistry(); + + expect(registry2.extensions).toEqual({ + 'grafana/dashboard/panel/menu': [ + { + pluginId: pluginId, + config: { + type: PluginExtensionTypes.link, + title: 'Link 1', + description: 'Link 1 description', + path: `/a/${pluginId}/declare-incident-1`, + extensionPointId: 'grafana/dashboard/panel/menu', + configure: expect.any(Function), + }, + }, + { + pluginId: pluginId, + config: { + type: PluginExtensionTypes.link, + title: 'Link 2', + description: 'Link 2 description', + path: `/a/${pluginId}/declare-incident-2`, + extensionPointId: 'grafana/dashboard/panel/menu', + configure: expect.any(Function), + }, + }, + ], + }); + }); + + it('should be possible to asynchronously register extensions for a different placement (same plugin)', async () => { + const pluginId = 'grafana-basic-app'; + const reactiveRegistry = new ReactivePluginExtensionsRegistry(); + + // Register extensions for the first extension point + reactiveRegistry.register({ + pluginId: pluginId, + extensionConfigs: [ + { + type: PluginExtensionTypes.link, + title: 'Link 1', + description: 'Link 1 description', + path: `/a/${pluginId}/declare-incident`, + extensionPointId: 'grafana/dashboard/panel/menu', + configure: jest.fn().mockReturnValue({}), + }, + ], + }); + + // Register extensions to a different extension point + reactiveRegistry.register({ + pluginId: pluginId, + extensionConfigs: [ + { + type: PluginExtensionTypes.link, + title: 'Link 2', + description: 'Link 2 description', + path: `/a/${pluginId}/declare-incident`, + extensionPointId: 'plugins/myorg-basic-app/start', + configure: jest.fn().mockReturnValue({}), + }, + ], + }); + + const registry2 = await reactiveRegistry.getRegistry(); + + expect(registry2.extensions).toEqual({ + 'grafana/dashboard/panel/menu': [ + { + pluginId: pluginId, + config: { + type: PluginExtensionTypes.link, + title: 'Link 1', + description: 'Link 1 description', + path: `/a/${pluginId}/declare-incident`, + extensionPointId: 'grafana/dashboard/panel/menu', + configure: expect.any(Function), + }, + }, + ], + 'plugins/myorg-basic-app/start': [ + { + pluginId: pluginId, + config: { + type: PluginExtensionTypes.link, + title: 'Link 2', + description: 'Link 2 description', + path: `/a/${pluginId}/declare-incident`, + extensionPointId: 'plugins/myorg-basic-app/start', + configure: expect.any(Function), + }, + }, + ], + }); + }); + + it('should notify subscribers when the registry changes', async () => { + const pluginId = 'grafana-basic-app'; + const reactiveRegistry = new ReactivePluginExtensionsRegistry(); + const observable = reactiveRegistry.asObservable(); + const subscribeCallback = jest.fn(); + + observable.subscribe(subscribeCallback); + + // Register extensions for the first plugin + reactiveRegistry.register({ + pluginId: pluginId, + extensionConfigs: [ + { + type: PluginExtensionTypes.link, + title: 'Link 1', + description: 'Link 1 description', + path: `/a/${pluginId}/declare-incident`, + extensionPointId: 'grafana/dashboard/panel/menu', + configure: jest.fn().mockReturnValue({}), + }, + ], + }); + + expect(subscribeCallback).toHaveBeenCalledTimes(2); + + // Register extensions for the first plugin + reactiveRegistry.register({ + pluginId: 'another-plugin', + extensionConfigs: [ + { + type: PluginExtensionTypes.link, + title: 'Link 1', + description: 'Link 1 description', + path: `/a/another-plugin/declare-incident`, + extensionPointId: 'grafana/dashboard/panel/menu', + configure: jest.fn().mockReturnValue({}), + }, + ], + }); + + expect(subscribeCallback).toHaveBeenCalledTimes(3); + + const registry = subscribeCallback.mock.calls[2][0]; + + expect(registry.extensions).toEqual({ + 'grafana/dashboard/panel/menu': [ + { + pluginId: pluginId, + config: { + type: PluginExtensionTypes.link, + title: 'Link 1', + description: 'Link 1 description', + path: `/a/${pluginId}/declare-incident`, + extensionPointId: 'grafana/dashboard/panel/menu', + configure: expect.any(Function), + }, + }, + { + pluginId: 'another-plugin', + config: { + type: PluginExtensionTypes.link, + title: 'Link 1', + description: 'Link 1 description', + path: `/a/another-plugin/declare-incident`, + extensionPointId: 'grafana/dashboard/panel/menu', + configure: expect.any(Function), + }, + }, + ], + }); + }); + + it('should give the last version of the registry for new subscribers', async () => { + const pluginId = 'grafana-basic-app'; + const reactiveRegistry = new ReactivePluginExtensionsRegistry(); + const observable = reactiveRegistry.asObservable(); + const subscribeCallback = jest.fn(); + + reactiveRegistry.register({ + pluginId: pluginId, + extensionConfigs: [ + { + type: PluginExtensionTypes.link, + title: 'Link 1', + description: 'Link 1 description', + path: `/a/${pluginId}/declare-incident`, + extensionPointId: 'grafana/dashboard/panel/menu', + configure: jest.fn().mockReturnValue({}), + }, + ], + }); + + observable.subscribe(subscribeCallback); + expect(subscribeCallback).toHaveBeenCalledTimes(1); + + const registry = subscribeCallback.mock.calls[0][0]; + + expect(registry.extensions).toEqual({ + 'grafana/dashboard/panel/menu': [ + { + pluginId: pluginId, + config: { + type: PluginExtensionTypes.link, + title: 'Link 1', + description: 'Link 1 description', + path: `/a/${pluginId}/declare-incident`, + extensionPointId: 'grafana/dashboard/panel/menu', + configure: expect.any(Function), + }, + }, + ], + }); + }); + + it('should not register extensions for a plugin that had errors', () => { + const pluginId = 'grafana-basic-app'; + const reactiveRegistry = new ReactivePluginExtensionsRegistry(); + const observable = reactiveRegistry.asObservable(); + const subscribeCallback = jest.fn(); + + reactiveRegistry.register({ + error: new Error('Something is broken'), + pluginId: pluginId, + extensionConfigs: [ + { + type: PluginExtensionTypes.link, + title: 'Link 1', + description: 'Link 1 description', + path: `/a/${pluginId}/declare-incident`, + extensionPointId: 'grafana/dashboard/panel/menu', + configure: jest.fn().mockReturnValue({}), + }, + ], + }); + + expect(consoleWarn).toHaveBeenCalled(); + + observable.subscribe(subscribeCallback); + expect(subscribeCallback).toHaveBeenCalledTimes(1); + + const registry = subscribeCallback.mock.calls[0][0]; + expect(registry.extensions).toEqual({}); + }); + + it('should not register an extension if it has an invalid configure() function', () => { + const pluginId = 'grafana-basic-app'; + const reactiveRegistry = new ReactivePluginExtensionsRegistry(); + const observable = reactiveRegistry.asObservable(); + const subscribeCallback = jest.fn(); + + reactiveRegistry.register({ + pluginId: pluginId, + extensionConfigs: [ + { + type: PluginExtensionTypes.link, + title: 'Link 1', + description: 'Link 1 description', + path: `/a/${pluginId}/declare-incident`, + extensionPointId: 'grafana/dashboard/panel/menu', + //@ts-ignore + configure: '...', + }, + ], + }); + + expect(consoleWarn).toHaveBeenCalled(); + + observable.subscribe(subscribeCallback); + expect(subscribeCallback).toHaveBeenCalledTimes(1); + + const registry = subscribeCallback.mock.calls[0][0]; + expect(registry.extensions).toEqual({}); + }); + + it('should not register an extension if it has invalid properties (empty title / description)', () => { + const pluginId = 'grafana-basic-app'; + const reactiveRegistry = new ReactivePluginExtensionsRegistry(); + const observable = reactiveRegistry.asObservable(); + const subscribeCallback = jest.fn(); + + reactiveRegistry.register({ + pluginId: pluginId, + extensionConfigs: [ + { + type: PluginExtensionTypes.link, + title: '', + description: '', + path: `/a/${pluginId}/declare-incident`, + extensionPointId: 'grafana/dashboard/panel/menu', + configure: jest.fn().mockReturnValue({}), + }, + ], + }); + + expect(consoleWarn).toHaveBeenCalled(); + + observable.subscribe(subscribeCallback); + expect(subscribeCallback).toHaveBeenCalledTimes(1); + + const registry = subscribeCallback.mock.calls[0][0]; + expect(registry.extensions).toEqual({}); + }); + + it('should not register link extensions with invalid path configured', () => { + const pluginId = 'grafana-basic-app'; + const reactiveRegistry = new ReactivePluginExtensionsRegistry(); + const observable = reactiveRegistry.asObservable(); + const subscribeCallback = jest.fn(); + + reactiveRegistry.register({ + pluginId: pluginId, + extensionConfigs: [ + { + type: PluginExtensionTypes.link, + title: 'Title 1', + description: 'Description 1', + path: `/a/another-plugin/declare-incident`, + extensionPointId: 'grafana/dashboard/panel/menu', + configure: jest.fn().mockReturnValue({}), + }, + ], + }); + + expect(consoleWarn).toHaveBeenCalled(); + + observable.subscribe(subscribeCallback); + expect(subscribeCallback).toHaveBeenCalledTimes(1); + + const registry = subscribeCallback.mock.calls[0][0]; + expect(registry.extensions).toEqual({}); + }); +}); diff --git a/public/app/features/plugins/extensions/reactivePluginExtensionRegistry.ts b/public/app/features/plugins/extensions/reactivePluginExtensionRegistry.ts new file mode 100644 index 00000000000..016b97e0a4b --- /dev/null +++ b/public/app/features/plugins/extensions/reactivePluginExtensionRegistry.ts @@ -0,0 +1,79 @@ +import { Observable, ReplaySubject, Subject, firstValueFrom, map, scan, startWith } from 'rxjs'; +import { v4 as uuidv4 } from 'uuid'; + +import { PluginPreloadResult } from '../pluginPreloader'; + +import { PluginExtensionRegistry, PluginExtensionRegistryItem } from './types'; +import { deepFreeze, logWarning } from './utils'; +import { isPluginExtensionConfigValid } from './validators'; + +export class ReactivePluginExtensionsRegistry { + private resultSubject: Subject; + private registrySubject: ReplaySubject; + + constructor() { + this.resultSubject = new Subject(); + // This is the subject that we expose. + // (It will buffer the last value on the stream - the registry - and emit it to new subscribers immediately.) + this.registrySubject = new ReplaySubject(1); + + this.resultSubject + .pipe( + scan(resultsToRegistry, { id: '', extensions: {} }), + // Emit an empty registry to start the stream (it is only going to do it once during construction, and then just passes down the values) + startWith({ id: '', extensions: {} }), + map((registry) => deepFreeze(registry)) + ) + // Emitting the new registry to `this.registrySubject` + .subscribe(this.registrySubject); + } + + register(result: PluginPreloadResult): void { + this.resultSubject.next(result); + } + + asObservable(): Observable { + return this.registrySubject.asObservable(); + } + + getRegistry(): Promise { + return firstValueFrom(this.asObservable()); + } +} + +function resultsToRegistry(registry: PluginExtensionRegistry, result: PluginPreloadResult): PluginExtensionRegistry { + const { pluginId, extensionConfigs, error } = result; + + // TODO: We should probably move this section to where we load the plugin since this is only used + // to provide a log to the user. + if (error) { + logWarning(`"${pluginId}" plugin failed to load, skip registering its extensions.`); + return registry; + } + + for (const extensionConfig of extensionConfigs) { + const { extensionPointId } = extensionConfig; + + if (!extensionConfig || !isPluginExtensionConfigValid(pluginId, extensionConfig)) { + return registry; + } + + let registryItem: PluginExtensionRegistryItem = { + config: extensionConfig, + + // Additional meta information about the extension + pluginId, + }; + + if (!Array.isArray(registry.extensions[extensionPointId])) { + registry.extensions[extensionPointId] = [registryItem]; + } else { + registry.extensions[extensionPointId].push(registryItem); + } + } + + // Add a unique ID to the registry (the registry object itself is immutable) + registry.id = uuidv4(); + + return registry; +} diff --git a/public/app/features/plugins/extensions/types.ts b/public/app/features/plugins/extensions/types.ts index b5e5e31cc7e..9642cc6627d 100644 --- a/public/app/features/plugins/extensions/types.ts +++ b/public/app/features/plugins/extensions/types.ts @@ -9,4 +9,7 @@ export type PluginExtensionRegistryItem = { }; // A map of placement names to a list of extensions -export type PluginExtensionRegistry = Record; +export type PluginExtensionRegistry = { + id: string; + extensions: Record; +}; diff --git a/public/app/features/plugins/extensions/usePluginExtensions.test.tsx b/public/app/features/plugins/extensions/usePluginExtensions.test.tsx new file mode 100644 index 00000000000..49f29e4b1b1 --- /dev/null +++ b/public/app/features/plugins/extensions/usePluginExtensions.test.tsx @@ -0,0 +1,225 @@ +import { act } from '@testing-library/react'; +import { renderHook } from '@testing-library/react-hooks'; + +import { PluginExtensionTypes } from '@grafana/data'; + +import { ReactivePluginExtensionsRegistry } from './reactivePluginExtensionRegistry'; +import { createPluginExtensionsHook } from './usePluginExtensions'; + +describe('usePluginExtensions()', () => { + let reactiveRegistry: ReactivePluginExtensionsRegistry; + + beforeEach(() => { + reactiveRegistry = new ReactivePluginExtensionsRegistry(); + }); + + it('should return an empty array if there are no extensions registered for the extension point', () => { + const usePluginExtensions = createPluginExtensionsHook(reactiveRegistry); + const { result } = renderHook(() => + usePluginExtensions({ + extensionPointId: 'foo/bar', + }) + ); + + expect(result.current.extensions).toEqual([]); + }); + + it('should return the plugin extensions from the registry', () => { + const extensionPointId = 'plugins/foo/bar'; + const pluginId = 'my-app-plugin'; + + reactiveRegistry.register({ + pluginId, + extensionConfigs: [ + { + type: PluginExtensionTypes.link, + extensionPointId, + title: '1', + description: '1', + path: `/a/${pluginId}/2`, + }, + { + type: PluginExtensionTypes.link, + extensionPointId, + title: '2', + description: '2', + path: `/a/${pluginId}/2`, + }, + ], + }); + + const usePluginExtensions = createPluginExtensionsHook(reactiveRegistry); + const { result } = renderHook(() => usePluginExtensions({ extensionPointId })); + + expect(result.current.extensions.length).toBe(2); + expect(result.current.extensions[0].title).toBe('1'); + expect(result.current.extensions[1].title).toBe('2'); + }); + + it('should dynamically update the extensions registered for a certain extension point', () => { + const extensionPointId = 'plugins/foo/bar'; + const pluginId = 'my-app-plugin'; + const usePluginExtensions = createPluginExtensionsHook(reactiveRegistry); + let { result, rerender } = renderHook(() => usePluginExtensions({ extensionPointId })); + + // No extensions yet + expect(result.current.extensions.length).toBe(0); + + // Add extensions to the registry + act(() => { + reactiveRegistry.register({ + pluginId, + extensionConfigs: [ + { + type: PluginExtensionTypes.link, + extensionPointId, + title: '1', + description: '1', + path: `/a/${pluginId}/2`, + }, + { + type: PluginExtensionTypes.link, + extensionPointId, + title: '2', + description: '2', + path: `/a/${pluginId}/2`, + }, + ], + }); + }); + + // Check if the hook returns the new extensions + rerender(); + + expect(result.current.extensions.length).toBe(2); + expect(result.current.extensions[0].title).toBe('1'); + expect(result.current.extensions[1].title).toBe('2'); + }); + + it('should only render the hook once', () => { + const spy = jest.spyOn(reactiveRegistry, 'asObservable'); + const extensionPointId = 'plugins/foo/bar'; + const usePluginExtensions = createPluginExtensionsHook(reactiveRegistry); + + renderHook(() => usePluginExtensions({ extensionPointId })); + expect(spy).toHaveBeenCalledTimes(1); + }); + + it('should return the same extensions object if the context object is the same', () => { + const extensionPointId = 'plugins/foo/bar'; + const pluginId = 'my-app-plugin'; + const usePluginExtensions = createPluginExtensionsHook(reactiveRegistry); + + // Add extensions to the registry + act(() => { + reactiveRegistry.register({ + pluginId, + extensionConfigs: [ + { + type: PluginExtensionTypes.link, + extensionPointId, + title: '1', + description: '1', + path: `/a/${pluginId}/2`, + }, + { + type: PluginExtensionTypes.link, + extensionPointId, + title: '2', + description: '2', + path: `/a/${pluginId}/2`, + }, + ], + }); + }); + + // Check if it returns the same extensions object in case nothing changes + const context = {}; + const firstResults = renderHook(() => usePluginExtensions({ extensionPointId, context })); + const secondResults = renderHook(() => usePluginExtensions({ extensionPointId, context })); + expect(firstResults.result.current.extensions === secondResults.result.current.extensions).toBe(true); + }); + + it('should return a new extensions object if the context object is different', () => { + const extensionPointId = 'plugins/foo/bar'; + const pluginId = 'my-app-plugin'; + const usePluginExtensions = createPluginExtensionsHook(reactiveRegistry); + + // Add extensions to the registry + act(() => { + reactiveRegistry.register({ + pluginId, + extensionConfigs: [ + { + type: PluginExtensionTypes.link, + extensionPointId, + title: '1', + description: '1', + path: `/a/${pluginId}/2`, + }, + { + type: PluginExtensionTypes.link, + extensionPointId, + title: '2', + description: '2', + path: `/a/${pluginId}/2`, + }, + ], + }); + }); + + // Check if it returns a different extensions object in case the context object changes + const firstResults = renderHook(() => usePluginExtensions({ extensionPointId, context: {} })); + const secondResults = renderHook(() => usePluginExtensions({ extensionPointId, context: {} })); + expect(firstResults.result.current.extensions === secondResults.result.current.extensions).toBe(false); + }); + + it('should return a new extensions object if the registry changes but the context object is the same', () => { + const extensionPointId = 'plugins/foo/bar'; + const pluginId = 'my-app-plugin'; + const context = {}; + const usePluginExtensions = createPluginExtensionsHook(reactiveRegistry); + + // Add the first extension + act(() => { + reactiveRegistry.register({ + pluginId, + extensionConfigs: [ + { + type: PluginExtensionTypes.link, + extensionPointId, + title: '1', + description: '1', + path: `/a/${pluginId}/2`, + }, + ], + }); + }); + + const { result, rerender } = renderHook(() => usePluginExtensions({ extensionPointId, context })); + const firstExtensions = result.current.extensions; + + // Add the second extension + act(() => { + reactiveRegistry.register({ + pluginId, + extensionConfigs: [ + { + type: PluginExtensionTypes.link, + extensionPointId, + // extensionPointId: 'plugins/foo/bar/zed', // A different extension point (to be sure that it's also returning a new object when the actual extension point doesn't change) + title: '2', + description: '2', + path: `/a/${pluginId}/2`, + }, + ], + }); + }); + + rerender(); + + const secondExtensions = result.current.extensions; + + expect(firstExtensions === secondExtensions).toBe(false); + }); +}); diff --git a/public/app/features/plugins/extensions/usePluginExtensions.tsx b/public/app/features/plugins/extensions/usePluginExtensions.tsx new file mode 100644 index 00000000000..4eb55c700d3 --- /dev/null +++ b/public/app/features/plugins/extensions/usePluginExtensions.tsx @@ -0,0 +1,54 @@ +import { useObservable } from 'react-use'; + +import { PluginExtension } from '@grafana/data'; +import { GetPluginExtensionsOptions, UsePluginExtensionsResult } from '@grafana/runtime'; + +import { getPluginExtensions } from './getPluginExtensions'; +import { ReactivePluginExtensionsRegistry } from './reactivePluginExtensionRegistry'; + +export function createPluginExtensionsHook(extensionsRegistry: ReactivePluginExtensionsRegistry) { + const observableRegistry = extensionsRegistry.asObservable(); + const cache: { + id: string; + extensions: Record; + } = { + id: '', + extensions: {}, + }; + + return function usePluginExtensions(options: GetPluginExtensionsOptions): UsePluginExtensionsResult { + const registry = useObservable(observableRegistry); + + if (!registry) { + return { extensions: [], isLoading: false }; + } + + if (registry.id !== cache.id) { + cache.id = registry.id; + cache.extensions = {}; + } + + // `getPluginExtensions` will return a new array of objects even if it is called with the same options, as it always constructing a frozen objects. + // Due to this we are caching the result of `getPluginExtensions` to avoid unnecessary re-renders for components that are using this hook. + // (NOTE: we are only checking referential equality of `context` object, so it is important to not mutate the object passed to this hook.) + const key = `${options.extensionPointId}-${options.limitPerPlugin}`; + if (cache.extensions[key] && cache.extensions[key].context === options.context) { + return { + extensions: cache.extensions[key].extensions, + isLoading: false, + }; + } + + const { extensions } = getPluginExtensions({ ...options, registry }); + + cache.extensions[key] = { + context: options.context, + extensions, + }; + + return { + extensions, + isLoading: false, + }; + }; +} diff --git a/public/app/features/plugins/loader/cache.ts b/public/app/features/plugins/loader/cache.ts index 3db74fd2a70..7bc30c405f9 100644 --- a/public/app/features/plugins/loader/cache.ts +++ b/public/app/features/plugins/loader/cache.ts @@ -1,17 +1,22 @@ import { clearPluginSettingsCache } from '../pluginSettings'; -const cache: Record = {}; +const cache: Record = {}; const initializedAt: number = Date.now(); type CacheablePlugin = { path: string; version: string; + isAngular?: boolean; }; -export function registerPluginInCache({ path, version }: CacheablePlugin): void { +export function registerPluginInCache({ path, version, isAngular }: CacheablePlugin): void { const key = extractPath(path); if (key && !cache[key]) { - cache[key] = encodeURI(version); + cache[key] = { + version: encodeURI(version), + isAngular, + path, + }; } } @@ -28,12 +33,19 @@ export function resolveWithCache(url: string, defaultBust = initializedAt): stri if (!path) { return `${url}?_cache=${defaultBust}`; } - - const version = cache[path]; + const version = cache[path]?.version; const bust = version || defaultBust; return `${url}?_cache=${bust}`; } +export function getPluginFromCache(path: string): CacheablePlugin | undefined { + const key = extractPath(path); + if (!key) { + return; + } + return cache[key]; +} + function extractPath(address: string): string | undefined { const match = /\/?.+\/(plugins\/.+\/module)\.js/i.exec(address); if (!match) { diff --git a/public/app/features/plugins/loader/types.ts b/public/app/features/plugins/loader/types.ts index 023f6a3986c..108f034d0b4 100644 --- a/public/app/features/plugins/loader/types.ts +++ b/public/app/features/plugins/loader/types.ts @@ -1,7 +1,7 @@ // Extend the System type with the loader hooks we use // to provide backwards compatibility with older version of Systemjs export type SystemJSWithLoaderHooks = typeof System & { - shouldFetch: () => Boolean; + shouldFetch: (url: string) => Boolean; fetch: (url: string, options?: Record) => Promise; onload: (err: unknown, id: string) => void; }; diff --git a/public/app/features/plugins/pluginPreloader.ts b/public/app/features/plugins/pluginPreloader.ts index 6306a443183..1a00a69716d 100644 --- a/public/app/features/plugins/pluginPreloader.ts +++ b/public/app/features/plugins/pluginPreloader.ts @@ -3,6 +3,7 @@ import type { AppPluginConfig } from '@grafana/runtime'; import { startMeasure, stopMeasure } from 'app/core/utils/metrics'; import { getPluginSettings } from 'app/features/plugins/pluginSettings'; +import { ReactivePluginExtensionsRegistry } from './extensions/reactivePluginExtensionRegistry'; import * as pluginLoader from './plugin_loader'; export type PluginPreloadResult = { @@ -11,12 +12,20 @@ export type PluginPreloadResult = { extensionConfigs: PluginExtensionConfig[]; }; -export async function preloadPlugins(apps: Record = {}): Promise { - startMeasure('frontend_plugins_preload'); - const pluginsToPreload = Object.values(apps).filter((app) => app.preload); - const result = await Promise.all(pluginsToPreload.map(preload)); - stopMeasure('frontend_plugins_preload'); - return result; +export async function preloadPlugins( + apps: AppPluginConfig[] = [], + registry: ReactivePluginExtensionsRegistry, + eventName = 'frontend_plugins_preload' +) { + startMeasure(eventName); + const promises = apps.filter((config) => config.preload).map((config) => preload(config)); + const preloadedPlugins = await Promise.all(promises); + + for (const preloadedPlugin of preloadedPlugins) { + registry.register(preloadedPlugin); + } + + stopMeasure(eventName); } async function preload(config: AppPluginConfig): Promise { diff --git a/public/app/features/plugins/plugin_loader.ts b/public/app/features/plugins/plugin_loader.ts index 0593d1e3eea..86fa82d8897 100644 --- a/public/app/features/plugins/plugin_loader.ts +++ b/public/app/features/plugins/plugin_loader.ts @@ -11,14 +11,14 @@ import { DataQuery } from '@grafana/schema'; import { GenericDataSourcePlugin } from '../datasources/types'; import builtInPlugins from './built_in_plugins'; -import { registerPluginInCache } from './loader/cache'; +import { getPluginFromCache, registerPluginInCache } from './loader/cache'; // SystemJS has to be imported before the sharedDependenciesMap import { SystemJS } from './loader/systemjs'; // eslint-disable-next-line import/order import { sharedDependenciesMap } from './loader/sharedDependencies'; import { decorateSystemJSFetch, decorateSystemJSResolve, decorateSystemJsOnload } from './loader/systemjsHooks'; import { SystemJSWithLoaderHooks } from './loader/types'; -import { buildImportMap, resolveModulePath } from './loader/utils'; +import { buildImportMap, isHostedOnCDN, resolveModulePath } from './loader/utils'; import { importPluginModuleInSandbox } from './sandbox/sandbox_plugin_loader'; import { isFrontendSandboxSupported } from './sandbox/utils'; @@ -28,9 +28,13 @@ SystemJS.addImportMap({ imports }); const systemJSPrototype: SystemJSWithLoaderHooks = SystemJS.constructor.prototype; -// Monaco Editors reliance on RequireJS means we need to transform -// the content of the plugin code at runtime which can only be done with fetch/eval. -systemJSPrototype.shouldFetch = () => true; +// This instructs SystemJS to load a plugin using fetch and eval if it returns a truthy value, otherwise it will load the plugin using a script tag. +// We only want to fetch and eval plugins that are hosted on a CDN or are Angular plugins. +systemJSPrototype.shouldFetch = function (url) { + const pluginInfo = getPluginFromCache(url); + + return isHostedOnCDN(url) || Boolean(pluginInfo?.isAngular); +}; const originalImport = systemJSPrototype.import; // Hook Systemjs import to support plugins that only have a default export. @@ -68,7 +72,7 @@ export async function importPluginModule({ isAngular?: boolean; }): Promise { if (version) { - registerPluginInCache({ path, version }); + registerPluginInCache({ path, version, isAngular }); } const builtIn = builtInPlugins[path]; diff --git a/public/app/features/plugins/sandbox/code_loader.ts b/public/app/features/plugins/sandbox/code_loader.ts index 3f2d614bf41..04bd418d33a 100644 --- a/public/app/features/plugins/sandbox/code_loader.ts +++ b/public/app/features/plugins/sandbox/code_loader.ts @@ -1,10 +1,11 @@ -import { PluginMeta, patchArrayVectorProrotypeMethods } from '@grafana/data'; +import { PluginType, patchArrayVectorProrotypeMethods } from '@grafana/data'; +import { config } from '@grafana/runtime'; import { transformPluginSourceForCDN } from '../cdn/utils'; import { resolveWithCache } from '../loader/cache'; import { isHostedOnCDN, resolveModulePath } from '../loader/utils'; -import { SandboxEnvironment } from './types'; +import { SandboxEnvironment, SandboxPluginMeta } from './types'; function isSameDomainAsHost(url: string): boolean { const locationUrl = new URL(window.location.href); @@ -12,7 +13,7 @@ function isSameDomainAsHost(url: string): boolean { return locationUrl.host === paramUrl.host; } -export async function loadScriptIntoSandbox(url: string, meta: PluginMeta, sandboxEnv: SandboxEnvironment) { +export async function loadScriptIntoSandbox(url: string, sandboxEnv: SandboxEnvironment) { let scriptCode = ''; // same-domain @@ -46,7 +47,7 @@ export async function loadScriptIntoSandbox(url: string, meta: PluginMeta, sandb sandboxEnv.evaluate(scriptCode); } -export async function getPluginCode(meta: PluginMeta): Promise { +export async function getPluginCode(meta: SandboxPluginMeta): Promise { if (isHostedOnCDN(meta.module)) { // Load plugin from CDN, no need for "resolveWithCache" as CDN URLs already include the version const url = meta.module; @@ -88,3 +89,33 @@ export function patchSandboxEnvironmentPrototype(sandboxEnvironment: SandboxEnvi `${patchArrayVectorProrotypeMethods.toString()};${patchArrayVectorProrotypeMethods.name}()` ); } + +export function getPluginLoadData(pluginId: string): SandboxPluginMeta { + // find it in datasources + for (const datasource of Object.values(config.datasources)) { + if (datasource.type === pluginId) { + return datasource.meta; + } + } + + //find it in panels + for (const panel of Object.values(config.panels)) { + if (panel.id === pluginId) { + return panel; + } + } + + //find it in apps + //the information inside the apps object is more limited + for (const app of Object.values(config.apps)) { + if (app.id === pluginId) { + return { + id: pluginId, + type: PluginType.app, + module: app.path, + }; + } + } + + throw new Error(`Could not find plugin ${pluginId}`); +} diff --git a/public/app/features/plugins/sandbox/distortion_map.ts b/public/app/features/plugins/sandbox/distortion_map.ts index a883b9a763f..2989172874d 100644 --- a/public/app/features/plugins/sandbox/distortion_map.ts +++ b/public/app/features/plugins/sandbox/distortion_map.ts @@ -1,14 +1,13 @@ import { ProxyTarget } from '@locker/near-membrane-shared'; import { cloneDeep, isFunction } from 'lodash'; -import { PluginMeta } from '@grafana/data'; import { config } from '@grafana/runtime'; import { Monaco } from '@grafana/ui'; import { loadScriptIntoSandbox } from './code_loader'; import { forbiddenElements } from './constants'; import { recursivePatchObjectAsLiveTarget } from './document_sandbox'; -import { SandboxEnvironment } from './types'; +import { SandboxEnvironment, SandboxPluginMeta } from './types'; import { logWarning, unboxRegexesFromMembraneProxy } from './utils'; /** @@ -64,7 +63,7 @@ import { logWarning, unboxRegexesFromMembraneProxy } from './utils'; type DistortionMap = Map< unknown, - (originalAttrOrMethod: unknown, pluginMeta: PluginMeta, sandboxEnv?: SandboxEnvironment) => unknown + (originalAttrOrMethod: unknown, pluginMeta: SandboxPluginMeta, sandboxEnv?: SandboxEnvironment) => unknown >; const generalDistortionMap: DistortionMap = new Map(); @@ -91,7 +90,7 @@ export function getGeneralSandboxDistortionMap() { return generalDistortionMap; } -function failToSet(originalAttrOrMethod: unknown, meta: PluginMeta) { +function failToSet(originalAttrOrMethod: unknown, meta: SandboxPluginMeta) { logWarning(`Plugin ${meta.id} tried to set a sandboxed property`, { pluginId: meta.id, attrOrMethod: String(originalAttrOrMethod), @@ -112,7 +111,7 @@ function distortIframeAttributes(distortions: DistortionMap) { for (const property of iframeHtmlForbiddenProperties) { const descriptor = Object.getOwnPropertyDescriptor(HTMLIFrameElement.prototype, property); if (descriptor) { - function fail(originalAttrOrMethod: unknown, meta: PluginMeta) { + function fail(originalAttrOrMethod: unknown, meta: SandboxPluginMeta) { const pluginId = meta.id; logWarning(`Plugin ${pluginId} tried to access iframe.${property}`, { pluginId, @@ -146,7 +145,7 @@ function distortIframeAttributes(distortions: DistortionMap) { function distortConsole(distortions: DistortionMap) { const descriptor = Object.getOwnPropertyDescriptor(window, 'console'); if (descriptor?.value) { - function getSandboxConsole(originalAttrOrMethod: unknown, meta: PluginMeta) { + function getSandboxConsole(originalAttrOrMethod: unknown, meta: SandboxPluginMeta) { const pluginId = meta.id; // we don't monitor the console because we expect a high volume of calls if (monitorOnly) { @@ -175,7 +174,7 @@ function distortConsole(distortions: DistortionMap) { // set distortions to alert to always output to the console function distortAlert(distortions: DistortionMap) { - function getAlertDistortion(originalAttrOrMethod: unknown, meta: PluginMeta) { + function getAlertDistortion(originalAttrOrMethod: unknown, meta: SandboxPluginMeta) { const pluginId = meta.id; logWarning(`Plugin ${pluginId} accessed window.alert`, { pluginId, @@ -201,7 +200,7 @@ function distortAlert(distortions: DistortionMap) { } function distortInnerHTML(distortions: DistortionMap) { - function getInnerHTMLDistortion(originalMethod: unknown, meta: PluginMeta) { + function getInnerHTMLDistortion(originalMethod: unknown, meta: SandboxPluginMeta) { const pluginId = meta.id; return function innerHTMLDistortion(this: HTMLElement, ...args: string[]) { for (const arg of args) { @@ -246,7 +245,7 @@ function distortInnerHTML(distortions: DistortionMap) { } function distortCreateElement(distortions: DistortionMap) { - function getCreateElementDistortion(originalMethod: unknown, meta: PluginMeta) { + function getCreateElementDistortion(originalMethod: unknown, meta: SandboxPluginMeta) { const pluginId = meta.id; return function createElementDistortion(this: HTMLElement, arg?: string, options?: unknown) { if (arg && forbiddenElements.includes(arg)) { @@ -272,7 +271,7 @@ function distortCreateElement(distortions: DistortionMap) { } function distortInsert(distortions: DistortionMap) { - function getInsertDistortion(originalMethod: unknown, meta: PluginMeta) { + function getInsertDistortion(originalMethod: unknown, meta: SandboxPluginMeta) { const pluginId = meta.id; return function insertChildDistortion(this: HTMLElement, node?: Node, ref?: Node) { const nodeType = node?.nodeName?.toLowerCase() || ''; @@ -294,7 +293,7 @@ function distortInsert(distortions: DistortionMap) { }; } - function getinsertAdjacentElementDistortion(originalMethod: unknown, meta: PluginMeta) { + function getinsertAdjacentElementDistortion(originalMethod: unknown, meta: SandboxPluginMeta) { const pluginId = meta.id; return function insertAdjacentElementDistortion(this: HTMLElement, position?: string, node?: Node) { const nodeType = node?.nodeName?.toLowerCase() || ''; @@ -336,7 +335,7 @@ function distortInsert(distortions: DistortionMap) { // set distortions to append elements to the document function distortAppend(distortions: DistortionMap) { // append accepts an array of nodes to append https://developer.mozilla.org/en-US/docs/Web/API/Node/append - function getAppendDistortion(originalMethod: unknown, meta: PluginMeta) { + function getAppendDistortion(originalMethod: unknown, meta: SandboxPluginMeta) { const pluginId = meta.id; return function appendDistortion(this: HTMLElement, ...args: Node[]) { let acceptedNodes = args; @@ -363,7 +362,7 @@ function distortAppend(distortions: DistortionMap) { } // appendChild accepts a single node to add https://developer.mozilla.org/en-US/docs/Web/API/Node/appendChild - function getAppendChildDistortion(originalMethod: unknown, meta: PluginMeta, sandboxEnv?: SandboxEnvironment) { + function getAppendChildDistortion(originalMethod: unknown, meta: SandboxPluginMeta, sandboxEnv?: SandboxEnvironment) { const pluginId = meta.id; return function appendChildDistortion(this: HTMLElement, arg?: Node) { const nodeType = arg?.nodeName?.toLowerCase() || ''; @@ -383,7 +382,7 @@ function distortAppend(distortions: DistortionMap) { // this allows webpack chunks to be loaded into the sandbox // loadScriptIntoSandbox has restrictions on what scripts can be loaded if (sandboxEnv && arg && nodeType === 'script' && arg instanceof HTMLScriptElement) { - loadScriptIntoSandbox(arg.src, meta, sandboxEnv) + loadScriptIntoSandbox(arg.src, sandboxEnv) .then(() => { arg.onload?.call(arg, new Event('load')); }) diff --git a/public/app/features/plugins/sandbox/document_sandbox.ts b/public/app/features/plugins/sandbox/document_sandbox.ts index 9dcb1a3a1aa..fdd4b0c9740 100644 --- a/public/app/features/plugins/sandbox/document_sandbox.ts +++ b/public/app/features/plugins/sandbox/document_sandbox.ts @@ -6,7 +6,7 @@ import { CustomVariableSupport, DataSourceApi } from '@grafana/data'; import { config } from '@grafana/runtime'; import { forbiddenElements } from './constants'; -import { isReactClassComponent, logWarning } from './utils'; +import { isReactClassComponent, logWarning, unboxNearMembraneProxies } from './utils'; // IMPORTANT: NEVER export this symbol from a public (e.g `@grafana/*`) package const SANDBOX_LIVE_VALUE = Symbol.for('@@SANDBOX_LIVE_VALUE'); @@ -194,9 +194,30 @@ export function patchWebAPIs() { if (!nativeAPIsPatched) { nativeAPIsPatched = true; patchHistoryReplaceState(); + patchWorkerPostMessage(); } } +/* + * + * Worker.postMessage uses internally structureClone which won't work with proxies. + * + * In case where the blue realm code is directly handling proxy objects that + * should be send over a post message the blue realm will call postMessage and try to + * send the proxy resulting in an error. + * + * This makes sure all proxies are unboxed before being sent over the post message + */ +function patchWorkerPostMessage() { + const originalPostMessage = Worker.prototype.postMessage; + Object.defineProperty(Worker.prototype, 'postMessage', { + value: function (...args: Parameters) { + // eslint-disable-next-line + return originalPostMessage.apply(this, unboxNearMembraneProxies(args) as typeof args); + }, + }); +} + /* * window.history.replaceState is a native API that won't work with proxies * so we need to patch it to unwrap any possible proxies you pass to it. diff --git a/public/app/features/plugins/sandbox/sandbox_components.tsx b/public/app/features/plugins/sandbox/sandbox_components.tsx index 9ff02f2da2f..6e26d86a37b 100644 --- a/public/app/features/plugins/sandbox/sandbox_components.tsx +++ b/public/app/features/plugins/sandbox/sandbox_components.tsx @@ -1,9 +1,9 @@ import { isFunction } from 'lodash'; import React, { ComponentType, FC } from 'react'; -import { GrafanaPlugin, PluginExtensionConfig, PluginMeta, PluginType } from '@grafana/data'; +import { GrafanaPlugin, PluginExtensionConfig, PluginType } from '@grafana/data'; -import { SandboxedPluginObject } from './types'; +import { SandboxPluginMeta, SandboxedPluginObject } from './types'; import { isSandboxedPluginObject } from './utils'; /** @@ -27,7 +27,7 @@ import { isSandboxedPluginObject } from './utils'; */ export async function sandboxPluginComponents( pluginExports: System.Module, - meta: PluginMeta + meta: SandboxPluginMeta ): Promise { if (!isSandboxedPluginObject(pluginExports)) { // we should monitor these cases. There should not be any plugins without a plugin export loaded inside the sandbox @@ -89,7 +89,7 @@ export async function sandboxPluginComponents( const withSandboxWrapper =

( WrappedComponent: ComponentType

, - pluginMeta: PluginMeta + pluginMeta: SandboxPluginMeta ): React.MemoExoticComponent> => { const WithWrapper = React.memo((props: P) => { return ( diff --git a/public/app/features/plugins/sandbox/sandbox_plugin_loader.ts b/public/app/features/plugins/sandbox/sandbox_plugin_loader.ts index 78619d3b93a..dc0b26dc2c0 100644 --- a/public/app/features/plugins/sandbox/sandbox_plugin_loader.ts +++ b/public/app/features/plugins/sandbox/sandbox_plugin_loader.ts @@ -1,13 +1,11 @@ import createVirtualEnvironment from '@locker/near-membrane-dom'; import { ProxyTarget } from '@locker/near-membrane-shared'; -import { BootData, PluginMeta } from '@grafana/data'; +import { BootData } from '@grafana/data'; import { config } from '@grafana/runtime'; import { defaultTrustedTypesPolicy } from 'app/core/trustedTypePolicies'; -import { getPluginSettings } from '../pluginSettings'; - -import { getPluginCode, patchSandboxEnvironmentPrototype } from './code_loader'; +import { getPluginCode, getPluginLoadData, patchSandboxEnvironmentPrototype } from './code_loader'; import { getGeneralSandboxDistortionMap, distortLiveApis } from './distortion_map'; import { getSafeSandboxDomElement, @@ -19,7 +17,7 @@ import { } from './document_sandbox'; import { sandboxPluginDependencies } from './plugin_dependencies'; import { sandboxPluginComponents } from './sandbox_components'; -import { CompartmentDependencyModule, PluginFactoryFunction, SandboxEnvironment } from './types'; +import { CompartmentDependencyModule, PluginFactoryFunction, SandboxEnvironment, SandboxPluginMeta } from './types'; import { logError, logInfo } from './utils'; // Loads near membrane custom formatter for near membrane proxy objects. @@ -33,7 +31,7 @@ const pluginLogCache: Record = {}; export async function importPluginModuleInSandbox({ pluginId }: { pluginId: string }): Promise { patchWebAPIs(); try { - const pluginMeta = await getPluginSettings(pluginId); + const pluginMeta = getPluginLoadData(pluginId); if (!pluginImportCache.has(pluginId)) { pluginImportCache.set(pluginId, doImportPluginModuleInSandbox(pluginMeta)); } @@ -48,7 +46,7 @@ export async function importPluginModuleInSandbox({ pluginId }: { pluginId: stri } } -async function doImportPluginModuleInSandbox(meta: PluginMeta): Promise { +async function doImportPluginModuleInSandbox(meta: SandboxPluginMeta): Promise { logInfo('Loading with sandbox', { pluginId: meta.id, }); diff --git a/public/app/features/plugins/sandbox/types.ts b/public/app/features/plugins/sandbox/types.ts index aacd915ce0d..3b9e104f801 100644 --- a/public/app/features/plugins/sandbox/types.ts +++ b/public/app/features/plugins/sandbox/types.ts @@ -1,6 +1,6 @@ import createVirtualEnvironment from '@locker/near-membrane-dom'; -import { GrafanaPlugin } from '@grafana/data'; +import { GrafanaPlugin, PluginMeta } from '@grafana/data'; export type CompartmentDependencyModule = unknown; export type PluginFactoryFunction = (...args: CompartmentDependencyModule[]) => SandboxedPluginObject; @@ -10,3 +10,5 @@ export type SandboxedPluginObject = { }; export type SandboxEnvironment = ReturnType; + +export type SandboxPluginMeta = Pick; diff --git a/public/app/features/plugins/sandbox/utils.ts b/public/app/features/plugins/sandbox/utils.ts index 30aab341881..af97b19c9ec 100644 --- a/public/app/features/plugins/sandbox/utils.ts +++ b/public/app/features/plugins/sandbox/utils.ts @@ -1,4 +1,5 @@ import { isNearMembraneProxy } from '@locker/near-membrane-shared'; +import { cloneDeep } from 'lodash'; import React from 'react'; import { PluginSignatureType, PluginType } from '@grafana/data'; @@ -112,3 +113,24 @@ export function unboxRegexesFromMembraneProxy(structure: unknown): unknown { } return structure; } + +export function unboxNearMembraneProxies(structure: unknown): unknown { + if (!structure) { + return structure; + } + + if (isNearMembraneProxy(structure)) { + return cloneDeep(structure); + } + + if (Array.isArray(structure)) { + return structure.map(unboxNearMembraneProxies); + } + if (typeof structure === 'object') { + return Object.keys(structure).reduce((acc, key) => { + Reflect.set(acc, key, unboxNearMembraneProxies(Reflect.get(structure, key))); + return acc; + }, {}); + } + return structure; +} diff --git a/public/app/features/profile/UserProfileEditPage.test.tsx b/public/app/features/profile/UserProfileEditPage.test.tsx index 1f69b45c495..e81b5bfc50e 100644 --- a/public/app/features/profile/UserProfileEditPage.test.tsx +++ b/public/app/features/profile/UserProfileEditPage.test.tsx @@ -4,7 +4,7 @@ import React from 'react'; import { OrgRole, PluginExtensionComponent, PluginExtensionTypes } from '@grafana/data'; import { selectors } from '@grafana/e2e-selectors'; -import { setPluginExtensionGetter, GetPluginExtensions } from '@grafana/runtime'; +import { setPluginExtensionsHook, UsePluginExtensions } from '@grafana/runtime'; import * as useQueryParams from 'app/core/hooks/useQueryParams'; import { TestProvider } from '../../../test/helpers/TestProvider'; @@ -170,9 +170,11 @@ async function getTestContext(overrides: Partial = jest.fn().mockReturnValue({ extensions }); + const getter: UsePluginExtensions = jest + .fn() + .mockReturnValue({ extensions, isLoading: false }); - setPluginExtensionGetter(getter); + setPluginExtensionsHook(getter); const props = { ...defaultProps, ...overrides }; const { rerender } = render( diff --git a/public/app/features/profile/UserProfileEditPage.tsx b/public/app/features/profile/UserProfileEditPage.tsx index 2e99bca080f..613bf924891 100644 --- a/public/app/features/profile/UserProfileEditPage.tsx +++ b/public/app/features/profile/UserProfileEditPage.tsx @@ -1,10 +1,10 @@ -import React, { useMemo, useState } from 'react'; +import React, { useState } from 'react'; import { connect, ConnectedProps } from 'react-redux'; import { useMount } from 'react-use'; import { PluginExtensionComponent, PluginExtensionPoints } from '@grafana/data'; import { selectors } from '@grafana/e2e-selectors'; -import { getPluginComponentExtensions } from '@grafana/runtime'; +import { usePluginComponentExtensions } from '@grafana/runtime'; import { Tab, TabsBar, TabContent, Stack } from '@grafana/ui'; import { Page } from 'app/core/components/Page/Page'; import SharedPreferences from 'app/core/components/SharedPreferences/SharedPreferences'; @@ -76,30 +76,21 @@ export function UserProfileEditPage({ useMount(() => initUserProfilePage()); - const extensionComponents = useMemo(() => { - const { extensions } = getPluginComponentExtensions({ - extensionPointId: PluginExtensionPoints.UserProfileTab, - }); + const { extensions } = usePluginComponentExtensions({ extensionPointId: PluginExtensionPoints.UserProfileTab }); - return extensions; - }, []); - - const groupedExtensionComponents = extensionComponents.reduce>( - (acc, extension) => { - const { title } = extension; - if (acc[title]) { - acc[title].push(extension); - } else { - acc[title] = [extension]; - } - return acc; - }, - {} - ); + const groupedExtensionComponents = extensions.reduce>((acc, extension) => { + const { title } = extension; + if (acc[title]) { + acc[title].push(extension); + } else { + acc[title] = [extension]; + } + return acc; + }, {}); const convertExtensionComponentTitleToTabId = (title: string) => title.toLowerCase(); - const showTabs = extensionComponents.length > 0; + const showTabs = extensions.length > 0; const tabs: TabInfo[] = [ { id: GENERAL_SETTINGS_TAB, diff --git a/public/app/features/serviceaccounts/ServiceAccountPage.tsx b/public/app/features/serviceaccounts/ServiceAccountPage.tsx index 86be0ebee93..f99bc03cb4a 100644 --- a/public/app/features/serviceaccounts/ServiceAccountPage.tsx +++ b/public/app/features/serviceaccounts/ServiceAccountPage.tsx @@ -2,7 +2,7 @@ import React, { useEffect, useState } from 'react'; import { connect, ConnectedProps } from 'react-redux'; import { getTimeZone, NavModelItem } from '@grafana/data'; -import { Button, ConfirmModal, HorizontalGroup, IconButton } from '@grafana/ui'; +import { Button, ConfirmModal, IconButton, Stack } from '@grafana/ui'; import { Page } from 'app/core/components/Page/Page'; import { contextSrv } from 'app/core/core'; import { GrafanaRouteComponentProps } from 'app/core/navigation/types'; @@ -137,7 +137,7 @@ export const ServiceAccountPageUnconnected = ({

{serviceAccount && !serviceAccount.isExternal && ( - + )} - + {tokens && ( )} {!isLoading && noServiceAccountsCreated && ( - <> - - + + Add service account + + } + message={t('service-accounts.empty-state.title', "You haven't created any service accounts yet")} + > + + Remember, you can provide specific permissions for API access to other applications + + )} {(isLoading || serviceAccounts.length !== 0) && ( diff --git a/public/app/features/storage/RootView.tsx b/public/app/features/storage/RootView.tsx index 62bda7bb4e1..c6f855a7024 100644 --- a/public/app/features/storage/RootView.tsx +++ b/public/app/features/storage/RootView.tsx @@ -3,19 +3,7 @@ import React, { useMemo, useState } from 'react'; import { useAsync } from 'react-use'; import { DataFrame, GrafanaTheme2 } from '@grafana/data'; -import { - Alert, - Button, - Card, - FilterInput, - HorizontalGroup, - Icon, - IconName, - TagList, - useStyles2, - VerticalGroup, - InlineField, -} from '@grafana/ui'; +import { Alert, Button, Card, FilterInput, Icon, IconName, TagList, useStyles2, Stack, InlineField } from '@grafana/ui'; import { getGrafanaStorage } from './storage'; import { StorageInfo, StorageView } from './types'; @@ -62,7 +50,7 @@ export function RootView({ root, onPathChange }: Props) { const renderRoots = (pfix: string, roots: StorageInfo[]) => { return ( - + {roots.map((s) => { const ok = s.ready; return ( @@ -75,9 +63,9 @@ export function RootView({ root, onPathChange }: Props) { {s.notice?.map((notice) => )} - + - + @@ -85,7 +73,7 @@ export function RootView({ root, onPathChange }: Props) { ); })} - + ); }; diff --git a/public/app/features/storage/StoragePage.tsx b/public/app/features/storage/StoragePage.tsx index dc7be8568f6..8875241987c 100644 --- a/public/app/features/storage/StoragePage.tsx +++ b/public/app/features/storage/StoragePage.tsx @@ -4,7 +4,7 @@ import { useAsync } from 'react-use'; import { DataFrame, GrafanaTheme2, isDataFrame, ValueLinkConfig } from '@grafana/data'; import { locationService } from '@grafana/runtime'; -import { useStyles2, Spinner, TabsBar, Tab, Button, HorizontalGroup, Alert, toIconName } from '@grafana/ui'; +import { useStyles2, Spinner, TabsBar, Tab, Button, Stack, Box, Alert, toIconName } from '@grafana/ui'; import appEvents from 'app/core/app_events'; import { Page } from 'app/core/components/Page/Page'; import { useNavModel } from 'app/core/hooks/useNavModel'; @@ -163,9 +163,9 @@ export default function StoragePage(props: Props) { return (
- + - + {canAddFolder && ( <> @@ -200,8 +200,8 @@ export default function StoragePage(props: Props) { Delete )} - - + + {errorMessages.length > 0 && getErrorMessages()} diff --git a/public/app/features/teams/TeamList.tsx b/public/app/features/teams/TeamList.tsx index 9830d3b7248..4940ec5bb60 100644 --- a/public/app/features/teams/TeamList.tsx +++ b/public/app/features/teams/TeamList.tsx @@ -19,10 +19,9 @@ import { TextLink, useStyles2, } from '@grafana/ui'; -import EmptyListCTA from 'app/core/components/EmptyListCTA/EmptyListCTA'; import { Page } from 'app/core/components/Page/Page'; import { fetchRoleOptions } from 'app/core/components/RolePicker/api'; -import { t } from 'app/core/internationalization'; +import { Trans, t } from 'app/core/internationalization'; import { contextSrv } from 'app/core/services/context_srv'; import { AccessControlAction, Role, StoreState, Team } from 'app/types'; @@ -208,24 +207,31 @@ export const TeamList = ({ - New Team - + !noTeams ? ( + + New Team + + ) : undefined } > {noTeams ? ( - + + New team + + } + message={t('teams.empty-state.title', "You haven't created any teams yet")} + > + + Assign folder and dashboard permissions to teams instead of users to ease administration.{' '} + + Learn more + + + ) : ( <>
diff --git a/public/app/features/templating/template_srv.ts b/public/app/features/templating/template_srv.ts index 9aa3133080c..033bf8b4d79 100644 --- a/public/app/features/templating/template_srv.ts +++ b/public/app/features/templating/template_srv.ts @@ -253,7 +253,8 @@ export class TemplateSrv implements BaseTemplateSrv { scopedVars.__sceneObject.value, target, scopedVars, - format as string | VariableCustomFormatterFn | undefined + format as string | VariableCustomFormatterFn | undefined, + interpolations ); } @@ -263,7 +264,8 @@ export class TemplateSrv implements BaseTemplateSrv { window.__grafanaSceneContext, target, scopedVars, - format as string | VariableCustomFormatterFn | undefined + format as string | VariableCustomFormatterFn | undefined, + interpolations ); } diff --git a/public/app/features/trails/DataTrail.test.tsx b/public/app/features/trails/DataTrail.test.tsx index 9b62ddeff41..a6ccc31f354 100644 --- a/public/app/features/trails/DataTrail.test.tsx +++ b/public/app/features/trails/DataTrail.test.tsx @@ -1,4 +1,5 @@ import { locationService, setDataSourceSrv } from '@grafana/runtime'; +import { AdHocFiltersVariable, sceneGraph } from '@grafana/scenes'; import { MockDataSourceSrv, mockDataSource } from '../alerting/unified/mocks'; import { DataSourceType } from '../alerting/unified/utils/datasource'; @@ -7,7 +8,7 @@ import { activateFullSceneTree } from '../dashboard-scene/utils/test-utils'; import { DataTrail } from './DataTrail'; import { MetricScene } from './MetricScene'; import { MetricSelectScene } from './MetricSelectScene'; -import { MetricSelectedEvent } from './shared'; +import { MetricSelectedEvent, VAR_FILTERS } from './shared'; describe('DataTrail', () => { beforeAll(() => { @@ -61,10 +62,6 @@ describe('DataTrail', () => { expect(trail.state.history.state.steps[1].type).toBe('metric'); }); - it('Should set history current step to 1', () => { - expect(trail.state.history.state.currentStep).toBe(1); - }); - it('Should set history currentStep to 1', () => { expect(trail.state.history.state.currentStep).toBe(1); }); @@ -73,6 +70,10 @@ describe('DataTrail', () => { expect(trail.state.history.state.steps[1].parentIndex).toBe(0); }); + it('Should have time range `from` be default "now-6h"', () => { + expect(trail.state.$timeRange?.state.from).toBe('now-6h'); + }); + describe('And browser back button is pressed', () => { locationService.getHistory().goBack(); @@ -81,6 +82,271 @@ describe('DataTrail', () => { expect(pathname).toEqual(preTrailUrl); }); }); + + describe('And when changing the time range `from` to "now-1h"', () => { + beforeEach(() => { + trail.state.$timeRange?.setState({ from: 'now-1h' }); + }); + + it('should sync state with url', () => { + expect(locationService.getSearchObject().from).toBe('now-1h'); + }); + + it('should add history step', () => { + expect(trail.state.history.state.steps[2].type).toBe('time'); + }); + + it('Should set history currentStep to 2', () => { + expect(trail.state.history.state.currentStep).toBe(2); + }); + + it('Should set history step 2 parentIndex to 1', () => { + expect(trail.state.history.state.steps[2].parentIndex).toBe(1); + }); + + it('Should have time range `from` be updated "now-1h"', () => { + expect(trail.state.$timeRange?.state.from).toBe('now-1h'); + }); + + it('Previous history step should have previous default `from` of "now-6h"', () => { + expect(trail.state.history.state.steps[1].trailState.$timeRange?.state.from).toBe('now-6h'); + }); + + it('Current history step should have new `from` of "now-1h"', () => { + expect(trail.state.history.state.steps[2].trailState.$timeRange?.state.from).toBe('now-1h'); + }); + + describe('And when traversing back to step 1', () => { + beforeEach(() => { + trail.state.history.goBackToStep(1); + }); + + it('Should set history currentStep to 1', () => { + expect(trail.state.history.state.currentStep).toBe(1); + }); + + it('should sync state with url', () => { + expect(locationService.getSearchObject().from).toBe('now-6h'); + }); + + it('Should have time range `from` be set back to "now-6h"', () => { + expect(trail.state.$timeRange?.state.from).toBe('now-6h'); + }); + + describe('And then when changing the time range `from` to "now-15m"', () => { + beforeEach(() => { + trail.state.$timeRange?.setState({ from: 'now-15m' }); + }); + + it('should sync state with url', () => { + expect(locationService.getSearchObject().from).toBe('now-15m'); + }); + + it('should add history step', () => { + expect(trail.state.history.state.steps[3].type).toBe('time'); + }); + + it('Should set history currentStep to 3', () => { + expect(trail.state.history.state.currentStep).toBe(3); + }); + + it('Should set history step 3 parentIndex to 1', () => { + expect(trail.state.history.state.steps[3].parentIndex).toBe(1); + }); + + it('Should have time range `from` be updated "now-15m"', () => { + expect(trail.state.$timeRange?.state.from).toBe('now-15m'); + }); + + it('History step 1 (parent) should have previous default `from` of "now-6h"', () => { + expect(trail.state.history.state.steps[1].trailState.$timeRange?.state.from).toBe('now-6h'); + }); + + it('History step 2 should still have `from` of "now-1h"', () => { + expect(trail.state.history.state.steps[2].trailState.$timeRange?.state.from).toBe('now-1h'); + }); + + describe('And then when returning again to step 1', () => { + beforeEach(() => { + trail.state.history.goBackToStep(1); + }); + + it('Should set history currentStep to 1', () => { + expect(trail.state.history.state.currentStep).toBe(1); + }); + + it('should sync state with url', () => { + expect(locationService.getSearchObject().from).toBe('now-6h'); + }); + + it('History step 1 (parent) should have previous default `from` of "now-6h"', () => { + expect(trail.state.history.state.steps[1].trailState.$timeRange?.state.from).toBe('now-6h'); + }); + + it('History step 2 should still have `from` of "now-1h"', () => { + expect(trail.state.history.state.steps[2].trailState.$timeRange?.state.from).toBe('now-1h'); + }); + + it('History step 3 should still have `from` of "now-15m"', () => { + expect(trail.state.history.state.steps[3].trailState.$timeRange?.state.from).toBe('now-15m'); + }); + + it('Should have time range `from` be set back to "now-6h"', () => { + expect(trail.state.$timeRange?.state.from).toBe('now-6h'); + }); + }); + }); + }); + }); + + function getFilterVar() { + const variable = sceneGraph.lookupVariable(VAR_FILTERS, trail); + if (variable instanceof AdHocFiltersVariable) { + return variable; + } + throw new Error('getFilterVar failed'); + } + + function getStepFilterVar(step: number) { + const variable = trail.state.history.state.steps[step].trailState.$variables?.getByName(VAR_FILTERS); + if (variable instanceof AdHocFiltersVariable) { + return variable; + } + throw new Error(`getStepFilterVar failed for step ${step}`); + } + + it('Should have default empty filter', () => { + expect(getFilterVar().state.filters.length).toBe(0); + }); + + describe('And when changing the filter to zone=a', () => { + beforeEach(() => { + getFilterVar().setState({ filters: [{ key: 'zone', operator: '=', value: 'a' }] }); + }); + + it('should sync state with url', () => { + expect(decodeURIComponent(locationService.getSearchObject()['var-filters']?.toString()!)).toBe('zone|=|a'); + }); + + it('should add history step', () => { + expect(trail.state.history.state.steps[2].type).toBe('filters'); + }); + + it('Should set history currentStep to 2', () => { + expect(trail.state.history.state.currentStep).toBe(2); + }); + + it('Should set history step 2 parentIndex to 1', () => { + expect(trail.state.history.state.steps[2].parentIndex).toBe(1); + }); + + it('Should have filter be updated to "zone=a"', () => { + expect(getFilterVar().state.filters[0].key).toBe('zone'); + expect(getFilterVar().state.filters[0].value).toBe('a'); + }); + + it('Previous history step should have empty filter', () => { + expect(getStepFilterVar(1).state.filters.length).toBe(0); + }); + + it('Current history step should have new filter zone=a', () => { + expect(getStepFilterVar(2).state.filters[0].key).toBe('zone'); + expect(getStepFilterVar(2).state.filters[0].value).toBe('a'); + }); + + describe('And when traversing back to step 1', () => { + beforeEach(() => { + trail.state.history.goBackToStep(1); + }); + + it('Should set history currentStep to 1', () => { + expect(trail.state.history.state.currentStep).toBe(1); + }); + + it('should sync state with url', () => { + expect(locationService.getSearchObject()['var-filters']).toBe(''); + }); + + it('Should have filters set back to empty', () => { + expect(getFilterVar().state.filters.length).toBe(0); + }); + + describe('And when changing the filter to zone=b', () => { + beforeEach(() => { + getFilterVar().setState({ filters: [{ key: 'zone', operator: '=', value: 'b' }] }); + }); + + it('should sync state with url', () => { + expect(decodeURIComponent(locationService.getSearchObject()['var-filters']?.toString()!)).toBe( + 'zone|=|b' + ); + }); + + it('should add history step', () => { + expect(trail.state.history.state.steps[3].type).toBe('filters'); + }); + + it('Should set history currentStep to 3', () => { + expect(trail.state.history.state.currentStep).toBe(3); + }); + + it('Should set history step 3 parentIndex to 1', () => { + expect(trail.state.history.state.steps[3].parentIndex).toBe(1); + }); + + it('Should have filter be updated to "zone=b"', () => { + expect(getFilterVar().state.filters[0].key).toBe('zone'); + expect(getFilterVar().state.filters[0].value).toBe('b'); + }); + + it('Parent history step 1 should still have empty filter', () => { + expect(getStepFilterVar(1).state.filters.length).toBe(0); + }); + + it('History step 2 should still have old filter zone=a', () => { + expect(getStepFilterVar(2).state.filters[0].key).toBe('zone'); + expect(getStepFilterVar(2).state.filters[0].value).toBe('a'); + }); + + it('Current history step 3 should have new filter zone=b', () => { + expect(getStepFilterVar(3).state.filters[0].key).toBe('zone'); + expect(getStepFilterVar(3).state.filters[0].value).toBe('b'); + }); + + describe('And then when returning again to step 1', () => { + beforeEach(() => { + trail.state.history.goBackToStep(1); + }); + + it('Should set history currentStep to 1', () => { + expect(trail.state.history.state.currentStep).toBe(1); + }); + + it('should sync state with url', () => { + expect(locationService.getSearchObject()['var-filters']).toBe(''); + }); + + it('Should have filters set back to empty', () => { + expect(getFilterVar().state.filters.length).toBe(0); + }); + + it('History step 1 should still have empty filter', () => { + expect(getStepFilterVar(1).state.filters.length).toBe(0); + }); + + it('History step 2 should still have old filter zone=a', () => { + expect(getStepFilterVar(2).state.filters[0].key).toBe('zone'); + expect(getStepFilterVar(2).state.filters[0].value).toBe('a'); + }); + + it('History step 3 should have new filter zone=b', () => { + expect(getStepFilterVar(3).state.filters[0].key).toBe('zone'); + expect(getStepFilterVar(3).state.filters[0].value).toBe('b'); + }); + }); + }); + }); + }); }); describe('When going back to history step 1', () => { diff --git a/public/app/features/trails/DataTrail.tsx b/public/app/features/trails/DataTrail.tsx index 73d75201d87..4b3820f5f96 100644 --- a/public/app/features/trails/DataTrail.tsx +++ b/public/app/features/trails/DataTrail.tsx @@ -18,6 +18,7 @@ import { SceneRefreshPicker, SceneTimePicker, SceneTimeRange, + sceneUtils, SceneVariable, SceneVariableSet, VariableDependencyConfig, @@ -26,7 +27,7 @@ import { import { useStyles2 } from '@grafana/ui'; import { DataTrailSettings } from './DataTrailSettings'; -import { DataTrailHistory, DataTrailHistoryStep } from './DataTrailsHistory'; +import { DataTrailHistory } from './DataTrailsHistory'; import { MetricScene } from './MetricScene'; import { MetricSelectScene } from './MetricSelectScene'; import { MetricsHeader } from './MetricsHeader'; @@ -81,59 +82,48 @@ export class DataTrail extends SceneObjectBase { // Some scene elements publish this this.subscribeToEvent(MetricSelectedEvent, this._handleMetricSelectedEvent.bind(this)); - // Pay attention to changes in history (i.e., changing the step) - this.state.history.subscribeToState((newState, oldState) => { - const oldNumberOfSteps = oldState.steps.length; - const newNumberOfSteps = newState.steps.length; - - const newStepWasAppended = newNumberOfSteps > oldNumberOfSteps; - - if (newStepWasAppended) { - // A new step is a significant change. Update the URL to match the new state. - this.syncTrailToUrl(); - // In order for the `useBookmarkState` to re-evaluate after a new step was made: - this.forceRender(); - // Do nothing else because the step state is already up to date -- it created a new step! - return; - } - - if (oldState.currentStep === newState.currentStep) { - // The same step was clicked on -- no need to change anything. - return; - } - - // History changed because a different node was selected - const step = newState.steps[newState.currentStep]; - - if (!step) { - return; - } - - this.goBackToStep(step); - }); - const filtersVariable = sceneGraph.lookupVariable(VAR_FILTERS, this); - const stateSubscription = - filtersVariable instanceof AdHocFiltersVariable && - filtersVariable?.subscribeToState((newState, prevState) => { - if (!this._addingFilterWithoutReportingInteraction) { - reportChangeInLabelFilters(newState.filters, prevState.filters); - } - }); + if (filtersVariable instanceof AdHocFiltersVariable) { + this._subs.add( + filtersVariable?.subscribeToState((newState, prevState) => { + if (!this._addingFilterWithoutReportingInteraction) { + reportChangeInLabelFilters(newState.filters, prevState.filters); + } + }) + ); + } + + this.enableUrlSync(); + + // Save the current trail as a recent if the browser closes or reloads + const saveRecentTrail = () => getTrailStore().setRecentTrail(this); + window.addEventListener('unload', saveRecentTrail); return () => { + this.disableUrlSync(); + if (!this.state.embedded) { - getTrailStore().setRecentTrail(this); - } - if (stateSubscription) { - stateSubscription?.unsubscribe(); + saveRecentTrail(); } + window.removeEventListener('unload', saveRecentTrail); }; } + private enableUrlSync() { + if (!this.state.embedded) { + getUrlSyncManager().initSync(this); + } + } + + private disableUrlSync() { + if (!this.state.embedded) { + getUrlSyncManager().cleanUp(this); + } + } + protected _variableDependency = new VariableDependencyConfig(this, { variableNames: [VAR_DATASOURCE], - onReferencedVariableValueChanged: async (variable: SceneVariable) => { + onReferencedVariableValueChanged: (variable: SceneVariable) => { const { name } = variable.state; if (name === VAR_DATASOURCE) { this.datasourceHelper.reset(); @@ -153,13 +143,13 @@ export class DataTrail extends SceneObjectBase { } this._addingFilterWithoutReportingInteraction = true; - variable.setState({ - filters: [...variable.state.filters, filter], - }); + + variable.setState({ filters: [...variable.state.filters, filter] }); + this._addingFilterWithoutReportingInteraction = false; } - private _addingFilterWithoutReportingInteraction = false; + private _addingFilterWithoutReportingInteraction = false; private datasourceHelper = new MetricDatasourceHelper(this); public getMetricMetadata(metric?: string) { @@ -170,25 +160,21 @@ export class DataTrail extends SceneObjectBase { return this.getMetricMetadata(this.state.metric); } - private goBackToStep(step: DataTrailHistoryStep) { - if (!step.trailState.metric) { - step.trailState.metric = undefined; - } + public restoreFromHistoryStep(state: DataTrailState) { + this.disableUrlSync(); - this.setState(step.trailState); - this.syncTrailToUrl(); - } - - private syncTrailToUrl() { - if (this.state.embedded) { - // Embedded trails should not be altering the URL - return; - } + this.setState( + sceneUtils.cloneSceneObjectState(state, { + history: this.state.history, + metric: !state.metric ? undefined : state.metric, + }) + ); const urlState = getUrlSyncManager().getUrlState(this); const fullUrl = urlUtil.renderUrl(locationService.getLocation().pathname, urlState); + locationService.replace(fullUrl); - locationService.replace(encodeURI(fullUrl)); + this.enableUrlSync(); } private _handleMetricSelectedEvent(evt: MetricSelectedEvent) { diff --git a/public/app/features/trails/DataTrailsApp.tsx b/public/app/features/trails/DataTrailsApp.tsx index 9622851ca5a..9f2511306be 100644 --- a/public/app/features/trails/DataTrailsApp.tsx +++ b/public/app/features/trails/DataTrailsApp.tsx @@ -75,14 +75,6 @@ function DataTrailView({ trail }: { trail: DataTrail }) { useEffect(() => { if (!isInitialized) { - // Set the initial state based on the URL. - getUrlSyncManager().initSync(trail); - // Any further changes to the state should occur directly to the state, not through the URL. - // We want to stop automatically syncing the URL state (and vice versa) to the trail after this point. - // Moving forward in the lifecycle of the trail, we will make explicit calls to trail.syncTrailToUrl() - // so we can ensure the URL is kept up to date at key points. - getUrlSyncManager().cleanUp(trail); - getTrailStore().setRecentTrail(trail); setIsInitialized(true); } @@ -100,7 +92,7 @@ let dataTrailsApp: DataTrailsApp; export function getDataTrailsApp() { if (!dataTrailsApp) { dataTrailsApp = new DataTrailsApp({ - trail: newMetricsTrail(), + trail: getInitialTrail(), home: new DataTrailsHome({}), }); } @@ -108,6 +100,34 @@ export function getDataTrailsApp() { return dataTrailsApp; } +/** + * Get the initial trail for the app to work with based on the current URL + * + * It will either be a new trail that will be started based on the state represented + * in the URL parameters, or it will be the most recently used trail (according to the trail store) + * which has its current history step matching the URL parameters. + * + * The reason for trying to reinitialize from the recent trail is to resolve an issue + * where refreshing the browser would wipe the step history. This allows you to preserve + * it between browser refreshes, or when reaccessing the same URL. + */ +function getInitialTrail() { + const newTrail = newMetricsTrail(); + + // Set the initial state of the newTrail based on the URL, + // In case we are initializing from an externally created URL or a page reload + getUrlSyncManager().initSync(newTrail); + // Remove the URL sync for now. It will be restored on the trail if it is activated. + getUrlSyncManager().cleanUp(newTrail); + + // If one of the recent trails is a match to the newTrail derived from the current URL, + // let's restore that trail so that a page refresh doesn't create a new trail. + const recentMatchingTrail = getTrailStore().findMatchingRecentTrail(newTrail)?.resolve(); + + // If there is a matching trail, initialize with that. Otherwise, use the new trail. + return recentMatchingTrail || newTrail; +} + function getStyles(theme: GrafanaTheme2) { return { customPage: css({ diff --git a/public/app/features/trails/DataTrailsHistory.tsx b/public/app/features/trails/DataTrailsHistory.tsx index b69be31ab89..539174d277f 100644 --- a/public/app/features/trails/DataTrailsHistory.tsx +++ b/public/app/features/trails/DataTrailsHistory.tsx @@ -23,6 +23,10 @@ export interface DataTrailsHistoryState extends SceneObjectState { steps: DataTrailHistoryStep[]; } +export function isDataTrailsHistoryState(state: SceneObjectState): state is DataTrailsHistoryState { + return 'currentStep' in state && 'steps' in state; +} + export interface DataTrailHistoryStep { description: string; type: TrailStepType; @@ -124,14 +128,17 @@ export class DataTrailHistory extends SceneObjectBase { return; } - this.stepTransitionInProgress = true; const step = this.state.steps[stepIndex]; const type = step.type === 'metric' && step.trailState.metric === undefined ? 'metric-clear' : step.type; + reportExploreMetrics('history_step_clicked', { type }); + this.stepTransitionInProgress = true; this.setState({ currentStep: stepIndex }); - // The URL will update + getTrailFor(this).restoreFromHistoryStep(step.trailState); + + // The URL will update this.stepTransitionInProgress = false; } diff --git a/public/app/features/trails/Integrations/dashboardIntegration.ts b/public/app/features/trails/Integrations/dashboardIntegration.ts index 82b2c437b57..12fa49cc771 100644 --- a/public/app/features/trails/Integrations/dashboardIntegration.ts +++ b/public/app/features/trails/Integrations/dashboardIntegration.ts @@ -1,55 +1,55 @@ -import { isString } from 'lodash'; - -import { PanelMenuItem, PanelModel } from '@grafana/data'; +import { PanelMenuItem } from '@grafana/data'; +import { PromQuery } from '@grafana/prometheus'; import { getDataSourceSrv } from '@grafana/runtime'; import { SceneTimeRangeLike, VizPanel } from '@grafana/scenes'; -import { DataSourceRef } from '@grafana/schema'; +import { DataQuery, DataSourceRef } from '@grafana/schema'; +import { getQueryRunnerFor } from 'app/features/dashboard-scene/utils/utils'; -import { DashboardModel } from '../../dashboard/state'; import { DashboardScene } from '../../dashboard-scene/scene/DashboardScene'; import { MetricScene } from '../MetricScene'; import { reportExploreMetrics } from '../interactions'; import { DataTrailEmbedded, DataTrailEmbeddedState } from './DataTrailEmbedded'; -import { SceneDrawerAsScene, launchSceneDrawerInGlobalModal } from './SceneDrawer'; +import { SceneDrawerAsScene } from './SceneDrawer'; import { QueryMetric, getQueryMetrics } from './getQueryMetrics'; -import { - createAdHocFilters, - getPanelType, - getQueryMetricLabel, - getQueryRunner, - getTimeRangeFromDashboard, -} from './utils'; +import { createAdHocFilters, getQueryMetricLabel, getTimeRangeFromDashboard } from './utils'; -export function addDataTrailPanelAction( - dashboard: DashboardScene | DashboardModel, - panel: VizPanel | PanelModel, - items: PanelMenuItem[] -) { - const panelType = getPanelType(panel); - if (panelType !== 'timeseries') { +export async function addDataTrailPanelAction(dashboard: DashboardScene, panel: VizPanel, items: PanelMenuItem[]) { + if (panel.state.pluginId !== 'timeseries') { return; } - const queryRunner = getQueryRunner(panel); - if (!queryRunner) { + const queryRunner = getQueryRunnerFor(panel); + if (queryRunner == null) { return; } - const ds = getDataSourceSrv().getInstanceSettings(queryRunner.state.datasource); + const { queries, datasource, data } = queryRunner.state; - if (ds?.meta.id !== 'prometheus') { + if (datasource == null) { return; } - const queries = queryRunner.state.queries.map((q) => q.expr).filter(isString); + if (datasource.type !== 'prometheus') { + return; + } - const queryMetrics = getQueryMetrics(queries); + const dataSourceApi = await getDataSourceSrv().get(datasource); + + if (dataSourceApi.interpolateVariablesInQueries == null) { + return; + } + + const interpolated = dataSourceApi + .interpolateVariablesInQueries(queries, { __sceneObject: { value: panel } }, data?.request?.filters) + .filter(isPromQuery); + + const queryMetrics = getQueryMetrics(interpolated.map((q) => q.expr)); const subMenu: PanelMenuItem[] = queryMetrics.map((item) => { return { text: getQueryMetricLabel(item), - onClick: createClickHandler(item, dashboard, ds), + onClick: createClickHandler(item, dashboard, dataSourceApi), }; }); @@ -88,11 +88,7 @@ function getEmbeddedTrailsState( return state; } -function createCommonEmbeddedTrailStateProps( - item: QueryMetric, - dashboard: DashboardScene | DashboardModel, - ds: DataSourceRef -) { +function createCommonEmbeddedTrailStateProps(item: QueryMetric, dashboard: DashboardScene, ds: DataSourceRef) { const timeRange = getTimeRangeFromDashboard(dashboard); const trailState = getEmbeddedTrailsState(item, timeRange, ds.uid); const embeddedTrail: DataTrailEmbedded = new DataTrailEmbedded(trailState); @@ -111,21 +107,18 @@ function createCommonEmbeddedTrailStateProps( return commonProps; } -function createClickHandler(item: QueryMetric, dashboard: DashboardScene | DashboardModel, ds: DataSourceRef) { - if (dashboard instanceof DashboardScene) { - return () => { - const commonProps = createCommonEmbeddedTrailStateProps(item, dashboard, ds); - const drawerScene = new SceneDrawerAsScene({ - ...commonProps, - onDismiss: () => dashboard.closeModal(), - }); - reportExploreMetrics('exploration_started', { cause: 'dashboard_panel' }); - dashboard.showModal(drawerScene); - }; - } else { - return () => { - reportExploreMetrics('exploration_started', { cause: 'dashboard_panel' }); - launchSceneDrawerInGlobalModal(createCommonEmbeddedTrailStateProps(item, dashboard, ds)); - }; - } +function createClickHandler(item: QueryMetric, dashboard: DashboardScene, ds: DataSourceRef) { + return () => { + const commonProps = createCommonEmbeddedTrailStateProps(item, dashboard, ds); + const drawerScene = new SceneDrawerAsScene({ + ...commonProps, + onDismiss: () => dashboard.closeModal(), + }); + reportExploreMetrics('exploration_started', { cause: 'dashboard_panel' }); + dashboard.showModal(drawerScene); + }; +} + +export function isPromQuery(model: DataQuery): model is PromQuery { + return 'expr' in model; } diff --git a/public/app/features/trails/Integrations/utils.ts b/public/app/features/trails/Integrations/utils.ts index f5faf434c02..372d81ae999 100644 --- a/public/app/features/trails/Integrations/utils.ts +++ b/public/app/features/trails/Integrations/utils.ts @@ -1,9 +1,5 @@ -import { PanelModel } from '@grafana/data'; import { QueryBuilderLabelFilter } from '@grafana/prometheus/src/querybuilder/shared/types'; -import { SceneQueryRunner, SceneTimeRange, VizPanel } from '@grafana/scenes'; -import { DashboardModel } from 'app/features/dashboard/state'; import { DashboardScene } from 'app/features/dashboard-scene/scene/DashboardScene'; -import { getQueryRunnerFor } from 'app/features/dashboard-scene/utils/utils'; import { QueryMetric } from './getQueryMetrics'; @@ -12,22 +8,8 @@ export function isEquals(labelFilter: QueryBuilderLabelFilter) { return labelFilter.op === '='; } -export function getQueryRunner(panel: VizPanel | PanelModel) { - if (panel instanceof VizPanel) { - return getQueryRunnerFor(panel); - } - - return new SceneQueryRunner({ datasource: panel.datasource || undefined, queries: panel.targets || [] }); -} - -export function getTimeRangeFromDashboard(dashboard: DashboardScene | DashboardModel) { - if (dashboard instanceof DashboardScene) { - return dashboard.state.$timeRange!.clone(); - } - if (dashboard instanceof DashboardModel) { - return new SceneTimeRange({ ...dashboard.time }); - } - return new SceneTimeRange(); +export function getTimeRangeFromDashboard(dashboard: DashboardScene) { + return dashboard.state.$timeRange!.clone(); } export function getQueryMetricLabel({ metric, labelFilters }: QueryMetric) { @@ -43,10 +25,3 @@ export function getQueryMetricLabel({ metric, labelFilters }: QueryMetric) { export function createAdHocFilters(labels: QueryBuilderLabelFilter[]) { return labels?.map((label) => ({ key: label.label, value: label.value, operator: label.op })); } - -export function getPanelType(panel: VizPanel | PanelModel) { - if (panel instanceof VizPanel) { - return panel.state.pluginId; - } - return panel.type; -} diff --git a/public/app/features/trails/TrailStore/TrailStore.test.ts b/public/app/features/trails/TrailStore/TrailStore.test.ts index 1b6c1447fa6..8fe31ff9181 100644 --- a/public/app/features/trails/TrailStore/TrailStore.test.ts +++ b/public/app/features/trails/TrailStore/TrailStore.test.ts @@ -165,7 +165,184 @@ describe('TrailStore', () => { // There should now be two trails expect(store.recent.length).toBe(2); }); + + test('deserializeTrail must show state of current step when not last step', () => { + const trailSerialized: SerializedTrail = { + history: [ + history[0], + history[1], + { + ...history[1], + urlValues: { + ...history[1].urlValues, + metric: 'something_else', + }, + parentIndex: 1, + }, + ], + currentStep: 1, + }; + + // @ts-ignore #2341 -- deliberately access private method to construct trail object for testing purposes + const trail = getTrailStore()._deserializeTrail(trailSerialized); + + // + expect(trail.state.metric).not.toEqual('something_else'); + expect(trail.state.metric).toEqual(history[1].urlValues.metric); + }); }); + + describe('Initialize store with one recent trail with non final current step', () => { + const history: SerializedTrail['history'] = [ + { + urlValues: { + from: 'now-1h', + to: 'now', + 'var-ds': 'ds', + 'var-filters': [], + refresh: '', + }, + type: 'start', + description: 'Test', + parentIndex: -1, + }, + { + urlValues: { + metric: 'current_metric', + from: 'now-1h', + to: 'now', + 'var-ds': 'ds', + 'var-filters': [], + refresh: '', + }, + type: 'metric', + description: 'Test', + parentIndex: 0, + }, + { + urlValues: { + metric: 'final_metric', + from: 'now-1h', + to: 'now', + 'var-ds': 'ds', + 'var-filters': [], + refresh: '', + }, + type: 'metric', + description: 'Test', + parentIndex: 1, + }, + ]; + + beforeEach(() => { + localStorage.clear(); + localStorage.setItem(RECENT_TRAILS_KEY, JSON.stringify([{ history, currentStep: 1 }])); + getTrailStore().load(); + }); + + it('should accurately load recent trails', () => { + const store = getTrailStore(); + expect(store.recent.length).toBe(1); + const trail = store.recent[0].resolve(); + expect(trail.state.history.state.steps.length).toBe(3); + expect(trail.state.history.state.steps[0].type).toBe('start'); + expect(trail.state.history.state.steps[1].type).toBe('metric'); + expect(trail.state.history.state.steps[1].trailState.metric).toBe('current_metric'); + expect(trail.state.history.state.steps[2].type).toBe('metric'); + expect(trail.state.history.state.steps[2].trailState.metric).toBe('final_metric'); + expect(trail.state.history.state.currentStep).toBe(1); + }); + + it('should have no bookmarked trails', () => { + const store = getTrailStore(); + expect(store.bookmarks.length).toBe(0); + }); + + describe('Add a new recent trail with equivalent current step state', () => { + const store = getTrailStore(); + + const duplicateTrailSerialized: SerializedTrail = { + history: [ + history[0], + history[1], + history[2], + { + ...history[2], + urlValues: { + ...history[1].urlValues, + metric: 'different_metric_in_the_middle', + }, + }, + { + ...history[1], + }, + ], + currentStep: 4, + }; + + beforeEach(() => { + // We expect the initialized trail to be there + expect(store.recent.length).toBe(1); + expect(store.recent[0].resolve().state.history.state.steps.length).toBe(3); + + // @ts-ignore #2341 -- deliberately access private method to construct trail object for testing purposes + const duplicateTrail = store._deserializeTrail(duplicateTrailSerialized); + store.setRecentTrail(duplicateTrail); + }); + + it('should still be only one recent trail', () => { + expect(store.recent.length).toBe(1); + }); + + it('it should only contain the new trail', () => { + const newRecentTrail = store.recent[0].resolve(); + expect(newRecentTrail.state.history.state.steps.length).toBe(duplicateTrailSerialized.history.length); + + // @ts-ignore #2341 -- deliberately access private method to construct trail object for testing purposes + const newRecent = store._serializeTrail(newRecentTrail); + expect(newRecent.currentStep).toBe(duplicateTrailSerialized.currentStep); + expect(newRecent.history.length).toBe(duplicateTrailSerialized.history.length); + }); + }); + + it.each([ + ['metric', 'different_metric'], + ['from', 'now-1y'], + ['to', 'now-30m'], + ['var-ds', '1234'], + ['var-groupby', 'job'], + ['var-filters', 'cluster|=|dev-eu-west-2'], + ])(`new recent trails with a different '%p' value should insert new entry`, (key, differentValue) => { + const store = getTrailStore(); + // We expect the initialized trail to be there + expect(store.recent.length).toBe(1); + + const differentTrailSerialized: SerializedTrail = { + history: [ + history[0], + history[1], + history[2], + { + ...history[2], + urlValues: { + ...history[1].urlValues, + [key]: differentValue, + }, + parentIndex: 1, + }, + ], + currentStep: 3, + }; + + // @ts-ignore #2341 -- deliberately access private method to construct trail object for testing purposes + const differentTrail = store._deserializeTrail(differentTrailSerialized); + store.setRecentTrail(differentTrail); + + // There should now be two trails + expect(store.recent.length).toBe(2); + }); + }); + describe('Initialize store with one bookmark trail', () => { beforeEach(() => { localStorage.clear(); @@ -264,4 +441,111 @@ describe('TrailStore', () => { expect(localStorage.getItem(BOOKMARKED_TRAILS_KEY)).toBe('[]'); }); }); + + describe('Initialize store with one bookmark trail not on final step', () => { + beforeEach(() => { + localStorage.clear(); + localStorage.setItem( + BOOKMARKED_TRAILS_KEY, + JSON.stringify([ + { + history: [ + { + urlValues: { + from: 'now-1h', + to: 'now', + 'var-ds': 'prom-mock', + 'var-filters': [], + refresh: '', + }, + type: 'start', + }, + { + urlValues: { + metric: 'bookmarked_metric', + from: 'now-1h', + to: 'now', + 'var-ds': 'prom-mock', + 'var-filters': [], + refresh: '', + }, + type: 'time', + }, + { + urlValues: { + metric: 'some_other_metric', + from: 'now-1h', + to: 'now', + 'var-ds': 'prom-mock', + 'var-filters': [], + refresh: '', + }, + type: 'metric', + }, + ], + currentStep: 1, + }, + ]) + ); + getTrailStore().load(); + }); + + const store = getTrailStore(); + + it('should have no recent trails', () => { + expect(store.recent.length).toBe(0); + }); + + it('should accurately load bookmarked trails', () => { + expect(store.bookmarks.length).toBe(1); + const trail = store.bookmarks[0].resolve(); + expect(trail.state.history.state.steps.length).toBe(3); + expect(trail.state.history.state.steps[0].type).toBe('start'); + expect(trail.state.history.state.steps[1].type).toBe('time'); + expect(trail.state.history.state.steps[2].type).toBe('metric'); + }); + + it('should save a new recent trail based on the bookmark', () => { + expect(store.recent.length).toBe(0); + const trail = store.bookmarks[0].resolve(); + store.setRecentTrail(trail); + expect(store.recent.length).toBe(1); + }); + + it('should be able to obtain index of bookmark', () => { + const trail = store.bookmarks[0].resolve(); + const index = store.getBookmarkIndex(trail); + expect(index).toBe(0); + }); + + it('index should be undefined for removed bookmarks', () => { + const trail = store.bookmarks[0].resolve(); + store.removeBookmark(0); + const index = store.getBookmarkIndex(trail); + expect(index).toBe(undefined); + }); + + it('index should be undefined for a trail that has changed since it was bookmarked', () => { + const trail = store.bookmarks[0].resolve(); + trail.setState({ metric: 'something_completely_different' }); + const index = store.getBookmarkIndex(trail); + expect(index).toBe(undefined); + }); + + it('should be able to obtain index of a bookmark for a trail that changed back to bookmarked state', () => { + const trail = store.bookmarks[0].resolve(); + trail.setState({ metric: 'something_completely_different' }); + expect(store.getBookmarkIndex(trail)).toBe(undefined); + trail.setState({ metric: 'bookmarked_metric' }); + expect(store.getBookmarkIndex(trail)).toBe(0); + }); + + it('should remove a bookmark', () => { + expect(store.bookmarks.length).toBe(1); + store.removeBookmark(0); + expect(store.bookmarks.length).toBe(0); + jest.advanceTimersByTime(2000); + expect(localStorage.getItem(BOOKMARKED_TRAILS_KEY)).toBe('[]'); + }); + }); }); diff --git a/public/app/features/trails/TrailStore/TrailStore.ts b/public/app/features/trails/TrailStore/TrailStore.ts index 082a2a80099..02c7cd92446 100644 --- a/public/app/features/trails/TrailStore/TrailStore.ts +++ b/public/app/features/trails/TrailStore/TrailStore.ts @@ -26,12 +26,12 @@ export interface SerializedTrail { export class TrailStore { private _recent: Array> = []; private _bookmarks: Array> = []; - private _save; + private _save: () => void; constructor() { this.load(); - this._save = debounce(() => { + const doSave = () => { const serializedRecent = this._recent .slice(0, MAX_RECENT_TRAILS) .map((trail) => this._serializeTrail(trail.resolve())); @@ -39,7 +39,15 @@ export class TrailStore { const serializedBookmarks = this._bookmarks.map((trail) => this._serializeTrail(trail.resolve())); localStorage.setItem(BOOKMARKED_TRAILS_KEY, JSON.stringify(serializedBookmarks)); - }, 1000); + }; + + this._save = debounce(doSave, 1000); + + window.addEventListener('beforeunload', (ev) => { + // Before closing or reloading the page, we want to remove the debounce from `_save` so that + // any calls to is on event `unload` are actualized. Debouncing would cause a delay until after the page has been unloaded. + this._save = doSave; + }); } private _loadFromStorage(key: string) { @@ -70,6 +78,8 @@ export class TrailStore { const currentStep = t.currentStep ?? trail.state.history.state.steps.length - 1; trail.state.history.setState({ currentStep }); + // The state change listeners aren't activated yet, so maually change to the current step state + trail.setState(trail.state.history.state.steps[currentStep].trailState); return trail; } @@ -107,29 +117,45 @@ export class TrailStore { this._refreshBookmarkIndexMap(); } - setRecentTrail(trail: DataTrail) { - this._recent = this._recent.filter((t) => t !== trail.getRef()); + setRecentTrail(recentTrail: DataTrail) { + const { steps } = recentTrail.state.history.state; + if (steps.length === 0 || (steps.length === 1 && steps[0].type === 'start')) { + // We do not set an uninitialized trail, or a single node "start" trail as recent + return; + } - // Check if any existing "recent" entries have equivalent 'current' urlValue to the new trail - const newTrailUrlValues = getCurrentUrlValues(this._serializeTrail(trail)) || {}; + // Remove the `recentTrail` from the list if it already exists there + this._recent = this._recent.filter((t) => t !== recentTrail.getRef()); + + // Check if any existing "recent" entries have equivalent urlState to the new recentTrail + const recentUrlState = getUrlStateForComparison(recentTrail); // this._recent = this._recent.filter((t) => { // Use the current step urlValues to filter out equivalent states - const urlValues = getCurrentUrlValues(this._serializeTrail(t.resolve())); + const urlState = getUrlStateForComparison(t.resolve()); // Only keep trails with sufficiently unique urlValues on their current step - return !isEqual(newTrailUrlValues, urlValues); + return !isEqual(recentUrlState, urlState); }); - this._recent.unshift(trail.getRef()); + this._recent.unshift(recentTrail.getRef()); this._save(); } + findMatchingRecentTrail(trail: DataTrail) { + const matchUrlState = getUrlStateForComparison(trail); + return this._recent.find((t) => { + const urlState = getUrlStateForComparison(t.resolve()); + return isEqual(matchUrlState, urlState); + }); + } + // Bookmarked Trails get bookmarks() { return this._bookmarks; } addBookmark(trail: DataTrail) { - this._bookmarks.unshift(trail.getRef()); + const bookmark = new DataTrail(sceneUtils.cloneSceneObjectState(trail.state)); + this._bookmarks.unshift(bookmark.getRef()); this._refreshBookmarkIndexMap(); this._save(); dispatch(notifyApp(createBookmarkSavedNotification())); @@ -155,6 +181,7 @@ export class TrailStore { this._bookmarkIndexMap.clear(); this._bookmarks.forEach((bookmarked, index) => { const trail = bookmarked.resolve(); + const key = getBookmarkKey(trail); // If there are duplicate bookmarks, the latest index will be kept this._bookmarkIndexMap.set(key, index); @@ -162,15 +189,24 @@ export class TrailStore { } } -function getBookmarkKey(trail: DataTrail) { +function getUrlStateForComparison(trail: DataTrail) { const urlState = getUrlSyncManager().getUrlState(trail); - // Not part of state + // Make a few corrections + + // Omit some URL parameters that are not useful for state comparison delete urlState.actionView; + delete urlState.layout; + // Populate defaults if (urlState['var-groupby'] === '') { urlState['var-groupby'] = '$__all'; } - const key = JSON.stringify(urlState); + + return urlState; +} + +function getBookmarkKey(trail: DataTrail) { + const key = JSON.stringify(getUrlStateForComparison(trail)); return key; } @@ -182,7 +218,3 @@ export function getTrailStore(): TrailStore { return store; } - -function getCurrentUrlValues({ history, currentStep }: SerializedTrail) { - return history[currentStep]?.urlValues || history.at(-1)?.urlValues; -} diff --git a/public/app/features/trails/TrailStore/useBookmarkState.ts b/public/app/features/trails/TrailStore/useBookmarkState.ts index 632d26e0695..516c73018e3 100644 --- a/public/app/features/trails/TrailStore/useBookmarkState.ts +++ b/public/app/features/trails/TrailStore/useBookmarkState.ts @@ -1,6 +1,9 @@ -import { useState } from 'react'; +import { useEffect, useState } from 'react'; + +import { SceneObjectStateChangedEvent } from '@grafana/scenes'; import { DataTrail } from '../DataTrail'; +import { isDataTrailsHistoryState } from '../DataTrailsHistory'; import { reportExploreMetrics } from '../interactions'; import { getTrailStore } from './TrailStore'; @@ -14,6 +17,18 @@ export function useBookmarkState(trail: DataTrail) { const [bookmarkIndex, setBookmarkIndex] = useState(indexOnRender); + useEffect(() => { + const sub = trail.subscribeToEvent(SceneObjectStateChangedEvent, ({ payload: { prevState, newState } }) => { + if (isDataTrailsHistoryState(prevState) && isDataTrailsHistoryState(newState)) { + if (newState.steps.length > prevState.steps.length) { + // When we add new steps, we need to re-evaluate whether or not it is still a bookmark + setBookmarkIndex(getTrailStore().getBookmarkIndex(trail)); + } + } + }); + return () => sub.unsubscribe(); + }, [trail]); + // Check if index changed and force a re-render if (indexOnRender !== bookmarkIndex) { setBookmarkIndex(indexOnRender); diff --git a/public/app/features/variables/editor/VariableEditorList.tsx b/public/app/features/variables/editor/VariableEditorList.tsx index 32720af0ab6..7dc62f8e86d 100644 --- a/public/app/features/variables/editor/VariableEditorList.tsx +++ b/public/app/features/variables/editor/VariableEditorList.tsx @@ -5,8 +5,8 @@ import { DragDropContext, Droppable, DropResult } from 'react-beautiful-dnd'; import { TypedVariableModel } from '@grafana/data'; import { selectors } from '@grafana/e2e-selectors'; import { reportInteraction } from '@grafana/runtime'; -import { Button, useStyles2, Stack } from '@grafana/ui'; -import EmptyListCTA from 'app/core/components/EmptyListCTA/EmptyListCTA'; +import { Button, useStyles2, Stack, EmptyState, TextLink } from '@grafana/ui'; +import { t, Trans } from 'app/core/internationalization'; import { VariablesDependenciesButton } from '../inspect/VariablesDependenciesButton'; import { UsagesToNetwork, VariableUsageTree } from '../inspect/utils'; @@ -107,30 +107,38 @@ export function VariableEditorList({ function EmptyVariablesList({ onAdd }: { onAdd: () => void }): ReactElement { return ( -
- - Variables enable more interactive and dynamic dashboards. Instead of hard-coding things like server - or sensor names in your metric queries you can use variables in their place. Variables are shown as - list boxes at the top of the dashboard. These drop-down lists make it easy to change the data - being displayed in your dashboard. Check out the - - Templates and variables documentation - - for more information. -

`, - }} - infoBoxTitle="What do variables do?" - onClick={(event) => { - event.preventDefault(); - onAdd(); - }} - /> -
+ + + Add variable + + } + message={t('variables.empty-state.title', 'There are no variables added yet')} + > +

+ + Variables enable more interactive and dynamic dashboards. Instead of hard-coding things like server or + sensor names in your metric queries you can use variables in their place. Variables are shown as list boxes + at the top of the dashboard. These drop-down lists make it easy to change the data being displayed in your + dashboard. + +

+ + Check out the{' '} + + Templates and variables documentation + {' '} + for more information. + +
+
); } diff --git a/public/app/features/visualization/data-hover/ExemplarHoverView.tsx b/public/app/features/visualization/data-hover/ExemplarHoverView.tsx index 39718bb536a..42157a961f8 100644 --- a/public/app/features/visualization/data-hover/ExemplarHoverView.tsx +++ b/public/app/features/visualization/data-hover/ExemplarHoverView.tsx @@ -2,7 +2,7 @@ import { css } from '@emotion/css'; import React from 'react'; import { GrafanaTheme2, LinkModel } from '@grafana/data'; -import { LinkButton, useStyles2 } from '@grafana/ui'; +import { DataLinkButton, useStyles2 } from '@grafana/ui'; import { VizTooltipRow } from '@grafana/ui/src/components/VizTooltip/VizTooltipRow'; import { renderValue } from 'app/plugins/panel/geomap/utils/uiUtils'; @@ -42,9 +42,7 @@ export const ExemplarHoverView = ({ displayValues, links, header = 'Exemplar' }: {links && links.length > 0 && (
{links.map((link, i) => ( - - {link.title} - + ))}
)} diff --git a/public/app/plugins/datasource/azuremonitor/package.json b/public/app/plugins/datasource/azuremonitor/package.json index e663a544930..2a218c264e7 100644 --- a/public/app/plugins/datasource/azuremonitor/package.json +++ b/public/app/plugins/datasource/azuremonitor/package.json @@ -18,6 +18,8 @@ "monaco-editor": "0.34.1", "prismjs": "1.29.0", "react": "18.2.0", + "react-dom": "18.2.0", + "react-select": "5.8.0", "react-use": "17.5.0", "rxjs": "7.8.1", "tslib": "2.6.2" @@ -25,6 +27,7 @@ "devDependencies": { "@grafana/e2e-selectors": "11.1.0-pre", "@grafana/plugin-configs": "11.1.0-pre", + "@testing-library/dom": "10.0.0", "@testing-library/react": "15.0.2", "@testing-library/user-event": "14.5.2", "@types/jest": "29.5.12", @@ -32,6 +35,7 @@ "@types/node": "20.12.7", "@types/prismjs": "1.26.3", "@types/react": "18.2.79", + "@types/react-dom": "18.2.25", "@types/testing-library__jest-dom": "5.14.9", "react-select-event": "5.5.1", "ts-node": "10.9.2", diff --git a/public/app/plugins/datasource/cloud-monitoring/package.json b/public/app/plugins/datasource/cloud-monitoring/package.json index 722924daac7..8ecf182da20 100644 --- a/public/app/plugins/datasource/cloud-monitoring/package.json +++ b/public/app/plugins/datasource/cloud-monitoring/package.json @@ -19,6 +19,8 @@ "monaco-editor": "0.34.1", "prismjs": "1.29.0", "react": "18.2.0", + "react-dom": "18.2.0", + "react-select": "5.8.0", "react-use": "17.5.0", "rxjs": "7.8.1", "tslib": "2.6.2" @@ -26,6 +28,7 @@ "devDependencies": { "@grafana/e2e-selectors": "11.1.0-pre", "@grafana/plugin-configs": "11.1.0-pre", + "@testing-library/dom": "10.0.0", "@testing-library/react": "15.0.2", "@testing-library/user-event": "14.5.2", "@types/debounce-promise": "3.1.9", @@ -34,6 +37,7 @@ "@types/node": "20.12.7", "@types/prismjs": "1.26.3", "@types/react": "18.2.79", + "@types/react-dom": "18.2.25", "@types/react-test-renderer": "18.0.7", "@types/testing-library__jest-dom": "5.14.9", "react-select-event": "5.5.1", diff --git a/public/app/plugins/datasource/cloudwatch/components/QueryEditor/MetricsQueryEditor/SQLBuilderEditor/utils.ts b/public/app/plugins/datasource/cloudwatch/components/QueryEditor/MetricsQueryEditor/SQLBuilderEditor/utils.ts index b1d72e0e6cf..b25dd6fa45e 100644 --- a/public/app/plugins/datasource/cloudwatch/components/QueryEditor/MetricsQueryEditor/SQLBuilderEditor/utils.ts +++ b/public/app/plugins/datasource/cloudwatch/components/QueryEditor/MetricsQueryEditor/SQLBuilderEditor/utils.ts @@ -224,13 +224,16 @@ export function setMetricName(query: CloudWatchMetricsQuery, metricName: string) name: metricName, }; - return setSql(query, { - select: { - type: QueryEditorExpressionType.Function, - ...(query.sql?.select ?? {}), - parameters: [param], - }, - }); + return setSql( + { ...query, metricName }, + { + select: { + type: QueryEditorExpressionType.Function, + ...(query.sql?.select ?? {}), + parameters: [param], + }, + } + ); } export function removeMetricName(query: CloudWatchMetricsQuery): CloudWatchMetricsQuery { diff --git a/public/app/plugins/datasource/grafana-pyroscope-datasource/QueryEditor/QueryEditor.test.tsx b/public/app/plugins/datasource/grafana-pyroscope-datasource/QueryEditor/QueryEditor.test.tsx index 14b89c583b7..1f1b89830ec 100644 --- a/public/app/plugins/datasource/grafana-pyroscope-datasource/QueryEditor/QueryEditor.test.tsx +++ b/public/app/plugins/datasource/grafana-pyroscope-datasource/QueryEditor/QueryEditor.test.tsx @@ -3,7 +3,7 @@ import userEvent from '@testing-library/user-event'; import React from 'react'; import { CoreApp, PluginType } from '@grafana/data'; -import { setPluginExtensionGetter } from '@grafana/runtime'; +import { setPluginExtensionsHook } from '@grafana/runtime'; import { PyroscopeDataSource } from '../datasource'; import { mockFetchPyroscopeDatasourceSettings } from '../datasource.test'; @@ -13,7 +13,7 @@ import { Props, QueryEditor } from './QueryEditor'; describe('QueryEditor', () => { beforeEach(() => { - setPluginExtensionGetter(() => ({ extensions: [] })); // No extensions + setPluginExtensionsHook(() => ({ extensions: [], isLoading: false })); // No extensions mockFetchPyroscopeDatasourceSettings(); }); diff --git a/public/app/plugins/datasource/grafana-pyroscope-datasource/QueryEditor/QueryLinkExtension.test.tsx b/public/app/plugins/datasource/grafana-pyroscope-datasource/QueryEditor/QueryLinkExtension.test.tsx index 893a23f5b53..dd57142644a 100644 --- a/public/app/plugins/datasource/grafana-pyroscope-datasource/QueryEditor/QueryLinkExtension.test.tsx +++ b/public/app/plugins/datasource/grafana-pyroscope-datasource/QueryEditor/QueryLinkExtension.test.tsx @@ -3,7 +3,7 @@ import React from 'react'; import { act } from 'react-dom/test-utils'; import { PluginType, rangeUtil, PluginExtensionLink, PluginExtensionTypes } from '@grafana/data'; -import { getPluginLinkExtensions } from '@grafana/runtime'; +import { usePluginLinkExtensions } from '@grafana/runtime'; import { PyroscopeDataSource } from '../datasource'; import { mockFetchPyroscopeDatasourceSettings } from '../datasource.test'; @@ -15,8 +15,7 @@ const EXTENSION_POINT_ID = 'plugins/grafana-pyroscope-datasource/query-links'; jest.mock('@grafana/runtime', () => ({ ...jest.requireActual('@grafana/runtime'), - setPluginExtensionGetter: jest.fn(), - getPluginLinkExtensions: jest.fn(), + usePluginLinkExtensions: jest.fn(), getTemplateSrv: () => { return { replace: (query: string): string => { @@ -26,7 +25,7 @@ jest.mock('@grafana/runtime', () => ({ }, })); -const getPluginLinkExtensionsMock = jest.mocked(getPluginLinkExtensions); +const usePluginLinkExtensionsMock = jest.mocked(usePluginLinkExtensions); const defaultPyroscopeDataSourceSettings = { uid: 'default-pyroscope', @@ -60,12 +59,12 @@ describe('PyroscopeQueryLinkExtensions', () => { resetPyroscopeQueryLinkExtensionsFetches(); mockFetchPyroscopeDatasourceSettings(defaultPyroscopeDataSourceSettings); - getPluginLinkExtensionsMock.mockRestore(); - getPluginLinkExtensionsMock.mockReturnValue({ extensions: [] }); // Unless stated otherwise, no extensions + usePluginLinkExtensionsMock.mockRestore(); + usePluginLinkExtensionsMock.mockReturnValue({ extensions: [], isLoading: false }); // Unless stated otherwise, no extensions }); it('should render if extension present', async () => { - getPluginLinkExtensionsMock.mockReturnValue({ extensions: [createExtension()] }); // Default extension + usePluginLinkExtensionsMock.mockReturnValue({ extensions: [createExtension()], isLoading: false }); // Default extension await act(setup); expect(await screen.findAllByText(EXPECTED_BUTTON_LABEL)).toBeDefined(); diff --git a/public/app/plugins/datasource/grafana-pyroscope-datasource/QueryEditor/QueryLinkExtension.tsx b/public/app/plugins/datasource/grafana-pyroscope-datasource/QueryEditor/QueryLinkExtension.tsx index 21a8a727ad5..8a6761966df 100644 --- a/public/app/plugins/datasource/grafana-pyroscope-datasource/QueryEditor/QueryLinkExtension.tsx +++ b/public/app/plugins/datasource/grafana-pyroscope-datasource/QueryEditor/QueryLinkExtension.tsx @@ -3,7 +3,7 @@ import React from 'react'; import { useAsync } from 'react-use'; import { GrafanaTheme2, QueryEditorProps, TimeRange } from '@grafana/data'; -import { getBackendSrv, getPluginLinkExtensions } from '@grafana/runtime'; +import { getBackendSrv, usePluginLinkExtensions } from '@grafana/runtime'; import { LinkButton, useStyles2 } from '@grafana/ui'; import { PyroscopeDataSource } from '../datasource'; @@ -64,7 +64,7 @@ export function PyroscopeQueryLinkExtensions(props: Props) { datasourceSettings, }; - const { extensions } = getPluginLinkExtensions({ + const { extensions } = usePluginLinkExtensions({ extensionPointId: EXTENSION_POINT_ID, context, }); diff --git a/public/app/plugins/datasource/grafana-pyroscope-datasource/datasource.test.ts b/public/app/plugins/datasource/grafana-pyroscope-datasource/datasource.test.ts index b2bc6791e09..9d1c9246dba 100644 --- a/public/app/plugins/datasource/grafana-pyroscope-datasource/datasource.test.ts +++ b/public/app/plugins/datasource/grafana-pyroscope-datasource/datasource.test.ts @@ -5,27 +5,14 @@ import { PluginMetaInfo, PluginType, DataSourceJsonData, + makeTimeRange, } from '@grafana/data'; -import { setPluginExtensionGetter, getBackendSrv, setBackendSrv, getTemplateSrv } from '@grafana/runtime'; +import { setPluginExtensionsHook, getBackendSrv, setBackendSrv, TemplateSrv } from '@grafana/runtime'; import { defaultPyroscopeQueryType } from './dataquery.gen'; import { normalizeQuery, PyroscopeDataSource } from './datasource'; import { Query } from './types'; -jest.mock('@grafana/runtime', () => { - const actual = jest.requireActual('@grafana/runtime'); - return { - ...actual, - getTemplateSrv: () => { - return { - replace: (query: string): string => { - return query.replace(/\$var/g, 'interpolated'); - }, - }; - }, - }; -}); - /** The datasource QueryEditor fetches datasource settings to send to the extension's `configure` method */ export function mockFetchPyroscopeDatasourceSettings( datasourceSettings?: Partial> @@ -46,16 +33,21 @@ export function mockFetchPyroscopeDatasourceSettings( }); } -describe('Pyroscope data source', () => { - let ds: PyroscopeDataSource; - beforeEach(() => { - mockFetchPyroscopeDatasourceSettings(); - setPluginExtensionGetter(() => ({ extensions: [] })); // No extensions - ds = new PyroscopeDataSource(defaultSettings); - }); +function setupDatasource() { + mockFetchPyroscopeDatasourceSettings(); + setPluginExtensionsHook(() => ({ extensions: [], isLoading: false })); // No extensions + const templateSrv = { + replace: (query: string): string => { + return query.replace(/\$var/g, 'interpolated'); + }, + } as unknown as TemplateSrv; + return new PyroscopeDataSource(defaultSettings, templateSrv); +} +describe('Pyroscope data source', () => { describe('importing queries', () => { it('keeps all labels and values', async () => { + const ds = setupDatasource(); const queries = await ds.importFromAbstractQueries([ { refId: 'A', @@ -71,6 +63,7 @@ describe('Pyroscope data source', () => { describe('exporting queries', () => { it('keeps all labels and values', async () => { + const ds = setupDatasource(); const queries = await ds.exportToAbstractQueries([ { refId: 'A', @@ -93,10 +86,8 @@ describe('Pyroscope data source', () => { }); describe('applyTemplateVariables', () => { - const templateSrv = getTemplateSrv(); - it('should not update labelSelector if there are no template variables', () => { - ds = new PyroscopeDataSource(defaultSettings, templateSrv); + const ds = setupDatasource(); const query = ds.applyTemplateVariables(defaultQuery({ labelSelector: `no var`, profileTypeId: 'no var' }), {}); expect(query).toMatchObject({ labelSelector: `no var`, @@ -105,7 +96,7 @@ describe('Pyroscope data source', () => { }); it('should update labelSelector if there are template variables', () => { - ds = new PyroscopeDataSource(defaultSettings, templateSrv); + const ds = setupDatasource(); const query = ds.applyTemplateVariables( defaultQuery({ labelSelector: `{$var="$var"}`, profileTypeId: '$var' }), {} @@ -113,6 +104,30 @@ describe('Pyroscope data source', () => { expect(query).toMatchObject({ labelSelector: `{interpolated="interpolated"}`, profileTypeId: 'interpolated' }); }); }); + + it('implements ad hoc variable support for keys', async () => { + const ds = setupDatasource(); + jest.spyOn(ds, 'getResource').mockImplementationOnce(async (cb) => ['foo', 'bar', 'baz']); + const keys = await ds.getTagKeys({ + filters: [], + timeRange: makeTimeRange('2024-01-01T00:00:00', '2024-01-01T01:00:00'), + }); + expect(keys).toEqual(['foo', 'bar', 'baz'].map((v) => ({ text: v }))); + }); + + it('implements ad hoc variable support for values', async () => { + const ds = setupDatasource(); + jest.spyOn(ds, 'getResource').mockImplementationOnce(async (path, params) => { + expect(params?.label).toEqual('foo'); + return ['xyz', 'tuv']; + }); + const keys = await ds.getTagValues({ + key: 'foo', + filters: [], + timeRange: makeTimeRange('2024-01-01T00:00:00', '2024-01-01T01:00:00'), + }); + expect(keys).toEqual(['xyz', 'tuv'].map((v) => ({ text: v }))); + }); }); describe('normalizeQuery', () => { diff --git a/public/app/plugins/datasource/grafana-pyroscope-datasource/datasource.ts b/public/app/plugins/datasource/grafana-pyroscope-datasource/datasource.ts index 52d50b6995a..a748d560269 100644 --- a/public/app/plugins/datasource/grafana-pyroscope-datasource/datasource.ts +++ b/public/app/plugins/datasource/grafana-pyroscope-datasource/datasource.ts @@ -1,12 +1,16 @@ -import Prism, { Grammar } from 'prismjs'; +import Prism from 'prismjs'; import { Observable, of } from 'rxjs'; import { AbstractQuery, + AdHocVariableFilter, CoreApp, DataQueryRequest, DataQueryResponse, + DataSourceGetTagKeysOptions, + DataSourceGetTagValuesOptions, DataSourceInstanceSettings, + MetricFindValue, ScopedVars, } from '@grafana/data'; import { DataSourceWithBackend, getTemplateSrv, TemplateSrv } from '@grafana/runtime'; @@ -14,7 +18,7 @@ import { DataSourceWithBackend, getTemplateSrv, TemplateSrv } from '@grafana/run import { VariableSupport } from './VariableSupport'; import { defaultGrafanaPyroscopeDataQuery, defaultPyroscopeQueryType } from './dataquery.gen'; import { PyroscopeDataSourceOptions, Query, ProfileTypeMessage } from './types'; -import { extractLabelMatchers, toPromLikeExpr } from './utils'; +import { addLabelToQuery, extractLabelMatchers, grammar, toPromLikeExpr } from './utils'; export class PyroscopeDataSource extends DataSourceWithBackend { constructor( @@ -71,10 +75,37 @@ export class PyroscopeDataSource extends DataSourceWithBackend): Promise { + const data = this.adhocFilterData(options); + const labels = await this.getLabelNames(data.query, data.from, data.to); + return labels.map((label) => ({ text: label })); + } + + // By implementing getTagKeys and getTagValues we add ad-hoc filters functionality + async getTagValues(options: DataSourceGetTagValuesOptions): Promise { + const data = this.adhocFilterData(options); + const labels = await this.getLabelValues(data.query, options.key, data.from, data.to); + return labels.map((label) => ({ text: label })); + } + + private adhocFilterData(options: DataSourceGetTagKeysOptions | DataSourceGetTagValuesOptions) { + const from = options.timeRange?.from.valueOf() ?? Date.now() - 1000 * 60 * 60 * 24; + const to = options.timeRange?.to.valueOf() ?? Date.now(); + const query = '{' + options.filters.map((f) => `${f.key}${f.operator}"${f.value}"`).join(',') + '}'; + return { from, to, query }; + } + + applyTemplateVariables(query: Query, scopedVars: ScopedVars, filters?: AdHocVariableFilter[]): Query { + let labelSelector = this.templateSrv.replace(query.labelSelector ?? '', scopedVars); + if (filters && labelSelector) { + for (const filter of filters) { + labelSelector = addLabelToQuery(labelSelector, filter.key, filter.value, filter.operator); + } + } return { ...query, - labelSelector: this.templateSrv.replace(query.labelSelector ?? '', scopedVars), + labelSelector, profileTypeId: this.templateSrv.replace(query.profileTypeId ?? '', scopedVars), }; } @@ -86,7 +117,7 @@ export class PyroscopeDataSource extends DataSourceWithBackend): AbstractLabelMatcher[] { const labelMatchers: AbstractLabelMatcher[] = []; @@ -47,8 +47,8 @@ export function extractLabelMatchers(tokens: Array): AbstractLab return labelMatchers; } -export function toPromLikeExpr(labelBasedQuery: AbstractQuery): string { - const expr = labelBasedQuery.labelMatchers +export function toPromLikeExpr(labelMatchers: AbstractLabelMatcher[]): string { + const expr = labelMatchers .map((selector: AbstractLabelMatcher) => { const operator = ToPromLikeMap[selector.operator]; if (operator) { @@ -82,3 +82,52 @@ const ToPromLikeMap: Record = invert(FromPromLike AbstractLabelOperator, string >; + +/** + * Modifies query, adding a new label=value pair to it while preserving other parts of the query. This operates on a + * string representation of the query which needs to be parsed and then rendered to string again. + */ +export function addLabelToQuery(query: string, key: string, value: string | number, operator = '='): string { + if (!key || !value) { + throw new Error('Need label to add to query.'); + } + + const tokens = Prism.tokenize(query, grammar); + let labels = extractLabelMatchers(tokens); + + // If we already have such label in the query, remove it and we will replace it. If we didn't we would end up + // with query like `a=b,a=c` which won't return anything. Replacing also seems more meaningful here than just + // ignoring the filter and keeping the old value. + labels = labels.filter((l) => l.name !== key); + labels.push({ + name: key, + value: value.toString(), + operator: FromPromLikeMap[operator] ?? AbstractLabelOperator.Equal, + }); + + return toPromLikeExpr(labels); +} + +export const grammar: Grammar = { + 'context-labels': { + pattern: /\{[^}]*(?=}?)/, + greedy: true, + inside: { + comment: { + pattern: /#.*/, + }, + 'label-key': { + pattern: /[a-zA-Z_]\w*(?=\s*(=|!=|=~|!~))/, + alias: 'attr-name', + greedy: true, + }, + 'label-value': { + pattern: /"(?:\\.|[^\\"])*"/, + greedy: true, + alias: 'attr-value', + }, + punctuation: /[{]/, + }, + }, + punctuation: /[{}(),.]/, +}; diff --git a/public/app/plugins/datasource/grafana-testdata-datasource/package.json b/public/app/plugins/datasource/grafana-testdata-datasource/package.json index 30af532cc33..0523f1b75ec 100644 --- a/public/app/plugins/datasource/grafana-testdata-datasource/package.json +++ b/public/app/plugins/datasource/grafana-testdata-datasource/package.json @@ -14,6 +14,8 @@ "lodash": "4.17.21", "micro-memoize": "^4.1.2", "react": "18.2.0", + "react-dom": "18.2.0", + "react-select": "5.8.0", "react-use": "17.5.0", "rxjs": "7.8.1", "tslib": "2.6.2", @@ -22,6 +24,7 @@ "devDependencies": { "@grafana/e2e-selectors": "11.1.0-pre", "@grafana/plugin-configs": "11.1.0-pre", + "@testing-library/dom": "10.0.0", "@testing-library/react": "15.0.2", "@testing-library/user-event": "14.5.2", "@types/d3-random": "^3.0.2", @@ -29,9 +32,11 @@ "@types/lodash": "4.17.0", "@types/node": "20.12.7", "@types/react": "18.2.79", + "@types/react-dom": "18.2.25", "@types/testing-library__jest-dom": "5.14.9", "@types/uuid": "9.0.8", "ts-node": "10.9.2", + "typescript": "5.4.5", "webpack": "5.91.0" }, "peerDependencies": { diff --git a/public/app/plugins/datasource/influxdb/__mocks__/datasource.ts b/public/app/plugins/datasource/influxdb/__mocks__/datasource.ts new file mode 100644 index 00000000000..6630252bd64 --- /dev/null +++ b/public/app/plugins/datasource/influxdb/__mocks__/datasource.ts @@ -0,0 +1,89 @@ +import { of } from 'rxjs'; + +import { AdHocVariableFilter, DataSourceInstanceSettings, PluginType, ScopedVars } from '@grafana/data'; +import { FetchResponse, getBackendSrv, setBackendSrv, VariableInterpolation } from '@grafana/runtime'; + +import { TemplateSrv } from '../../../../features/templating/template_srv'; +import InfluxDatasource from '../datasource'; +import { InfluxOptions, InfluxVersion } from '../types'; + +const getAdhocFiltersMock = jest.fn().mockImplementation(() => []); +const replaceMock = jest.fn().mockImplementation((a: string, ...rest: unknown[]) => a); + +export const templateSrvStub = { + getAdhocFilters: getAdhocFiltersMock, + replace: replaceMock, +} as unknown as TemplateSrv; + +export function mockTemplateSrv( + getAdhocFiltersMock: (datasourceName: string) => AdHocVariableFilter[], + replaceMock: ( + target?: string, + scopedVars?: ScopedVars, + format?: string | Function | undefined, + interpolations?: VariableInterpolation[] + ) => string +): TemplateSrv { + return { + getAdhocFilters: getAdhocFiltersMock, + replace: replaceMock, + } as unknown as TemplateSrv; +} + +export function mockBackendService(response: FetchResponse) { + const fetchMock = jest.fn().mockReturnValue(of(response)); + const origBackendSrv = getBackendSrv(); + setBackendSrv({ + ...origBackendSrv, + fetch: fetchMock, + }); + return fetchMock; +} + +export function getMockInfluxDS( + instanceSettings: DataSourceInstanceSettings = getMockDSInstanceSettings(), + templateSrv: TemplateSrv = templateSrvStub +): InfluxDatasource { + return new InfluxDatasource(instanceSettings, templateSrv); +} + +export function getMockDSInstanceSettings( + overrideJsonData?: Partial +): DataSourceInstanceSettings { + return { + id: 123, + url: 'proxied', + access: 'proxy', + name: 'influxDb', + readOnly: false, + uid: 'influxdb-test', + type: 'influxdb', + meta: { + id: 'influxdb-meta', + type: PluginType.datasource, + name: 'influxdb-test', + info: { + author: { + name: 'observability-metrics', + }, + version: 'v0.0.1', + description: 'test', + links: [], + logos: { + large: '', + small: '', + }, + updated: '', + screenshots: [], + }, + module: '', + baseUrl: '', + }, + jsonData: { + version: InfluxVersion.InfluxQL, + httpMode: 'POST', + dbName: 'site', + ...(overrideJsonData ? overrideJsonData : {}), + }, + }; +} diff --git a/public/app/plugins/datasource/influxdb/__mocks__/query.ts b/public/app/plugins/datasource/influxdb/__mocks__/query.ts new file mode 100644 index 00000000000..0d74ecf113e --- /dev/null +++ b/public/app/plugins/datasource/influxdb/__mocks__/query.ts @@ -0,0 +1,32 @@ +import { DataQueryRequest, dateTime } from '@grafana/data'; + +import { InfluxQuery } from '../types'; + +const now = dateTime('2023-09-16T21:26:00Z'); + +export const queryOptions: DataQueryRequest = { + app: 'dashboard', + interval: '10', + intervalMs: 10, + requestId: 'A-testing', + startTime: 0, + range: { + from: dateTime(now).subtract(15, 'minutes'), + to: now, + raw: { + from: 'now-15m', + to: 'now', + }, + }, + rangeRaw: { + from: 'now-15m', + to: 'now', + }, + targets: [], + timezone: 'UTC', + scopedVars: { + interval: { text: '1m', value: '1m' }, + __interval: { text: '1m', value: '1m' }, + __interval_ms: { text: 60000, value: 60000 }, + }, +}; diff --git a/public/app/plugins/datasource/influxdb/__mocks__/request.ts b/public/app/plugins/datasource/influxdb/__mocks__/request.ts new file mode 100644 index 00000000000..986c780922d --- /dev/null +++ b/public/app/plugins/datasource/influxdb/__mocks__/request.ts @@ -0,0 +1,99 @@ +import { AdHocVariableFilter, DataQueryRequest, dateTime } from '@grafana/data'; +import { SQLQuery } from '@grafana/sql'; + +import { InfluxQuery } from '../types'; + +type QueryType = InfluxQuery & SQLQuery; + +export const mockInfluxQueryRequest = (targets?: QueryType[]): DataQueryRequest => { + return { + app: 'explore', + interval: '1m', + intervalMs: 60000, + range: { + from: dateTime(0), + to: dateTime(10), + raw: { from: dateTime(0), to: dateTime(10) }, + }, + rangeRaw: { + from: dateTime(0), + to: dateTime(10), + }, + requestId: '', + scopedVars: {}, + startTime: 0, + targets: targets ?? mockTargets(), + timezone: '', + }; +}; + +export const mockTargets = (): QueryType[] => { + return [ + { + refId: 'A', + datasource: { + type: 'influxdb', + uid: 'vA4bkHenk', + }, + policy: 'default', + resultFormat: 'time_series', + orderByTime: 'ASC', + tags: [], + groupBy: [ + { + type: 'time', + params: ['$__interval'], + }, + { + type: 'fill', + params: ['null'], + }, + ], + select: [ + [ + { + type: 'field', + params: ['value'], + }, + { + type: 'mean', + params: [], + }, + ], + ], + measurement: 'cpu', + }, + ]; +}; + +export const mockInfluxQueryWithTemplateVars = (adhocFilters: AdHocVariableFilter[]): InfluxQuery => ({ + refId: 'x', + alias: '$var1', + measurement: '$var1', + policy: '$var1', + limit: '$var1', + slimit: '$var1', + tz: '$var1', + tags: [ + { + key: 'drive', + operator: '=~', + value: '/^$path$/', + }, + ], + groupBy: [ + { + params: ['$var1'], + type: 'tag', + }, + ], + select: [ + [ + { + params: ['$var1'], + type: 'field', + }, + ], + ], + adhocFilters, +}); diff --git a/public/app/plugins/datasource/influxdb/mocks.ts b/public/app/plugins/datasource/influxdb/__mocks__/response.ts similarity index 56% rename from public/app/plugins/datasource/influxdb/mocks.ts rename to public/app/plugins/datasource/influxdb/__mocks__/response.ts index 8aee8e79b5c..23b6cef2595 100644 --- a/public/app/plugins/datasource/influxdb/mocks.ts +++ b/public/app/plugins/datasource/influxdb/__mocks__/response.ts @@ -1,108 +1,5 @@ -import { of } from 'rxjs'; - -import { - AdHocVariableFilter, - DataQueryRequest, - DataSourceInstanceSettings, - dateTime, - FieldType, - PluginType, - ScopedVars, -} from '@grafana/data'; -import { - BackendDataSourceResponse, - FetchResponse, - getBackendSrv, - setBackendSrv, - VariableInterpolation, -} from '@grafana/runtime'; -import { SQLQuery } from '@grafana/sql'; - -import { TemplateSrv } from '../../../features/templating/template_srv'; - -import InfluxDatasource from './datasource'; -import { InfluxOptions, InfluxQuery, InfluxVersion } from './types'; - -const getAdhocFiltersMock = jest.fn().mockImplementation(() => []); -const replaceMock = jest.fn().mockImplementation((a: string, ...rest: unknown[]) => a); - -export const templateSrvStub = { - getAdhocFilters: getAdhocFiltersMock, - replace: replaceMock, -} as unknown as TemplateSrv; - -export function mockTemplateSrv( - getAdhocFiltersMock: (datasourceName: string) => AdHocVariableFilter[], - replaceMock: ( - target?: string, - scopedVars?: ScopedVars, - format?: string | Function | undefined, - interpolations?: VariableInterpolation[] - ) => string -): TemplateSrv { - return { - getAdhocFilters: getAdhocFiltersMock, - replace: replaceMock, - } as unknown as TemplateSrv; -} - -export function mockBackendService(response: FetchResponse) { - const fetchMock = jest.fn().mockReturnValue(of(response)); - const origBackendSrv = getBackendSrv(); - setBackendSrv({ - ...origBackendSrv, - fetch: fetchMock, - }); - return fetchMock; -} - -export function getMockInfluxDS( - instanceSettings: DataSourceInstanceSettings = getMockDSInstanceSettings(), - templateSrv: TemplateSrv = templateSrvStub -): InfluxDatasource { - return new InfluxDatasource(instanceSettings, templateSrv); -} - -export function getMockDSInstanceSettings( - overrideJsonData?: Partial -): DataSourceInstanceSettings { - return { - id: 123, - url: 'proxied', - access: 'proxy', - name: 'influxDb', - readOnly: false, - uid: 'influxdb-test', - type: 'influxdb', - meta: { - id: 'influxdb-meta', - type: PluginType.datasource, - name: 'influxdb-test', - info: { - author: { - name: 'observability-metrics', - }, - version: 'v0.0.1', - description: 'test', - links: [], - logos: { - large: '', - small: '', - }, - updated: '', - screenshots: [], - }, - module: '', - baseUrl: '', - }, - jsonData: { - version: InfluxVersion.InfluxQL, - httpMode: 'POST', - dbName: 'site', - ...(overrideJsonData ? overrideJsonData : {}), - }, - }; -} +import { FieldType } from '@grafana/data'; +import { BackendDataSourceResponse, FetchResponse } from '@grafana/runtime'; export const mockInfluxFetchResponse = ( overrides?: Partial> @@ -133,6 +30,7 @@ export const mockInfluxFetchResponse = ( ...overrides, }; }; + export const mockInfluxTSDBQueryResponse = [ { schema: { @@ -220,6 +118,33 @@ export const mockInfluxTSDBQueryResponse = [ }, ]; +export const metricFindQueryResponse = { + config: { + url: 'mock-response-url', + }, + headers: new Headers(), + ok: false, + redirected: false, + status: 0, + statusText: '', + type: 'basic', + url: '', + data: { + status: 'success', + results: [ + { + series: [ + { + name: 'measurement', + columns: ['name'], + values: [['cpu']], + }, + ], + }, + ], + }, +}; + export const mockInfluxRetentionPolicyResponse = [ { schema: { @@ -230,101 +155,6 @@ export const mockInfluxRetentionPolicyResponse = [ }, ]; -type QueryType = InfluxQuery & SQLQuery; - -export const mockInfluxQueryRequest = (targets?: QueryType[]): DataQueryRequest => { - return { - app: 'explore', - interval: '1m', - intervalMs: 60000, - range: { - from: dateTime(0), - to: dateTime(10), - raw: { from: dateTime(0), to: dateTime(10) }, - }, - rangeRaw: { - from: dateTime(0), - to: dateTime(10), - }, - requestId: '', - scopedVars: {}, - startTime: 0, - targets: targets ?? mockTargets(), - timezone: '', - }; -}; - -export const mockTargets = (): QueryType[] => { - return [ - { - refId: 'A', - datasource: { - type: 'influxdb', - uid: 'vA4bkHenk', - }, - policy: 'default', - resultFormat: 'time_series', - orderByTime: 'ASC', - tags: [], - groupBy: [ - { - type: 'time', - params: ['$__interval'], - }, - { - type: 'fill', - params: ['null'], - }, - ], - select: [ - [ - { - type: 'field', - params: ['value'], - }, - { - type: 'mean', - params: [], - }, - ], - ], - measurement: 'cpu', - }, - ]; -}; - -export const mockInfluxQueryWithTemplateVars = (adhocFilters: AdHocVariableFilter[]): InfluxQuery => ({ - refId: 'x', - alias: '$interpolationVar', - measurement: '$interpolationVar', - policy: '$interpolationVar', - limit: '$interpolationVar', - slimit: '$interpolationVar', - tz: '$interpolationVar', - tags: [ - { - key: 'cpu', - operator: '=~', - value: '/^$interpolationVar,$interpolationVar2$/', - }, - ], - groupBy: [ - { - params: ['$interpolationVar'], - type: 'tag', - }, - ], - select: [ - [ - { - params: ['$interpolationVar'], - type: 'field', - }, - ], - ], - adhocFilters, -}); - export const mockInfluxSQLFetchResponse: FetchResponse = { config: { url: 'mock-response-url', @@ -433,3 +263,51 @@ export const mockInfluxSQLVariableFetchResponse: FetchResponse { +describe('datasource initialization', () => { + it('should read the http method from jsonData', () => { + let ds = getMockInfluxDS(getMockDSInstanceSettings({ httpMode: 'GET' })); + expect(ds.httpMode).toBe('GET'); + ds = getMockInfluxDS(getMockDSInstanceSettings({ httpMode: 'POST' })); + expect(ds.httpMode).toBe('POST'); + }); +}); + +// Remove this suite when influxdbBackendMigration feature toggle removed +describe('InfluxDataSource Frontend Mode [influxdbBackendMigration=false]', () => { beforeEach(() => { + // we want only frontend mode in this suite + config.featureToggles.influxdbBackendMigration = false; jest.clearAllMocks(); }); - it('should throw an error if there is 200 response with error', async () => { - const ds = getMockInfluxDS(); - fetchMock.mockImplementation(() => { - return of({ - data: { - results: [ - { - error: 'Query timeout', - }, - ], - }, + describe('general checks', () => { + it('should throw an error if there is 200 response with error', async () => { + const ds = getMockInfluxDS(); + fetchMock.mockImplementation(() => { + return of({ + data: { + results: [ + { + error: 'Query timeout', + }, + ], + }, + }); }); + + try { + await lastValueFrom(ds.query(mockInfluxQueryRequest())); + } catch (err) { + if (err instanceof Error) { + expect(err.message).toBe('InfluxDB Error: Query timeout'); + } + } }); - try { - await lastValueFrom(ds.query(mockInfluxQueryRequest())); - } catch (err) { - if (err instanceof Error) { - expect(err.message).toBe('InfluxDB Error: Query timeout'); - } - } - }); - - describe('outdated browser mode', () => { - it('should throw an error when querying data', async () => { + it('should throw an error when querying data when deprecated access mode', async () => { expect.assertions(1); const instanceSettings = getMockDSInstanceSettings(); instanceSettings.access = 'direct'; @@ -68,7 +72,7 @@ describe('InfluxDataSource Frontend Mode', () => { }); }); - describe('metricFindQuery with HTTP GET', () => { + describe('metricFindQuery', () => { let ds: InfluxDatasource; const query = 'SELECT max(value) FROM measurement WHERE $timeFilter'; const queryOptions = { @@ -77,14 +81,7 @@ describe('InfluxDataSource Frontend Mode', () => { to: '2018-01-02T00:00:00Z', }, }; - - let requestQuery: string; - let requestMethod: string | undefined; - let requestData: string | null; const fetchMockImpl = (req: BackendSrvRequest) => { - requestMethod = req.method; - requestQuery = req.params?.q; - requestData = req.data; return of({ data: { status: 'success', @@ -108,36 +105,29 @@ describe('InfluxDataSource Frontend Mode', () => { fetchMock.mockImplementation(fetchMockImpl); }); - it('should read the http method from jsonData', async () => { - ds = getMockInfluxDS(getMockDSInstanceSettings({ httpMode: 'GET' })); - await ds.metricFindQuery(query, queryOptions); - expect(requestMethod).toBe('GET'); - ds = getMockInfluxDS(getMockDSInstanceSettings({ httpMode: 'POST' })); - await ds.metricFindQuery(query, queryOptions); - expect(requestMethod).toBe('POST'); - }); - it('should replace $timefilter', async () => { ds = getMockInfluxDS(getMockDSInstanceSettings({ httpMode: 'GET' })); await ds.metricFindQuery(query, queryOptions); - expect(requestQuery).toMatch('time >= 1514764800000ms and time <= 1514851200000ms'); + expect(fetchMock.mock.lastCall[0].params?.q).toMatch('time >= 1514764800000ms and time <= 1514851200000ms'); ds = getMockInfluxDS(getMockDSInstanceSettings({ httpMode: 'POST' })); await ds.metricFindQuery(query, queryOptions); - expect(requestQuery).toBeFalsy(); - expect(requestData).toMatch('time%20%3E%3D%201514764800000ms%20and%20time%20%3C%3D%201514851200000ms'); + expect(fetchMock.mock.lastCall[0].params?.q).toBeFalsy(); + expect(fetchMock.mock.lastCall[0].data).toMatch( + 'time%20%3E%3D%201514764800000ms%20and%20time%20%3C%3D%201514851200000ms' + ); }); it('should not have any data in request body if http mode is GET', async () => { ds = getMockInfluxDS(getMockDSInstanceSettings({ httpMode: 'GET' })); await ds.metricFindQuery(query, queryOptions); - expect(requestData).toBeNull(); + expect(fetchMock.mock.lastCall[0].data).toBeNull(); }); it('should have data in request body if http mode is POST', async () => { ds = getMockInfluxDS(getMockDSInstanceSettings({ httpMode: 'POST' })); await ds.metricFindQuery(query, queryOptions); - expect(requestData).not.toBeNull(); - expect(requestData).toMatch('q=SELECT'); + expect(fetchMock.mock.lastCall[0].data).not.toBeNull(); + expect(fetchMock.mock.lastCall[0].data).toMatch('q=SELECT'); }); it('parse response correctly', async () => { @@ -150,6 +140,7 @@ describe('InfluxDataSource Frontend Mode', () => { }); }); + // Update this after starting to use TemplateSrv from @grafana/runtime package describe('adhoc variables', () => { const adhocFilters = [ { @@ -163,8 +154,6 @@ describe('InfluxDataSource Frontend Mode', () => { mockTemplateService.getAdhocFilters = jest.fn((_: string) => adhocFilters); let ds = getMockInfluxDS(getMockDSInstanceSettings(), mockTemplateService); - // const fetchMock = jest.fn().mockReturnValue(fetchResult); - it('query should contain the ad-hoc variable', () => { ds.query(mockInfluxQueryRequest()); const expected = encodeURIComponent( @@ -252,105 +241,75 @@ describe('InfluxDataSource Frontend Mode', () => { ds.getTagValues({ key: 'test', filters: [] }); expect(metricFindQueryMock).toHaveBeenCalled(); }); + + it('should use dbName instead of database', () => { + const instanceSettings = getMockDSInstanceSettings(); + instanceSettings.database = 'should_not_be_used'; + ds = getMockInfluxDS(instanceSettings); + expect(ds.database).toBe('site'); + }); + + it('should fallback to use use database is dbName is not exist', () => { + const instanceSettings = getMockDSInstanceSettings(); + instanceSettings.database = 'fallback'; + instanceSettings.jsonData.dbName = undefined; + ds = getMockInfluxDS(instanceSettings); + expect(ds.database).toBe('fallback'); + }); }); describe('variable interpolation', () => { - const text = 'interpolationText'; - const text2 = 'interpolationText2'; - const textWithoutFormatRegex = 'interpolationText,interpolationText2'; - const textWithFormatRegex = 'interpolationText,interpolationText2'; - const justText = 'interpolationText'; - const variableMap: Record = { - $interpolationVar: text, - $interpolationVar2: text2, - }; - const adhocFilters = [ - { - key: 'adhoc', - operator: '=', - value: 'val', - condition: '', - }, + const variablesMock = [ + queryBuilder().withId('var1').withName('var1').withCurrent('var1_value').build(), + queryBuilder().withId('path').withName('path').withCurrent('/etc/hosts').build(), ]; - const templateSrv = mockTemplateSrv( - jest.fn((_: string) => adhocFilters), - jest.fn((target?: string, scopedVars?: ScopedVars, format?: string | Function): string => { - if (!format) { - return variableMap[target!] || ''; - } - if (format === 'regex') { - return textWithFormatRegex; - } - return textWithoutFormatRegex; - }) - ); - const ds = new InfluxDatasource(getMockDSInstanceSettings(), templateSrv); - - function influxChecks(query: InfluxQuery) { - expect(templateSrv.replace).toBeCalledTimes(12); - expect(query.alias).toBe(text); - expect(query.measurement).toBe(textWithFormatRegex); - expect(query.policy).toBe(justText); - expect(query.limit).toBe(justText); - expect(query.slimit).toBe(justText); - expect(query.tz).toBe(text); - expect(query.tags![0].value).toBe(textWithFormatRegex); - expect(query.groupBy![0].params![0]).toBe(justText); - expect(query.select![0][0].params![0]).toBe(justText); - expect(query.adhocFilters?.[0].key).toBe(adhocFilters[0].key); - } + const mockTemplateService = new TemplateSrv({ + getVariables: () => variablesMock, + getVariableWithName: (name: string) => variablesMock.filter((v) => v.name === name)[0], + getFilteredVariables: jest.fn(), + }); + // Remove this after start using TemplateSrv from @grafana/runtime + mockTemplateService.getAdhocFilters = jest.fn(); describe('when interpolating query variables for dashboard->explore', () => { it('should interpolate all variables with Flux mode', () => { - ds.version = InfluxVersion.Flux; + const ds = getMockInfluxDS(getMockDSInstanceSettings({ version: InfluxVersion.Flux }), mockTemplateService); const fluxQuery = { refId: 'x', - query: '$interpolationVar,$interpolationVar2', + query: 'some query with $var1 and $path', }; - const queries = ds.interpolateVariablesInQueries([fluxQuery], { - interpolationVar: { text: text, value: text }, - interpolationVar2: { text: text2, value: text2 }, - }); - expect(templateSrv.replace).toBeCalledTimes(1); - expect(queries[0].query).toBe(textWithFormatRegex); + const queries = ds.interpolateVariablesInQueries([fluxQuery], {}); + expect(queries[0].query).toBe('some query with var1_value and /etc/hosts'); }); it('should interpolate all variables with InfluxQL mode', () => { - ds.version = InfluxVersion.InfluxQL; - const queries = ds.interpolateVariablesInQueries([mockInfluxQueryWithTemplateVars(adhocFilters)], { - interpolationVar: { text: text, value: text }, - interpolationVar2: { text: text2, value: text2 }, - }); - influxChecks(queries[0]); + const ds = getMockInfluxDS(getMockDSInstanceSettings({ version: InfluxVersion.InfluxQL }), mockTemplateService); + const [query] = ds.interpolateVariablesInQueries([mockInfluxQueryWithTemplateVars([])], {}); + expect(query.alias).toBe('var1_value'); + expect(query.measurement).toBe('var1_value'); + expect(query.policy).toBe('var1_value'); + expect(query.limit).toBe('var1_value'); + expect(query.slimit).toBe('var1_value'); + expect(query.tz).toBe('var1_value'); + expect(query.tags![0].value).toBe(`/^\\/etc\\/hosts$/`); + expect(query.groupBy![0].params![0]).toBe('var1_value'); + expect(query.select![0][0].params![0]).toBe('var1_value'); }); }); - describe('when interpolating template variables', () => { + describe('applyTemplateVariables', () => { it('should apply all template variables with Flux mode', () => { - ds.version = InfluxVersion.Flux; + const ds = getMockInfluxDS(getMockDSInstanceSettings({ version: InfluxVersion.Flux }), mockTemplateService); const fluxQuery = { refId: 'x', - query: '$interpolationVar', + query: '$var1', }; - const query = ds.applyTemplateVariables(fluxQuery, { - interpolationVar: { - text: text, - value: text, - }, - }); - expect(templateSrv.replace).toBeCalledTimes(1); - expect(query.query).toBe(text); + const query = ds.applyTemplateVariables(fluxQuery, {}); + expect(query.query).toBe('var1_value'); }); }); describe('variable interpolation with chained variables with frontend mode', () => { - const variablesMock = [queryBuilder().withId('var1').withName('var1').withCurrent('var1').build()]; - const mockTemplateService = new TemplateSrv({ - getVariables: () => variablesMock, - getVariableWithName: (name: string) => variablesMock.filter((v) => v.name === name)[0], - getFilteredVariables: jest.fn(), - }); - mockTemplateService.getAdhocFilters = jest.fn((_: string) => []); let ds = getMockInfluxDS(getMockDSInstanceSettings(), mockTemplateService); const fetchMockImpl = () => of({ @@ -416,114 +375,298 @@ describe('InfluxDataSource Frontend Mode', () => { expect(qData).toBe(qe); }); }); + }); +}); - describe('interpolateQueryExpr', () => { - let ds = getMockInfluxDS(getMockDSInstanceSettings(), new TemplateSrv()); - it('should return the value as it is', () => { - const value = 'normalValue'; - const variableMock = queryBuilder().withId('tempVar').withName('tempVar').withMulti(false).build(); - const result = ds.interpolateQueryExpr(value, variableMock, 'my query $tempVar'); - const expectation = 'normalValue'; - expect(result).toBe(expectation); - }); +describe('InfluxDataSource Backend Mode [influxdbBackendMigration=true]', () => { + beforeEach(() => { + // we want only backend mode in this suite + config.featureToggles.influxdbBackendMigration = true; + jest.clearAllMocks(); + }); - it('should return the escaped value if the value wrapped in regex', () => { - const value = '/special/path'; - const variableMock = queryBuilder().withId('tempVar').withName('tempVar').withMulti(false).build(); - const result = ds.interpolateQueryExpr(value, variableMock, 'select that where path = /$tempVar/'); - const expectation = `\\/special\\/path`; - expect(result).toBe(expectation); - }); + describe('metric find query', () => { + let ds = getMockInfluxDS(getMockDSInstanceSettings()); + it('handles multiple frames', async () => { + const fetchMockImpl = () => { + return of(mockMetricFindQueryResponse); + }; - it('should return the escaped value if the value wrapped in regex 2', () => { - const value = '/special/path'; - const variableMock = queryBuilder().withId('tempVar').withName('tempVar').withMulti(false).build(); - const result = ds.interpolateQueryExpr(value, variableMock, 'select that where path = /^$tempVar$/'); - const expectation = `\\/special\\/path`; - expect(result).toBe(expectation); - }); + fetchMock.mockImplementation(fetchMockImpl); + const values = await ds.getTagValues({ key: 'test_id', filters: [] }); + expect(fetchMock).toHaveBeenCalled(); + expect(values.length).toBe(5); + expect(values[0].text).toBe('test-t2-1'); + }); + }); - it('should return the escaped value if the value wrapped in regex 3', () => { - const value = ['env', 'env2', 'env3']; - const variableMock = queryBuilder() - .withId('tempVar') - .withName('tempVar') - .withMulti(false) - .withIncludeAll(true) - .build(); - const result = ds.interpolateQueryExpr(value, variableMock, 'select from /^($tempVar)$/'); - const expectation = `(env|env2|env3)`; - expect(result).toBe(expectation); - }); - - it('should **not** return the escaped value if the value **is not** wrapped in regex', () => { - const value = '/special/path'; - const variableMock = queryBuilder().withId('tempVar').withName('tempVar').withMulti(false).build(); - const result = ds.interpolateQueryExpr(value, variableMock, `select that where path = '$tempVar'`); - const expectation = `/special/path`; - expect(result).toBe(expectation); - }); - - it('should **not** return the escaped value if the value **is not** wrapped in regex 2', () => { - const value = '12.2'; - const variableMock = queryBuilder().withId('tempVar').withName('tempVar').withMulti(false).build(); - const result = ds.interpolateQueryExpr(value, variableMock, `select that where path = '$tempVar'`); - const expectation = `12.2`; - expect(result).toBe(expectation); - }); - - it('should escape the value **always** if the variable is a multi-value variable', () => { - const value = [`/special/path`, `/some/other/path`]; - const variableMock = queryBuilder().withId('tempVar').withName('tempVar').withMulti().build(); - const result = ds.interpolateQueryExpr(value, variableMock, `select that where path = '$tempVar'`); - const expectation = `(\\/special\\/path|\\/some\\/other\\/path)`; - expect(result).toBe(expectation); - }); - - it('should escape and join with the pipe even the variable is not multi-value', () => { - const variableMock = queryBuilder() - .withId('tempVar') - .withName('tempVar') - .withCurrent('All', '$__all') - .withMulti(false) - .withAllValue('') - .withIncludeAll() - .withOptions( + describe('variable interpolation with chained variables with backend mode', () => { + const variablesMock = [ + queryBuilder().withId('var1').withName('var1').withCurrent('var1').build(), + queryBuilder().withId('path').withName('path').withCurrent('/etc/hosts').build(), + queryBuilder() + .withId('field_var') + .withName('field_var') + .withMulti(true) + .withOptions( + { + text: `field_1`, + value: `field_1`, + }, + { + text: `field_2`, + value: `field_2`, + }, + { + text: `field_3`, + value: `field_3`, + } + ) + .withCurrent(['field_1', 'field_3']) + .build(), + ]; + const mockTemplateService = new TemplateSrv({ + getVariables: () => variablesMock, + getVariableWithName: (name: string) => variablesMock.filter((v) => v.name === name)[0], + getFilteredVariables: jest.fn(), + }); + mockTemplateService.getAdhocFilters = jest.fn((_: string) => []); + let ds = getMockInfluxDS(getMockDSInstanceSettings(), mockTemplateService); + const fetchMockImpl = () => + of({ + data: { + status: 'success', + results: [ { - text: 'All', - value: '$__all', + series: [ + { + name: 'measurement', + columns: ['name'], + values: [['cpu']], + }, + ], + }, + ], + }, + }); + + beforeEach(() => { + jest.clearAllMocks(); + fetchMock.mockImplementation(fetchMockImpl); + }); + + it('should render chained regex variables with floating point number', () => { + ds.metricFindQuery(`SELECT sum("piece_count") FROM "rp"."pdata" WHERE diameter <= $maxSED`, { + ...queryOptions, + scopedVars: { maxSED: { text: '8.1', value: '8.1' } }, + }); + const qe = `SELECT sum("piece_count") FROM "rp"."pdata" WHERE diameter <= 8.1`; + const qData = fetchMock.mock.calls[0][0].data.queries[0].query; + expect(qData).toBe(qe); + }); + + it('should render chained regex variables with URL', () => { + ds.metricFindQuery('SHOW TAG VALUES WITH KEY = "agent_url" WHERE agent_url =~ /^$var1$/', { + ...queryOptions, + scopedVars: { + var1: { + text: 'https://aaaa-aa-aaa.bbb.ccc.ddd:8443/ggggg', + value: 'https://aaaa-aa-aaa.bbb.ccc.ddd:8443/ggggg', + }, + }, + }); + const qe = `SHOW TAG VALUES WITH KEY = "agent_url" WHERE agent_url =~ /^https:\\/\\/aaaa-aa-aaa\\.bbb\\.ccc\\.ddd:8443\\/ggggg$/`; + expect(fetchMock).toHaveBeenCalled(); + const qData = fetchMock.mock.calls[0][0].data.queries[0].query; + expect(qData).toBe(qe); + }); + + it('should render chained regex variables with floating point number and url', () => { + ds.metricFindQuery( + 'SELECT sum("piece_count") FROM "rp"."pdata" WHERE diameter <= $maxSED AND agent_url =~ /^$var1$/', + { + ...queryOptions, + scopedVars: { + var1: { + text: 'https://aaaa-aa-aaa.bbb.ccc.ddd:8443/ggggg', + value: 'https://aaaa-aa-aaa.bbb.ccc.ddd:8443/ggggg', + }, + maxSED: { text: '8.1', value: '8.1' }, + }, + } + ); + const qe = `SELECT sum("piece_count") FROM "rp"."pdata" WHERE diameter <= 8.1 AND agent_url =~ /^https:\\/\\/aaaa-aa-aaa\\.bbb\\.ccc\\.ddd:8443\\/ggggg$/`; + const qData = fetchMock.mock.calls[0][0].data.queries[0].query; + expect(qData).toBe(qe); + }); + + it('should interpolate variable inside a regex pattern', () => { + const query: InfluxQuery = { + refId: 'A', + tags: [ + { + key: 'key', + operator: '=~', + value: '/^.*-$var1$/', + }, + ], + }; + const res = ds.applyVariables(query, {}); + const expected = `/^.*-var1$/`; + expect(res.tags?.[0].value).toEqual(expected); + }); + + it('should remove regex wrappers when operator is not a regex operator', () => { + const query: InfluxQuery = { + refId: 'A', + tags: [ + { + key: 'key', + operator: '=', + value: '/^$path$/', + }, + ], + }; + const res = ds.applyVariables(query, {}); + const expected = `/etc/hosts`; + expect(res.tags?.[0].value).toEqual(expected); + }); + + it('should interpolate field keys with given scopedVars', () => { + const query: InfluxQuery = { + refId: 'A', + tags: [ + { + key: 'key', + operator: '=', + value: 'value', + }, + ], + select: [ + [ + { + type: 'field', + params: ['$field_var'], }, { - text: `/special/path`, - value: `/special/path`, + type: 'mean', + params: [], }, - { - text: `/some/other/path`, - value: `/some/other/path`, - } - ) - .build(); - const value = [`/special/path`, `/some/other/path`]; - const result = ds.interpolateQueryExpr(value, variableMock, `select that where path = /$tempVar/`); - const expectation = `(\\/special\\/path|\\/some\\/other\\/path)`; - expect(result).toBe(expectation); - }); - - it('should return floating point number as it is', () => { - const variableMock = queryBuilder() - .withId('tempVar') - .withName('tempVar') - .withMulti(false) - .withOptions({ - text: `1.0`, - value: `1.0`, - }) - .build(); - const value = `1.0`; - const result = ds.interpolateQueryExpr(value, variableMock, `select value / $tempVar from /^measurement$/`); - const expectation = `1.0`; - expect(result).toBe(expectation); - }); + ], + ], + }; + const res = ds.applyVariables(query, { field_var: { text: 'field_3', value: 'field_3' } }); + const expected = `field_3`; + expect(res.select?.[0][0].params?.[0]).toEqual(expected); }); }); }); + +describe('interpolateQueryExpr', () => { + let ds = getMockInfluxDS(getMockDSInstanceSettings(), new TemplateSrv()); + it('should return the value as it is', () => { + const value = 'normalValue'; + const variableMock = queryBuilder().withId('tempVar').withName('tempVar').withMulti(false).build(); + const result = ds.interpolateQueryExpr(value, variableMock, 'my query $tempVar'); + const expectation = 'normalValue'; + expect(result).toBe(expectation); + }); + + it('should return the escaped value if the value wrapped in regex', () => { + const value = '/special/path'; + const variableMock = queryBuilder().withId('tempVar').withName('tempVar').withMulti(false).build(); + const result = ds.interpolateQueryExpr(value, variableMock, 'select that where path = /$tempVar/'); + const expectation = `\\/special\\/path`; + expect(result).toBe(expectation); + }); + + it('should return the escaped value if the value wrapped in regex 2', () => { + const value = '/special/path'; + const variableMock = queryBuilder().withId('tempVar').withName('tempVar').withMulti(false).build(); + const result = ds.interpolateQueryExpr(value, variableMock, 'select that where path = /^$tempVar$/'); + const expectation = `\\/special\\/path`; + expect(result).toBe(expectation); + }); + + it('should return the escaped value if the value wrapped in regex 3', () => { + const value = ['env', 'env2', 'env3']; + const variableMock = queryBuilder() + .withId('tempVar') + .withName('tempVar') + .withMulti(false) + .withIncludeAll(true) + .build(); + const result = ds.interpolateQueryExpr(value, variableMock, 'select from /^($tempVar)$/'); + const expectation = `(env|env2|env3)`; + expect(result).toBe(expectation); + }); + + it('should **not** return the escaped value if the value **is not** wrapped in regex', () => { + const value = '/special/path'; + const variableMock = queryBuilder().withId('tempVar').withName('tempVar').withMulti(false).build(); + const result = ds.interpolateQueryExpr(value, variableMock, `select that where path = '$tempVar'`); + const expectation = `/special/path`; + expect(result).toBe(expectation); + }); + + it('should **not** return the escaped value if the value **is not** wrapped in regex 2', () => { + const value = '12.2'; + const variableMock = queryBuilder().withId('tempVar').withName('tempVar').withMulti(false).build(); + const result = ds.interpolateQueryExpr(value, variableMock, `select that where path = '$tempVar'`); + const expectation = `12.2`; + expect(result).toBe(expectation); + }); + + it('should escape the value **always** if the variable is a multi-value variable', () => { + const value = [`/special/path`, `/some/other/path`]; + const variableMock = queryBuilder().withId('tempVar').withName('tempVar').withMulti().build(); + const result = ds.interpolateQueryExpr(value, variableMock, `select that where path = '$tempVar'`); + const expectation = `(\\/special\\/path|\\/some\\/other\\/path)`; + expect(result).toBe(expectation); + }); + + it('should escape and join with the pipe even the variable is not multi-value', () => { + const variableMock = queryBuilder() + .withId('tempVar') + .withName('tempVar') + .withCurrent('All', '$__all') + .withMulti(false) + .withAllValue('') + .withIncludeAll() + .withOptions( + { + text: 'All', + value: '$__all', + }, + { + text: `/special/path`, + value: `/special/path`, + }, + { + text: `/some/other/path`, + value: `/some/other/path`, + } + ) + .build(); + const value = [`/special/path`, `/some/other/path`]; + const result = ds.interpolateQueryExpr(value, variableMock, `select that where path = /$tempVar/`); + const expectation = `(\\/special\\/path|\\/some\\/other\\/path)`; + expect(result).toBe(expectation); + }); + + it('should return floating point number as it is', () => { + const variableMock = queryBuilder() + .withId('tempVar') + .withName('tempVar') + .withMulti(false) + .withOptions({ + text: `1.0`, + value: `1.0`, + }) + .build(); + const value = `1.0`; + const result = ds.interpolateQueryExpr(value, variableMock, `select value / $tempVar from /^measurement$/`); + const expectation = `1.0`; + expect(result).toBe(expectation); + }); +}); diff --git a/public/app/plugins/datasource/influxdb/datasource.ts b/public/app/plugins/datasource/influxdb/datasource.ts index e54bde8734d..e1f514efa84 100644 --- a/public/app/plugins/datasource/influxdb/datasource.ts +++ b/public/app/plugins/datasource/influxdb/datasource.ts @@ -174,7 +174,7 @@ export default class InfluxDatasource extends DataSourceWithBackend { - const text = 'interpolationText'; - const text2 = 'interpolationText2'; - const textWithoutFormatRegex = 'interpolationText,interpolationText2'; - const textWithFormatRegex = 'interpolationText|interpolationText2'; - const variableMap: Record = { - $interpolationVar: text, - $interpolationVar2: text2, - }; - const adhocFilters = [ - { - key: 'adhoc', - operator: '=', - value: 'val', - condition: '', - }, - ]; - const templateSrv = mockTemplateSrv( - jest.fn(() => { - return adhocFilters; - }), - jest.fn((target?: string, scopedVars?: ScopedVars, format?: string | Function): string => { - if (!format) { - return variableMap[target!] || ''; - } - if (format === 'regex') { - return textWithFormatRegex; - } - return textWithoutFormatRegex; - }) - ); - - let queryOptions: DataQueryRequest; - let influxQuery: InfluxQuery; - const now = dateTime('2023-09-16T21:26:00Z'); - - beforeEach(() => { - queryOptions = { - app: 'dashboard', - interval: '10', - intervalMs: 10, - requestId: 'A-testing', - startTime: 0, - range: { - from: dateTime(now).subtract(15, 'minutes'), - to: now, - raw: { - from: 'now-15m', - to: 'now', - }, - }, - rangeRaw: { - from: 'now-15m', - to: 'now', - }, - targets: [], - timezone: 'UTC', - scopedVars: { - interval: { text: '1m', value: '1m' }, - __interval: { text: '1m', value: '1m' }, - __interval_ms: { text: 60000, value: 60000 }, - }, - }; - - influxQuery = { - refId: 'x', - alias: '$interpolationVar', - measurement: '$interpolationVar', - policy: '$interpolationVar', - limit: '$interpolationVar', - slimit: '$interpolationVar', - tz: '$interpolationVar', - tags: [ - { - key: 'cpu', - operator: '=~', - value: '/^$interpolationVar,$interpolationVar2$/', - }, - ], - groupBy: [ - { - params: ['$interpolationVar'], - type: 'tag', - }, - ], - select: [ - [ - { - params: ['$interpolationVar'], - type: 'field', - }, - ], - ], - }; - }); - - describe('adhoc filters', () => { - let fetchReq: { queries: InfluxQuery[] }; - const ctx = { - ds: getMockInfluxDS(getMockDSInstanceSettings(), templateSrv), - }; - beforeEach(async () => { - fetchMock.mockImplementation((req) => { - fetchReq = req.data; - return of(mockInfluxFetchResponse() as FetchResponse); - }); - const req = { - ...queryOptions, - targets: [...queryOptions.targets, { ...influxQuery, adhocFilters }], - }; - ctx.ds.query(req); - }); - - it('should add adhocFilters to the tags in the query', () => { - expect(fetchMock).toHaveBeenCalled(); - expect(fetchReq).not.toBeNull(); - expect(fetchReq.queries.length).toBe(1); - expect(fetchReq.queries[0].tags).toBeDefined(); - expect(fetchReq.queries[0].tags?.length).toBe(2); - expect(fetchReq.queries[0].tags?.[1].key).toBe(adhocFilters[0].key); - expect(fetchReq.queries[0].tags?.[1].value).toBe(adhocFilters[0].value); - }); - }); - - describe('when interpolating template variables', () => { - const text = 'interpolationText'; - const text2 = 'interpolationText2'; - const textWithoutFormatRegex = 'interpolationText,interpolationText2'; - const textWithFormatRegex = 'interpolationText,interpolationText2'; - const justText = 'interpolationText'; - const variableMap: Record = { - $interpolationVar: text, - $interpolationVar2: text2, - }; - const adhocFilters = [ - { - key: 'adhoc', - operator: '=', - value: 'val', - condition: '', - }, - ]; - const templateSrv = mockTemplateSrv( - jest.fn((_: string) => adhocFilters), - jest.fn((target?: string, scopedVars?: ScopedVars, format?: string | Function): string => { - if (!format) { - return variableMap[target!] || ''; - } - if (format === 'regex') { - return textWithFormatRegex; - } - return textWithoutFormatRegex; - }) - ); - const ds = new InfluxDatasource(getMockDSInstanceSettings(), templateSrv); - - function influxChecks(query: InfluxQuery) { - expect(templateSrv.replace).toBeCalledTimes(12); - expect(query.alias).toBe(text); - expect(query.measurement).toBe(textWithFormatRegex); - expect(query.policy).toBe(justText); - expect(query.limit).toBe(justText); - expect(query.slimit).toBe(justText); - expect(query.tz).toBe(text); - expect(query.tags![0].value).toBe(textWithFormatRegex); - expect(query.groupBy![0].params![0]).toBe(justText); - expect(query.select![0][0].params![0]).toBe(justText); - expect(query.adhocFilters?.[0].key).toBe(adhocFilters[0].key); - } - - it('should apply all template variables with InfluxQL mode', () => { - ds.version = ds.version = InfluxVersion.InfluxQL; - ds.access = 'proxy'; - const query = ds.applyTemplateVariables(mockInfluxQueryWithTemplateVars(adhocFilters), { - interpolationVar: { text: text, value: text }, - interpolationVar2: { text: 'interpolationText2', value: 'interpolationText2' }, - }); - influxChecks(query); - }); - - it('should apply all scopedVars to tags', () => { - ds.version = InfluxVersion.InfluxQL; - ds.access = 'proxy'; - const query = ds.applyTemplateVariables(mockInfluxQueryWithTemplateVars(adhocFilters), { - interpolationVar: { text: text, value: text }, - interpolationVar2: { text: 'interpolationText2', value: 'interpolationText2' }, - }); - if (!query.tags?.length) { - throw new Error('Tags are not defined'); - } - const value = query.tags[0].value; - const scopedVars = 'interpolationText,interpolationText2'; - expect(value).toBe(scopedVars); - }); - }); - - describe('variable interpolation with chained variables with backend mode', () => { - const variablesMock = [ - queryBuilder().withId('var1').withName('var1').withCurrent('var1').build(), - queryBuilder().withId('path').withName('path').withCurrent('/etc/hosts').build(), - queryBuilder() - .withId('field_var') - .withName('field_var') - .withMulti(true) - .withOptions( - { - text: `field_1`, - value: `field_1`, - }, - { - text: `field_2`, - value: `field_2`, - }, - { - text: `field_3`, - value: `field_3`, - } - ) - .withCurrent(['field_1', 'field_3']) - .build(), - ]; - const mockTemplateService = new TemplateSrv({ - getVariables: () => variablesMock, - getVariableWithName: (name: string) => variablesMock.filter((v) => v.name === name)[0], - getFilteredVariables: jest.fn(), - }); - mockTemplateService.getAdhocFilters = jest.fn((_: string) => []); - let ds = getMockInfluxDS(getMockDSInstanceSettings(), mockTemplateService); - const fetchMockImpl = () => - of({ - data: { - status: 'success', - results: [ - { - series: [ - { - name: 'measurement', - columns: ['name'], - values: [['cpu']], - }, - ], - }, - ], - }, - }); - - beforeEach(() => { - jest.clearAllMocks(); - fetchMock.mockImplementation(fetchMockImpl); - }); - - it('should render chained regex variables with floating point number', () => { - ds.metricFindQuery(`SELECT sum("piece_count") FROM "rp"."pdata" WHERE diameter <= $maxSED`, { - ...queryOptions, - scopedVars: { maxSED: { text: '8.1', value: '8.1' } }, - }); - const qe = `SELECT sum("piece_count") FROM "rp"."pdata" WHERE diameter <= 8.1`; - const qData = fetchMock.mock.calls[0][0].data.queries[0].query; - expect(qData).toBe(qe); - }); - - it('should render chained regex variables with URL', () => { - ds.metricFindQuery('SHOW TAG VALUES WITH KEY = "agent_url" WHERE agent_url =~ /^$var1$/', { - ...queryOptions, - scopedVars: { - var1: { - text: 'https://aaaa-aa-aaa.bbb.ccc.ddd:8443/ggggg', - value: 'https://aaaa-aa-aaa.bbb.ccc.ddd:8443/ggggg', - }, - }, - }); - const qe = `SHOW TAG VALUES WITH KEY = "agent_url" WHERE agent_url =~ /^https:\\/\\/aaaa-aa-aaa\\.bbb\\.ccc\\.ddd:8443\\/ggggg$/`; - expect(fetchMock).toHaveBeenCalled(); - const qData = fetchMock.mock.calls[0][0].data.queries[0].query; - expect(qData).toBe(qe); - }); - - it('should render chained regex variables with floating point number and url', () => { - ds.metricFindQuery( - 'SELECT sum("piece_count") FROM "rp"."pdata" WHERE diameter <= $maxSED AND agent_url =~ /^$var1$/', - { - ...queryOptions, - scopedVars: { - var1: { - text: 'https://aaaa-aa-aaa.bbb.ccc.ddd:8443/ggggg', - value: 'https://aaaa-aa-aaa.bbb.ccc.ddd:8443/ggggg', - }, - maxSED: { text: '8.1', value: '8.1' }, - }, - } - ); - const qe = `SELECT sum("piece_count") FROM "rp"."pdata" WHERE diameter <= 8.1 AND agent_url =~ /^https:\\/\\/aaaa-aa-aaa\\.bbb\\.ccc\\.ddd:8443\\/ggggg$/`; - const qData = fetchMock.mock.calls[0][0].data.queries[0].query; - expect(qData).toBe(qe); - }); - - it('should interpolate variable inside a regex pattern', () => { - const query: InfluxQuery = { - refId: 'A', - tags: [ - { - key: 'key', - operator: '=~', - value: '/^.*-$var1$/', - }, - ], - }; - const res = ds.applyVariables(query, {}); - const expected = `/^.*-var1$/`; - expect(res.tags?.[0].value).toEqual(expected); - }); - - it('should remove regex wrappers when operator is not a regex operator', () => { - const query: InfluxQuery = { - refId: 'A', - tags: [ - { - key: 'key', - operator: '=', - value: '/^$path$/', - }, - ], - }; - const res = ds.applyVariables(query, {}); - const expected = `/etc/hosts`; - expect(res.tags?.[0].value).toEqual(expected); - }); - - it('should interpolate field keys with given scopedVars', () => { - const query: InfluxQuery = { - refId: 'A', - tags: [ - { - key: 'key', - operator: '=', - value: 'value', - }, - ], - select: [ - [ - { - type: 'field', - params: ['$field_var'], - }, - { - type: 'mean', - params: [], - }, - ], - ], - }; - const res = ds.applyVariables(query, { field_var: { text: 'field_3', value: 'field_3' } }); - const expected = `field_3`; - expect(res.select?.[0][0].params?.[0]).toEqual(expected); - }); - }); - - describe('metric find query', () => { - let ds = getMockInfluxDS(getMockDSInstanceSettings()); - it('handles multiple frames', async () => { - const fetchMockImpl = () => { - return of(mockMetricFindQueryResponse); - }; - - fetchMock.mockImplementation(fetchMockImpl); - const values = await ds.getTagValues({ key: 'test_id', filters: [] }); - expect(fetchMock).toHaveBeenCalled(); - expect(values.length).toBe(5); - expect(values[0].text).toBe('test-t2-1'); - }); - }); -}); - -const mockMetricFindQueryResponse = { - data: { - results: { - metricFindQuery: { - status: 200, - frames: [ - { - schema: { - name: 'NoneNone', - refId: 'metricFindQuery', - fields: [ - { - name: 'Value', - type: 'string', - typeInfo: { - frame: 'string', - }, - }, - ], - }, - data: { - values: [['test-t2-1', 'test-t2-10']], - }, - }, - { - schema: { - name: 'some-other', - refId: 'metricFindQuery', - fields: [ - { - name: 'Value', - type: 'string', - typeInfo: { - frame: 'string', - }, - }, - ], - }, - data: { - values: [['test-t2-1', 'test-t2-10', 'test-t2-2', 'test-t2-3', 'test-t2-4']], - }, - }, - ], - }, - }, - }, -}; diff --git a/public/app/plugins/datasource/influxdb/datasource_sql.test.ts b/public/app/plugins/datasource/influxdb/datasource_sql.test.ts index 705b370a264..bc516995873 100644 --- a/public/app/plugins/datasource/influxdb/datasource_sql.test.ts +++ b/public/app/plugins/datasource/influxdb/datasource_sql.test.ts @@ -3,14 +3,10 @@ import { lastValueFrom } from 'rxjs'; import { SQLQuery } from '@grafana/sql'; import config from 'app/core/config'; +import { getMockDSInstanceSettings, mockBackendService, mockTemplateSrv } from './__mocks__/datasource'; +import { mockInfluxQueryRequest } from './__mocks__/request'; +import { mockInfluxSQLFetchResponse } from './__mocks__/response'; import InfluxDatasource from './datasource'; -import { - getMockDSInstanceSettings, - mockBackendService, - mockInfluxQueryRequest, - mockInfluxSQLFetchResponse, - mockTemplateSrv, -} from './mocks'; import { InfluxVersion } from './types'; config.featureToggles.influxdbBackendMigration = true; diff --git a/public/app/plugins/datasource/influxdb/fsql/datasource.flightsql.test.ts b/public/app/plugins/datasource/influxdb/fsql/datasource.flightsql.test.ts index 30b0855893a..bea4f7bfc98 100644 --- a/public/app/plugins/datasource/influxdb/fsql/datasource.flightsql.test.ts +++ b/public/app/plugins/datasource/influxdb/fsql/datasource.flightsql.test.ts @@ -1,6 +1,7 @@ import { TemplateSrv } from '@grafana/runtime'; -import { getMockDSInstanceSettings, mockBackendService, mockInfluxSQLVariableFetchResponse } from '../mocks'; +import { getMockDSInstanceSettings, mockBackendService } from '../__mocks__/datasource'; +import { mockInfluxSQLVariableFetchResponse } from '../__mocks__/response'; import { FlightSQLDatasource } from './datasource.flightsql'; diff --git a/public/app/plugins/datasource/influxdb/influxql_metadata_query.test.ts b/public/app/plugins/datasource/influxdb/influxql_metadata_query.test.ts index e27429beb93..897eff27c05 100644 --- a/public/app/plugins/datasource/influxdb/influxql_metadata_query.test.ts +++ b/public/app/plugins/datasource/influxdb/influxql_metadata_query.test.ts @@ -1,7 +1,7 @@ import config from 'app/core/config'; +import { getMockInfluxDS } from './__mocks__/datasource'; import { getAllMeasurements, getAllPolicies, getFieldKeys, getTagKeys, getTagValues } from './influxql_metadata_query'; -import { getMockInfluxDS } from './mocks'; import { InfluxQuery } from './types'; describe('influx_metadata_query', () => { diff --git a/public/app/plugins/datasource/influxdb/influxql_query_builder.test.ts b/public/app/plugins/datasource/influxdb/influxql_query_builder.test.ts index d5c70c4c572..4e86911da25 100644 --- a/public/app/plugins/datasource/influxdb/influxql_query_builder.test.ts +++ b/public/app/plugins/datasource/influxdb/influxql_query_builder.test.ts @@ -1,5 +1,5 @@ +import { templateSrvStub as templateService } from './__mocks__/datasource'; import { buildMetadataQuery } from './influxql_query_builder'; -import { templateSrvStub as templateService } from './mocks'; import { DEFAULT_POLICY } from './types'; describe('influxql-query-builder', () => { diff --git a/public/app/plugins/datasource/influxdb/response_parser.test.ts b/public/app/plugins/datasource/influxdb/response_parser.test.ts index 4a606d605ea..e206dca77a8 100644 --- a/public/app/plugins/datasource/influxdb/response_parser.test.ts +++ b/public/app/plugins/datasource/influxdb/response_parser.test.ts @@ -6,8 +6,8 @@ import { FetchResponse } from '@grafana/runtime'; import config from 'app/core/config'; import { backendSrv } from 'app/core/services/backend_srv'; // will use the version in __mocks__ +import { getMockDSInstanceSettings, getMockInfluxDS } from './__mocks__/datasource'; import InfluxQueryModel from './influx_query_model'; -import { getMockDSInstanceSettings, getMockInfluxDS } from './mocks'; import ResponseParser, { getSelectedParams } from './response_parser'; import { InfluxQuery } from './types'; diff --git a/public/app/plugins/datasource/jaeger/package.json b/public/app/plugins/datasource/jaeger/package.json index e7e41afe73f..9e48c142955 100644 --- a/public/app/plugins/datasource/jaeger/package.json +++ b/public/app/plugins/datasource/jaeger/package.json @@ -6,12 +6,16 @@ "dependencies": { "@emotion/css": "11.11.2", "@grafana/data": "workspace:*", + "@grafana/e2e-selectors": "workspace:*", "@grafana/experimental": "1.7.10", "@grafana/o11y-ds-frontend": "workspace:*", "@grafana/runtime": "workspace:*", "@grafana/ui": "workspace:*", "lodash": "4.17.21", "logfmt": "^1.3.2", + "react": "18.2.0", + "react-dom": "18.2.0", + "react-select": "5.8.0", "react-window": "1.8.10", "rxjs": "7.8.1", "stream-browserify": "3.0.0", @@ -20,15 +24,20 @@ }, "devDependencies": { "@grafana/plugin-configs": "workspace:*", + "@testing-library/dom": "10.0.0", "@testing-library/jest-dom": "6.4.2", "@testing-library/react": "15.0.2", "@testing-library/user-event": "14.5.2", "@types/jest": "29.5.12", "@types/lodash": "4.17.0", "@types/logfmt": "^1.2.3", + "@types/node": "20.12.7", + "@types/react": "18.2.79", + "@types/react-dom": "18.2.25", "@types/react-window": "1.8.8", "@types/uuid": "9.0.8", "ts-node": "10.9.2", + "typescript": "5.4.5", "webpack": "5.91.0" }, "peerDependencies": { diff --git a/public/app/plugins/datasource/loki/LogContextProvider.test.ts b/public/app/plugins/datasource/loki/LogContextProvider.test.ts index 0eb962a89df..7d6216a8b10 100644 --- a/public/app/plugins/datasource/loki/LogContextProvider.test.ts +++ b/public/app/plugins/datasource/loki/LogContextProvider.test.ts @@ -37,6 +37,11 @@ const defaultLogRow = { type: FieldType.time, values: [0], }, + { + name: 'labelTypes', + type: FieldType.other, + values: [{ bar: 'I', foo: 'S', xyz: 'I' }], + }, ], }), labels: { bar: 'baz', foo: 'uniqueParsedLabel', xyz: 'abc' }, @@ -75,7 +80,7 @@ describe('LogContextProvider', () => { ); expect(logContextProvider.getInitContextFilters).toBeCalled(); expect(logContextProvider.getInitContextFilters).toHaveBeenCalledWith( - { bar: 'baz', foo: 'uniqueParsedLabel', xyz: 'abc' }, + expect.objectContaining({ labels: { bar: 'baz', foo: 'uniqueParsedLabel', xyz: 'abc' } }), { expr: '{bar="baz"}', refId: 'A' }, { from: dateTime(defaultLogRow.timeEpochMs), @@ -399,7 +404,7 @@ describe('LogContextProvider', () => { }; it('should correctly create contextFilters', async () => { - const result = await logContextProvider.getInitContextFilters(defaultLogRow.labels, queryWithoutParser); + const result = await logContextProvider.getInitContextFilters(defaultLogRow, queryWithoutParser); expect(result.contextFilters).toEqual([ { enabled: true, nonIndexed: false, label: 'bar', value: 'baz' }, { enabled: false, nonIndexed: true, label: 'foo', value: 'uniqueParsedLabel' }, @@ -409,28 +414,29 @@ describe('LogContextProvider', () => { }); it('should return empty contextFilters if no query', async () => { - const filters = (await logContextProvider.getInitContextFilters(defaultLogRow.labels, undefined)) - .contextFilters; + const filters = (await logContextProvider.getInitContextFilters(defaultLogRow, undefined)).contextFilters; expect(filters).toEqual([]); }); it('should return empty contextFilters if no labels', async () => { - const filters = (await logContextProvider.getInitContextFilters({}, queryWithoutParser)).contextFilters; + const filters = ( + await logContextProvider.getInitContextFilters({ labels: [] } as unknown as LogRowModel, queryWithoutParser) + ).contextFilters; expect(filters).toEqual([]); }); it('should call fetchSeriesLabels if parser', async () => { - await logContextProvider.getInitContextFilters(defaultLogRow.labels, queryWithParser); + await logContextProvider.getInitContextFilters(defaultLogRow, queryWithParser); expect(defaultLanguageProviderMock.fetchSeriesLabels).toBeCalled(); }); it('should call fetchSeriesLabels with given time range', async () => { - await logContextProvider.getInitContextFilters(defaultLogRow.labels, queryWithParser, timeRange); + await logContextProvider.getInitContextFilters(defaultLogRow, queryWithParser, timeRange); expect(defaultLanguageProviderMock.fetchSeriesLabels).toBeCalledWith(`{bar="baz"}`, { timeRange }); }); it('should call `languageProvider.start` if no parser with given time range', async () => { - await logContextProvider.getInitContextFilters(defaultLogRow.labels, queryWithoutParser, timeRange); + await logContextProvider.getInitContextFilters(defaultLogRow, queryWithoutParser, timeRange); expect(defaultLanguageProviderMock.start).toBeCalledWith(timeRange); }); }); @@ -442,7 +448,7 @@ describe('LogContextProvider', () => { }; it('should correctly create contextFilters', async () => { - const result = await logContextProvider.getInitContextFilters(defaultLogRow.labels, queryWithParser); + const result = await logContextProvider.getInitContextFilters(defaultLogRow, queryWithParser); expect(result.contextFilters).toEqual([ { enabled: true, nonIndexed: false, label: 'bar', value: 'baz' }, { enabled: false, nonIndexed: true, label: 'foo', value: 'uniqueParsedLabel' }, @@ -452,13 +458,14 @@ describe('LogContextProvider', () => { }); it('should return empty contextFilters if no query', async () => { - const filters = (await logContextProvider.getInitContextFilters(defaultLogRow.labels, undefined)) - .contextFilters; + const filters = (await logContextProvider.getInitContextFilters(defaultLogRow, undefined)).contextFilters; expect(filters).toEqual([]); }); it('should return empty contextFilters if no labels', async () => { - const filters = (await logContextProvider.getInitContextFilters({}, queryWithParser)).contextFilters; + const filters = ( + await logContextProvider.getInitContextFilters({ labels: [] } as unknown as LogRowModel, queryWithParser) + ).contextFilters; expect(filters).toEqual([]); }); }); @@ -477,7 +484,7 @@ describe('LogContextProvider', () => { selectedExtractedLabels: ['foo'], }) ); - const result = await logContextProvider.getInitContextFilters(defaultLogRow.labels, queryWithParser); + const result = await logContextProvider.getInitContextFilters(defaultLogRow, queryWithParser); expect(result.contextFilters).toEqual([ { enabled: false, nonIndexed: false, label: 'bar', value: 'baz' }, // disabled real label { enabled: true, nonIndexed: true, label: 'foo', value: 'uniqueParsedLabel' }, // enabled parsed label @@ -494,7 +501,7 @@ describe('LogContextProvider', () => { selectedExtractedLabels: ['foo'], }) ); - const result = await logContextProvider.getInitContextFilters(defaultLogRow.labels, queryWithParser); + const result = await logContextProvider.getInitContextFilters(defaultLogRow, queryWithParser); expect(result.contextFilters).toEqual([ { enabled: true, nonIndexed: false, label: 'bar', value: 'baz' }, // enabled real label { enabled: false, nonIndexed: true, label: 'foo', value: 'uniqueParsedLabel' }, @@ -511,7 +518,7 @@ describe('LogContextProvider', () => { selectedExtractedLabels: ['foo', 'new'], }) ); - const result = await logContextProvider.getInitContextFilters(defaultLogRow.labels, queryWithParser); + const result = await logContextProvider.getInitContextFilters(defaultLogRow, queryWithParser); expect(result.contextFilters).toEqual([ { enabled: false, nonIndexed: false, label: 'bar', value: 'baz' }, { enabled: true, nonIndexed: true, label: 'foo', value: 'uniqueParsedLabel' }, diff --git a/public/app/plugins/datasource/loki/LogContextProvider.ts b/public/app/plugins/datasource/loki/LogContextProvider.ts index e7e45999ddd..ada99fad88c 100644 --- a/public/app/plugins/datasource/loki/LogContextProvider.ts +++ b/public/app/plugins/datasource/loki/LogContextProvider.ts @@ -16,11 +16,10 @@ import { dateTime, } from '@grafana/data'; import { LabelParser, LabelFilter, LineFilters, PipelineStage, Logfmt, Json } from '@grafana/lezer-logql'; -import { Labels } from '@grafana/schema'; import { LokiContextUi } from './components/LokiContextUi'; import { LokiDatasource, makeRequest, REF_ID_STARTER_LOG_ROW_CONTEXT } from './datasource'; -import { escapeLabelValueInExactSelector } from './languageUtils'; +import { escapeLabelValueInExactSelector, getLabelTypeFromFrame } from './languageUtils'; import { addLabelToQuery, addParserToQuery } from './modifyQuery'; import { getNodePositionsFromQuery, @@ -61,7 +60,7 @@ export class LogContextProvider { // to use the cached filters, we need to reinitialize them. if (this.cachedContextFilters.length === 0 || !cacheFilters) { const filters = ( - await this.getInitContextFilters(row.labels, origQuery, { + await this.getInitContextFilters(row, origQuery, { from: dateTime(row.timeEpochMs), to: dateTime(row.timeEpochMs), raw: { from: dateTime(row.timeEpochMs), to: dateTime(row.timeEpochMs) }, @@ -312,14 +311,15 @@ export class LogContextProvider { }; getInitContextFilters = async ( - labels: Labels, + row: LogRowModel, query?: LokiQuery, timeRange?: TimeRange ): Promise<{ contextFilters: ContextFilter[]; preservedFiltersApplied: boolean }> => { let preservedFiltersApplied = false; - if (!query || isEmpty(labels)) { + if (!query || isEmpty(row.labels)) { return { contextFilters: [], preservedFiltersApplied }; } + const rowLabels = row.labels; // 1. First we need to get all labels from the log row's label // and correctly set parsed and not parsed labels @@ -338,12 +338,12 @@ export class LogContextProvider { } const contextFilters: ContextFilter[] = []; - Object.entries(labels).forEach(([label, value]) => { + Object.entries(rowLabels).forEach(([label, value]) => { const filter: ContextFilter = { label, value: value, enabled: allLabels.includes(label), - nonIndexed: !allLabels.includes(label), + nonIndexed: getLabelTypeFromFrame(label, row.dataFrame, row.rowIndex) !== LabelType.Indexed, }; contextFilters.push(filter); diff --git a/public/app/plugins/datasource/loki/components/LokiContextUi.test.tsx b/public/app/plugins/datasource/loki/components/LokiContextUi.test.tsx index 0ba89331811..7d9d7220877 100644 --- a/public/app/plugins/datasource/loki/components/LokiContextUi.test.tsx +++ b/public/app/plugins/datasource/loki/components/LokiContextUi.test.tsx @@ -122,7 +122,7 @@ describe('LokiContextUi', () => { render(); await waitFor(() => { - expect(props.logContextProvider.getInitContextFilters).toHaveBeenCalledWith(props.row.labels, props.origQuery, { + expect(props.logContextProvider.getInitContextFilters).toHaveBeenCalledWith(props.row, props.origQuery, { from: dateTime(props.row.timeEpochMs), to: dateTime(props.row.timeEpochMs), raw: { from: dateTime(props.row.timeEpochMs), to: dateTime(props.row.timeEpochMs) }, diff --git a/public/app/plugins/datasource/loki/components/LokiContextUi.tsx b/public/app/plugins/datasource/loki/components/LokiContextUi.tsx index abea3b584c2..fe181e08ea8 100644 --- a/public/app/plugins/datasource/loki/components/LokiContextUi.tsx +++ b/public/app/plugins/datasource/loki/components/LokiContextUi.tsx @@ -205,7 +205,7 @@ export function LokiContextUi(props: LokiContextUiProps) { useAsync(async () => { setLoading(true); - const initContextFilters = await logContextProvider.getInitContextFilters(row.labels, origQuery, { + const initContextFilters = await logContextProvider.getInitContextFilters(row, origQuery, { from: dateTime(row.timeEpochMs), to: dateTime(row.timeEpochMs), raw: { from: dateTime(row.timeEpochMs), to: dateTime(row.timeEpochMs) }, diff --git a/public/app/plugins/datasource/loki/components/LokiLabelBrowser.tsx b/public/app/plugins/datasource/loki/components/LokiLabelBrowser.tsx index bbe205f93fa..cbc8930bfa5 100644 --- a/public/app/plugins/datasource/loki/components/LokiLabelBrowser.tsx +++ b/public/app/plugins/datasource/loki/components/LokiLabelBrowser.tsx @@ -8,13 +8,13 @@ import { reportInteraction } from '@grafana/runtime'; import { Button, HighlightPart, - HorizontalGroup, Input, Label, LoadingPlaceholder, withTheme2, BrowserLabel as LokiLabel, fuzzyMatch, + Stack, } from '@grafana/ui'; import LokiLanguageProvider from '../LanguageProvider'; @@ -533,7 +533,7 @@ export class UnthemedLokiLabelBrowser extends React.Component {error || status}
- + @@ -556,7 +556,7 @@ export class UnthemedLokiLabelBrowser extends React.Component Clear - +
); diff --git a/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryCodeEditor.tsx b/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryCodeEditor.tsx index 5db5313f985..2d45f8c96eb 100644 --- a/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryCodeEditor.tsx +++ b/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryCodeEditor.tsx @@ -3,7 +3,7 @@ import React from 'react'; import { GrafanaTheme2 } from '@grafana/data'; import { config } from '@grafana/runtime'; -import { useStyles2, HorizontalGroup, IconButton, Tooltip, Icon } from '@grafana/ui'; +import { useStyles2, IconButton, Tooltip, Icon, Stack } from '@grafana/ui'; import { testIds } from '../../components/LokiQueryEditor'; import { LokiQueryField } from '../../components/LokiQueryField'; @@ -49,7 +49,7 @@ export function LokiQueryCodeEditor({ {lokiFormatQuery && (
- + - +
)} diff --git a/public/app/plugins/datasource/mysql/CHANGELOG.md b/public/app/plugins/datasource/mysql/CHANGELOG.md new file mode 100644 index 00000000000..825c32f0d03 --- /dev/null +++ b/public/app/plugins/datasource/mysql/CHANGELOG.md @@ -0,0 +1 @@ +# Changelog diff --git a/public/app/plugins/datasource/mysql/package.json b/public/app/plugins/datasource/mysql/package.json new file mode 100644 index 00000000000..b9894086ecc --- /dev/null +++ b/public/app/plugins/datasource/mysql/package.json @@ -0,0 +1,41 @@ +{ + "name": "@grafana-plugins/mysql", + "description": "MySQL data source plugin", + "private": true, + "version": "11.1.0-pre", + "dependencies": { + "@emotion/css": "11.11.2", + "@grafana/data": "11.1.0-pre", + "@grafana/experimental": "1.7.10", + "@grafana/runtime": "11.1.0-pre", + "@grafana/sql": "11.1.0-pre", + "@grafana/ui": "11.1.0-pre", + "lodash": "4.17.21", + "react": "18.2.0", + "rxjs": "7.8.1", + "tslib": "2.6.2" + }, + "devDependencies": { + "@grafana/e2e-selectors": "11.1.0-pre", + "@grafana/plugin-configs": "11.1.0-pre", + "@testing-library/react": "15.0.2", + "@testing-library/user-event": "14.5.2", + "@types/jest": "29.5.12", + "@types/lodash": "4.17.0", + "@types/node": "20.12.7", + "@types/react": "18.2.79", + "@types/testing-library__jest-dom": "5.14.9", + "ts-node": "10.9.2", + "typescript": "5.4.5", + "webpack": "5.91.0" + }, + "peerDependencies": { + "@grafana/runtime": "*" + }, + "scripts": { + "build": "webpack -c ./webpack.config.ts --env production", + "build:commit": "webpack -c ./webpack.config.ts --env production --env commit=$(git rev-parse --short HEAD)", + "dev": "webpack -w -c ./webpack.config.ts --env development" + }, + "packageManager": "yarn@4.1.1" +} diff --git a/public/app/plugins/datasource/mysql/tsconfig.json b/public/app/plugins/datasource/mysql/tsconfig.json new file mode 100644 index 00000000000..7daf2ee8aba --- /dev/null +++ b/public/app/plugins/datasource/mysql/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "@grafana/plugin-configs/tsconfig.json", + "include": ["."] +} diff --git a/public/app/plugins/datasource/mysql/webpack.config.ts b/public/app/plugins/datasource/mysql/webpack.config.ts new file mode 100644 index 00000000000..55a880fef5c --- /dev/null +++ b/public/app/plugins/datasource/mysql/webpack.config.ts @@ -0,0 +1,4 @@ +import config from '@grafana/plugin-configs/webpack.config'; + +// eslint-disable-next-line no-barrel-files/no-barrel-files +export default config; diff --git a/public/app/plugins/datasource/parca/package.json b/public/app/plugins/datasource/parca/package.json index 6e9e05609f9..ff0cc19c114 100644 --- a/public/app/plugins/datasource/parca/package.json +++ b/public/app/plugins/datasource/parca/package.json @@ -12,17 +12,22 @@ "lodash": "4.17.21", "monaco-editor": "0.34.1", "react": "18.2.0", + "react-dom": "18.2.0", "react-use": "17.5.0", "rxjs": "7.8.1", "tslib": "2.6.2" }, "devDependencies": { "@grafana/plugin-configs": "11.1.0-pre", + "@testing-library/dom": "10.0.0", "@testing-library/react": "15.0.2", "@testing-library/user-event": "14.5.2", "@types/lodash": "4.17.0", + "@types/node": "20.12.7", "@types/react": "18.2.79", + "@types/react-dom": "18.2.25", "ts-node": "10.9.2", + "typescript": "5.4.5", "webpack": "5.91.0" }, "peerDependencies": { diff --git a/public/app/plugins/datasource/tempo/package.json b/public/app/plugins/datasource/tempo/package.json index b4893668436..9d410bc3364 100644 --- a/public/app/plugins/datasource/tempo/package.json +++ b/public/app/plugins/datasource/tempo/package.json @@ -29,6 +29,7 @@ "prismjs": "1.29.0", "react": "18.2.0", "react-dom": "18.2.0", + "react-select": "5.8.0", "react-use": "17.5.0", "rxjs": "7.8.1", "semver": "7.6.0", @@ -39,6 +40,7 @@ }, "devDependencies": { "@grafana/plugin-configs": "11.1.0-pre", + "@testing-library/dom": "10.0.0", "@testing-library/jest-dom": "6.4.2", "@testing-library/react": "15.0.2", "@testing-library/user-event": "14.5.2", diff --git a/public/app/plugins/datasource/zipkin/package.json b/public/app/plugins/datasource/zipkin/package.json index 784d41aebeb..8e3ce4d09c5 100644 --- a/public/app/plugins/datasource/zipkin/package.json +++ b/public/app/plugins/datasource/zipkin/package.json @@ -6,12 +6,15 @@ "dependencies": { "@emotion/css": "11.11.2", "@grafana/data": "workspace:*", + "@grafana/e2e-selectors": "workspace:*", "@grafana/experimental": "1.7.10", "@grafana/o11y-ds-frontend": "workspace:*", "@grafana/runtime": "workspace:*", "@grafana/ui": "workspace:*", "lodash": "4.17.21", "react": "18.2.0", + "react-dom": "18.2.0", + "react-select": "5.8.0", "react-use": "17.5.0", "rxjs": "7.8.1", "tslib": "2.6.2" @@ -22,8 +25,11 @@ "@testing-library/react": "15.0.2", "@types/jest": "29.5.12", "@types/lodash": "4.17.0", + "@types/node": "20.12.7", "@types/react": "18.2.79", + "@types/react-dom": "18.2.25", "ts-node": "10.9.2", + "typescript": "5.4.5", "webpack": "5.91.0" }, "peerDependencies": { diff --git a/public/app/plugins/panel/alertlist/GroupByWithLoading.tsx b/public/app/plugins/panel/alertlist/GroupByWithLoading.tsx index ed5713720f7..dcf90a7f5b0 100644 --- a/public/app/plugins/panel/alertlist/GroupByWithLoading.tsx +++ b/public/app/plugins/panel/alertlist/GroupByWithLoading.tsx @@ -14,8 +14,7 @@ import { AlertingRule } from 'app/types/unified-alerting'; import { PromRuleType } from 'app/types/unified-alerting-dto'; import { fetchPromRulesAction } from '../../../features/alerting/unified/state/actions'; - -import { isPrivateLabel } from './util'; +import { isPrivateLabelKey } from '../../../features/alerting/unified/utils/labels'; interface Props { id: string; @@ -56,7 +55,7 @@ export const GroupBy = (props: Props) => { .flatMap((group) => group.rules.filter((rule): rule is AlertingRule => rule.type === PromRuleType.Alerting)) .flatMap((rule) => rule.alerts ?? []) .map((alert) => Object.keys(alert.labels ?? {})) - .flatMap((labels) => labels.filter(isPrivateLabel)); + .flatMap((labels) => labels.filter((label) => !isPrivateLabelKey(label))); return uniq(allLabels); }, [allRequestsReady, promRulesByDatasource]); diff --git a/public/app/plugins/panel/alertlist/UnifiedalertList.test.tsx b/public/app/plugins/panel/alertlist/UnifiedalertList.test.tsx index d4847dd8289..bc8430d453a 100644 --- a/public/app/plugins/panel/alertlist/UnifiedalertList.test.tsx +++ b/public/app/plugins/panel/alertlist/UnifiedalertList.test.tsx @@ -5,7 +5,7 @@ import { Provider } from 'react-redux'; import { byRole, byText } from 'testing-library-selector'; import { FieldConfigSource, getDefaultTimeRange, LoadingState, PanelProps, PluginExtensionTypes } from '@grafana/data'; -import { getPluginLinkExtensions, TimeRangeUpdatedEvent } from '@grafana/runtime'; +import { TimeRangeUpdatedEvent, usePluginLinkExtensions } from '@grafana/runtime'; import { setupMswServer } from 'app/features/alerting/unified/mockApi'; import { mockPromRulesApiResponse } from 'app/features/alerting/unified/mocks/alertRuleApi'; import { mockRulerRulesApiResponse } from 'app/features/alerting/unified/mocks/rulerApi'; @@ -57,12 +57,12 @@ const grafanaRuleMock = { jest.mock('@grafana/runtime', () => ({ ...jest.requireActual('@grafana/runtime'), - getPluginLinkExtensions: jest.fn(), + usePluginLinkExtensions: jest.fn(), })); jest.mock('app/features/alerting/unified/api/alertmanager'); const mocks = { - getPluginLinkExtensionsMock: jest.mocked(getPluginLinkExtensions), + usePluginLinkExtensionsMock: jest.mocked(usePluginLinkExtensions), }; const fakeResponse: PromRulesResponse = { @@ -85,7 +85,7 @@ beforeEach(() => { mockRulerRulesApiResponse(server, 'grafana', { 'folder-one': [{ name: 'group1', interval: '20s', rules: [originRule] }], }); - mocks.getPluginLinkExtensionsMock.mockReturnValue({ + mocks.usePluginLinkExtensionsMock.mockReturnValue({ extensions: [ { pluginId: 'grafana-ml-app', @@ -97,6 +97,7 @@ beforeEach(() => { onClick: jest.fn(), }, ], + isLoading: false, }); }); diff --git a/public/app/plugins/panel/alertlist/util.ts b/public/app/plugins/panel/alertlist/util.ts index 47479c7004e..5acf905d27c 100644 --- a/public/app/plugins/panel/alertlist/util.ts +++ b/public/app/plugins/panel/alertlist/util.ts @@ -36,7 +36,3 @@ export function filterAlerts( ); }); } - -export function isPrivateLabel(label: string) { - return !(label.startsWith('__') && label.endsWith('__')); -} diff --git a/public/app/plugins/panel/canvas/components/connections/ConnectionAnchors.tsx b/public/app/plugins/panel/canvas/components/connections/ConnectionAnchors.tsx index cbd13b41e24..ed9903b30c0 100644 --- a/public/app/plugins/panel/canvas/components/connections/ConnectionAnchors.tsx +++ b/public/app/plugins/panel/canvas/components/connections/ConnectionAnchors.tsx @@ -7,6 +7,7 @@ import { ConnectionCoordinates } from 'app/features/canvas'; type Props = { setRef: (anchorElement: HTMLDivElement) => void; + setAnchorsRef: (anchorsElement: HTMLDivElement) => void; handleMouseLeave: ( event: React.MouseEvent | React.FocusEvent ) => boolean; @@ -15,13 +16,33 @@ type Props = { export const CONNECTION_ANCHOR_DIV_ID = 'connectionControl'; export const CONNECTION_ANCHOR_ALT = 'connection anchor'; export const CONNECTION_ANCHOR_HIGHLIGHT_OFFSET = 8; +// Unit is percentage from the middle of the element +// 0, 0 middle; -1, -1 bottom left; 1, 1 top right +export const ANCHORS = [ + { x: -1, y: 1 }, + { x: -0.5, y: 1 }, + { x: 0, y: 1 }, + { x: 0.5, y: 1 }, + { x: 1, y: 1 }, + { x: 1, y: 0.5 }, + { x: 1, y: 0 }, + { x: 1, y: -0.5 }, + { x: 1, y: -1 }, + { x: 0.5, y: -1 }, + { x: 0, y: -1 }, + { x: -0.5, y: -1 }, + { x: -1, y: -1 }, + { x: -1, y: -0.5 }, + { x: -1, y: 0 }, + { x: -1, y: 0.5 }, +]; -const ANCHOR_PADDING = 3; +export const ANCHOR_PADDING = 3; +export const HALF_SIZE = 2.5; -export const ConnectionAnchors = ({ setRef, handleMouseLeave }: Props) => { +export const ConnectionAnchors = ({ setRef, setAnchorsRef, handleMouseLeave }: Props) => { const highlightEllipseRef = useRef(null); const styles = useStyles2(getStyles); - const halfSize = 2.5; const halfSizeHighlightEllipse = 5.5; const anchorImage = 'data:image/svg+xml;base64,PCFET0NUWVBFIHN2ZyBQVUJMSUMgIi0vL1czQy8vRFREIFNWRyAxLjEvL0VOIiAiaHR0cDovL3d3dy53My5vcmcvR3JhcGhpY3MvU1ZHLzEuMS9EVEQvc3ZnMTEuZHRkIj48c3ZnIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHdpZHRoPSI1cHgiIGhlaWdodD0iNXB4IiB2ZXJzaW9uPSIxLjEiPjxwYXRoIGQ9Im0gMCAwIEwgNSA1IE0gMCA1IEwgNSAwIiBzdHJva2Utd2lkdGg9IjIiIHN0eWxlPSJzdHJva2Utb3BhY2l0eTowLjQiIHN0cm9rZT0iI2ZmZmZmZiIvPjxwYXRoIGQ9Im0gMCAwIEwgNSA1IE0gMCA1IEwgNSAwIiBzdHJva2U9IiMyOWI2ZjIiLz48L3N2Zz4='; @@ -54,35 +75,14 @@ export const ConnectionAnchors = ({ setRef, handleMouseLeave }: Props) => { } }; - // Unit is percentage from the middle of the element - // 0, 0 middle; -1, -1 bottom left; 1, 1 top right - const ANCHORS = [ - { x: -1, y: 1 }, - { x: -0.5, y: 1 }, - { x: 0, y: 1 }, - { x: 0.5, y: 1 }, - { x: 1, y: 1 }, - { x: 1, y: 0.5 }, - { x: 1, y: 0 }, - { x: 1, y: -0.5 }, - { x: 1, y: -1 }, - { x: 0.5, y: -1 }, - { x: 0, y: -1 }, - { x: -0.5, y: -1 }, - { x: -1, y: -1 }, - { x: -1, y: -0.5 }, - { x: -1, y: 0 }, - { x: -1, y: 0.5 }, - ]; - const generateAnchors = (anchors: ConnectionCoordinates[] = ANCHORS) => { return anchors.map((anchor) => { const id = `${anchor.x},${anchor.y}`; // Convert anchor coords to relative percentage const style = { - top: `calc(${-anchor.y * 50 + 50}% - ${halfSize}px - ${ANCHOR_PADDING}px)`, - left: `calc(${anchor.x * 50 + 50}% - ${halfSize}px - ${ANCHOR_PADDING}px)`, + top: `calc(${-anchor.y * 50 + 50}% - ${HALF_SIZE}px - ${ANCHOR_PADDING}px)`, + left: `calc(${anchor.x * 50 + 50}% - ${HALF_SIZE}px - ${ANCHOR_PADDING}px)`, }; return ( @@ -114,7 +114,7 @@ export const ConnectionAnchors = ({ setRef, handleMouseLeave }: Props) => { className={styles.highlightElement} onMouseLeave={onMouseLeaveHighlightElement} /> - {generateAnchors()} +
{generateAnchors()}
); }; diff --git a/public/app/plugins/panel/canvas/components/connections/ConnectionSVG.tsx b/public/app/plugins/panel/canvas/components/connections/ConnectionSVG.tsx index 5c15496e1c2..40ea21ee2b4 100644 --- a/public/app/plugins/panel/canvas/components/connections/ConnectionSVG.tsx +++ b/public/app/plugins/panel/canvas/components/connections/ConnectionSVG.tsx @@ -128,7 +128,7 @@ export const ConnectionSVG = ({ // Render selected connection last, ensuring it is above other connections .sort((_a, b) => (selectedConnection === b && scene.panel.context.instanceState.selectedConnection ? -1 : 0)) .map((v, idx) => { - const { source, target, info, vertices } = v; + const { source, target, info, vertices, index } = v; const sourceRect = source.div?.getBoundingClientRect(); const parent = source.div?.parentElement; const transformScale = scene.scale; @@ -146,6 +146,15 @@ export const ConnectionSVG = ({ yStart = v.sourceOriginal.y; xEnd = v.targetOriginal.x; yEnd = v.targetOriginal.y; + } else if (source.options.connections) { + // If original source or target coordinates are not set for the current connection, set them + if ( + !source.options.connections[index].sourceOriginal || + !source.options.connections[index].targetOriginal + ) { + source.options.connections[index].sourceOriginal = { x: x1, y: y1 }; + source.options.connections[index].targetOriginal = { x: x2, y: y2 }; + } } const midpoint = calculateMidpoint(x1, y1, x2, y2); @@ -196,7 +205,7 @@ export const ConnectionSVG = ({ const Yn = vertices[index + 1].y * yDist + yStart; if (index === 0) { // First vertex - angle1 = calculateAngle(xStart, yStart, X, Y); + angle1 = calculateAngle(x1, y1, X, Y); angle2 = calculateAngle(X, Y, Xn, Yn); } else { // All vertices @@ -239,7 +248,7 @@ export const ConnectionSVG = ({ // Only calculate arcs if there is a radius if (radius) { // Length of segment - const lSegment = calculateDistance(X, Y, xStart, yStart); + const lSegment = calculateDistance(X, Y, x1, y1); if (Math.abs(lHalfArc) > 0.5 * Math.abs(lSegment)) { // Limit curve control points to mid segment lHalfArc = 0.5 * lSegment; @@ -262,15 +271,15 @@ export const ConnectionSVG = ({ } // Calculate arc control points const lDelta = lSegment - lHalfArc; - xa = lDelta * Math.cos(angle1) + xStart; - ya = lDelta * Math.sin(angle1) + yStart; + xa = lDelta * Math.cos(angle1) + x1; + ya = lDelta * Math.sin(angle1) + y1; xb = lHalfArc * Math.cos(angle2) + X; yb = lHalfArc * Math.sin(angle2) + Y; // Check if arc control points are inside of segment, otherwise swap sign - if ((xa > X && xa > xStart) || (xa < X && xa < xStart)) { - xa = (lDelta + 2 * lHalfArc) * Math.cos(angle1) + xStart; - ya = (lDelta + 2 * lHalfArc) * Math.sin(angle1) + yStart; + if ((xa > X && xa > x1) || (xa < X && xa < x1)) { + xa = (lDelta + 2 * lHalfArc) * Math.cos(angle1) + x1; + ya = (lDelta + 2 * lHalfArc) * Math.sin(angle1) + y1; xb = -lHalfArc * Math.cos(angle2) + X; yb = -lHalfArc * Math.sin(angle2) + Y; } diff --git a/public/app/plugins/panel/canvas/components/connections/Connections.tsx b/public/app/plugins/panel/canvas/components/connections/Connections.tsx index b0e794ff8c1..2c066bcea63 100644 --- a/public/app/plugins/panel/canvas/components/connections/Connections.tsx +++ b/public/app/plugins/panel/canvas/components/connections/Connections.tsx @@ -16,7 +16,14 @@ import { isConnectionTarget, } from '../../utils'; -import { CONNECTION_ANCHOR_ALT, ConnectionAnchors, CONNECTION_ANCHOR_HIGHLIGHT_OFFSET } from './ConnectionAnchors'; +import { + CONNECTION_ANCHOR_ALT, + ConnectionAnchors, + CONNECTION_ANCHOR_HIGHLIGHT_OFFSET, + ANCHORS, + ANCHOR_PADDING, + HALF_SIZE, +} from './ConnectionAnchors'; import { ConnectionSVG } from './ConnectionSVG'; export const CONNECTION_VERTEX_ID = 'vertex'; @@ -27,6 +34,7 @@ const CONNECTION_VERTEX_SNAP_TOLERANCE = (5 / 180) * Math.PI; // Multi-segment s export class Connections { scene: Scene; connectionAnchorDiv?: HTMLDivElement; + anchorsDiv?: HTMLDivElement; connectionSVG?: SVGElement; connectionLine?: SVGLineElement; connectionSVGVertex?: SVGElement; @@ -70,6 +78,10 @@ export class Connections { this.connectionAnchorDiv = anchorElement; }; + setAnchorsRef = (anchorsElement: HTMLDivElement) => { + this.anchorsDiv = anchorsElement; + }; + setConnectionSVGRef = (connectionSVG: SVGSVGElement) => { this.connectionSVG = connectionSVG; }; @@ -130,6 +142,25 @@ export class Connections { } } + const customElementAnchors = element?.item.customConnectionAnchors || ANCHORS; + // This type cast is necessary as TS doesn't understand that `Element` is an `HTMLElement` + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions + const anchors = Array.from(this.anchorsDiv?.children as HTMLCollectionOf); + const anchorsAmount = customElementAnchors.length; + + // re-calculate the position of the existing anchors on hover + // and hide the rest of the anchors if there are more than the custom ones + anchors.forEach((anchor, index) => { + if (index >= anchorsAmount) { + anchor.style.display = 'none'; + } else { + const { x, y } = customElementAnchors[index]; + anchor.style.top = `calc(${-y * 50 + 50}% - ${HALF_SIZE}px - ${ANCHOR_PADDING}px)`; + anchor.style.left = `calc(${x * 50 + 50}% - ${HALF_SIZE}px - ${ANCHOR_PADDING}px)`; + anchor.style.display = 'block'; + } + }); + const elementBoundingRect = element.div!.getBoundingClientRect(); const transformScale = this.scene.scale; const parentBoundingRect = getParentBoundingClientRect(this.scene); @@ -648,7 +679,11 @@ export class Connections { render() { return ( <> - + ) { const [dashboards, setDashboards] = useState(new Map()); const dispatch = useDispatch(); + const navIndex = useSelector((state) => state.navIndex); useEffect(() => { fetchDashboards(props.options, props.replaceVariables).then((dashes) => { @@ -119,6 +122,23 @@ export function DashList(props: PanelProps) { updatedDashboards.set(dash?.uid ?? '', { ...dash, isStarred }); setDashboards(updatedDashboards); dispatch(setStarred({ id: uid ?? '', title, url, isStarred })); + + const starredNavItem = navIndex['starred']; + if (isStarred) { + starredNavItem.children?.push({ + id: ID_PREFIX + uid, + text: title, + url: url ?? '', + parentItem: starredNavItem, + }); + } else { + dispatch(removeNavIndex(ID_PREFIX + uid)); + const indexToRemove = starredNavItem.children?.findIndex((element) => element.id === ID_PREFIX + uid); + if (indexToRemove) { + starredNavItem.children?.splice(indexToRemove, 1); + } + } + dispatch(updateNavIndex(starredNavItem)); }; const [starredDashboards, recentDashboards, searchedDashboards] = useMemo(() => { diff --git a/public/app/plugins/panel/heatmap/renderHistogram.tsx b/public/app/plugins/panel/heatmap/renderHistogram.tsx index 6169151089c..14278fcfcc2 100644 --- a/public/app/plugins/panel/heatmap/renderHistogram.tsx +++ b/public/app/plugins/panel/heatmap/renderHistogram.tsx @@ -10,11 +10,12 @@ export function renderHistogram( let histCtx = can.current?.getContext('2d'); if (histCtx != null) { + const barsGap = 1; let fromIdx = index; - while (xVals[fromIdx--] === xVals[index]) {} - - fromIdx++; + while (xVals[fromIdx - 1] === xVals[index]) { + fromIdx--; + } let toIdx = fromIdx + yBucketCount; @@ -37,16 +38,14 @@ export function renderHistogram( if (c > 0) { let pctY = c / maxCount; - let pctX = j / (yBucketCount + 1); + let pctX = j / yBucketCount; let p = i === index ? pHov : pRest; - p.rect( - Math.round(histCanWidth * pctX), - Math.round(histCanHeight * (1 - pctY)), - Math.round(histCanWidth / yBucketCount), - Math.round(histCanHeight * pctY) - ); + const xCoord = histCanWidth * pctX + barsGap; + const width = histCanWidth / yBucketCount - barsGap; + + p.rect(xCoord, Math.round(histCanHeight * (1 - pctY)), width, Math.round(histCanHeight * pctY)); } i++; diff --git a/public/app/plugins/panel/timeseries/migrations.ts b/public/app/plugins/panel/timeseries/migrations.ts index c27205c84a6..edb9eba6a57 100644 --- a/public/app/plugins/panel/timeseries/migrations.ts +++ b/public/app/plugins/panel/timeseries/migrations.ts @@ -339,9 +339,22 @@ export function graphToTimeseriesOptions(angular: any): { if (angular.stack) { graph.stacking = { - mode: StackingMode.Normal, + mode: angular.percentage ? StackingMode.Percent : StackingMode.Normal, group: defaultGraphConfig.stacking!.group, }; + + if (angular.percentage) { + if (angular.yaxis) { + delete y1.min; + delete y1.max; + + // TimeSeries currently uses 0-1 for percent, so allowing zero leaves only top and bottom ticks. + // removing it feels better. probably should fix in TimeSeries, but let's kick it down the road + if (y1.decimals === 0) { + delete y1.decimals; + } + } + } } y1.custom = omitBy(graph, isNil); diff --git a/public/app/plugins/panel/timeseries/plugins/ExemplarMarker.tsx b/public/app/plugins/panel/timeseries/plugins/ExemplarMarker.tsx index 54c42a22830..04c1c95b574 100644 --- a/public/app/plugins/panel/timeseries/plugins/ExemplarMarker.tsx +++ b/public/app/plugins/panel/timeseries/plugins/ExemplarMarker.tsx @@ -307,7 +307,9 @@ const getExemplarMarkerStyles = (theme: GrafanaTheme2) => { marble: css({ display: 'block', opacity: 0.5, - transition: 'transform 0.15s ease-out', + [theme.transitions.handleMotion('no-preference')]: { + transition: 'transform 0.15s ease-out', + }, }), activeMarble: css({ transform: 'scale(1.3)', diff --git a/public/app/plugins/panel/xychart/v2/XYChartTooltip.tsx b/public/app/plugins/panel/xychart/v2/XYChartTooltip.tsx index bc80bffe01f..bfc25494772 100644 --- a/public/app/plugins/panel/xychart/v2/XYChartTooltip.tsx +++ b/public/app/plugins/panel/xychart/v2/XYChartTooltip.tsx @@ -23,6 +23,14 @@ export interface Props { xySeries: XYSeries[]; } +function stripSeriesName(fieldName: string, seriesName: string) { + if (fieldName.includes(' ')) { + fieldName = fieldName.replace(seriesName, '').trim(); + } + + return fieldName; +} + export const XYChartTooltip = ({ dataIdxs, seriesIdx, data, xySeries, dismiss, isPinned }: Props) => { const styles = useStyles2(getStyles); @@ -51,18 +59,18 @@ export const XYChartTooltip = ({ dataIdxs, seriesIdx, data, xySeries, dismiss, i const contentItems: VizTooltipItem[] = [ { - label: xField.state?.displayName ?? xField.name, + label: stripSeriesName(xField.state?.displayName ?? xField.name, label), value: fmt(xField, xField.values[rowIndex]), }, { - label: yField.state?.displayName ?? yField.name, + label: stripSeriesName(yField.state?.displayName ?? yField.name, label), value: fmt(yField, yField.values[rowIndex]), }, ]; series._rest.forEach((field) => { contentItems.push({ - label: field.state?.displayName ?? field.name, + label: stripSeriesName(field.state?.displayName ?? field.name, label), value: fmt(field, field.values[rowIndex]), }); }); diff --git a/public/app/types/explore.ts b/public/app/types/explore.ts index 3f91e26c6d4..865d656e3dc 100644 --- a/public/app/types/explore.ts +++ b/public/app/types/explore.ts @@ -63,11 +63,6 @@ export interface ExploreState { panes: Record; - /** - * Is the drawer for query history showing - */ - showQueryHistory: boolean; - /** * History of all queries */ diff --git a/public/img/icons/custom/gf-traces.svg b/public/img/icons/custom/gf-traces.svg index a58acc03951..ea83d6e50fb 100644 --- a/public/img/icons/custom/gf-traces.svg +++ b/public/img/icons/custom/gf-traces.svg @@ -1,5 +1,5 @@ - + diff --git a/public/locales/de-DE/grafana.json b/public/locales/de-DE/grafana.json index bf7a0308c24..6d960138b72 100644 --- a/public/locales/de-DE/grafana.json +++ b/public/locales/de-DE/grafana.json @@ -25,6 +25,14 @@ "user": "Nutzer" } }, + "annotations": { + "empty-state": { + "button-title": "", + "info-box-content": "", + "info-box-content-2": "", + "title": "" + } + }, "api-keys": { "empty-state": { "message": "" @@ -72,6 +80,12 @@ "select-checkbox": "Auswählen", "tags-column": "Tags" }, + "empty-state": { + "button-title": "", + "pro-tip": "", + "title": "", + "title-folder": "" + }, "folder-actions-button": { "delete": "Löschen", "folder-actions": "Ordneraktionen", @@ -159,6 +173,11 @@ "sub-text": "<0>Text definieren, der die Korrelation beschreibt.", "title": "Korrelationsbezeichnung definieren (Schritt 1 von 3)" }, + "empty-state": { + "button-title": "", + "pro-tip": "", + "title": "" + }, "list": { "delete": "Korrelation löschen", "label": "Label", @@ -355,11 +374,18 @@ "validation-required": "Benötigt ein Dashboard JSON-Modell" } }, + "dashboard-links": { + "empty-state": { + "button-title": "", + "info-box-content": "", + "title": "" + } + }, "dashboard-settings": { "annotations": { "title": "Anmerkungen" }, - "dashboard-delete-button": "Dashboard löschen", + "dashboard-delete-button": "", "general": { "auto-refresh-description": "", "auto-refresh-label": "Automatisch aktualisieren", @@ -405,6 +431,13 @@ "title": "Versionen" } }, + "data-source-list": { + "empty-state": { + "button-title": "", + "pro-tip": "", + "title": "" + } + }, "data-source-picker": { "add-new-data-source": "Neue Datenquelle konfigurieren", "built-in-list": { @@ -439,6 +472,7 @@ "newest-first": "Neueste zuerst", "oldest-first": "Älteste zuerst", "query-history": "Abfrageverlauf", + "query-library": "", "settings": "Einstellungen", "starred": "Hervorgehoben" }, @@ -657,6 +691,10 @@ }, "add-widget": { "title": "Panel aus Panel-Bibliothek hinzufügen" + }, + "empty-state": { + "message": "", + "more-info": "" } }, "library-panels": { @@ -1230,9 +1268,8 @@ "confirm-text": "Löschen" }, "empty": { - "button": "Playlist erstellen", - "pro-tip": "Sie können Playlists verwenden, um Dashboards auf TV-Geräten ohne Benutzersteuerung zu steuern", - "pro-tip-link-title": "Mehr erfahren", + "button": "", + "pro-tip": "", "title": "Es gibt noch keine Playlists erstellt" } }, @@ -1346,6 +1383,10 @@ "orphaned-title": "<0>Verwaistes öffentliches Dashboard", "orphaned-tooltip": "Das verlinkte Dashboard wurde bereits gelöscht" }, + "empty-state": { + "message": "", + "more-info": "" + }, "toggle": { "pause-sharing-toggle-text": "Teilen pausieren" } @@ -1450,7 +1491,10 @@ }, "service-accounts": { "empty-state": { - "message": "" + "button-title": "", + "message": "", + "more-info": "", + "title": "" } }, "share-modal": { @@ -1570,7 +1614,17 @@ }, "title": "Einstellungen" }, + "silences": { + "empty-state": { + "button-title": "", + "title": "" + } + }, "snapshot": { + "empty-state": { + "message": "", + "more-info": "" + }, "external-badge": "Extern", "name-column-header": "Name", "url-column-header": "Snapshot url", @@ -1583,7 +1637,10 @@ }, "teams": { "empty-state": { - "message": "" + "button-title": "", + "message": "", + "pro-tip": "", + "title": "" } }, "time-picker": { @@ -1705,5 +1762,13 @@ "textbox": { "placeholder": "Variablenwert eingeben" } + }, + "variables": { + "empty-state": { + "button-title": "", + "info-box-content": "", + "info-box-content-2": "", + "title": "" + } } } \ No newline at end of file diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index 1e02e4d8c1d..812dcbcf66a 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -25,6 +25,14 @@ "user": "User" } }, + "annotations": { + "empty-state": { + "button-title": "Add annotation query", + "info-box-content": "<0>Annotations provide a way to integrate event data into your graphs. They are visualized as vertical lines and icons on all graph panels. When you hover over an annotation icon you can get event text & tags for the event. You can add annotation events directly from grafana by holding CTRL or CMD + click on graph (or drag region). These will be stored in Grafana's annotation database.", + "info-box-content-2": "Checkout the <2>Annotations documentation for more information.", + "title": "There are no custom annotation queries added yet" + } + }, "api-keys": { "empty-state": { "message": "No API keys found" @@ -72,6 +80,12 @@ "select-checkbox": "Select", "tags-column": "Tags" }, + "empty-state": { + "button-title": "Create dashboard", + "pro-tip": "Add/move dashboards to your folder at <2>Browse dashboards", + "title": "You haven't created any dashboards yet", + "title-folder": "This folder doesn't have any dashboards yet" + }, "folder-actions-button": { "delete": "Delete", "folder-actions": "Folder actions", @@ -159,6 +173,11 @@ "sub-text": "<0>Define text that will describe the correlation.", "title": "Define correlation label (Step 1 of 3)" }, + "empty-state": { + "button-title": "Add correlation", + "pro-tip": "You can also define correlations via datasource provisioning", + "title": "You haven't defined any correlations yet" + }, "list": { "delete": "delete correlation", "label": "Label", @@ -257,7 +276,7 @@ "add-widget-body": "Create lists, markdowns and other widgets", "add-widget-button": "Add widget", "add-widget-header": "Add a widget", - "import-a-dashboard-body": "Import dashboards from files or<1>grafana.com.", + "import-a-dashboard-body": "Import dashboards from files or <1>grafana.com.", "import-a-dashboard-header": "Import a dashboard", "import-dashboard-button": "Import dashboard" }, @@ -355,11 +374,18 @@ "validation-required": "Need a dashboard JSON model" } }, + "dashboard-links": { + "empty-state": { + "button-title": "Add dashboard link", + "info-box-content": "Dashboard links allow you to place links to other dashboards and web sites directly below the dashboard header. <2>Learn more", + "title": "There are no dashboard links added yet" + } + }, "dashboard-settings": { "annotations": { "title": "Annotations" }, - "dashboard-delete-button": "Delete Dashboard", + "dashboard-delete-button": "Delete dashboard", "general": { "auto-refresh-description": "Define the auto refresh intervals that should be available in the auto refresh list. Use the format '5s' for seconds, '1m' for minutes, '1h' for hours, and '1d' for days (e.g.: '5s,10s,30s,1m,5m,15m,30m,1h,2h,1d').", "auto-refresh-label": "Auto refresh", @@ -405,6 +431,13 @@ "title": "Versions" } }, + "data-source-list": { + "empty-state": { + "button-title": "Add data source", + "pro-tip": "You can also define data sources through configuration files. <2>Learn more", + "title": "No data sources defined" + } + }, "data-source-picker": { "add-new-data-source": "Configure a new data source", "built-in-list": { @@ -439,6 +472,7 @@ "newest-first": "Newest first", "oldest-first": "Oldest first", "query-history": "Query history", + "query-library": "Query library", "settings": "Settings", "starred": "Starred" }, @@ -550,11 +584,11 @@ "toolbar": { "aria-label": "Explore toolbar", "copy-link": "Copy URL", - "copy-link-abs-time": "Copy Absolute Shortened URL", + "copy-link-abs-time": "Copy absolute URL", "copy-links-absolute-category": "Time-sync URL links (share with time range intact)", "copy-links-normal-category": "Normal URL links", "copy-shortened-link": "Copy shortened URL", - "copy-shortened-link-abs-time": "Copy Absolute Shortened URL", + "copy-shortened-link-abs-time": "Copy absolute shortened URL", "copy-shortened-link-menu": "Open copy link options", "refresh-picker-cancel": "Cancel", "refresh-picker-run": "Run query", @@ -657,6 +691,10 @@ }, "add-widget": { "title": "Add panel from panel library" + }, + "empty-state": { + "message": "You haven't created any library panels yet", + "more-info": "Create a library panel from any existing dashboard panel through the panel context menu. <2>Learn more" } }, "library-panels": { @@ -1230,9 +1268,8 @@ "confirm-text": "Delete" }, "empty": { - "button": "Create Playlist", - "pro-tip": "You can use playlists to cycle dashboards on TVs without user control", - "pro-tip-link-title": "Learn more", + "button": "Create playlist", + "pro-tip": "You can use playlists to cycle dashboards on TVs without user control. <2>Learn more", "title": "There are no playlists created yet" } }, @@ -1346,6 +1383,10 @@ "orphaned-title": "<0>Orphaned public dashboard", "orphaned-tooltip": "The linked dashboard has already been deleted" }, + "empty-state": { + "message": "You haven't created any public dashboards yet", + "more-info": "Create a public dashboard from any existing dashboard through the <1>Share modal. <4>Learn more" + }, "toggle": { "pause-sharing-toggle-text": "Pause sharing" } @@ -1450,7 +1491,10 @@ }, "service-accounts": { "empty-state": { - "message": "No services accounts found" + "button-title": "Add service account", + "message": "No services accounts found", + "more-info": "Remember, you can provide specific permissions for API access to other applications", + "title": "You haven't created any service accounts yet" } }, "share-modal": { @@ -1481,7 +1525,7 @@ "info-text": "Create a direct link to this dashboard or panel, customized with the options below.", "link-url": "Link URL", "render-alert": "Image renderer plugin not installed", - "render-instructions": "To render a panel image, you must install the<1>Grafana image renderer plugin. Please contact your Grafana administrator to install the plugin.", + "render-instructions": "To render a panel image, you must install the <2>Grafana image renderer plugin. Please contact your Grafana administrator to install the plugin.", "rendered-image": "Direct link rendered image", "save-alert": "Dashboard is not saved", "save-dashboard": "To render a panel image, you must save the dashboard first.", @@ -1570,7 +1614,17 @@ }, "title": "Preferences" }, + "silences": { + "empty-state": { + "button-title": "Create silence", + "title": "You haven't created any silences yet" + } + }, "snapshot": { + "empty-state": { + "message": "You haven't created any snapshots yet", + "more-info": "You can create a snapshot of any dashboard through the <1>Share modal. <4>Learn more" + }, "external-badge": "External", "name-column-header": "Name", "url-column-header": "Snapshot url", @@ -1583,7 +1637,10 @@ }, "teams": { "empty-state": { - "message": "No teams found" + "button-title": "New team", + "message": "No teams found", + "pro-tip": "Assign folder and dashboard permissions to teams instead of users to ease administration. <2>Learn more", + "title": "You haven't created any teams yet" } }, "time-picker": { @@ -1705,5 +1762,13 @@ "textbox": { "placeholder": "Enter variable value" } + }, + "variables": { + "empty-state": { + "button-title": "Add variable", + "info-box-content": "Variables enable more interactive and dynamic dashboards. Instead of hard-coding things like server or sensor names in your metric queries you can use variables in their place. Variables are shown as list boxes at the top of the dashboard. These drop-down lists make it easy to change the data being displayed in your dashboard.", + "info-box-content-2": "Check out the <2>Templates and variables documentation for more information.", + "title": "There are no variables added yet" + } } } diff --git a/public/locales/es-ES/grafana.json b/public/locales/es-ES/grafana.json index 077fea16e7c..9df31e35888 100644 --- a/public/locales/es-ES/grafana.json +++ b/public/locales/es-ES/grafana.json @@ -25,6 +25,14 @@ "user": "Usuario" } }, + "annotations": { + "empty-state": { + "button-title": "", + "info-box-content": "", + "info-box-content-2": "", + "title": "" + } + }, "api-keys": { "empty-state": { "message": "" @@ -72,6 +80,12 @@ "select-checkbox": "Seleccionar", "tags-column": "Etiquetas" }, + "empty-state": { + "button-title": "", + "pro-tip": "", + "title": "", + "title-folder": "" + }, "folder-actions-button": { "delete": "Eliminar", "folder-actions": "Acciones de la carpeta", @@ -159,6 +173,11 @@ "sub-text": "<0>Defina el texto que describirá la correlación.", "title": "Definir la etiqueta de correlación (paso 1 de 3)" }, + "empty-state": { + "button-title": "", + "pro-tip": "", + "title": "" + }, "list": { "delete": "eliminar correlación", "label": "Etiqueta", @@ -355,11 +374,18 @@ "validation-required": "Se necesita un modelo JSON del tablero" } }, + "dashboard-links": { + "empty-state": { + "button-title": "", + "info-box-content": "", + "title": "" + } + }, "dashboard-settings": { "annotations": { "title": "Anotaciones" }, - "dashboard-delete-button": "Eliminar tablero", + "dashboard-delete-button": "", "general": { "auto-refresh-description": "", "auto-refresh-label": "Actualización automática", @@ -405,6 +431,13 @@ "title": "Versiones" } }, + "data-source-list": { + "empty-state": { + "button-title": "", + "pro-tip": "", + "title": "" + } + }, "data-source-picker": { "add-new-data-source": "Configurar una nueva fuente de datos", "built-in-list": { @@ -439,6 +472,7 @@ "newest-first": "El más reciente primero", "oldest-first": "El más antiguo primero", "query-history": "Historial de consultas", + "query-library": "", "settings": "Configuración", "starred": "Destacado" }, @@ -657,6 +691,10 @@ }, "add-widget": { "title": "Añadir panel de la biblioteca de paneles" + }, + "empty-state": { + "message": "", + "more-info": "" } }, "library-panels": { @@ -1230,9 +1268,8 @@ "confirm-text": "Eliminar" }, "empty": { - "button": "Crear lista de reproducción", - "pro-tip": "Puede usar las listas de reproducción para crear ciclos de tableros en televisiones sin control de usuario", - "pro-tip-link-title": "Más información", + "button": "", + "pro-tip": "", "title": "Aún no se ha creado ninguna lista de reproducción" } }, @@ -1346,6 +1383,10 @@ "orphaned-title": "<0>Tablero público huérfano", "orphaned-tooltip": "El tablero vinculado ya se ha eliminado" }, + "empty-state": { + "message": "", + "more-info": "" + }, "toggle": { "pause-sharing-toggle-text": "Pausar el uso compartido" } @@ -1450,7 +1491,10 @@ }, "service-accounts": { "empty-state": { - "message": "" + "button-title": "", + "message": "", + "more-info": "", + "title": "" } }, "share-modal": { @@ -1570,7 +1614,17 @@ }, "title": "Preferencias" }, + "silences": { + "empty-state": { + "button-title": "", + "title": "" + } + }, "snapshot": { + "empty-state": { + "message": "", + "more-info": "" + }, "external-badge": "Externo", "name-column-header": "Nombre", "url-column-header": "URL de la instantánea", @@ -1583,7 +1637,10 @@ }, "teams": { "empty-state": { - "message": "" + "button-title": "", + "message": "", + "pro-tip": "", + "title": "" } }, "time-picker": { @@ -1705,5 +1762,13 @@ "textbox": { "placeholder": "Introducir el valor de la variable" } + }, + "variables": { + "empty-state": { + "button-title": "", + "info-box-content": "", + "info-box-content-2": "", + "title": "" + } } } \ No newline at end of file diff --git a/public/locales/fr-FR/grafana.json b/public/locales/fr-FR/grafana.json index c054c1a56a0..d6e980bf798 100644 --- a/public/locales/fr-FR/grafana.json +++ b/public/locales/fr-FR/grafana.json @@ -25,6 +25,14 @@ "user": "Utilisateur" } }, + "annotations": { + "empty-state": { + "button-title": "", + "info-box-content": "", + "info-box-content-2": "", + "title": "" + } + }, "api-keys": { "empty-state": { "message": "" @@ -72,6 +80,12 @@ "select-checkbox": "Sélectionner", "tags-column": "Étiquettes" }, + "empty-state": { + "button-title": "", + "pro-tip": "", + "title": "", + "title-folder": "" + }, "folder-actions-button": { "delete": "Supprimer", "folder-actions": "Actions sur le dossier", @@ -159,6 +173,11 @@ "sub-text": "<0>Définissez le texte qui décrira la corrélation.", "title": "Définir l'étiquette de corrélation (Étape 1 sur 3)" }, + "empty-state": { + "button-title": "", + "pro-tip": "", + "title": "" + }, "list": { "delete": "supprimer la corrélation", "label": "Étiquette", @@ -355,11 +374,18 @@ "validation-required": "Nécessite un modèle JSON de tableau de bord" } }, + "dashboard-links": { + "empty-state": { + "button-title": "", + "info-box-content": "", + "title": "" + } + }, "dashboard-settings": { "annotations": { "title": "Annotations" }, - "dashboard-delete-button": "Supprimer le tableau de bord", + "dashboard-delete-button": "", "general": { "auto-refresh-description": "", "auto-refresh-label": "Actualisation automatique", @@ -405,6 +431,13 @@ "title": "Versions" } }, + "data-source-list": { + "empty-state": { + "button-title": "", + "pro-tip": "", + "title": "" + } + }, "data-source-picker": { "add-new-data-source": "Configurer une nouvelle source de données", "built-in-list": { @@ -439,6 +472,7 @@ "newest-first": "Plus récent en premier", "oldest-first": "Plus ancien en premier", "query-history": "Historique des requêtes", + "query-library": "", "settings": "Paramètres", "starred": "Favoris" }, @@ -657,6 +691,10 @@ }, "add-widget": { "title": "Ajouter un panneau depuis la bibliothèque de panneau" + }, + "empty-state": { + "message": "", + "more-info": "" } }, "library-panels": { @@ -1230,9 +1268,8 @@ "confirm-text": "Supprimer" }, "empty": { - "button": "Créer une playlist", - "pro-tip": "Vous pouvez utiliser des playlists pour faire défiler des tableaux de bord sur des téléviseurs sans contrôle utilisateur", - "pro-tip-link-title": "En savoir plus", + "button": "", + "pro-tip": "", "title": "Il n'y a aucune playlist créée" } }, @@ -1346,6 +1383,10 @@ "orphaned-title": "<0>Tableau de bord public orphelin", "orphaned-tooltip": "Le tableau de bord lié a déjà été supprimé" }, + "empty-state": { + "message": "", + "more-info": "" + }, "toggle": { "pause-sharing-toggle-text": "Suspendre le partage" } @@ -1450,7 +1491,10 @@ }, "service-accounts": { "empty-state": { - "message": "" + "button-title": "", + "message": "", + "more-info": "", + "title": "" } }, "share-modal": { @@ -1570,7 +1614,17 @@ }, "title": "Préférences" }, + "silences": { + "empty-state": { + "button-title": "", + "title": "" + } + }, "snapshot": { + "empty-state": { + "message": "", + "more-info": "" + }, "external-badge": "Externe", "name-column-header": "Nom", "url-column-header": "URL de l'instantané", @@ -1583,7 +1637,10 @@ }, "teams": { "empty-state": { - "message": "" + "button-title": "", + "message": "", + "pro-tip": "", + "title": "" } }, "time-picker": { @@ -1705,5 +1762,13 @@ "textbox": { "placeholder": "Entrer la valeur de la variable" } + }, + "variables": { + "empty-state": { + "button-title": "", + "info-box-content": "", + "info-box-content-2": "", + "title": "" + } } } \ No newline at end of file diff --git a/public/locales/pseudo-LOCALE/grafana.json b/public/locales/pseudo-LOCALE/grafana.json index 07cb64d01df..6da92d43459 100644 --- a/public/locales/pseudo-LOCALE/grafana.json +++ b/public/locales/pseudo-LOCALE/grafana.json @@ -25,6 +25,14 @@ "user": "Ůşęř" } }, + "annotations": { + "empty-state": { + "button-title": "Åđđ äʼnʼnőŧäŧįőʼn qūęřy", + "info-box-content": "<0>Åʼnʼnőŧäŧįőʼnş přővįđę ä ŵäy ŧő įʼnŧęģřäŧę ęvęʼnŧ đäŧä įʼnŧő yőūř ģřäpĥş. Ŧĥęy äřę vįşūäľįžęđ äş vęřŧįčäľ ľįʼnęş äʼnđ įčőʼnş őʼn äľľ ģřäpĥ päʼnęľş. Ŵĥęʼn yőū ĥővęř ővęř äʼn äʼnʼnőŧäŧįőʼn įčőʼn yőū čäʼn ģęŧ ęvęʼnŧ ŧęχŧ & ŧäģş ƒőř ŧĥę ęvęʼnŧ. Ÿőū čäʼn äđđ äʼnʼnőŧäŧįőʼn ęvęʼnŧş đįřęčŧľy ƒřőm ģřäƒäʼnä þy ĥőľđįʼnģ CŦŖĿ őř CMĐ + čľįčĸ őʼn ģřäpĥ (őř đřäģ řęģįőʼn). Ŧĥęşę ŵįľľ þę şŧőřęđ įʼn Ğřäƒäʼnä'ş äʼnʼnőŧäŧįőʼn đäŧäþäşę.", + "info-box-content-2": "Cĥęčĸőūŧ ŧĥę <2>Åʼnʼnőŧäŧįőʼnş đőčūmęʼnŧäŧįőʼn ƒőř mőřę įʼnƒőřmäŧįőʼn.", + "title": "Ŧĥęřę äřę ʼnő čūşŧőm äʼnʼnőŧäŧįőʼn qūęřįęş äđđęđ yęŧ" + } + }, "api-keys": { "empty-state": { "message": "Ńő ÅPĨ ĸęyş ƒőūʼnđ" @@ -72,6 +80,12 @@ "select-checkbox": "Ŝęľęčŧ", "tags-column": "Ŧäģş" }, + "empty-state": { + "button-title": "Cřęäŧę đäşĥþőäřđ", + "pro-tip": "Åđđ/mővę đäşĥþőäřđş ŧő yőūř ƒőľđęř äŧ <2>ßřőŵşę đäşĥþőäřđş", + "title": "Ÿőū ĥävęʼn'ŧ čřęäŧęđ äʼny đäşĥþőäřđş yęŧ", + "title-folder": "Ŧĥįş ƒőľđęř đőęşʼn'ŧ ĥävę äʼny đäşĥþőäřđş yęŧ" + }, "folder-actions-button": { "delete": "Đęľęŧę", "folder-actions": "Főľđęř äčŧįőʼnş", @@ -159,6 +173,11 @@ "sub-text": "<0>Đęƒįʼnę ŧęχŧ ŧĥäŧ ŵįľľ đęşčřįþę ŧĥę čőřřęľäŧįőʼn.", "title": "Đęƒįʼnę čőřřęľäŧįőʼn ľäþęľ (Ŝŧęp 1 őƒ 3)" }, + "empty-state": { + "button-title": "Åđđ čőřřęľäŧįőʼn", + "pro-tip": "Ÿőū čäʼn äľşő đęƒįʼnę čőřřęľäŧįőʼnş vįä đäŧäşőūřčę přővįşįőʼnįʼnģ", + "title": "Ÿőū ĥävęʼn'ŧ đęƒįʼnęđ äʼny čőřřęľäŧįőʼnş yęŧ" + }, "list": { "delete": "đęľęŧę čőřřęľäŧįőʼn", "label": "Ŀäþęľ", @@ -257,7 +276,7 @@ "add-widget-body": "Cřęäŧę ľįşŧş, mäřĸđőŵʼnş äʼnđ őŧĥęř ŵįđģęŧş", "add-widget-button": "Åđđ ŵįđģęŧ", "add-widget-header": "Åđđ ä ŵįđģęŧ", - "import-a-dashboard-body": "Ĩmpőřŧ đäşĥþőäřđş ƒřőm ƒįľęş őř<1>ģřäƒäʼnä.čőm.", + "import-a-dashboard-body": "Ĩmpőřŧ đäşĥþőäřđş ƒřőm ƒįľęş őř <1>ģřäƒäʼnä.čőm.", "import-a-dashboard-header": "Ĩmpőřŧ ä đäşĥþőäřđ", "import-dashboard-button": "Ĩmpőřŧ đäşĥþőäřđ" }, @@ -355,11 +374,18 @@ "validation-required": "Ńęęđ ä đäşĥþőäřđ ĴŜØŃ mőđęľ" } }, + "dashboard-links": { + "empty-state": { + "button-title": "Åđđ đäşĥþőäřđ ľįʼnĸ", + "info-box-content": "Đäşĥþőäřđ ľįʼnĸş äľľőŵ yőū ŧő pľäčę ľįʼnĸş ŧő őŧĥęř đäşĥþőäřđş äʼnđ ŵęþ şįŧęş đįřęčŧľy þęľőŵ ŧĥę đäşĥþőäřđ ĥęäđęř. <2>Ŀęäřʼn mőřę", + "title": "Ŧĥęřę äřę ʼnő đäşĥþőäřđ ľįʼnĸş äđđęđ yęŧ" + } + }, "dashboard-settings": { "annotations": { "title": "Åʼnʼnőŧäŧįőʼnş" }, - "dashboard-delete-button": "Đęľęŧę Đäşĥþőäřđ", + "dashboard-delete-button": "Đęľęŧę đäşĥþőäřđ", "general": { "auto-refresh-description": "Đęƒįʼnę ŧĥę äūŧő řęƒřęşĥ įʼnŧęřväľş ŧĥäŧ şĥőūľđ þę äväįľäþľę įʼn ŧĥę äūŧő řęƒřęşĥ ľįşŧ. Ůşę ŧĥę ƒőřmäŧ '5ş' ƒőř şęčőʼnđş, '1m' ƒőř mįʼnūŧęş, '1ĥ' ƒőř ĥőūřş, äʼnđ '1đ' ƒőř đäyş (ę.ģ.: '5ş,10ş,30ş,1m,5m,15m,30m,1ĥ,2ĥ,1đ').", "auto-refresh-label": "Åūŧő řęƒřęşĥ", @@ -405,6 +431,13 @@ "title": "Vęřşįőʼnş" } }, + "data-source-list": { + "empty-state": { + "button-title": "Åđđ đäŧä şőūřčę", + "pro-tip": "Ÿőū čäʼn äľşő đęƒįʼnę đäŧä şőūřčęş ŧĥřőūģĥ čőʼnƒįģūřäŧįőʼn ƒįľęş. <2>Ŀęäřʼn mőřę", + "title": "Ńő đäŧä şőūřčęş đęƒįʼnęđ" + } + }, "data-source-picker": { "add-new-data-source": "Cőʼnƒįģūřę ä ʼnęŵ đäŧä şőūřčę", "built-in-list": { @@ -439,6 +472,7 @@ "newest-first": "Ńęŵęşŧ ƒįřşŧ", "oldest-first": "Øľđęşŧ ƒįřşŧ", "query-history": "Qūęřy ĥįşŧőřy", + "query-library": "Qūęřy ľįþřäřy", "settings": "Ŝęŧŧįʼnģş", "starred": "Ŝŧäřřęđ" }, @@ -550,11 +584,11 @@ "toolbar": { "aria-label": "Ēχpľőřę ŧőőľþäř", "copy-link": "Cőpy ŮŖĿ", - "copy-link-abs-time": "Cőpy Åþşőľūŧę Ŝĥőřŧęʼnęđ ŮŖĿ", + "copy-link-abs-time": "Cőpy äþşőľūŧę ŮŖĿ", "copy-links-absolute-category": "Ŧįmę-şyʼnč ŮŖĿ ľįʼnĸş (şĥäřę ŵįŧĥ ŧįmę řäʼnģę įʼnŧäčŧ)", "copy-links-normal-category": "Ńőřmäľ ŮŖĿ ľįʼnĸş", "copy-shortened-link": "Cőpy şĥőřŧęʼnęđ ŮŖĿ", - "copy-shortened-link-abs-time": "Cőpy Åþşőľūŧę Ŝĥőřŧęʼnęđ ŮŖĿ", + "copy-shortened-link-abs-time": "Cőpy äþşőľūŧę şĥőřŧęʼnęđ ŮŖĿ", "copy-shortened-link-menu": "Øpęʼn čőpy ľįʼnĸ őpŧįőʼnş", "refresh-picker-cancel": "Cäʼnčęľ", "refresh-picker-run": "Ŗūʼn qūęřy", @@ -657,6 +691,10 @@ }, "add-widget": { "title": "Åđđ päʼnęľ ƒřőm päʼnęľ ľįþřäřy" + }, + "empty-state": { + "message": "Ÿőū ĥävęʼn'ŧ čřęäŧęđ äʼny ľįþřäřy päʼnęľş yęŧ", + "more-info": "Cřęäŧę ä ľįþřäřy päʼnęľ ƒřőm äʼny ęχįşŧįʼnģ đäşĥþőäřđ päʼnęľ ŧĥřőūģĥ ŧĥę päʼnęľ čőʼnŧęχŧ męʼnū. <2>Ŀęäřʼn mőřę" } }, "library-panels": { @@ -1230,9 +1268,8 @@ "confirm-text": "Đęľęŧę" }, "empty": { - "button": "Cřęäŧę Pľäyľįşŧ", - "pro-tip": "Ÿőū čäʼn ūşę pľäyľįşŧş ŧő čyčľę đäşĥþőäřđş őʼn ŦVş ŵįŧĥőūŧ ūşęř čőʼnŧřőľ", - "pro-tip-link-title": "Ŀęäřʼn mőřę", + "button": "Cřęäŧę pľäyľįşŧ", + "pro-tip": "Ÿőū čäʼn ūşę pľäyľįşŧş ŧő čyčľę đäşĥþőäřđş őʼn ŦVş ŵįŧĥőūŧ ūşęř čőʼnŧřőľ. <2>Ŀęäřʼn mőřę", "title": "Ŧĥęřę äřę ʼnő pľäyľįşŧş čřęäŧęđ yęŧ" } }, @@ -1346,6 +1383,10 @@ "orphaned-title": "<0>Øřpĥäʼnęđ pūþľįč đäşĥþőäřđ", "orphaned-tooltip": "Ŧĥę ľįʼnĸęđ đäşĥþőäřđ ĥäş äľřęäđy þęęʼn đęľęŧęđ" }, + "empty-state": { + "message": "Ÿőū ĥävęʼn'ŧ čřęäŧęđ äʼny pūþľįč đäşĥþőäřđş yęŧ", + "more-info": "Cřęäŧę ä pūþľįč đäşĥþőäřđ ƒřőm äʼny ęχįşŧįʼnģ đäşĥþőäřđ ŧĥřőūģĥ ŧĥę <1>Ŝĥäřę mőđäľ. <4>Ŀęäřʼn mőřę" + }, "toggle": { "pause-sharing-toggle-text": "Päūşę şĥäřįʼnģ" } @@ -1450,7 +1491,10 @@ }, "service-accounts": { "empty-state": { - "message": "Ńő şęřvįčęş äččőūʼnŧş ƒőūʼnđ" + "button-title": "Åđđ şęřvįčę äččőūʼnŧ", + "message": "Ńő şęřvįčęş äččőūʼnŧş ƒőūʼnđ", + "more-info": "Ŗęmęmþęř, yőū čäʼn přővįđę şpęčįƒįč pęřmįşşįőʼnş ƒőř ÅPĨ äččęşş ŧő őŧĥęř äppľįčäŧįőʼnş", + "title": "Ÿőū ĥävęʼn'ŧ čřęäŧęđ äʼny şęřvįčę äččőūʼnŧş yęŧ" } }, "share-modal": { @@ -1481,7 +1525,7 @@ "info-text": "Cřęäŧę ä đįřęčŧ ľįʼnĸ ŧő ŧĥįş đäşĥþőäřđ őř päʼnęľ, čūşŧőmįžęđ ŵįŧĥ ŧĥę őpŧįőʼnş þęľőŵ.", "link-url": "Ŀįʼnĸ ŮŖĿ", "render-alert": "Ĩmäģę řęʼnđęřęř pľūģįʼn ʼnőŧ įʼnşŧäľľęđ", - "render-instructions": "Ŧő řęʼnđęř ä päʼnęľ įmäģę, yőū mūşŧ įʼnşŧäľľ ŧĥę<1>Ğřäƒäʼnä įmäģę řęʼnđęřęř pľūģįʼn. Pľęäşę čőʼnŧäčŧ yőūř Ğřäƒäʼnä äđmįʼnįşŧřäŧőř ŧő įʼnşŧäľľ ŧĥę pľūģįʼn.", + "render-instructions": "Ŧő řęʼnđęř ä päʼnęľ įmäģę, yőū mūşŧ įʼnşŧäľľ ŧĥę <2>Ğřäƒäʼnä įmäģę řęʼnđęřęř pľūģįʼn. Pľęäşę čőʼnŧäčŧ yőūř Ğřäƒäʼnä äđmįʼnįşŧřäŧőř ŧő įʼnşŧäľľ ŧĥę pľūģįʼn.", "rendered-image": "Đįřęčŧ ľįʼnĸ řęʼnđęřęđ įmäģę", "save-alert": "Đäşĥþőäřđ įş ʼnőŧ şävęđ", "save-dashboard": "Ŧő řęʼnđęř ä päʼnęľ įmäģę, yőū mūşŧ şävę ŧĥę đäşĥþőäřđ ƒįřşŧ.", @@ -1570,7 +1614,17 @@ }, "title": "Přęƒęřęʼnčęş" }, + "silences": { + "empty-state": { + "button-title": "Cřęäŧę şįľęʼnčę", + "title": "Ÿőū ĥävęʼn'ŧ čřęäŧęđ äʼny şįľęʼnčęş yęŧ" + } + }, "snapshot": { + "empty-state": { + "message": "Ÿőū ĥävęʼn'ŧ čřęäŧęđ äʼny şʼnäpşĥőŧş yęŧ", + "more-info": "Ÿőū čäʼn čřęäŧę ä şʼnäpşĥőŧ őƒ äʼny đäşĥþőäřđ ŧĥřőūģĥ ŧĥę <1>Ŝĥäřę mőđäľ. <4>Ŀęäřʼn mőřę" + }, "external-badge": "Ēχŧęřʼnäľ", "name-column-header": "Ńämę", "url-column-header": "Ŝʼnäpşĥőŧ ūřľ", @@ -1583,7 +1637,10 @@ }, "teams": { "empty-state": { - "message": "Ńő ŧęämş ƒőūʼnđ" + "button-title": "Ńęŵ ŧęäm", + "message": "Ńő ŧęämş ƒőūʼnđ", + "pro-tip": "Åşşįģʼn ƒőľđęř äʼnđ đäşĥþőäřđ pęřmįşşįőʼnş ŧő ŧęämş įʼnşŧęäđ őƒ ūşęřş ŧő ęäşę äđmįʼnįşŧřäŧįőʼn. <2>Ŀęäřʼn mőřę", + "title": "Ÿőū ĥävęʼn'ŧ čřęäŧęđ äʼny ŧęämş yęŧ" } }, "time-picker": { @@ -1705,5 +1762,13 @@ "textbox": { "placeholder": "Ēʼnŧęř väřįäþľę väľūę" } + }, + "variables": { + "empty-state": { + "button-title": "Åđđ väřįäþľę", + "info-box-content": "Väřįäþľęş ęʼnäþľę mőřę įʼnŧęřäčŧįvę äʼnđ đyʼnämįč đäşĥþőäřđş. Ĩʼnşŧęäđ őƒ ĥäřđ-čőđįʼnģ ŧĥįʼnģş ľįĸę şęřvęř őř şęʼnşőř ʼnämęş įʼn yőūř męŧřįč qūęřįęş yőū čäʼn ūşę väřįäþľęş įʼn ŧĥęįř pľäčę. Väřįäþľęş äřę şĥőŵʼn äş ľįşŧ þőχęş äŧ ŧĥę ŧőp őƒ ŧĥę đäşĥþőäřđ. Ŧĥęşę đřőp-đőŵʼn ľįşŧş mäĸę įŧ ęäşy ŧő čĥäʼnģę ŧĥę đäŧä þęįʼnģ đįşpľäyęđ įʼn yőūř đäşĥþőäřđ.", + "info-box-content-2": "Cĥęčĸ őūŧ ŧĥę <2>Ŧęmpľäŧęş äʼnđ väřįäþľęş đőčūmęʼnŧäŧįőʼn ƒőř mőřę įʼnƒőřmäŧįőʼn.", + "title": "Ŧĥęřę äřę ʼnő väřįäþľęş äđđęđ yęŧ" + } } } diff --git a/public/locales/pt-BR/grafana.json b/public/locales/pt-BR/grafana.json index 5cc66282b4a..56c9b5ed931 100644 --- a/public/locales/pt-BR/grafana.json +++ b/public/locales/pt-BR/grafana.json @@ -25,6 +25,14 @@ "user": "Usuário" } }, + "annotations": { + "empty-state": { + "button-title": "", + "info-box-content": "", + "info-box-content-2": "", + "title": "" + } + }, "api-keys": { "empty-state": { "message": "" @@ -72,6 +80,12 @@ "select-checkbox": "Selecionar", "tags-column": "Tags" }, + "empty-state": { + "button-title": "", + "pro-tip": "", + "title": "", + "title-folder": "" + }, "folder-actions-button": { "delete": "Excluir", "folder-actions": "Ações da pasta", @@ -159,6 +173,11 @@ "sub-text": "<0>Defina o texto que descreverá a correlação.", "title": "Definir rótulo de correlação (Passo 1 de 3)" }, + "empty-state": { + "button-title": "", + "pro-tip": "", + "title": "" + }, "list": { "delete": "excluir correlação", "label": "Etiqueta", @@ -355,11 +374,18 @@ "validation-required": "Precisa de um modelo JSON de painel de controle" } }, + "dashboard-links": { + "empty-state": { + "button-title": "", + "info-box-content": "", + "title": "" + } + }, "dashboard-settings": { "annotations": { "title": "Anotações" }, - "dashboard-delete-button": "Excluir painel de controle", + "dashboard-delete-button": "", "general": { "auto-refresh-description": "", "auto-refresh-label": "Atualização automática", @@ -405,6 +431,13 @@ "title": "Versões" } }, + "data-source-list": { + "empty-state": { + "button-title": "", + "pro-tip": "", + "title": "" + } + }, "data-source-picker": { "add-new-data-source": "Configurar uma nova fonte de dados", "built-in-list": { @@ -439,6 +472,7 @@ "newest-first": "Mais recentes primeiro", "oldest-first": "Mais antigos primeiro", "query-history": "Histórico de consultas", + "query-library": "", "settings": "Configurações", "starred": "Favorito" }, @@ -657,6 +691,10 @@ }, "add-widget": { "title": "Adicionar painel a partir da biblioteca de painéis" + }, + "empty-state": { + "message": "", + "more-info": "" } }, "library-panels": { @@ -1230,9 +1268,8 @@ "confirm-text": "Excluir" }, "empty": { - "button": "Criar lista de reprodução", - "pro-tip": "Você pode usar listas de reprodução para alternar entre painéis de controle na TVs sem controle de usuário", - "pro-tip-link-title": "Saiba mais", + "button": "", + "pro-tip": "", "title": "Não há listas de reprodução criadas ainda" } }, @@ -1346,6 +1383,10 @@ "orphaned-title": "<0>Painel de controle público órfão", "orphaned-tooltip": "O painel de controle vinculado já foi excluído" }, + "empty-state": { + "message": "", + "more-info": "" + }, "toggle": { "pause-sharing-toggle-text": "Pausar compartilhamento" } @@ -1450,7 +1491,10 @@ }, "service-accounts": { "empty-state": { - "message": "" + "button-title": "", + "message": "", + "more-info": "", + "title": "" } }, "share-modal": { @@ -1570,7 +1614,17 @@ }, "title": "Preferências" }, + "silences": { + "empty-state": { + "button-title": "", + "title": "" + } + }, "snapshot": { + "empty-state": { + "message": "", + "more-info": "" + }, "external-badge": "Externo", "name-column-header": "Nome", "url-column-header": "URL da captura", @@ -1583,7 +1637,10 @@ }, "teams": { "empty-state": { - "message": "" + "button-title": "", + "message": "", + "pro-tip": "", + "title": "" } }, "time-picker": { @@ -1705,5 +1762,13 @@ "textbox": { "placeholder": "Inserir valor da variável" } + }, + "variables": { + "empty-state": { + "button-title": "", + "info-box-content": "", + "info-box-content-2": "", + "title": "" + } } } \ No newline at end of file diff --git a/public/locales/zh-Hans/grafana.json b/public/locales/zh-Hans/grafana.json index 16453d4cefa..32ff244a529 100644 --- a/public/locales/zh-Hans/grafana.json +++ b/public/locales/zh-Hans/grafana.json @@ -25,6 +25,14 @@ "user": "用户" } }, + "annotations": { + "empty-state": { + "button-title": "", + "info-box-content": "", + "info-box-content-2": "", + "title": "" + } + }, "api-keys": { "empty-state": { "message": "" @@ -67,6 +75,12 @@ "select-checkbox": "选择", "tags-column": "标签" }, + "empty-state": { + "button-title": "", + "pro-tip": "", + "title": "", + "title-folder": "" + }, "folder-actions-button": { "delete": "删除", "folder-actions": "文件夹操作", @@ -154,6 +168,11 @@ "sub-text": "<0>定义将会描述关联的文本。", "title": "定义关联标签(第 1 步,共 3 步)" }, + "empty-state": { + "button-title": "", + "pro-tip": "", + "title": "" + }, "list": { "delete": "删除关联", "label": "标签", @@ -350,11 +369,18 @@ "validation-required": "需要一个仪表板 JSON 模型" } }, + "dashboard-links": { + "empty-state": { + "button-title": "", + "info-box-content": "", + "title": "" + } + }, "dashboard-settings": { "annotations": { "title": "注释" }, - "dashboard-delete-button": "删除仪表板", + "dashboard-delete-button": "", "general": { "auto-refresh-description": "", "auto-refresh-label": "自动刷新", @@ -400,6 +426,13 @@ "title": "版本" } }, + "data-source-list": { + "empty-state": { + "button-title": "", + "pro-tip": "", + "title": "" + } + }, "data-source-picker": { "add-new-data-source": "配置新数据源", "built-in-list": { @@ -434,6 +467,7 @@ "newest-first": "最新在前", "oldest-first": "最早在前", "query-history": "查询历史记录", + "query-library": "", "settings": "设置", "starred": "已加星标" }, @@ -652,6 +686,10 @@ }, "add-widget": { "title": "从面板库中添加面板" + }, + "empty-state": { + "message": "", + "more-info": "" } }, "library-panels": { @@ -1224,9 +1262,8 @@ "confirm-text": "删除" }, "empty": { - "button": "创建播放列表", - "pro-tip": "您可以使用播放列表来循环电视上的仪表板而不受用户控制", - "pro-tip-link-title": "了解更多", + "button": "", + "pro-tip": "", "title": "尚未创建播放列表" } }, @@ -1340,6 +1377,10 @@ "orphaned-title": "<0>孤立的公共仪表板", "orphaned-tooltip": "链接的仪表板已被删除" }, + "empty-state": { + "message": "", + "more-info": "" + }, "toggle": { "pause-sharing-toggle-text": "暂停共享" } @@ -1444,7 +1485,10 @@ }, "service-accounts": { "empty-state": { - "message": "" + "button-title": "", + "message": "", + "more-info": "", + "title": "" } }, "share-modal": { @@ -1564,7 +1608,17 @@ }, "title": "首选项" }, + "silences": { + "empty-state": { + "button-title": "", + "title": "" + } + }, "snapshot": { + "empty-state": { + "message": "", + "more-info": "" + }, "external-badge": "外部", "name-column-header": "名称", "url-column-header": "快照网址", @@ -1577,7 +1631,10 @@ }, "teams": { "empty-state": { - "message": "" + "button-title": "", + "message": "", + "pro-tip": "", + "title": "" } }, "time-picker": { @@ -1699,5 +1756,13 @@ "textbox": { "placeholder": "输入变量值" } + }, + "variables": { + "empty-state": { + "button-title": "", + "info-box-content": "", + "info-box-content-2": "", + "title": "" + } } } \ No newline at end of file diff --git a/public/openapi3.json b/public/openapi3.json index 0b10f5b2011..50c16267a07 100644 --- a/public/openapi3.json +++ b/public/openapi3.json @@ -1179,7 +1179,7 @@ "type": "string" }, "settings": { - "additionalProperties": false, + "additionalProperties": {}, "type": "object" }, "source": { @@ -1459,7 +1459,7 @@ "type": "string" }, "settings": { - "additionalProperties": false, + "additionalProperties": {}, "type": "object" }, "source": { @@ -2482,7 +2482,7 @@ "type": "string" }, "model": { - "additionalProperties": false, + "additionalProperties": {}, "type": "object" }, "queryType": { @@ -3253,6 +3253,123 @@ "title": "BasicAuth contains basic HTTP authentication credentials.", "type": "object" }, + "CacheConfig": { + "description": "Config defines the internal representation of a cache configuration, including fields not set by the API caller", + "properties": { + "created": { + "format": "date-time", + "type": "string" + }, + "dataSourceID": { + "description": "Fields that can be set by the API caller - read/write", + "format": "int64", + "type": "integer" + }, + "dataSourceUID": { + "type": "string" + }, + "defaultTTLMs": { + "description": "These are returned by the HTTP API, but are managed internally - read-only\nNote: 'created' and 'updated' are special properties managed automatically by xorm, but we are setting them manually", + "format": "int64", + "type": "integer" + }, + "enabled": { + "type": "boolean" + }, + "ttlQueriesMs": { + "description": "TTL MS, or \"time to live\", is how long a cached item will stay in the cache before it is removed (in milliseconds)", + "format": "int64", + "type": "integer" + }, + "ttlResourcesMs": { + "format": "int64", + "type": "integer" + }, + "updated": { + "format": "date-time", + "type": "string" + }, + "useDefaultTTL": { + "description": "If UseDefaultTTL is enabled, then the TTLQueriesMS and TTLResourcesMS in this object is always sent as the default TTL located in grafana.ini", + "type": "boolean" + } + }, + "type": "object" + }, + "CacheConfigResponse": { + "properties": { + "created": { + "format": "date-time", + "type": "string" + }, + "dataSourceID": { + "description": "Fields that can be set by the API caller - read/write", + "format": "int64", + "type": "integer" + }, + "dataSourceUID": { + "type": "string" + }, + "defaultTTLMs": { + "description": "These are returned by the HTTP API, but are managed internally - read-only\nNote: 'created' and 'updated' are special properties managed automatically by xorm, but we are setting them manually", + "format": "int64", + "type": "integer" + }, + "enabled": { + "type": "boolean" + }, + "message": { + "type": "string" + }, + "ttlQueriesMs": { + "description": "TTL MS, or \"time to live\", is how long a cached item will stay in the cache before it is removed (in milliseconds)", + "format": "int64", + "type": "integer" + }, + "ttlResourcesMs": { + "format": "int64", + "type": "integer" + }, + "updated": { + "format": "date-time", + "type": "string" + }, + "useDefaultTTL": { + "description": "If UseDefaultTTL is enabled, then the TTLQueriesMS and TTLResourcesMS in this object is always sent as the default TTL located in grafana.ini", + "type": "boolean" + } + }, + "type": "object" + }, + "CacheConfigSetter": { + "description": "ConfigSetter defines the cache parameters that users can configure per datasource\nThis is only intended to be consumed by the SetCache HTTP Handler", + "properties": { + "dataSourceID": { + "format": "int64", + "type": "integer" + }, + "dataSourceUID": { + "type": "string" + }, + "enabled": { + "type": "boolean" + }, + "ttlQueriesMs": { + "description": "TTL MS, or \"time to live\", is how long a cached item will stay in the cache before it is removed (in milliseconds)", + "format": "int64", + "type": "integer" + }, + "ttlResourcesMs": { + "format": "int64", + "type": "integer" + }, + "useDefaultTTL": { + "description": "If UseDefaultTTL is enabled, then the TTLQueriesMS and TTLResourcesMS in this object is always sent as the default TTL located in grafana.ini", + "type": "boolean" + } + }, + "type": "object" + }, "CalculateDiffTarget": { "properties": { "dashboardId": { @@ -3415,7 +3532,15 @@ }, "type": "array" }, + "Policies": { + "description": "Policies contains all policy identifiers included in the certificate.\nIn Go 1.22, encoding/gob cannot handle and ignores this field.", + "items": { + "$ref": "#/components/schemas/OID" + }, + "type": "array" + }, "PolicyIdentifiers": { + "description": "PolicyIdentifiers contains asn1.ObjectIdentifiers, the components\nof which are limited to int32. If a certificate contains a policy which\ncannot be represented by asn1.ObjectIdentifier, it will not be included in\nPolicyIdentifiers, but will be present in Policies, which contains all parsed\npolicy OIDs.", "items": { "$ref": "#/components/schemas/ObjectIdentifier" }, @@ -3640,15 +3765,15 @@ "CookiePreferences": { "properties": { "analytics": { - "additionalProperties": false, + "additionalProperties": {}, "type": "object" }, "functional": { - "additionalProperties": false, + "additionalProperties": {}, "type": "object" }, "performance": { - "additionalProperties": false, + "additionalProperties": {}, "type": "object" } }, @@ -3710,7 +3835,7 @@ "type": "string" }, "target": { - "additionalProperties": false, + "additionalProperties": {}, "description": "Target data query", "example": { "prop1": "value1", @@ -3743,7 +3868,7 @@ "type": "string" }, "target": { - "additionalProperties": false, + "additionalProperties": {}, "description": "Target data query", "example": { "prop1": "value1", @@ -5043,12 +5168,12 @@ "FieldConfig": { "properties": { "color": { - "additionalProperties": false, + "additionalProperties": {}, "description": "Map values to a display color\nNOTE: this interface is under development in the frontend... so simple map for now", "type": "object" }, "custom": { - "additionalProperties": false, + "additionalProperties": {}, "description": "Panel Specific Values", "type": "object" }, @@ -6085,7 +6210,7 @@ "type": "object" }, "IPMask": { - "description": "See type IPNet and func ParseCIDR for details.", + "description": "See type [IPNet] and func [ParseCIDR] for details.", "items": { "format": "uint8", "type": "integer" @@ -6808,7 +6933,7 @@ "type": "array" }, "Name": { - "description": "Name represents an X.509 distinguished name. This only includes the common\nelements of a DN. Note that Name is only an approximation of the X.509\nstructure. If an accurate representation is needed, asn1.Unmarshal the raw\nsubject or issuer as an RDNSequence.", + "description": "Name represents an X.509 distinguished name. This only includes the common\nelements of a DN. Note that Name is only an approximation of the X.509\nstructure. If an accurate representation is needed, asn1.Unmarshal the raw\nsubject or issuer as an [RDNSequence].", "properties": { "Country": { "items": { @@ -7046,6 +7171,10 @@ "title": "OAuth2 is the oauth2 client configuration.", "type": "object" }, + "OID": { + "title": "An OID represents an ASN.1 OBJECT IDENTIFIER.", + "type": "object" + }, "ObjectIdentifier": { "items": { "format": "int64", @@ -8334,7 +8463,7 @@ "description": "PublicError is derived from Error and only contains information\navailable to the end user.", "properties": { "extra": { - "additionalProperties": false, + "additionalProperties": {}, "type": "object" }, "message": { @@ -8515,12 +8644,12 @@ "description": "The embedded FieldConfig's display name must be set.\nIt corresponds to the QueryResultMetaStat on the frontend (https://github.com/grafana/grafana/blob/master/packages/grafana-data/src/types/data.ts#L53).", "properties": { "color": { - "additionalProperties": false, + "additionalProperties": {}, "description": "Map values to a display color\nNOTE: this interface is under development in the frontend... so simple map for now", "type": "object" }, "custom": { - "additionalProperties": false, + "additionalProperties": {}, "description": "Panel Specific Values", "type": "object" }, @@ -8758,7 +8887,7 @@ }, "queries": { "items": { - "additionalProperties": false, + "additionalProperties": {}, "type": "object" }, "type": "array" @@ -10839,7 +10968,7 @@ "type": "object" }, "URL": { - "description": "The general form represented is:\n\n[scheme:][//[userinfo@]host][/]path[?query][#fragment]\n\nURLs that do not start with a slash after the scheme are interpreted as:\n\nscheme:opaque[?query][#fragment]\n\nNote that the Path field is stored in decoded form: /%47%6f%2f becomes /Go/.\nA consequence is that it is impossible to tell which slashes in the Path were\nslashes in the raw URL and which were %2f. This distinction is rarely important,\nbut when it is, the code should use the EscapedPath method, which preserves\nthe original encoding of Path.\n\nThe RawPath field is an optional field which is only set when the default\nencoding of Path is different from the escaped path. See the EscapedPath method\nfor more details.\n\nURL's String method uses the EscapedPath method to obtain the path.", + "description": "The general form represented is:\n\n[scheme:][//[userinfo@]host][/]path[?query][#fragment]\n\nURLs that do not start with a slash after the scheme are interpreted as:\n\nscheme:opaque[?query][#fragment]\n\nThe Host field contains the host and port subcomponents of the URL.\nWhen the port is present, it is separated from the host with a colon.\nWhen the host is an IPv6 address, it must be enclosed in square brackets:\n\"[fe80::1]:80\". The [net.JoinHostPort] function combines a host and port\ninto a string suitable for the Host field, adding square brackets to\nthe host when necessary.\n\nNote that the Path field is stored in decoded form: /%47%6f%2f becomes /Go/.\nA consequence is that it is impossible to tell which slashes in the Path were\nslashes in the raw URL and which were %2f. This distinction is rarely important,\nbut when it is, the code should use the [URL.EscapedPath] method, which preserves\nthe original encoding of Path.\n\nThe RawPath field is an optional field which is only set when the default\nencoding of Path is different from the escaped path. See the EscapedPath method\nfor more details.\n\nURL's String method uses the EscapedPath method to obtain the path.", "properties": { "ForceQuery": { "type": "boolean" @@ -10882,7 +11011,7 @@ "description": "Unstructured allows objects that do not have Golang structs registered to be manipulated\ngenerically.", "properties": { "Object": { - "additionalProperties": false, + "additionalProperties": {}, "description": "Object is a JSON compatible map with string, float, int, bool, []interface{},\nor map[string]interface{} children.", "type": "object" } @@ -11477,7 +11606,7 @@ "type": "object" }, "Userinfo": { - "description": "The Userinfo type is an immutable encapsulation of username and\npassword details for a URL. An existing Userinfo value is guaranteed\nto have a username set (potentially empty, as allowed by RFC 2396),\nand optionally a password.", + "description": "The Userinfo type is an immutable encapsulation of username and\npassword details for a [URL]. An existing Userinfo value is guaranteed\nto have a username set (potentially empty, as allowed by RFC 2396),\nand optionally a password.", "type": "object" }, "ValidationError": { @@ -12112,7 +12241,7 @@ "description": "PublicError is derived from Error and only contains information\navailable to the end user.", "properties": { "extra": { - "additionalProperties": false, + "additionalProperties": {}, "description": "Extra Additional information about the error", "type": "object" }, @@ -16956,6 +17085,188 @@ ] } }, + "/datasources/{dataSourceUID}/cache": { + "get": { + "description": "get cache config for a single data source", + "operationId": "getDataSourceCacheConfig", + "parameters": [ + { + "in": "path", + "name": "dataSourceUID", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CacheConfigResponse" + } + } + }, + "description": "CacheConfigResponse" + }, + "500": { + "$ref": "#/components/responses/internalServerError" + } + }, + "tags": [ + "enterprise" + ] + }, + "post": { + "description": "set cache config for a single data source", + "operationId": "setDataSourceCacheConfig", + "parameters": [ + { + "in": "path", + "name": "dataSourceUID", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CacheConfigSetter" + } + } + }, + "required": true, + "x-originalParamName": "body" + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CacheConfigResponse" + } + } + }, + "description": "CacheConfigResponse" + }, + "400": { + "$ref": "#/components/responses/badRequestError" + }, + "500": { + "$ref": "#/components/responses/internalServerError" + } + }, + "tags": [ + "enterprise" + ] + } + }, + "/datasources/{dataSourceUID}/cache/clean": { + "post": { + "description": "clean cache for a single data source", + "operationId": "cleanDataSourceCache", + "parameters": [ + { + "in": "path", + "name": "dataSourceUID", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CacheConfigResponse" + } + } + }, + "description": "CacheConfigResponse" + }, + "500": { + "$ref": "#/components/responses/internalServerError" + } + }, + "tags": [ + "enterprise" + ] + } + }, + "/datasources/{dataSourceUID}/cache/disable": { + "post": { + "description": "disable cache for a single data source", + "operationId": "disableDataSourceCache", + "parameters": [ + { + "in": "path", + "name": "dataSourceUID", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CacheConfigResponse" + } + } + }, + "description": "CacheConfigResponse" + }, + "500": { + "$ref": "#/components/responses/internalServerError" + } + }, + "tags": [ + "enterprise" + ] + } + }, + "/datasources/{dataSourceUID}/cache/enable": { + "post": { + "description": "enable cache for a single data source", + "operationId": "enableDataSourceCache", + "parameters": [ + { + "in": "path", + "name": "dataSourceUID", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CacheConfigResponse" + } + } + }, + "description": "CacheConfigResponse" + }, + "500": { + "$ref": "#/components/responses/internalServerError" + } + }, + "tags": [ + "enterprise" + ] + } + }, "/datasources/{id}": { "delete": { "deprecated": true, @@ -24661,7 +24972,7 @@ "type": "string" }, "settings": { - "additionalProperties": false, + "additionalProperties": {}, "type": "object" } }, diff --git a/public/test/test-utils.tsx b/public/test/test-utils.tsx index b27a0666b54..ee6d2b6c2e7 100644 --- a/public/test/test-utils.tsx +++ b/public/test/test-utils.tsx @@ -1,12 +1,14 @@ import { ToolkitStore } from '@reduxjs/toolkit/dist/configureStore'; import { render, RenderOptions } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; -import React, { ComponentProps, Fragment, PropsWithChildren } from 'react'; +import { createMemoryHistory, MemoryHistoryBuildOptions } from 'history'; +import React, { Fragment, PropsWithChildren } from 'react'; import { Provider } from 'react-redux'; -import { MemoryRouter } from 'react-router-dom'; +import { Router } from 'react-router-dom'; import { PreloadedState } from 'redux'; import { getGrafanaContextMock } from 'test/mocks/getGrafanaContextMock'; +import { HistoryWrapper, setLocationService } from '@grafana/runtime'; import { GrafanaContext, GrafanaContextType } from 'app/core/context/GrafanaContext'; import { ModalsContextProvider } from 'app/core/context/ModalsContextProvider'; import { configureStore } from 'app/store/configureStore'; @@ -29,9 +31,9 @@ interface ExtendedRenderOptions extends RenderOptions { */ renderWithRouter?: boolean; /** - * Props to pass to `MemoryRouter`, if being used + * Props to pass to `createMemoryHistory`, if being used */ - routerOptions?: ComponentProps; + historyOptions?: MemoryHistoryBuildOptions; } /** @@ -41,7 +43,7 @@ interface ExtendedRenderOptions extends RenderOptions { const getWrapper = ({ store, renderWithRouter, - routerOptions, + historyOptions, grafanaContext, }: ExtendedRenderOptions & { grafanaContext?: GrafanaContextType; @@ -50,7 +52,13 @@ const getWrapper = ({ /** * Conditional router - either a MemoryRouter or just a Fragment */ - const PotentialRouter = renderWithRouter ? MemoryRouter : Fragment; + const PotentialRouter = renderWithRouter ? Router : Fragment; + + // Create a fresh location service for each test - otherwise we run the risk + // of it being stateful in between runs + const history = createMemoryHistory(historyOptions); + const locationService = new HistoryWrapper(history); + setLocationService(locationService); const context = { ...getGrafanaContextMock(), @@ -65,7 +73,7 @@ const getWrapper = ({ return ( - + {children} diff --git a/scripts/drone/variables.star b/scripts/drone/variables.star index 1024b73e5c5..07e2768f0c7 100644 --- a/scripts/drone/variables.star +++ b/scripts/drone/variables.star @@ -3,7 +3,7 @@ global variables """ grabpl_version = "v3.0.50" -golang_version = "1.21.9" +golang_version = "1.22.2" # nodejs_version should match what's in ".nvmrc", but without the v prefix. nodejs_version = "20.9.0" diff --git a/yarn.lock b/yarn.lock index 183976ad125..78eedb63a26 100644 --- a/yarn.lock +++ b/yarn.lock @@ -92,36 +92,13 @@ __metadata: languageName: node linkType: hard -"@babel/compat-data@npm:^7.22.6, @babel/compat-data@npm:^7.23.2, @babel/compat-data@npm:^7.23.5, @babel/compat-data@npm:^7.24.4": +"@babel/compat-data@npm:^7.22.6, @babel/compat-data@npm:^7.23.5, @babel/compat-data@npm:^7.24.4": version: 7.24.4 resolution: "@babel/compat-data@npm:7.24.4" checksum: 10/e51faec0ac8259f03cc5029d2b4a944b4fee44cb5188c11530769d5beb81f384d031dba951febc3e33dbb48ceb8045b1184f5c1ac4c5f86ab1f5e951e9aaf7af languageName: node linkType: hard -"@babel/core@npm:7.23.2": - version: 7.23.2 - resolution: "@babel/core@npm:7.23.2" - dependencies: - "@ampproject/remapping": "npm:^2.2.0" - "@babel/code-frame": "npm:^7.22.13" - "@babel/generator": "npm:^7.23.0" - "@babel/helper-compilation-targets": "npm:^7.22.15" - "@babel/helper-module-transforms": "npm:^7.23.0" - "@babel/helpers": "npm:^7.23.2" - "@babel/parser": "npm:^7.23.0" - "@babel/template": "npm:^7.22.15" - "@babel/traverse": "npm:^7.23.2" - "@babel/types": "npm:^7.23.0" - convert-source-map: "npm:^2.0.0" - debug: "npm:^4.1.0" - gensync: "npm:^1.0.0-beta.2" - json5: "npm:^2.2.3" - semver: "npm:^6.3.1" - checksum: 10/b69d7008695b2ac7a3a2db83c5c712fbb79f7031c4480f6351cde327930e38873003d1d021059b729a1d0cb48093f1d384c64269b78f6189f50051fe4f64dc2d - languageName: node - linkType: hard - "@babel/core@npm:7.24.4, @babel/core@npm:^7.11.6, @babel/core@npm:^7.12.3, @babel/core@npm:^7.13.16, @babel/core@npm:^7.21.3, @babel/core@npm:^7.22.9, @babel/core@npm:^7.7.5": version: 7.24.4 resolution: "@babel/core@npm:7.24.4" @@ -145,7 +122,7 @@ __metadata: languageName: node linkType: hard -"@babel/generator@npm:^7.12.11, @babel/generator@npm:^7.22.9, @babel/generator@npm:^7.23.0, @babel/generator@npm:^7.24.1, @babel/generator@npm:^7.24.4, @babel/generator@npm:^7.7.2": +"@babel/generator@npm:^7.12.11, @babel/generator@npm:^7.22.9, @babel/generator@npm:^7.24.1, @babel/generator@npm:^7.24.4, @babel/generator@npm:^7.7.2": version: 7.24.4 resolution: "@babel/generator@npm:7.24.4" dependencies: @@ -175,7 +152,7 @@ __metadata: languageName: node linkType: hard -"@babel/helper-compilation-targets@npm:^7.22.15, @babel/helper-compilation-targets@npm:^7.22.6, @babel/helper-compilation-targets@npm:^7.23.6": +"@babel/helper-compilation-targets@npm:^7.22.6, @babel/helper-compilation-targets@npm:^7.23.6": version: 7.23.6 resolution: "@babel/helper-compilation-targets@npm:7.23.6" dependencies: @@ -220,36 +197,6 @@ __metadata: languageName: node linkType: hard -"@babel/helper-define-polyfill-provider@npm:^0.4.4": - version: 0.4.4 - resolution: "@babel/helper-define-polyfill-provider@npm:0.4.4" - dependencies: - "@babel/helper-compilation-targets": "npm:^7.22.6" - "@babel/helper-plugin-utils": "npm:^7.22.5" - debug: "npm:^4.1.1" - lodash.debounce: "npm:^4.0.8" - resolve: "npm:^1.14.2" - peerDependencies: - "@babel/core": ^7.4.0 || ^8.0.0-0 <8.0.0 - checksum: 10/16c312e40ecf2ead81f3ab7275387079071012d2363022c04cf16d56fe0d781185f3a517b928f4556c716ae45e0567b817b636d5cd2fee8fb2ce2b18a04c5bcd - languageName: node - linkType: hard - -"@babel/helper-define-polyfill-provider@npm:^0.5.0": - version: 0.5.0 - resolution: "@babel/helper-define-polyfill-provider@npm:0.5.0" - dependencies: - "@babel/helper-compilation-targets": "npm:^7.22.6" - "@babel/helper-plugin-utils": "npm:^7.22.5" - debug: "npm:^4.1.1" - lodash.debounce: "npm:^4.0.8" - resolve: "npm:^1.14.2" - peerDependencies: - "@babel/core": ^7.4.0 || ^8.0.0-0 <8.0.0 - checksum: 10/f849e816ec4b182a3e8fa8e09ff016f88bb95259cd6b2190b815c48f83c3d3b68e973a8ec72acc5086bfe93705cbd46ec089c06476421d858597780e42235a03 - languageName: node - linkType: hard - "@babel/helper-define-polyfill-provider@npm:^0.6.1": version: 0.6.1 resolution: "@babel/helper-define-polyfill-provider@npm:0.6.1" @@ -309,7 +256,7 @@ __metadata: languageName: node linkType: hard -"@babel/helper-module-transforms@npm:^7.23.0, @babel/helper-module-transforms@npm:^7.23.3": +"@babel/helper-module-transforms@npm:^7.23.3": version: 7.23.3 resolution: "@babel/helper-module-transforms@npm:7.23.3" dependencies: @@ -425,7 +372,7 @@ __metadata: languageName: node linkType: hard -"@babel/helpers@npm:^7.23.2, @babel/helpers@npm:^7.24.4": +"@babel/helpers@npm:^7.24.4": version: 7.24.4 resolution: "@babel/helpers@npm:7.24.4" dependencies: @@ -448,7 +395,7 @@ __metadata: languageName: node linkType: hard -"@babel/parser@npm:^7.1.0, @babel/parser@npm:^7.13.16, @babel/parser@npm:^7.14.7, @babel/parser@npm:^7.20.7, @babel/parser@npm:^7.22.7, @babel/parser@npm:^7.23.0, @babel/parser@npm:^7.24.0, @babel/parser@npm:^7.24.1, @babel/parser@npm:^7.24.4": +"@babel/parser@npm:^7.1.0, @babel/parser@npm:^7.13.16, @babel/parser@npm:^7.14.7, @babel/parser@npm:^7.20.7, @babel/parser@npm:^7.22.7, @babel/parser@npm:^7.24.0, @babel/parser@npm:^7.24.1, @babel/parser@npm:^7.24.4": version: 7.24.4 resolution: "@babel/parser@npm:7.24.4" bin: @@ -469,7 +416,7 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@npm:^7.22.15, @babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@npm:^7.24.1": +"@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@npm:^7.24.1": version: 7.24.1 resolution: "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@npm:7.24.1" dependencies: @@ -480,7 +427,7 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@npm:^7.22.15, @babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@npm:^7.24.1": +"@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@npm:^7.24.1": version: 7.24.1 resolution: "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@npm:7.24.1" dependencies: @@ -640,7 +587,7 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-syntax-import-assertions@npm:^7.22.5, @babel/plugin-syntax-import-assertions@npm:^7.24.1": +"@babel/plugin-syntax-import-assertions@npm:^7.24.1": version: 7.24.1 resolution: "@babel/plugin-syntax-import-assertions@npm:7.24.1" dependencies: @@ -651,7 +598,7 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-syntax-import-attributes@npm:^7.22.5, @babel/plugin-syntax-import-attributes@npm:^7.24.1": +"@babel/plugin-syntax-import-attributes@npm:^7.24.1": version: 7.24.1 resolution: "@babel/plugin-syntax-import-attributes@npm:7.24.1" dependencies: @@ -806,7 +753,7 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-arrow-functions@npm:^7.22.5, @babel/plugin-transform-arrow-functions@npm:^7.24.1": +"@babel/plugin-transform-arrow-functions@npm:^7.24.1": version: 7.24.1 resolution: "@babel/plugin-transform-arrow-functions@npm:7.24.1" dependencies: @@ -817,7 +764,7 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-async-generator-functions@npm:^7.23.2, @babel/plugin-transform-async-generator-functions@npm:^7.24.3": +"@babel/plugin-transform-async-generator-functions@npm:^7.24.3": version: 7.24.3 resolution: "@babel/plugin-transform-async-generator-functions@npm:7.24.3" dependencies: @@ -831,7 +778,7 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-async-to-generator@npm:^7.22.5, @babel/plugin-transform-async-to-generator@npm:^7.24.1": +"@babel/plugin-transform-async-to-generator@npm:^7.24.1": version: 7.24.1 resolution: "@babel/plugin-transform-async-to-generator@npm:7.24.1" dependencies: @@ -844,7 +791,7 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-block-scoped-functions@npm:^7.22.5, @babel/plugin-transform-block-scoped-functions@npm:^7.24.1": +"@babel/plugin-transform-block-scoped-functions@npm:^7.24.1": version: 7.24.1 resolution: "@babel/plugin-transform-block-scoped-functions@npm:7.24.1" dependencies: @@ -855,7 +802,7 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-block-scoping@npm:^7.23.0, @babel/plugin-transform-block-scoping@npm:^7.24.4": +"@babel/plugin-transform-block-scoping@npm:^7.24.4": version: 7.24.4 resolution: "@babel/plugin-transform-block-scoping@npm:7.24.4" dependencies: @@ -866,7 +813,7 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-class-properties@npm:^7.22.5, @babel/plugin-transform-class-properties@npm:^7.24.1": +"@babel/plugin-transform-class-properties@npm:^7.24.1": version: 7.24.1 resolution: "@babel/plugin-transform-class-properties@npm:7.24.1" dependencies: @@ -878,7 +825,7 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-class-static-block@npm:^7.22.11, @babel/plugin-transform-class-static-block@npm:^7.24.4": +"@babel/plugin-transform-class-static-block@npm:^7.24.4": version: 7.24.4 resolution: "@babel/plugin-transform-class-static-block@npm:7.24.4" dependencies: @@ -891,7 +838,7 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-classes@npm:^7.22.15, @babel/plugin-transform-classes@npm:^7.24.1": +"@babel/plugin-transform-classes@npm:^7.24.1": version: 7.24.1 resolution: "@babel/plugin-transform-classes@npm:7.24.1" dependencies: @@ -909,7 +856,7 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-computed-properties@npm:^7.22.5, @babel/plugin-transform-computed-properties@npm:^7.24.1": +"@babel/plugin-transform-computed-properties@npm:^7.24.1": version: 7.24.1 resolution: "@babel/plugin-transform-computed-properties@npm:7.24.1" dependencies: @@ -921,7 +868,7 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-destructuring@npm:^7.23.0, @babel/plugin-transform-destructuring@npm:^7.24.1": +"@babel/plugin-transform-destructuring@npm:^7.24.1": version: 7.24.1 resolution: "@babel/plugin-transform-destructuring@npm:7.24.1" dependencies: @@ -932,7 +879,7 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-dotall-regex@npm:^7.22.5, @babel/plugin-transform-dotall-regex@npm:^7.24.1": +"@babel/plugin-transform-dotall-regex@npm:^7.24.1": version: 7.24.1 resolution: "@babel/plugin-transform-dotall-regex@npm:7.24.1" dependencies: @@ -944,7 +891,7 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-duplicate-keys@npm:^7.22.5, @babel/plugin-transform-duplicate-keys@npm:^7.24.1": +"@babel/plugin-transform-duplicate-keys@npm:^7.24.1": version: 7.24.1 resolution: "@babel/plugin-transform-duplicate-keys@npm:7.24.1" dependencies: @@ -955,7 +902,7 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-dynamic-import@npm:^7.22.11, @babel/plugin-transform-dynamic-import@npm:^7.24.1": +"@babel/plugin-transform-dynamic-import@npm:^7.24.1": version: 7.24.1 resolution: "@babel/plugin-transform-dynamic-import@npm:7.24.1" dependencies: @@ -967,7 +914,7 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-exponentiation-operator@npm:^7.22.5, @babel/plugin-transform-exponentiation-operator@npm:^7.24.1": +"@babel/plugin-transform-exponentiation-operator@npm:^7.24.1": version: 7.24.1 resolution: "@babel/plugin-transform-exponentiation-operator@npm:7.24.1" dependencies: @@ -979,7 +926,7 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-export-namespace-from@npm:^7.22.11, @babel/plugin-transform-export-namespace-from@npm:^7.24.1": +"@babel/plugin-transform-export-namespace-from@npm:^7.24.1": version: 7.24.1 resolution: "@babel/plugin-transform-export-namespace-from@npm:7.24.1" dependencies: @@ -1003,7 +950,7 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-for-of@npm:^7.22.15, @babel/plugin-transform-for-of@npm:^7.24.1": +"@babel/plugin-transform-for-of@npm:^7.24.1": version: 7.24.1 resolution: "@babel/plugin-transform-for-of@npm:7.24.1" dependencies: @@ -1015,7 +962,7 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-function-name@npm:^7.22.5, @babel/plugin-transform-function-name@npm:^7.24.1": +"@babel/plugin-transform-function-name@npm:^7.24.1": version: 7.24.1 resolution: "@babel/plugin-transform-function-name@npm:7.24.1" dependencies: @@ -1028,7 +975,7 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-json-strings@npm:^7.22.11, @babel/plugin-transform-json-strings@npm:^7.24.1": +"@babel/plugin-transform-json-strings@npm:^7.24.1": version: 7.24.1 resolution: "@babel/plugin-transform-json-strings@npm:7.24.1" dependencies: @@ -1040,7 +987,7 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-literals@npm:^7.22.5, @babel/plugin-transform-literals@npm:^7.24.1": +"@babel/plugin-transform-literals@npm:^7.24.1": version: 7.24.1 resolution: "@babel/plugin-transform-literals@npm:7.24.1" dependencies: @@ -1051,7 +998,7 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-logical-assignment-operators@npm:^7.22.11, @babel/plugin-transform-logical-assignment-operators@npm:^7.24.1": +"@babel/plugin-transform-logical-assignment-operators@npm:^7.24.1": version: 7.24.1 resolution: "@babel/plugin-transform-logical-assignment-operators@npm:7.24.1" dependencies: @@ -1063,7 +1010,7 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-member-expression-literals@npm:^7.22.5, @babel/plugin-transform-member-expression-literals@npm:^7.24.1": +"@babel/plugin-transform-member-expression-literals@npm:^7.24.1": version: 7.24.1 resolution: "@babel/plugin-transform-member-expression-literals@npm:7.24.1" dependencies: @@ -1074,7 +1021,7 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-modules-amd@npm:^7.23.0, @babel/plugin-transform-modules-amd@npm:^7.24.1": +"@babel/plugin-transform-modules-amd@npm:^7.24.1": version: 7.24.1 resolution: "@babel/plugin-transform-modules-amd@npm:7.24.1" dependencies: @@ -1099,7 +1046,7 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-modules-systemjs@npm:^7.23.0, @babel/plugin-transform-modules-systemjs@npm:^7.24.1": +"@babel/plugin-transform-modules-systemjs@npm:^7.24.1": version: 7.24.1 resolution: "@babel/plugin-transform-modules-systemjs@npm:7.24.1" dependencies: @@ -1113,7 +1060,7 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-modules-umd@npm:^7.22.5, @babel/plugin-transform-modules-umd@npm:^7.24.1": +"@babel/plugin-transform-modules-umd@npm:^7.24.1": version: 7.24.1 resolution: "@babel/plugin-transform-modules-umd@npm:7.24.1" dependencies: @@ -1137,7 +1084,7 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-new-target@npm:^7.22.5, @babel/plugin-transform-new-target@npm:^7.24.1": +"@babel/plugin-transform-new-target@npm:^7.24.1": version: 7.24.1 resolution: "@babel/plugin-transform-new-target@npm:7.24.1" dependencies: @@ -1148,7 +1095,7 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-nullish-coalescing-operator@npm:^7.22.11, @babel/plugin-transform-nullish-coalescing-operator@npm:^7.24.1": +"@babel/plugin-transform-nullish-coalescing-operator@npm:^7.24.1": version: 7.24.1 resolution: "@babel/plugin-transform-nullish-coalescing-operator@npm:7.24.1" dependencies: @@ -1160,7 +1107,7 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-numeric-separator@npm:^7.22.11, @babel/plugin-transform-numeric-separator@npm:^7.24.1": +"@babel/plugin-transform-numeric-separator@npm:^7.24.1": version: 7.24.1 resolution: "@babel/plugin-transform-numeric-separator@npm:7.24.1" dependencies: @@ -1172,7 +1119,7 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-object-rest-spread@npm:^7.22.15, @babel/plugin-transform-object-rest-spread@npm:^7.24.1": +"@babel/plugin-transform-object-rest-spread@npm:^7.24.1": version: 7.24.1 resolution: "@babel/plugin-transform-object-rest-spread@npm:7.24.1" dependencies: @@ -1186,7 +1133,7 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-object-super@npm:^7.22.5, @babel/plugin-transform-object-super@npm:^7.24.1": +"@babel/plugin-transform-object-super@npm:^7.24.1": version: 7.24.1 resolution: "@babel/plugin-transform-object-super@npm:7.24.1" dependencies: @@ -1198,7 +1145,7 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-optional-catch-binding@npm:^7.22.11, @babel/plugin-transform-optional-catch-binding@npm:^7.24.1": +"@babel/plugin-transform-optional-catch-binding@npm:^7.24.1": version: 7.24.1 resolution: "@babel/plugin-transform-optional-catch-binding@npm:7.24.1" dependencies: @@ -1210,7 +1157,7 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-optional-chaining@npm:^7.23.0, @babel/plugin-transform-optional-chaining@npm:^7.24.1": +"@babel/plugin-transform-optional-chaining@npm:^7.24.1": version: 7.24.1 resolution: "@babel/plugin-transform-optional-chaining@npm:7.24.1" dependencies: @@ -1223,7 +1170,7 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-parameters@npm:^7.22.15, @babel/plugin-transform-parameters@npm:^7.24.1": +"@babel/plugin-transform-parameters@npm:^7.24.1": version: 7.24.1 resolution: "@babel/plugin-transform-parameters@npm:7.24.1" dependencies: @@ -1234,7 +1181,7 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-private-methods@npm:^7.22.5, @babel/plugin-transform-private-methods@npm:^7.24.1": +"@babel/plugin-transform-private-methods@npm:^7.24.1": version: 7.24.1 resolution: "@babel/plugin-transform-private-methods@npm:7.24.1" dependencies: @@ -1246,7 +1193,7 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-private-property-in-object@npm:^7.22.11, @babel/plugin-transform-private-property-in-object@npm:^7.24.1": +"@babel/plugin-transform-private-property-in-object@npm:^7.24.1": version: 7.24.1 resolution: "@babel/plugin-transform-private-property-in-object@npm:7.24.1" dependencies: @@ -1260,7 +1207,7 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-property-literals@npm:^7.22.5, @babel/plugin-transform-property-literals@npm:^7.24.1": +"@babel/plugin-transform-property-literals@npm:^7.24.1": version: 7.24.1 resolution: "@babel/plugin-transform-property-literals@npm:7.24.1" dependencies: @@ -1320,7 +1267,7 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-regenerator@npm:^7.22.10, @babel/plugin-transform-regenerator@npm:^7.24.1": +"@babel/plugin-transform-regenerator@npm:^7.24.1": version: 7.24.1 resolution: "@babel/plugin-transform-regenerator@npm:7.24.1" dependencies: @@ -1332,7 +1279,7 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-reserved-words@npm:^7.22.5, @babel/plugin-transform-reserved-words@npm:^7.24.1": +"@babel/plugin-transform-reserved-words@npm:^7.24.1": version: 7.24.1 resolution: "@babel/plugin-transform-reserved-words@npm:7.24.1" dependencies: @@ -1343,7 +1290,7 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-shorthand-properties@npm:^7.22.5, @babel/plugin-transform-shorthand-properties@npm:^7.24.1": +"@babel/plugin-transform-shorthand-properties@npm:^7.24.1": version: 7.24.1 resolution: "@babel/plugin-transform-shorthand-properties@npm:7.24.1" dependencies: @@ -1354,7 +1301,7 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-spread@npm:^7.22.5, @babel/plugin-transform-spread@npm:^7.24.1": +"@babel/plugin-transform-spread@npm:^7.24.1": version: 7.24.1 resolution: "@babel/plugin-transform-spread@npm:7.24.1" dependencies: @@ -1366,7 +1313,7 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-sticky-regex@npm:^7.22.5, @babel/plugin-transform-sticky-regex@npm:^7.24.1": +"@babel/plugin-transform-sticky-regex@npm:^7.24.1": version: 7.24.1 resolution: "@babel/plugin-transform-sticky-regex@npm:7.24.1" dependencies: @@ -1377,7 +1324,7 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-template-literals@npm:^7.22.5, @babel/plugin-transform-template-literals@npm:^7.24.1": +"@babel/plugin-transform-template-literals@npm:^7.24.1": version: 7.24.1 resolution: "@babel/plugin-transform-template-literals@npm:7.24.1" dependencies: @@ -1388,7 +1335,7 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-typeof-symbol@npm:^7.22.5, @babel/plugin-transform-typeof-symbol@npm:^7.24.1": +"@babel/plugin-transform-typeof-symbol@npm:^7.24.1": version: 7.24.1 resolution: "@babel/plugin-transform-typeof-symbol@npm:7.24.1" dependencies: @@ -1413,7 +1360,7 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-unicode-escapes@npm:^7.22.10, @babel/plugin-transform-unicode-escapes@npm:^7.24.1": +"@babel/plugin-transform-unicode-escapes@npm:^7.24.1": version: 7.24.1 resolution: "@babel/plugin-transform-unicode-escapes@npm:7.24.1" dependencies: @@ -1424,7 +1371,7 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-unicode-property-regex@npm:^7.22.5, @babel/plugin-transform-unicode-property-regex@npm:^7.24.1": +"@babel/plugin-transform-unicode-property-regex@npm:^7.24.1": version: 7.24.1 resolution: "@babel/plugin-transform-unicode-property-regex@npm:7.24.1" dependencies: @@ -1436,7 +1383,7 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-unicode-regex@npm:^7.22.5, @babel/plugin-transform-unicode-regex@npm:^7.24.1": +"@babel/plugin-transform-unicode-regex@npm:^7.24.1": version: 7.24.1 resolution: "@babel/plugin-transform-unicode-regex@npm:7.24.1" dependencies: @@ -1448,7 +1395,7 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-unicode-sets-regex@npm:^7.22.5, @babel/plugin-transform-unicode-sets-regex@npm:^7.24.1": +"@babel/plugin-transform-unicode-sets-regex@npm:^7.24.1": version: 7.24.1 resolution: "@babel/plugin-transform-unicode-sets-regex@npm:7.24.1" dependencies: @@ -1470,96 +1417,6 @@ __metadata: languageName: node linkType: hard -"@babel/preset-env@npm:7.23.2": - version: 7.23.2 - resolution: "@babel/preset-env@npm:7.23.2" - dependencies: - "@babel/compat-data": "npm:^7.23.2" - "@babel/helper-compilation-targets": "npm:^7.22.15" - "@babel/helper-plugin-utils": "npm:^7.22.5" - "@babel/helper-validator-option": "npm:^7.22.15" - "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "npm:^7.22.15" - "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "npm:^7.22.15" - "@babel/plugin-proposal-private-property-in-object": "npm:7.21.0-placeholder-for-preset-env.2" - "@babel/plugin-syntax-async-generators": "npm:^7.8.4" - "@babel/plugin-syntax-class-properties": "npm:^7.12.13" - "@babel/plugin-syntax-class-static-block": "npm:^7.14.5" - "@babel/plugin-syntax-dynamic-import": "npm:^7.8.3" - "@babel/plugin-syntax-export-namespace-from": "npm:^7.8.3" - "@babel/plugin-syntax-import-assertions": "npm:^7.22.5" - "@babel/plugin-syntax-import-attributes": "npm:^7.22.5" - "@babel/plugin-syntax-import-meta": "npm:^7.10.4" - "@babel/plugin-syntax-json-strings": "npm:^7.8.3" - "@babel/plugin-syntax-logical-assignment-operators": "npm:^7.10.4" - "@babel/plugin-syntax-nullish-coalescing-operator": "npm:^7.8.3" - "@babel/plugin-syntax-numeric-separator": "npm:^7.10.4" - "@babel/plugin-syntax-object-rest-spread": "npm:^7.8.3" - "@babel/plugin-syntax-optional-catch-binding": "npm:^7.8.3" - "@babel/plugin-syntax-optional-chaining": "npm:^7.8.3" - "@babel/plugin-syntax-private-property-in-object": "npm:^7.14.5" - "@babel/plugin-syntax-top-level-await": "npm:^7.14.5" - "@babel/plugin-syntax-unicode-sets-regex": "npm:^7.18.6" - "@babel/plugin-transform-arrow-functions": "npm:^7.22.5" - "@babel/plugin-transform-async-generator-functions": "npm:^7.23.2" - "@babel/plugin-transform-async-to-generator": "npm:^7.22.5" - "@babel/plugin-transform-block-scoped-functions": "npm:^7.22.5" - "@babel/plugin-transform-block-scoping": "npm:^7.23.0" - "@babel/plugin-transform-class-properties": "npm:^7.22.5" - "@babel/plugin-transform-class-static-block": "npm:^7.22.11" - "@babel/plugin-transform-classes": "npm:^7.22.15" - "@babel/plugin-transform-computed-properties": "npm:^7.22.5" - "@babel/plugin-transform-destructuring": "npm:^7.23.0" - "@babel/plugin-transform-dotall-regex": "npm:^7.22.5" - "@babel/plugin-transform-duplicate-keys": "npm:^7.22.5" - "@babel/plugin-transform-dynamic-import": "npm:^7.22.11" - "@babel/plugin-transform-exponentiation-operator": "npm:^7.22.5" - "@babel/plugin-transform-export-namespace-from": "npm:^7.22.11" - "@babel/plugin-transform-for-of": "npm:^7.22.15" - "@babel/plugin-transform-function-name": "npm:^7.22.5" - "@babel/plugin-transform-json-strings": "npm:^7.22.11" - "@babel/plugin-transform-literals": "npm:^7.22.5" - "@babel/plugin-transform-logical-assignment-operators": "npm:^7.22.11" - "@babel/plugin-transform-member-expression-literals": "npm:^7.22.5" - "@babel/plugin-transform-modules-amd": "npm:^7.23.0" - "@babel/plugin-transform-modules-commonjs": "npm:^7.23.0" - "@babel/plugin-transform-modules-systemjs": "npm:^7.23.0" - "@babel/plugin-transform-modules-umd": "npm:^7.22.5" - "@babel/plugin-transform-named-capturing-groups-regex": "npm:^7.22.5" - "@babel/plugin-transform-new-target": "npm:^7.22.5" - "@babel/plugin-transform-nullish-coalescing-operator": "npm:^7.22.11" - "@babel/plugin-transform-numeric-separator": "npm:^7.22.11" - "@babel/plugin-transform-object-rest-spread": "npm:^7.22.15" - "@babel/plugin-transform-object-super": "npm:^7.22.5" - "@babel/plugin-transform-optional-catch-binding": "npm:^7.22.11" - "@babel/plugin-transform-optional-chaining": "npm:^7.23.0" - "@babel/plugin-transform-parameters": "npm:^7.22.15" - "@babel/plugin-transform-private-methods": "npm:^7.22.5" - "@babel/plugin-transform-private-property-in-object": "npm:^7.22.11" - "@babel/plugin-transform-property-literals": "npm:^7.22.5" - "@babel/plugin-transform-regenerator": "npm:^7.22.10" - "@babel/plugin-transform-reserved-words": "npm:^7.22.5" - "@babel/plugin-transform-shorthand-properties": "npm:^7.22.5" - "@babel/plugin-transform-spread": "npm:^7.22.5" - "@babel/plugin-transform-sticky-regex": "npm:^7.22.5" - "@babel/plugin-transform-template-literals": "npm:^7.22.5" - "@babel/plugin-transform-typeof-symbol": "npm:^7.22.5" - "@babel/plugin-transform-unicode-escapes": "npm:^7.22.10" - "@babel/plugin-transform-unicode-property-regex": "npm:^7.22.5" - "@babel/plugin-transform-unicode-regex": "npm:^7.22.5" - "@babel/plugin-transform-unicode-sets-regex": "npm:^7.22.5" - "@babel/preset-modules": "npm:0.1.6-no-external-plugins" - "@babel/types": "npm:^7.23.0" - babel-plugin-polyfill-corejs2: "npm:^0.4.6" - babel-plugin-polyfill-corejs3: "npm:^0.8.5" - babel-plugin-polyfill-regenerator: "npm:^0.5.3" - core-js-compat: "npm:^3.31.0" - semver: "npm:^6.3.1" - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: 10/7bc8aeed59047f99af2f608f3143044517582b6bd7b041e3c7a12eface47e0313a57e78fad2e0d450cda2ce6c58451d67493f3d3677c5c1031cf59b7db1161c3 - languageName: node - linkType: hard - "@babel/preset-env@npm:7.24.4, @babel/preset-env@npm:^7.22.9": version: 7.24.4 resolution: "@babel/preset-env@npm:7.24.4" @@ -1740,7 +1597,7 @@ __metadata: languageName: node linkType: hard -"@babel/runtime@npm:7.24.4, @babel/runtime@npm:^7.0.0, @babel/runtime@npm:^7.1.2, @babel/runtime@npm:^7.10.1, @babel/runtime@npm:^7.11.1, @babel/runtime@npm:^7.11.2, @babel/runtime@npm:^7.12.0, @babel/runtime@npm:^7.12.1, @babel/runtime@npm:^7.12.13, @babel/runtime@npm:^7.12.5, @babel/runtime@npm:^7.13.10, @babel/runtime@npm:^7.14.0, @babel/runtime@npm:^7.14.5, @babel/runtime@npm:^7.15.4, @babel/runtime@npm:^7.17.8, @babel/runtime@npm:^7.18.0, @babel/runtime@npm:^7.18.3, @babel/runtime@npm:^7.20.0, @babel/runtime@npm:^7.20.7, @babel/runtime@npm:^7.23.2, @babel/runtime@npm:^7.23.9, @babel/runtime@npm:^7.3.1, @babel/runtime@npm:^7.5.5, @babel/runtime@npm:^7.7.2, @babel/runtime@npm:^7.7.6, @babel/runtime@npm:^7.8.4, @babel/runtime@npm:^7.8.7, @babel/runtime@npm:^7.9.2": +"@babel/runtime@npm:7.24.4, @babel/runtime@npm:^7.0.0, @babel/runtime@npm:^7.1.2, @babel/runtime@npm:^7.10.1, @babel/runtime@npm:^7.11.1, @babel/runtime@npm:^7.11.2, @babel/runtime@npm:^7.12.0, @babel/runtime@npm:^7.12.1, @babel/runtime@npm:^7.12.13, @babel/runtime@npm:^7.12.5, @babel/runtime@npm:^7.13.10, @babel/runtime@npm:^7.14.0, @babel/runtime@npm:^7.15.4, @babel/runtime@npm:^7.17.8, @babel/runtime@npm:^7.18.0, @babel/runtime@npm:^7.18.3, @babel/runtime@npm:^7.20.0, @babel/runtime@npm:^7.20.7, @babel/runtime@npm:^7.23.2, @babel/runtime@npm:^7.23.9, @babel/runtime@npm:^7.3.1, @babel/runtime@npm:^7.5.5, @babel/runtime@npm:^7.7.2, @babel/runtime@npm:^7.7.6, @babel/runtime@npm:^7.8.4, @babel/runtime@npm:^7.8.7, @babel/runtime@npm:^7.9.2": version: 7.24.4 resolution: "@babel/runtime@npm:7.24.4" dependencies: @@ -1760,7 +1617,7 @@ __metadata: languageName: node linkType: hard -"@babel/traverse@npm:^7.1.6, @babel/traverse@npm:^7.22.8, @babel/traverse@npm:^7.23.2, @babel/traverse@npm:^7.24.1": +"@babel/traverse@npm:^7.1.6, @babel/traverse@npm:^7.22.8, @babel/traverse@npm:^7.24.1": version: 7.24.1 resolution: "@babel/traverse@npm:7.24.1" dependencies: @@ -2004,32 +1861,6 @@ __metadata: languageName: node linkType: hard -"@cypress/request@npm:^2.88.10": - version: 2.88.11 - resolution: "@cypress/request@npm:2.88.11" - dependencies: - aws-sign2: "npm:~0.7.0" - aws4: "npm:^1.8.0" - caseless: "npm:~0.12.0" - combined-stream: "npm:~1.0.6" - extend: "npm:~3.0.2" - forever-agent: "npm:~0.6.1" - form-data: "npm:~2.3.2" - http-signature: "npm:~1.3.6" - is-typedarray: "npm:~1.0.0" - isstream: "npm:~0.1.2" - json-stringify-safe: "npm:~5.0.1" - mime-types: "npm:~2.1.19" - performance-now: "npm:^2.1.0" - qs: "npm:~6.10.3" - safe-buffer: "npm:^5.1.2" - tough-cookie: "npm:~2.5.0" - tunnel-agent: "npm:^0.6.0" - uuid: "npm:^8.3.2" - checksum: 10/a185eea04924ce7cad2cb167d4f265bf5d8356c687b38b29e606f7c902710f6ed3914bd246bae4da506668af0e70bbcdbd86fe2d8805812c79a9edc73a82abc4 - languageName: node - linkType: hard - "@cypress/request@npm:^3.0.0": version: 3.0.1 resolution: "@cypress/request@npm:3.0.1" @@ -2056,22 +1887,6 @@ __metadata: languageName: node linkType: hard -"@cypress/webpack-preprocessor@npm:5.17.1": - version: 5.17.1 - resolution: "@cypress/webpack-preprocessor@npm:5.17.1" - dependencies: - bluebird: "npm:3.7.1" - debug: "npm:^4.3.4" - lodash: "npm:^4.17.20" - peerDependencies: - "@babel/core": ^7.0.1 - "@babel/preset-env": ^7.0.0 - babel-loader: ^8.0.2 || ^9 - webpack: ^4 || ^5 - checksum: 10/34cb577fb2fb49817ae72a02ca759bb4c0066ed3d90d4e9d0d3a0a028975d4df4dd787d19f209aeaeb6d6896c66a6d4ce81a41c6eb39886a09a6f783417e94e3 - languageName: node - linkType: hard - "@cypress/webpack-preprocessor@npm:6.0.1": version: 6.0.1 resolution: "@cypress/webpack-preprocessor@npm:6.0.1" @@ -2098,7 +1913,7 @@ __metadata: languageName: node linkType: hard -"@daybrush/utils@npm:1.13.0, @daybrush/utils@npm:^1.0.0, @daybrush/utils@npm:^1.1.1, @daybrush/utils@npm:^1.13.0, @daybrush/utils@npm:^1.4.0, @daybrush/utils@npm:^1.6.0, @daybrush/utils@npm:^1.7.1": +"@daybrush/utils@npm:^1.1.1, @daybrush/utils@npm:^1.13.0, @daybrush/utils@npm:^1.4.0, @daybrush/utils@npm:^1.6.0, @daybrush/utils@npm:^1.7.1": version: 1.13.0 resolution: "@daybrush/utils@npm:1.13.0" checksum: 10/79a09fe785868b486d61effe665823642682c843f77b9e6ae840d75e5570975e1c85dc5b882369c05f6c4dee3215df0d954a7747e9c41e0ddeec56573ae49151 @@ -2364,6 +2179,33 @@ __metadata: languageName: node linkType: hard +"@ericcornelissen/bash-parser@npm:0.5.2": + version: 0.5.2 + resolution: "@ericcornelissen/bash-parser@npm:0.5.2" + dependencies: + array-last: "npm:^1.1.1" + babylon: "npm:^6.9.1" + compose-function: "npm:^3.0.3" + deep-freeze: "npm:0.0.1" + filter-iterator: "npm:0.0.1" + filter-obj: "npm:^1.1.0" + has-own-property: "npm:^0.1.0" + identity-function: "npm:^1.0.0" + is-iterable: "npm:^1.1.0" + iterable-lookahead: "npm:^1.0.0" + lodash.curry: "npm:^4.1.1" + magic-string: "npm:^0.16.0" + map-obj: "npm:^2.0.0" + object-pairs: "npm:^0.1.0" + object-values: "npm:^1.0.0" + reverse-arguments: "npm:^1.0.0" + shell-quote-word: "npm:^1.0.1" + to-pascal-case: "npm:^1.0.0" + unescape-js: "npm:^1.0.5" + checksum: 10/b268c7a66f2d7527e964eaef23ddc661e842224d8cc79b1ae861ea05fda26f0ac0e13096a65ef725cb436ed026084045cdb73015b2b166e5100cbde93b85193f + languageName: node + linkType: hard + "@es-joy/jsdoccomment@npm:~0.40.1": version: 0.40.1 resolution: "@es-joy/jsdoccomment@npm:0.40.1" @@ -2386,9 +2228,9 @@ __metadata: languageName: node linkType: hard -"@esbuild/aix-ppc64@npm:0.20.1": - version: 0.20.1 - resolution: "@esbuild/aix-ppc64@npm:0.20.1" +"@esbuild/aix-ppc64@npm:0.20.2": + version: 0.20.2 + resolution: "@esbuild/aix-ppc64@npm:0.20.2" conditions: os=aix & cpu=ppc64 languageName: node linkType: hard @@ -2400,13 +2242,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/android-arm64@npm:0.18.12": - version: 0.18.12 - resolution: "@esbuild/android-arm64@npm:0.18.12" - conditions: os=android & cpu=arm64 - languageName: node - linkType: hard - "@esbuild/android-arm64@npm:0.18.20": version: 0.18.20 resolution: "@esbuild/android-arm64@npm:0.18.20" @@ -2414,9 +2249,9 @@ __metadata: languageName: node linkType: hard -"@esbuild/android-arm64@npm:0.20.1": - version: 0.20.1 - resolution: "@esbuild/android-arm64@npm:0.20.1" +"@esbuild/android-arm64@npm:0.20.2": + version: 0.20.2 + resolution: "@esbuild/android-arm64@npm:0.20.2" conditions: os=android & cpu=arm64 languageName: node linkType: hard @@ -2428,13 +2263,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/android-arm@npm:0.18.12": - version: 0.18.12 - resolution: "@esbuild/android-arm@npm:0.18.12" - conditions: os=android & cpu=arm - languageName: node - linkType: hard - "@esbuild/android-arm@npm:0.18.20": version: 0.18.20 resolution: "@esbuild/android-arm@npm:0.18.20" @@ -2442,9 +2270,9 @@ __metadata: languageName: node linkType: hard -"@esbuild/android-arm@npm:0.20.1": - version: 0.20.1 - resolution: "@esbuild/android-arm@npm:0.20.1" +"@esbuild/android-arm@npm:0.20.2": + version: 0.20.2 + resolution: "@esbuild/android-arm@npm:0.20.2" conditions: os=android & cpu=arm languageName: node linkType: hard @@ -2456,13 +2284,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/android-x64@npm:0.18.12": - version: 0.18.12 - resolution: "@esbuild/android-x64@npm:0.18.12" - conditions: os=android & cpu=x64 - languageName: node - linkType: hard - "@esbuild/android-x64@npm:0.18.20": version: 0.18.20 resolution: "@esbuild/android-x64@npm:0.18.20" @@ -2470,9 +2291,9 @@ __metadata: languageName: node linkType: hard -"@esbuild/android-x64@npm:0.20.1": - version: 0.20.1 - resolution: "@esbuild/android-x64@npm:0.20.1" +"@esbuild/android-x64@npm:0.20.2": + version: 0.20.2 + resolution: "@esbuild/android-x64@npm:0.20.2" conditions: os=android & cpu=x64 languageName: node linkType: hard @@ -2484,13 +2305,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/darwin-arm64@npm:0.18.12": - version: 0.18.12 - resolution: "@esbuild/darwin-arm64@npm:0.18.12" - conditions: os=darwin & cpu=arm64 - languageName: node - linkType: hard - "@esbuild/darwin-arm64@npm:0.18.20": version: 0.18.20 resolution: "@esbuild/darwin-arm64@npm:0.18.20" @@ -2498,9 +2312,9 @@ __metadata: languageName: node linkType: hard -"@esbuild/darwin-arm64@npm:0.20.1": - version: 0.20.1 - resolution: "@esbuild/darwin-arm64@npm:0.20.1" +"@esbuild/darwin-arm64@npm:0.20.2": + version: 0.20.2 + resolution: "@esbuild/darwin-arm64@npm:0.20.2" conditions: os=darwin & cpu=arm64 languageName: node linkType: hard @@ -2512,13 +2326,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/darwin-x64@npm:0.18.12": - version: 0.18.12 - resolution: "@esbuild/darwin-x64@npm:0.18.12" - conditions: os=darwin & cpu=x64 - languageName: node - linkType: hard - "@esbuild/darwin-x64@npm:0.18.20": version: 0.18.20 resolution: "@esbuild/darwin-x64@npm:0.18.20" @@ -2526,9 +2333,9 @@ __metadata: languageName: node linkType: hard -"@esbuild/darwin-x64@npm:0.20.1": - version: 0.20.1 - resolution: "@esbuild/darwin-x64@npm:0.20.1" +"@esbuild/darwin-x64@npm:0.20.2": + version: 0.20.2 + resolution: "@esbuild/darwin-x64@npm:0.20.2" conditions: os=darwin & cpu=x64 languageName: node linkType: hard @@ -2540,13 +2347,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/freebsd-arm64@npm:0.18.12": - version: 0.18.12 - resolution: "@esbuild/freebsd-arm64@npm:0.18.12" - conditions: os=freebsd & cpu=arm64 - languageName: node - linkType: hard - "@esbuild/freebsd-arm64@npm:0.18.20": version: 0.18.20 resolution: "@esbuild/freebsd-arm64@npm:0.18.20" @@ -2554,9 +2354,9 @@ __metadata: languageName: node linkType: hard -"@esbuild/freebsd-arm64@npm:0.20.1": - version: 0.20.1 - resolution: "@esbuild/freebsd-arm64@npm:0.20.1" +"@esbuild/freebsd-arm64@npm:0.20.2": + version: 0.20.2 + resolution: "@esbuild/freebsd-arm64@npm:0.20.2" conditions: os=freebsd & cpu=arm64 languageName: node linkType: hard @@ -2568,13 +2368,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/freebsd-x64@npm:0.18.12": - version: 0.18.12 - resolution: "@esbuild/freebsd-x64@npm:0.18.12" - conditions: os=freebsd & cpu=x64 - languageName: node - linkType: hard - "@esbuild/freebsd-x64@npm:0.18.20": version: 0.18.20 resolution: "@esbuild/freebsd-x64@npm:0.18.20" @@ -2582,9 +2375,9 @@ __metadata: languageName: node linkType: hard -"@esbuild/freebsd-x64@npm:0.20.1": - version: 0.20.1 - resolution: "@esbuild/freebsd-x64@npm:0.20.1" +"@esbuild/freebsd-x64@npm:0.20.2": + version: 0.20.2 + resolution: "@esbuild/freebsd-x64@npm:0.20.2" conditions: os=freebsd & cpu=x64 languageName: node linkType: hard @@ -2596,13 +2389,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-arm64@npm:0.18.12": - version: 0.18.12 - resolution: "@esbuild/linux-arm64@npm:0.18.12" - conditions: os=linux & cpu=arm64 - languageName: node - linkType: hard - "@esbuild/linux-arm64@npm:0.18.20": version: 0.18.20 resolution: "@esbuild/linux-arm64@npm:0.18.20" @@ -2610,9 +2396,9 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-arm64@npm:0.20.1": - version: 0.20.1 - resolution: "@esbuild/linux-arm64@npm:0.20.1" +"@esbuild/linux-arm64@npm:0.20.2": + version: 0.20.2 + resolution: "@esbuild/linux-arm64@npm:0.20.2" conditions: os=linux & cpu=arm64 languageName: node linkType: hard @@ -2624,13 +2410,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-arm@npm:0.18.12": - version: 0.18.12 - resolution: "@esbuild/linux-arm@npm:0.18.12" - conditions: os=linux & cpu=arm - languageName: node - linkType: hard - "@esbuild/linux-arm@npm:0.18.20": version: 0.18.20 resolution: "@esbuild/linux-arm@npm:0.18.20" @@ -2638,9 +2417,9 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-arm@npm:0.20.1": - version: 0.20.1 - resolution: "@esbuild/linux-arm@npm:0.20.1" +"@esbuild/linux-arm@npm:0.20.2": + version: 0.20.2 + resolution: "@esbuild/linux-arm@npm:0.20.2" conditions: os=linux & cpu=arm languageName: node linkType: hard @@ -2652,13 +2431,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-ia32@npm:0.18.12": - version: 0.18.12 - resolution: "@esbuild/linux-ia32@npm:0.18.12" - conditions: os=linux & cpu=ia32 - languageName: node - linkType: hard - "@esbuild/linux-ia32@npm:0.18.20": version: 0.18.20 resolution: "@esbuild/linux-ia32@npm:0.18.20" @@ -2666,9 +2438,9 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-ia32@npm:0.20.1": - version: 0.20.1 - resolution: "@esbuild/linux-ia32@npm:0.20.1" +"@esbuild/linux-ia32@npm:0.20.2": + version: 0.20.2 + resolution: "@esbuild/linux-ia32@npm:0.20.2" conditions: os=linux & cpu=ia32 languageName: node linkType: hard @@ -2680,13 +2452,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-loong64@npm:0.18.12": - version: 0.18.12 - resolution: "@esbuild/linux-loong64@npm:0.18.12" - conditions: os=linux & cpu=loong64 - languageName: node - linkType: hard - "@esbuild/linux-loong64@npm:0.18.20": version: 0.18.20 resolution: "@esbuild/linux-loong64@npm:0.18.20" @@ -2694,9 +2459,9 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-loong64@npm:0.20.1": - version: 0.20.1 - resolution: "@esbuild/linux-loong64@npm:0.20.1" +"@esbuild/linux-loong64@npm:0.20.2": + version: 0.20.2 + resolution: "@esbuild/linux-loong64@npm:0.20.2" conditions: os=linux & cpu=loong64 languageName: node linkType: hard @@ -2708,13 +2473,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-mips64el@npm:0.18.12": - version: 0.18.12 - resolution: "@esbuild/linux-mips64el@npm:0.18.12" - conditions: os=linux & cpu=mips64el - languageName: node - linkType: hard - "@esbuild/linux-mips64el@npm:0.18.20": version: 0.18.20 resolution: "@esbuild/linux-mips64el@npm:0.18.20" @@ -2722,9 +2480,9 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-mips64el@npm:0.20.1": - version: 0.20.1 - resolution: "@esbuild/linux-mips64el@npm:0.20.1" +"@esbuild/linux-mips64el@npm:0.20.2": + version: 0.20.2 + resolution: "@esbuild/linux-mips64el@npm:0.20.2" conditions: os=linux & cpu=mips64el languageName: node linkType: hard @@ -2736,13 +2494,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-ppc64@npm:0.18.12": - version: 0.18.12 - resolution: "@esbuild/linux-ppc64@npm:0.18.12" - conditions: os=linux & cpu=ppc64 - languageName: node - linkType: hard - "@esbuild/linux-ppc64@npm:0.18.20": version: 0.18.20 resolution: "@esbuild/linux-ppc64@npm:0.18.20" @@ -2750,9 +2501,9 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-ppc64@npm:0.20.1": - version: 0.20.1 - resolution: "@esbuild/linux-ppc64@npm:0.20.1" +"@esbuild/linux-ppc64@npm:0.20.2": + version: 0.20.2 + resolution: "@esbuild/linux-ppc64@npm:0.20.2" conditions: os=linux & cpu=ppc64 languageName: node linkType: hard @@ -2764,13 +2515,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-riscv64@npm:0.18.12": - version: 0.18.12 - resolution: "@esbuild/linux-riscv64@npm:0.18.12" - conditions: os=linux & cpu=riscv64 - languageName: node - linkType: hard - "@esbuild/linux-riscv64@npm:0.18.20": version: 0.18.20 resolution: "@esbuild/linux-riscv64@npm:0.18.20" @@ -2778,9 +2522,9 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-riscv64@npm:0.20.1": - version: 0.20.1 - resolution: "@esbuild/linux-riscv64@npm:0.20.1" +"@esbuild/linux-riscv64@npm:0.20.2": + version: 0.20.2 + resolution: "@esbuild/linux-riscv64@npm:0.20.2" conditions: os=linux & cpu=riscv64 languageName: node linkType: hard @@ -2792,13 +2536,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-s390x@npm:0.18.12": - version: 0.18.12 - resolution: "@esbuild/linux-s390x@npm:0.18.12" - conditions: os=linux & cpu=s390x - languageName: node - linkType: hard - "@esbuild/linux-s390x@npm:0.18.20": version: 0.18.20 resolution: "@esbuild/linux-s390x@npm:0.18.20" @@ -2806,9 +2543,9 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-s390x@npm:0.20.1": - version: 0.20.1 - resolution: "@esbuild/linux-s390x@npm:0.20.1" +"@esbuild/linux-s390x@npm:0.20.2": + version: 0.20.2 + resolution: "@esbuild/linux-s390x@npm:0.20.2" conditions: os=linux & cpu=s390x languageName: node linkType: hard @@ -2820,13 +2557,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-x64@npm:0.18.12": - version: 0.18.12 - resolution: "@esbuild/linux-x64@npm:0.18.12" - conditions: os=linux & cpu=x64 - languageName: node - linkType: hard - "@esbuild/linux-x64@npm:0.18.20": version: 0.18.20 resolution: "@esbuild/linux-x64@npm:0.18.20" @@ -2834,9 +2564,9 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-x64@npm:0.20.1": - version: 0.20.1 - resolution: "@esbuild/linux-x64@npm:0.20.1" +"@esbuild/linux-x64@npm:0.20.2": + version: 0.20.2 + resolution: "@esbuild/linux-x64@npm:0.20.2" conditions: os=linux & cpu=x64 languageName: node linkType: hard @@ -2848,13 +2578,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/netbsd-x64@npm:0.18.12": - version: 0.18.12 - resolution: "@esbuild/netbsd-x64@npm:0.18.12" - conditions: os=netbsd & cpu=x64 - languageName: node - linkType: hard - "@esbuild/netbsd-x64@npm:0.18.20": version: 0.18.20 resolution: "@esbuild/netbsd-x64@npm:0.18.20" @@ -2862,9 +2585,9 @@ __metadata: languageName: node linkType: hard -"@esbuild/netbsd-x64@npm:0.20.1": - version: 0.20.1 - resolution: "@esbuild/netbsd-x64@npm:0.20.1" +"@esbuild/netbsd-x64@npm:0.20.2": + version: 0.20.2 + resolution: "@esbuild/netbsd-x64@npm:0.20.2" conditions: os=netbsd & cpu=x64 languageName: node linkType: hard @@ -2876,13 +2599,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/openbsd-x64@npm:0.18.12": - version: 0.18.12 - resolution: "@esbuild/openbsd-x64@npm:0.18.12" - conditions: os=openbsd & cpu=x64 - languageName: node - linkType: hard - "@esbuild/openbsd-x64@npm:0.18.20": version: 0.18.20 resolution: "@esbuild/openbsd-x64@npm:0.18.20" @@ -2890,9 +2606,9 @@ __metadata: languageName: node linkType: hard -"@esbuild/openbsd-x64@npm:0.20.1": - version: 0.20.1 - resolution: "@esbuild/openbsd-x64@npm:0.20.1" +"@esbuild/openbsd-x64@npm:0.20.2": + version: 0.20.2 + resolution: "@esbuild/openbsd-x64@npm:0.20.2" conditions: os=openbsd & cpu=x64 languageName: node linkType: hard @@ -2904,13 +2620,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/sunos-x64@npm:0.18.12": - version: 0.18.12 - resolution: "@esbuild/sunos-x64@npm:0.18.12" - conditions: os=sunos & cpu=x64 - languageName: node - linkType: hard - "@esbuild/sunos-x64@npm:0.18.20": version: 0.18.20 resolution: "@esbuild/sunos-x64@npm:0.18.20" @@ -2918,9 +2627,9 @@ __metadata: languageName: node linkType: hard -"@esbuild/sunos-x64@npm:0.20.1": - version: 0.20.1 - resolution: "@esbuild/sunos-x64@npm:0.20.1" +"@esbuild/sunos-x64@npm:0.20.2": + version: 0.20.2 + resolution: "@esbuild/sunos-x64@npm:0.20.2" conditions: os=sunos & cpu=x64 languageName: node linkType: hard @@ -2932,13 +2641,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/win32-arm64@npm:0.18.12": - version: 0.18.12 - resolution: "@esbuild/win32-arm64@npm:0.18.12" - conditions: os=win32 & cpu=arm64 - languageName: node - linkType: hard - "@esbuild/win32-arm64@npm:0.18.20": version: 0.18.20 resolution: "@esbuild/win32-arm64@npm:0.18.20" @@ -2946,9 +2648,9 @@ __metadata: languageName: node linkType: hard -"@esbuild/win32-arm64@npm:0.20.1": - version: 0.20.1 - resolution: "@esbuild/win32-arm64@npm:0.20.1" +"@esbuild/win32-arm64@npm:0.20.2": + version: 0.20.2 + resolution: "@esbuild/win32-arm64@npm:0.20.2" conditions: os=win32 & cpu=arm64 languageName: node linkType: hard @@ -2960,13 +2662,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/win32-ia32@npm:0.18.12": - version: 0.18.12 - resolution: "@esbuild/win32-ia32@npm:0.18.12" - conditions: os=win32 & cpu=ia32 - languageName: node - linkType: hard - "@esbuild/win32-ia32@npm:0.18.20": version: 0.18.20 resolution: "@esbuild/win32-ia32@npm:0.18.20" @@ -2974,9 +2669,9 @@ __metadata: languageName: node linkType: hard -"@esbuild/win32-ia32@npm:0.20.1": - version: 0.20.1 - resolution: "@esbuild/win32-ia32@npm:0.20.1" +"@esbuild/win32-ia32@npm:0.20.2": + version: 0.20.2 + resolution: "@esbuild/win32-ia32@npm:0.20.2" conditions: os=win32 & cpu=ia32 languageName: node linkType: hard @@ -2988,13 +2683,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/win32-x64@npm:0.18.12": - version: 0.18.12 - resolution: "@esbuild/win32-x64@npm:0.18.12" - conditions: os=win32 & cpu=x64 - languageName: node - linkType: hard - "@esbuild/win32-x64@npm:0.18.20": version: 0.18.20 resolution: "@esbuild/win32-x64@npm:0.18.20" @@ -3002,9 +2690,9 @@ __metadata: languageName: node linkType: hard -"@esbuild/win32-x64@npm:0.20.1": - version: 0.20.1 - resolution: "@esbuild/win32-x64@npm:0.20.1" +"@esbuild/win32-x64@npm:0.20.2": + version: 0.20.2 + resolution: "@esbuild/win32-x64@npm:0.20.2" conditions: os=win32 & cpu=x64 languageName: node linkType: hard @@ -3311,6 +2999,7 @@ __metadata: "@grafana/schema": "npm:11.1.0-pre" "@grafana/ui": "npm:11.1.0-pre" "@kusto/monaco-kusto": "npm:^10.0.0" + "@testing-library/dom": "npm:10.0.0" "@testing-library/react": "npm:15.0.2" "@testing-library/user-event": "npm:14.5.2" "@types/jest": "npm:29.5.12" @@ -3318,6 +3007,7 @@ __metadata: "@types/node": "npm:20.12.7" "@types/prismjs": "npm:1.26.3" "@types/react": "npm:18.2.79" + "@types/react-dom": "npm:18.2.25" "@types/testing-library__jest-dom": "npm:5.14.9" fast-deep-equal: "npm:^3.1.3" i18next: "npm:^23.0.0" @@ -3326,6 +3016,8 @@ __metadata: monaco-editor: "npm:0.34.1" prismjs: "npm:1.29.0" react: "npm:18.2.0" + react-dom: "npm:18.2.0" + react-select: "npm:5.8.0" react-select-event: "npm:5.5.1" react-use: "npm:17.5.0" rxjs: "npm:7.8.1" @@ -3348,16 +3040,18 @@ __metadata: "@grafana/runtime": "npm:11.1.0-pre" "@grafana/schema": "npm:11.1.0-pre" "@grafana/ui": "npm:11.1.0-pre" + "@testing-library/dom": "npm:10.0.0" "@testing-library/jest-dom": "npm:6.4.2" "@testing-library/react": "npm:15.0.2" "@testing-library/user-event": "npm:14.5.2" "@types/jest": "npm:29.5.12" "@types/lodash": "npm:4.17.0" + "@types/node": "npm:20.12.7" "@types/prismjs": "npm:1.26.3" "@types/react": "npm:18.2.79" "@types/react-dom": "npm:18.2.25" "@types/testing-library__jest-dom": "npm:5.14.9" - css-loader: "npm:6.10.0" + css-loader: "npm:7.1.1" fast-deep-equal: "npm:^3.1.3" jest: "npm:29.7.0" lodash: "npm:4.17.21" @@ -3367,7 +3061,7 @@ __metadata: react-dom: "npm:18.2.0" react-use: "npm:17.5.0" rxjs: "npm:7.8.1" - style-loader: "npm:3.3.4" + style-loader: "npm:4.0.0" ts-node: "npm:10.9.2" tslib: "npm:2.6.2" typescript: "npm:5.4.5" @@ -3389,6 +3083,7 @@ __metadata: "@grafana/runtime": "npm:11.1.0-pre" "@grafana/schema": "npm:11.1.0-pre" "@grafana/ui": "npm:11.1.0-pre" + "@testing-library/dom": "npm:10.0.0" "@testing-library/react": "npm:15.0.2" "@testing-library/user-event": "npm:14.5.2" "@types/d3-random": "npm:^3.0.2" @@ -3396,16 +3091,20 @@ __metadata: "@types/lodash": "npm:4.17.0" "@types/node": "npm:20.12.7" "@types/react": "npm:18.2.79" + "@types/react-dom": "npm:18.2.25" "@types/testing-library__jest-dom": "npm:5.14.9" "@types/uuid": "npm:9.0.8" d3-random: "npm:^3.0.1" lodash: "npm:4.17.21" micro-memoize: "npm:^4.1.2" react: "npm:18.2.0" + react-dom: "npm:18.2.0" + react-select: "npm:5.8.0" react-use: "npm:17.5.0" rxjs: "npm:7.8.1" ts-node: "npm:10.9.2" tslib: "npm:2.6.2" + typescript: "npm:5.4.5" uuid: "npm:9.0.1" webpack: "npm:5.91.0" peerDependencies: @@ -3419,26 +3118,35 @@ __metadata: dependencies: "@emotion/css": "npm:11.11.2" "@grafana/data": "workspace:*" + "@grafana/e2e-selectors": "workspace:*" "@grafana/experimental": "npm:1.7.10" "@grafana/o11y-ds-frontend": "workspace:*" "@grafana/plugin-configs": "workspace:*" "@grafana/runtime": "workspace:*" "@grafana/ui": "workspace:*" + "@testing-library/dom": "npm:10.0.0" "@testing-library/jest-dom": "npm:6.4.2" "@testing-library/react": "npm:15.0.2" "@testing-library/user-event": "npm:14.5.2" "@types/jest": "npm:29.5.12" "@types/lodash": "npm:4.17.0" "@types/logfmt": "npm:^1.2.3" + "@types/node": "npm:20.12.7" + "@types/react": "npm:18.2.79" + "@types/react-dom": "npm:18.2.25" "@types/react-window": "npm:1.8.8" "@types/uuid": "npm:9.0.8" lodash: "npm:4.17.21" logfmt: "npm:^1.3.2" + react: "npm:18.2.0" + react-dom: "npm:18.2.0" + react-select: "npm:5.8.0" react-window: "npm:1.8.10" rxjs: "npm:7.8.1" stream-browserify: "npm:3.0.0" ts-node: "npm:10.9.2" tslib: "npm:2.6.2" + typescript: "npm:5.4.5" uuid: "npm:9.0.1" webpack: "npm:5.91.0" peerDependencies: @@ -3446,6 +3154,37 @@ __metadata: languageName: unknown linkType: soft +"@grafana-plugins/mysql@workspace:public/app/plugins/datasource/mysql": + version: 0.0.0-use.local + resolution: "@grafana-plugins/mysql@workspace:public/app/plugins/datasource/mysql" + dependencies: + "@emotion/css": "npm:11.11.2" + "@grafana/data": "npm:11.1.0-pre" + "@grafana/e2e-selectors": "npm:11.1.0-pre" + "@grafana/experimental": "npm:1.7.10" + "@grafana/plugin-configs": "npm:11.1.0-pre" + "@grafana/runtime": "npm:11.1.0-pre" + "@grafana/sql": "npm:11.1.0-pre" + "@grafana/ui": "npm:11.1.0-pre" + "@testing-library/react": "npm:15.0.2" + "@testing-library/user-event": "npm:14.5.2" + "@types/jest": "npm:29.5.12" + "@types/lodash": "npm:4.17.0" + "@types/node": "npm:20.12.7" + "@types/react": "npm:18.2.79" + "@types/testing-library__jest-dom": "npm:5.14.9" + lodash: "npm:4.17.21" + react: "npm:18.2.0" + rxjs: "npm:7.8.1" + ts-node: "npm:10.9.2" + tslib: "npm:2.6.2" + typescript: "npm:5.4.5" + webpack: "npm:5.91.0" + peerDependencies: + "@grafana/runtime": "*" + languageName: unknown + linkType: soft + "@grafana-plugins/parca@workspace:public/app/plugins/datasource/parca": version: 0.0.0-use.local resolution: "@grafana-plugins/parca@workspace:public/app/plugins/datasource/parca" @@ -3456,17 +3195,22 @@ __metadata: "@grafana/runtime": "npm:11.1.0-pre" "@grafana/schema": "npm:11.1.0-pre" "@grafana/ui": "npm:11.1.0-pre" + "@testing-library/dom": "npm:10.0.0" "@testing-library/react": "npm:15.0.2" "@testing-library/user-event": "npm:14.5.2" "@types/lodash": "npm:4.17.0" + "@types/node": "npm:20.12.7" "@types/react": "npm:18.2.79" + "@types/react-dom": "npm:18.2.25" lodash: "npm:4.17.21" monaco-editor: "npm:0.34.1" react: "npm:18.2.0" + react-dom: "npm:18.2.0" react-use: "npm:17.5.0" rxjs: "npm:7.8.1" ts-node: "npm:10.9.2" tslib: "npm:2.6.2" + typescript: "npm:5.4.5" webpack: "npm:5.91.0" peerDependencies: "@grafana/runtime": "*" @@ -3486,6 +3230,7 @@ __metadata: "@grafana/runtime": "npm:11.1.0-pre" "@grafana/schema": "npm:11.1.0-pre" "@grafana/ui": "npm:11.1.0-pre" + "@testing-library/dom": "npm:10.0.0" "@testing-library/react": "npm:15.0.2" "@testing-library/user-event": "npm:14.5.2" "@types/debounce-promise": "npm:3.1.9" @@ -3494,6 +3239,7 @@ __metadata: "@types/node": "npm:20.12.7" "@types/prismjs": "npm:1.26.3" "@types/react": "npm:18.2.79" + "@types/react-dom": "npm:18.2.25" "@types/react-test-renderer": "npm:18.0.7" "@types/testing-library__jest-dom": "npm:5.14.9" debounce-promise: "npm:3.1.2" @@ -3504,6 +3250,8 @@ __metadata: monaco-editor: "npm:0.34.1" prismjs: "npm:1.29.0" react: "npm:18.2.0" + react-dom: "npm:18.2.0" + react-select: "npm:5.8.0" react-select-event: "npm:5.5.1" react-test-renderer: "npm:18.2.0" react-use: "npm:17.5.0" @@ -3538,6 +3286,7 @@ __metadata: "@opentelemetry/api": "npm:1.8.0" "@opentelemetry/exporter-collector": "npm:0.25.0" "@opentelemetry/semantic-conventions": "npm:1.23.0" + "@testing-library/dom": "npm:10.0.0" "@testing-library/jest-dom": "npm:6.4.2" "@testing-library/react": "npm:15.0.2" "@testing-library/user-event": "npm:14.5.2" @@ -3559,6 +3308,7 @@ __metadata: prismjs: "npm:1.29.0" react: "npm:18.2.0" react-dom: "npm:18.2.0" + react-select: "npm:5.8.0" react-select-event: "npm:5.5.1" react-use: "npm:17.5.0" rxjs: "npm:7.8.1" @@ -3581,6 +3331,7 @@ __metadata: dependencies: "@emotion/css": "npm:11.11.2" "@grafana/data": "workspace:*" + "@grafana/e2e-selectors": "workspace:*" "@grafana/experimental": "npm:1.7.10" "@grafana/o11y-ds-frontend": "workspace:*" "@grafana/plugin-configs": "workspace:*" @@ -3590,13 +3341,18 @@ __metadata: "@testing-library/react": "npm:15.0.2" "@types/jest": "npm:29.5.12" "@types/lodash": "npm:4.17.0" + "@types/node": "npm:20.12.7" "@types/react": "npm:18.2.79" + "@types/react-dom": "npm:18.2.25" lodash: "npm:4.17.21" react: "npm:18.2.0" + react-dom: "npm:18.2.0" + react-select: "npm:5.8.0" react-use: "npm:17.5.0" rxjs: "npm:7.8.1" ts-node: "npm:10.9.2" tslib: "npm:2.6.2" + typescript: "npm:5.4.5" webpack: "npm:5.91.0" peerDependencies: "@grafana/runtime": "*" @@ -3643,7 +3399,7 @@ __metadata: d3-interpolate: "npm:3.0.1" date-fns: "npm:3.6.0" dompurify: "npm:^3.0.0" - esbuild: "npm:0.18.12" + esbuild: "npm:0.20.2" eventemitter3: "npm:5.0.1" fast_array_intersect: "npm:1.1.0" history: "npm:4.10.1" @@ -3693,7 +3449,7 @@ __metadata: "@grafana/tsconfig": "npm:^1.3.0-rc1" "@rollup/plugin-node-resolve": "npm:15.2.3" "@types/node": "npm:20.12.7" - esbuild: "npm:0.18.12" + esbuild: "npm:0.20.2" rimraf: "npm:5.0.5" rollup: "npm:2.79.1" rollup-plugin-dts: "npm:^5.0.0" @@ -3704,51 +3460,6 @@ __metadata: languageName: unknown linkType: soft -"@grafana/e2e@workspace:packages/grafana-e2e": - version: 0.0.0-use.local - resolution: "@grafana/e2e@workspace:packages/grafana-e2e" - dependencies: - "@babel/core": "npm:7.23.2" - "@babel/preset-env": "npm:7.23.2" - "@cypress/webpack-preprocessor": "npm:5.17.1" - "@grafana/e2e-selectors": "npm:11.1.0-pre" - "@grafana/schema": "npm:11.1.0-pre" - "@grafana/tsconfig": "npm:^1.3.0-rc1" - "@mochajs/json-file-reporter": "npm:^1.2.0" - "@rollup/plugin-node-resolve": "npm:15.2.3" - "@types/chrome-remote-interface": "npm:0.31.10" - "@types/lodash": "npm:4.14.195" - "@types/node": "npm:18.18.4" - "@types/uuid": "npm:9.0.2" - babel-loader: "npm:9.1.3" - blink-diff: "npm:1.0.13" - chrome-remote-interface: "npm:0.33.0" - commander: "npm:8.3.0" - cypress: "npm:9.5.1" - cypress-file-upload: "npm:5.0.8" - devtools-protocol: "npm:0.0.1170333" - esbuild: "npm:0.18.12" - execa: "npm:5.1.1" - lodash: "npm:4.17.21" - mocha: "npm:10.2.0" - resolve-bin: "npm:1.0.1" - rimraf: "npm:5.0.1" - rollup: "npm:2.79.1" - rollup-plugin-dts: "npm:^5.0.0" - rollup-plugin-esbuild: "npm:5.0.0" - rollup-plugin-node-externals: "npm:^5.0.0" - tracelib: "npm:1.0.1" - ts-loader: "npm:8.4.0" - tslib: "npm:2.6.0" - typescript: "npm:5.2.2" - uuid: "npm:9.0.0" - webpack: "npm:5.89.0" - yaml: "npm:^2.0.0" - bin: - grafana-e2e: bin/grafana-e2e.js - languageName: unknown - linkType: soft - "@grafana/eslint-config@npm:7.0.0": version: 7.0.0 resolution: "@grafana/eslint-config@npm:7.0.0" @@ -3861,17 +3572,20 @@ __metadata: "@grafana/ui": "npm:11.1.0-pre" "@leeoniya/ufuzzy": "npm:1.0.14" "@rollup/plugin-node-resolve": "npm:15.2.3" + "@testing-library/dom": "npm:10.0.0" "@testing-library/jest-dom": "npm:^6.1.2" "@testing-library/react": "npm:15.0.2" "@testing-library/user-event": "npm:14.5.2" "@types/d3": "npm:^7" "@types/jest": "npm:^29.5.4" "@types/lodash": "npm:4.17.0" + "@types/node": "npm:20.12.7" "@types/react": "npm:18.2.79" "@types/react-virtualized-auto-sizer": "npm:1.0.4" "@types/tinycolor2": "npm:1.4.6" babel-jest: "npm:29.7.0" d3: "npm:^7.8.5" + esbuild: "npm:0.20.2" jest: "npm:^29.6.4" jest-canvas-mock: "npm:2.5.2" lodash: "npm:4.17.21" @@ -3944,15 +3658,18 @@ __metadata: "@grafana/schema": "npm:11.1.0-pre" "@grafana/tsconfig": "npm:^1.3.0-rc1" "@grafana/ui": "npm:11.1.0-pre" + "@testing-library/dom": "npm:10.0.0" "@testing-library/jest-dom": "npm:^6.1.2" "@testing-library/react": "npm:15.0.2" "@testing-library/user-event": "npm:14.5.2" "@types/jest": "npm:^29.5.4" + "@types/node": "npm:20.12.7" "@types/react": "npm:18.2.79" "@types/systemjs": "npm:6.13.5" "@types/testing-library__jest-dom": "npm:5.14.9" jest: "npm:^29.6.4" react: "npm:18.2.0" + react-select: "npm:5.8.0" react-use: "npm:17.5.0" rxjs: "npm:7.8.1" ts-jest: "npm:29.1.2" @@ -3970,27 +3687,31 @@ __metadata: resolution: "@grafana/plugin-configs@workspace:packages/grafana-plugin-configs" dependencies: "@grafana/tsconfig": "npm:^1.3.0-rc1" + "@swc/core": "npm:1.4.2" + "@types/eslint": "npm:8.56.10" copy-webpack-plugin: "npm:12.0.2" + eslint: "npm:8.57.0" eslint-webpack-plugin: "npm:4.1.0" fork-ts-checker-webpack-plugin: "npm:9.0.2" glob: "npm:10.3.12" replace-in-file-webpack-plugin: "npm:1.0.6" swc-loader: "npm:0.2.6" tslib: "npm:2.6.2" + typescript: "npm:5.4.5" webpack: "npm:5.91.0" languageName: unknown linkType: soft -"@grafana/plugin-e2e@npm:1.1.1": - version: 1.1.1 - resolution: "@grafana/plugin-e2e@npm:1.1.1" +"@grafana/plugin-e2e@npm:1.2.0": + version: 1.2.0 + resolution: "@grafana/plugin-e2e@npm:1.2.0" dependencies: semver: "npm:^7.5.4" uuid: "npm:^9.0.1" yaml: "npm:^2.3.4" peerDependencies: "@playwright/test": ^1.41.2 - checksum: 10/81d7732fc483bf2b2dd096e3a1efa5b12a32a67a79da9e92d8d502521928b2a75c735666f6b6c0de34a0d530243789ce5e12cac7a4655d425ce2e4f0033bbb11 + checksum: 10/b7af12f214ef4daff0d917b6715fde181b29d72923d1d5c5a5d17e0f17084f23450be91e877d659aa3c2472002253d127d2465d80e25accb59154027228dbb8c languageName: node linkType: hard @@ -4043,15 +3764,15 @@ __metadata: "@typescript-eslint/eslint-plugin": "npm:6.21.0" "@typescript-eslint/parser": "npm:6.21.0" copy-webpack-plugin: "npm:12.0.2" - css-loader: "npm:6.10.0" + css-loader: "npm:7.1.1" d3: "npm:7.9.0" date-fns: "npm:3.6.0" debounce-promise: "npm:3.1.2" - esbuild: "npm:0.18.12" + esbuild: "npm:0.20.2" eslint: "npm:8.57.0" eslint-config-prettier: "npm:9.1.0" eslint-plugin-import: "npm:^2.26.0" - eslint-plugin-jest: "npm:27.9.0" + eslint-plugin-jest: "npm:28.2.0" eslint-plugin-jsdoc: "npm:48.2.3" eslint-plugin-jsx-a11y: "npm:6.8.0" eslint-plugin-lodash: "npm:7.4.0" @@ -4091,7 +3812,7 @@ __metadata: sass: "npm:1.75.0" sass-loader: "npm:14.2.1" semver: "npm:7.6.0" - style-loader: "npm:3.3.4" + style-loader: "npm:4.0.0" testing-library-selector: "npm:0.3.1" ts-node: "npm:10.9.2" tslib: "npm:2.6.2" @@ -4128,7 +3849,7 @@ __metadata: "@types/react": "npm:18.2.79" "@types/react-dom": "npm:18.2.25" "@types/systemjs": "npm:6.13.5" - esbuild: "npm:0.18.12" + esbuild: "npm:0.20.2" history: "npm:4.10.1" lodash: "npm:4.17.21" react: "npm:18.2.0" @@ -4152,18 +3873,21 @@ __metadata: version: 0.0.0-use.local resolution: "@grafana/saga-icons@workspace:packages/grafana-icons" dependencies: + "@babel/core": "npm:7.24.4" "@grafana/tsconfig": "npm:^1.3.0-rc1" "@rollup/plugin-node-resolve": "npm:^15.2.3" "@rollup/plugin-typescript": "npm:^11.1.6" "@svgr/babel-plugin-remove-jsx-attribute": "npm:^8.0.0" "@svgr/cli": "npm:^8.1.0" + "@svgr/core": "npm:8.1.0" "@svgr/plugin-jsx": "npm:^8.1.0" "@svgr/plugin-prettier": "npm:^8.1.0" "@svgr/plugin-svgo": "npm:^8.1.0" + "@types/babel__core": "npm:^7" "@types/node": "npm:20.12.7" "@types/react": "npm:18.2.79" "@types/react-dom": "npm:18.2.25" - esbuild: "npm:0.18.12" + esbuild: "npm:0.20.2" prettier: "npm:3.2.5" react: "npm:18.2.0" react-dom: "npm:18.2.0" @@ -4180,9 +3904,9 @@ __metadata: languageName: unknown linkType: soft -"@grafana/scenes@npm:^4.10.0": - version: 4.10.0 - resolution: "@grafana/scenes@npm:4.10.0" +"@grafana/scenes@npm:^4.13.0": + version: 4.13.0 + resolution: "@grafana/scenes@npm:4.13.0" dependencies: "@grafana/e2e-selectors": "npm:10.3.3" react-grid-layout: "npm:1.3.4" @@ -4196,7 +3920,7 @@ __metadata: "@grafana/ui": ^10.0.3 react: ^18.0.0 react-dom: ^18.0.0 - checksum: 10/00c2d5f4606184eac757ab01bbace495c5aacec792de3cf55a66dd4597a3ab1bf2bf97806d9271f18107d738244d6c89579fe3db2f1a52506b64243ba1a9d782 + checksum: 10/2785516164ff557b325b7425861f2aee895656ae635d9e9e6b9c882229f75ea19fa0f9402d4e827efcc46103918aa130f236ec850913e56dadc06f1ffeefb1f9 languageName: node linkType: hard @@ -4206,7 +3930,7 @@ __metadata: dependencies: "@grafana/tsconfig": "npm:^1.3.0-rc1" "@rollup/plugin-node-resolve": "npm:15.2.3" - esbuild: "npm:0.18.12" + esbuild: "npm:0.20.2" glob: "npm:^10.2.7" rimraf: "npm:5.0.5" rollup: "npm:2.79.1" @@ -4218,24 +3942,28 @@ __metadata: languageName: unknown linkType: soft -"@grafana/sql@workspace:*, @grafana/sql@workspace:packages/grafana-sql": +"@grafana/sql@npm:11.1.0-pre, @grafana/sql@workspace:*, @grafana/sql@workspace:packages/grafana-sql": version: 0.0.0-use.local resolution: "@grafana/sql@workspace:packages/grafana-sql" dependencies: "@emotion/css": "npm:11.11.2" "@grafana/data": "npm:11.1.0-pre" + "@grafana/e2e-selectors": "npm:11.1.0-pre" "@grafana/experimental": "npm:1.7.10" "@grafana/runtime": "npm:11.1.0-pre" "@grafana/tsconfig": "npm:^1.3.0-rc1" "@grafana/ui": "npm:11.1.0-pre" "@react-awesome-query-builder/ui": "npm:6.4.2" + "@testing-library/dom": "npm:10.0.0" "@testing-library/jest-dom": "npm:^6.1.2" "@testing-library/react": "npm:15.0.2" "@testing-library/react-hooks": "npm:^8.0.1" "@testing-library/user-event": "npm:14.5.2" "@types/jest": "npm:^29.5.4" "@types/lodash": "npm:4.17.0" + "@types/node": "npm:20.12.7" "@types/react": "npm:18.2.79" + "@types/react-dom": "npm:18.2.25" "@types/react-virtualized-auto-sizer": "npm:1.0.4" "@types/systemjs": "npm:6.13.5" "@types/testing-library__jest-dom": "npm:5.14.9" @@ -4244,6 +3972,8 @@ __metadata: jest: "npm:^29.6.4" lodash: "npm:4.17.21" react: "npm:18.2.0" + react-dom: "npm:18.2.0" + react-select: "npm:5.8.0" react-use: "npm:17.5.0" react-virtualized-auto-sizer: "npm:1.0.24" rxjs: "npm:7.8.1" @@ -4342,12 +4072,12 @@ __metadata: calculate-size: "npm:1.1.1" classnames: "npm:2.5.1" common-tags: "npm:1.8.2" - core-js: "npm:3.36.1" - css-loader: "npm:6.10.0" + core-js: "npm:3.37.0" + css-loader: "npm:7.1.1" csstype: "npm:3.1.3" d3: "npm:7.9.0" date-fns: "npm:3.6.0" - esbuild: "npm:0.18.12" + esbuild: "npm:0.20.2" expose-loader: "npm:5.0.0" hoist-non-react-statics: "npm:3.3.2" i18next: "npm:^23.0.0" @@ -4365,7 +4095,7 @@ __metadata: process: "npm:^0.11.10" rc-cascader: "npm:3.24.1" rc-drawer: "npm:7.1.0" - rc-slider: "npm:10.5.0" + rc-slider: "npm:10.6.2" rc-time-picker: "npm:^3.7.3" rc-tooltip: "npm:6.2.0" react: "npm:18.2.0" @@ -4377,7 +4107,7 @@ __metadata: react-dropzone: "npm:14.2.3" react-highlight-words: "npm:0.20.0" react-hook-form: "npm:^7.49.2" - react-i18next: "npm:^12.0.0" + react-i18next: "npm:^14.0.0" react-inlinesvg: "npm:3.0.2" react-loading-skeleton: "npm:3.4.0" react-router-dom: "npm:5.3.3" @@ -4403,7 +4133,7 @@ __metadata: storybook: "npm:7.4.5" storybook-addon-turbo-build: "npm:2.0.1" storybook-dark-mode: "npm:3.0.1" - style-loader: "npm:3.3.4" + style-loader: "npm:4.0.0" tinycolor2: "npm:1.6.0" tslib: "npm:2.6.2" typescript: "npm:5.4.5" @@ -5277,15 +5007,6 @@ __metadata: languageName: node linkType: hard -"@mochajs/json-file-reporter@npm:^1.2.0": - version: 1.3.0 - resolution: "@mochajs/json-file-reporter@npm:1.3.0" - peerDependencies: - mocha: 6.x || 7.x || 8.x - checksum: 10/b916b4ba048b2935026e61c09a84043f2f345c8f245a8505eb3e645fe300969fa9262fed6fc1391b25f1ea4e2120e5bfae35b33b49038d0306a44b86ef9675a3 - languageName: node - linkType: hard - "@monaco-editor/loader@npm:^1.4.0": version: 1.4.0 resolution: "@monaco-editor/loader@npm:1.4.0" @@ -5404,6 +5125,16 @@ __metadata: languageName: node linkType: hard +"@nodelib/fs.scandir@npm:3.0.0": + version: 3.0.0 + resolution: "@nodelib/fs.scandir@npm:3.0.0" + dependencies: + "@nodelib/fs.stat": "npm:3.0.0" + run-parallel: "npm:^1.2.0" + checksum: 10/5d4e48148b046afbdd7202e6484fb246412abfce6c3bec004eab663abf582e471a17d63c28b9f6638ff385a69f67c7abc4786d28e2de6b8d177687fe3b1c5a9e + languageName: node + linkType: hard + "@nodelib/fs.stat@npm:2.0.5, @nodelib/fs.stat@npm:^2.0.2": version: 2.0.5 resolution: "@nodelib/fs.stat@npm:2.0.5" @@ -5411,6 +5142,23 @@ __metadata: languageName: node linkType: hard +"@nodelib/fs.stat@npm:3.0.0": + version: 3.0.0 + resolution: "@nodelib/fs.stat@npm:3.0.0" + checksum: 10/93a93e19b64d0275b5120bed2cf85da4c5804014de1bdac6e9933b835b1cb9f88252dc990b148076bec034fc757bdd97d74cf5d99bc9f895e0f925aeabe7dbcf + languageName: node + linkType: hard + +"@nodelib/fs.walk@npm:2.0.0": + version: 2.0.0 + resolution: "@nodelib/fs.walk@npm:2.0.0" + dependencies: + "@nodelib/fs.scandir": "npm:3.0.0" + fastq: "npm:^1.15.0" + checksum: 10/0b343472c9a8feb0f928292a4990206e69117cc0b124522bbf576b04e1233d8f04575923a3badde17d74c2b5b3092fa705fad63fa612bf8fd7378b1d908d7061 + languageName: node + linkType: hard + "@nodelib/fs.walk@npm:^1.2.3, @nodelib/fs.walk@npm:^1.2.6, @nodelib/fs.walk@npm:^1.2.8": version: 1.2.8 resolution: "@nodelib/fs.walk@npm:1.2.8" @@ -6986,7 +6734,7 @@ __metadata: languageName: node linkType: hard -"@react-awesome-query-builder/core@npm:6.4.2, @react-awesome-query-builder/core@npm:^6.4.2": +"@react-awesome-query-builder/core@npm:^6.4.2": version: 6.4.2 resolution: "@react-awesome-query-builder/core@npm:6.4.2" dependencies: @@ -7117,7 +6865,7 @@ __metadata: languageName: node linkType: hard -"@remix-run/router@npm:1.14.2, @remix-run/router@npm:^1.5.0": +"@remix-run/router@npm:1.14.2": version: 1.14.2 resolution: "@remix-run/router@npm:1.14.2" checksum: 10/422844e88b985f1e287301b302c6cf8169c9eea792f80d40464f97b25393bb2e697228ebd7a7b61444d5a51c5873c4a637aad20acde5886a5caf62e833c5ceee @@ -7424,6 +7172,19 @@ __metadata: languageName: node linkType: hard +"@snyk/github-codeowners@npm:1.1.0": + version: 1.1.0 + resolution: "@snyk/github-codeowners@npm:1.1.0" + dependencies: + commander: "npm:^4.1.1" + ignore: "npm:^5.1.8" + p-map: "npm:^4.0.0" + bin: + github-codeowners: dist/cli.js + checksum: 10/34120ef622616fef1ed8af12869d8c1803842aafa3fbacca263805ee7c85f58d11bdc301ef698c9b41268b275b9fd090f5d9f6d89c556abe9d52196e72d1c510 + languageName: node + linkType: hard + "@storybook/addon-a11y@npm:7.4.5": version: 7.4.5 resolution: "@storybook/addon-a11y@npm:7.4.5" @@ -9136,6 +8897,19 @@ __metadata: languageName: node linkType: hard +"@types/babel__core@npm:^7": + version: 7.20.5 + resolution: "@types/babel__core@npm:7.20.5" + dependencies: + "@babel/parser": "npm:^7.20.7" + "@babel/types": "npm:^7.20.7" + "@types/babel__generator": "npm:*" + "@types/babel__template": "npm:*" + "@types/babel__traverse": "npm:*" + checksum: 10/c32838d280b5ab59d62557f9e331d3831f8e547ee10b4f85cb78753d97d521270cebfc73ce501e9fb27fe71884d1ba75e18658692c2f4117543f0fc4e3e118b3 + languageName: node + linkType: hard + "@types/babel__core@npm:^7.0.0, @types/babel__core@npm:^7.1.14": version: 7.20.0 resolution: "@types/babel__core@npm:7.20.0" @@ -9158,6 +8932,13 @@ __metadata: languageName: node linkType: hard +"@types/babel__preset-env@npm:^7": + version: 7.9.6 + resolution: "@types/babel__preset-env@npm:7.9.6" + checksum: 10/4783334e18e4a2e72314b900f9ac6bda002f05e91208cf88a1f631022443004f552565da4cacfebb9686ef9b06191972222d3395be1f2825cfe53cb77c5b856a + languageName: node + linkType: hard + "@types/babel__template@npm:*": version: 7.4.1 resolution: "@types/babel__template@npm:7.4.1" @@ -9203,15 +8984,6 @@ __metadata: languageName: node linkType: hard -"@types/chrome-remote-interface@npm:0.31.10": - version: 0.31.10 - resolution: "@types/chrome-remote-interface@npm:0.31.10" - dependencies: - devtools-protocol: "npm:0.0.927104" - checksum: 10/8e5632813c86e295075dc2d590668a6bc5925cae310b56be8f64c4791e550e705330b49dac6dcce5667c7124d222910bbf0bc86b89c843e0261804d6993950cd - languageName: node - linkType: hard - "@types/common-tags@npm:^1.8.0": version: 1.8.4 resolution: "@types/common-tags@npm:1.8.4" @@ -9573,9 +9345,9 @@ __metadata: linkType: hard "@types/diff@npm:^5": - version: 5.0.9 - resolution: "@types/diff@npm:5.0.9" - checksum: 10/6924740cb67a49771ea3753ee9b15c676860a6227b2bf0200ed9cef4111ff0f59fec8c51c1170bd30a8c7370b32673b308a9cd2da28525130f842194a822ef42 + version: 5.2.0 + resolution: "@types/diff@npm:5.2.0" + checksum: 10/e1d3e6e9fd9d5386496c8716dd89316288d139cd8159a064f886a079149d05d65289b7b725ce1e333d4e77ce8024e210c6e281e9875a636fc17b4c760c2cf85f languageName: node linkType: hard @@ -9915,13 +9687,6 @@ __metadata: languageName: node linkType: hard -"@types/lodash@npm:4.14.195": - version: 4.14.195 - resolution: "@types/lodash@npm:4.14.195" - checksum: 10/d7c0902684508a3d0fdb60fe939a855f9f244fd4bf828eb75388a39d00b44e955dac16faea6c90c0ae4592a57784e45ceb6d51354af8dfd401de38046c685775 - languageName: node - linkType: hard - "@types/logfmt@npm:^1.2.3": version: 1.2.6 resolution: "@types/logfmt@npm:1.2.6" @@ -10013,7 +9778,7 @@ __metadata: languageName: node linkType: hard -"@types/node-forge@npm:^1, @types/node-forge@npm:^1.3.0": +"@types/node-forge@npm:^1.3.0": version: 1.3.11 resolution: "@types/node-forge@npm:1.3.11" dependencies: @@ -10031,20 +9796,6 @@ __metadata: languageName: node linkType: hard -"@types/node@npm:18.18.4": - version: 18.18.4 - resolution: "@types/node@npm:18.18.4" - checksum: 10/2f55a7f0c603a3b56f2d24ec173d5f83be6470f638b551bab81821b6adf85d676656ed6ae267f994c029d48d4f1071b083ffdd5c7bbe7ae28533d1b4e1e83f12 - languageName: node - linkType: hard - -"@types/node@npm:^14.14.31": - version: 14.18.36 - resolution: "@types/node@npm:14.18.36" - checksum: 10/29e4c9fffea88e0b42345f76f93f4b13694dabfab6aab2fa9d05c8bea5731e878704c8bc2e25bc0f251f41e1b3fffe15263f4027a66a3de3504e12751d8ed43f - languageName: node - linkType: hard - "@types/node@npm:^16.0.0, @types/node@npm:^16.18.39": version: 16.18.50 resolution: "@types/node@npm:16.18.50" @@ -10325,7 +10076,7 @@ __metadata: languageName: node linkType: hard -"@types/semver@npm:7.5.8, @types/semver@npm:^7.3.12, @types/semver@npm:^7.3.4, @types/semver@npm:^7.5.0": +"@types/semver@npm:7.5.8, @types/semver@npm:^7.3.4, @types/semver@npm:^7.5.0": version: 7.5.8 resolution: "@types/semver@npm:7.5.8" checksum: 10/3496808818ddb36deabfe4974fd343a78101fa242c4690044ccdc3b95dcf8785b494f5d628f2f47f38a702f8db9c53c67f47d7818f2be1b79f2efb09692e1178 @@ -10463,7 +10214,7 @@ __metadata: languageName: node linkType: hard -"@types/trusted-types@npm:*, @types/trusted-types@npm:2.0.7": +"@types/trusted-types@npm:*": version: 2.0.7 resolution: "@types/trusted-types@npm:2.0.7" checksum: 10/8e4202766a65877efcf5d5a41b7dd458480b36195e580a3b1085ad21e948bc417d55d6f8af1fd2a7ad008015d4117d5fdfe432731157da3c68678487174e4ba3 @@ -10484,13 +10235,6 @@ __metadata: languageName: node linkType: hard -"@types/uuid@npm:9.0.2": - version: 9.0.2 - resolution: "@types/uuid@npm:9.0.2" - checksum: 10/1754bcf3444e1e3aeadd6e774fc328eb53bc956665e2e8fb6ec127aa8e1f43d9a224c3d22a9a6233dca8dd81a12dc7fed4d84b8876dd5ec82d40f574f7ff8b68 - languageName: node - linkType: hard - "@types/uuid@npm:9.0.8": version: 9.0.8 resolution: "@types/uuid@npm:9.0.8" @@ -10516,7 +10260,7 @@ __metadata: languageName: node linkType: hard -"@types/webpack-env@npm:1.18.4": +"@types/webpack-env@npm:^1.18.4": version: 1.18.4 resolution: "@types/webpack-env@npm:1.18.4" checksum: 10/a1c37f8eb93fa86077b2406d3b8dd50c7505cd2b6ec46044786aa9681b0114ad6c3c3cd6402f8ecce9e4de6626c2fd91e5acda62d5d450c1f531533c3812b7c0 @@ -10650,16 +10394,6 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/scope-manager@npm:5.59.9": - version: 5.59.9 - resolution: "@typescript-eslint/scope-manager@npm:5.59.9" - dependencies: - "@typescript-eslint/types": "npm:5.59.9" - "@typescript-eslint/visitor-keys": "npm:5.59.9" - checksum: 10/83b538212fc422cd6a26eee49deab60a29fa6d8bbd0dffca6daa02318959c76ddf1dc00db9ce0236258f26c1f726be78a25d2f6c5603233f591716d6299480e5 - languageName: node - linkType: hard - "@typescript-eslint/scope-manager@npm:6.18.1": version: 6.18.1 resolution: "@typescript-eslint/scope-manager@npm:6.18.1" @@ -10714,13 +10448,6 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/types@npm:5.59.9": - version: 5.59.9 - resolution: "@typescript-eslint/types@npm:5.59.9" - checksum: 10/49226e5384ac801db245fe668b4bd7610a11c5ade9c05ee93767fd188462c4d25755b8592f21210cc9856fae3c5566d4811ed0f7fefe30e48e5823e71ab4623e - languageName: node - linkType: hard - "@typescript-eslint/types@npm:6.18.1": version: 6.18.1 resolution: "@typescript-eslint/types@npm:6.18.1" @@ -10735,24 +10462,6 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/typescript-estree@npm:5.59.9": - version: 5.59.9 - resolution: "@typescript-eslint/typescript-estree@npm:5.59.9" - dependencies: - "@typescript-eslint/types": "npm:5.59.9" - "@typescript-eslint/visitor-keys": "npm:5.59.9" - debug: "npm:^4.3.4" - globby: "npm:^11.1.0" - is-glob: "npm:^4.0.3" - semver: "npm:^7.3.7" - tsutils: "npm:^3.21.0" - peerDependenciesMeta: - typescript: - optional: true - checksum: 10/79cf330815244f2ab12762df9296812c20f3ff859f14dc997a79ce09eabd7c8d0d190ed00fcdf380288a2b4035ca40c9f0002dc9c6c2875885ad3b94c2eab58b - languageName: node - linkType: hard - "@typescript-eslint/typescript-estree@npm:6.18.1": version: 6.18.1 resolution: "@typescript-eslint/typescript-estree@npm:6.18.1" @@ -10825,34 +10534,6 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/utils@npm:^5.10.0": - version: 5.59.9 - resolution: "@typescript-eslint/utils@npm:5.59.9" - dependencies: - "@eslint-community/eslint-utils": "npm:^4.2.0" - "@types/json-schema": "npm:^7.0.9" - "@types/semver": "npm:^7.3.12" - "@typescript-eslint/scope-manager": "npm:5.59.9" - "@typescript-eslint/types": "npm:5.59.9" - "@typescript-eslint/typescript-estree": "npm:5.59.9" - eslint-scope: "npm:^5.1.1" - semver: "npm:^7.3.7" - peerDependencies: - eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 - checksum: 10/e48429d9dd83d7ae1b95c64b35af790e36cd8c1b2b9b63b2f69b5f804bb58a12918396f2f0540afd413673e1e0d22399a2cd2e2ad6534e50af2990a04e8ca7c4 - languageName: node - linkType: hard - -"@typescript-eslint/visitor-keys@npm:5.59.9": - version: 5.59.9 - resolution: "@typescript-eslint/visitor-keys@npm:5.59.9" - dependencies: - "@typescript-eslint/types": "npm:5.59.9" - eslint-visitor-keys: "npm:^3.3.0" - checksum: 10/85761ef0be6910cb4de841b3cd8f39a734f5373ed92f808365882ef357f0a33ad6f75c4b3bf0b408f0399781ac5d14f12033de3e4b53a46b61015444b05854c0 - languageName: node - linkType: hard - "@typescript-eslint/visitor-keys@npm:6.18.1": version: 6.18.1 resolution: "@typescript-eslint/visitor-keys@npm:6.18.1" @@ -11020,7 +10701,7 @@ __metadata: languageName: node linkType: hard -"@webassemblyjs/ast@npm:1.12.1, @webassemblyjs/ast@npm:^1.11.5, @webassemblyjs/ast@npm:^1.12.1": +"@webassemblyjs/ast@npm:1.12.1, @webassemblyjs/ast@npm:^1.12.1": version: 1.12.1 resolution: "@webassemblyjs/ast@npm:1.12.1" dependencies: @@ -11106,7 +10787,7 @@ __metadata: languageName: node linkType: hard -"@webassemblyjs/wasm-edit@npm:^1.11.5, @webassemblyjs/wasm-edit@npm:^1.12.1": +"@webassemblyjs/wasm-edit@npm:^1.12.1": version: 1.12.1 resolution: "@webassemblyjs/wasm-edit@npm:1.12.1" dependencies: @@ -11147,7 +10828,7 @@ __metadata: languageName: node linkType: hard -"@webassemblyjs/wasm-parser@npm:1.12.1, @webassemblyjs/wasm-parser@npm:^1.11.5, @webassemblyjs/wasm-parser@npm:^1.12.1": +"@webassemblyjs/wasm-parser@npm:1.12.1, @webassemblyjs/wasm-parser@npm:^1.12.1": version: 1.12.1 resolution: "@webassemblyjs/wasm-parser@npm:1.12.1" dependencies: @@ -11527,7 +11208,7 @@ __metadata: languageName: node linkType: hard -"ajv@npm:^6.12.3, ajv@npm:^6.12.4, ajv@npm:^6.12.5": +"ajv@npm:^6.12.4, ajv@npm:^6.12.5": version: 6.12.6 resolution: "ajv@npm:6.12.6" dependencies: @@ -11579,7 +11260,7 @@ __metadata: languageName: node linkType: hard -"ansi-colors@npm:4.1.1, ansi-colors@npm:^4.1.1": +"ansi-colors@npm:^4.1.1": version: 4.1.1 resolution: "ansi-colors@npm:4.1.1" checksum: 10/e862fddd0a9ca88f1e7c9312ea70674cec3af360c994762309f6323730525e92c77d2715ee5f08aa8f438b7ca18efe378af647f501fc92b15b8e4b3b52d09db4 @@ -11789,6 +11470,13 @@ __metadata: languageName: node linkType: hard +"arity-n@npm:^1.0.4": + version: 1.0.4 + resolution: "arity-n@npm:1.0.4" + checksum: 10/bc7089228f47f4dc40e2fe77f0c1e351e64b4ef175ee59052fdce5778661a8e11da8e7a01820fad3cee66f3b756fc53ca4c5c117fd6e21e9c9c44b7b9f8a88eb + languageName: node + linkType: hard + "array-buffer-byte-length@npm:^1.0.1": version: 1.0.1 resolution: "array-buffer-byte-length@npm:1.0.1" @@ -11833,6 +11521,15 @@ __metadata: languageName: node linkType: hard +"array-last@npm:^1.1.1": + version: 1.3.0 + resolution: "array-last@npm:1.3.0" + dependencies: + is-number: "npm:^4.0.0" + checksum: 10/7631c7df9b44ea26f49e2f6eeb7a7d4d95b3798586b917e1efae4a321b6362e449e00b011e88eb0260959fbfc940fbdfce1d2a35765ea080de6d71e3fc3cf1dd + languageName: node + linkType: hard + "array-tree-filter@npm:^2.1.0": version: 2.1.0 resolution: "array-tree-filter@npm:2.1.0" @@ -11952,13 +11649,6 @@ __metadata: languageName: node linkType: hard -"asap@npm:~1.0.0": - version: 1.0.0 - resolution: "asap@npm:1.0.0" - checksum: 10/2844bc7ea09aa1840d7b415cb5f3b3a2a3ecd35c7b865a94a2ed4f09d2af763001d70951f950e495ee0a54b8284ffc8c01d2f39ecb0ae7d48ce74783d7cf9279 - languageName: node - linkType: hard - "asn1@npm:~0.2.3": version: 0.2.6 resolution: "asn1@npm:0.2.6" @@ -12288,7 +11978,7 @@ __metadata: languageName: node linkType: hard -"babel-plugin-polyfill-corejs2@npm:^0.4.10, babel-plugin-polyfill-corejs2@npm:^0.4.6": +"babel-plugin-polyfill-corejs2@npm:^0.4.10": version: 0.4.10 resolution: "babel-plugin-polyfill-corejs2@npm:0.4.10" dependencies: @@ -12313,29 +12003,6 @@ __metadata: languageName: node linkType: hard -"babel-plugin-polyfill-corejs3@npm:^0.8.5": - version: 0.8.7 - resolution: "babel-plugin-polyfill-corejs3@npm:0.8.7" - dependencies: - "@babel/helper-define-polyfill-provider": "npm:^0.4.4" - core-js-compat: "npm:^3.33.1" - peerDependencies: - "@babel/core": ^7.4.0 || ^8.0.0-0 <8.0.0 - checksum: 10/defbc6de3d309c9639dd31223b5011707fcc0384037ac5959a1aefe16eb314562e1c1e5cfbce0af14a220d639ef92dfe5baf66664e9e6054656aca2841677622 - languageName: node - linkType: hard - -"babel-plugin-polyfill-regenerator@npm:^0.5.3": - version: 0.5.5 - resolution: "babel-plugin-polyfill-regenerator@npm:0.5.5" - dependencies: - "@babel/helper-define-polyfill-provider": "npm:^0.5.0" - peerDependencies: - "@babel/core": ^7.4.0 || ^8.0.0-0 <8.0.0 - checksum: 10/3a9b4828673b23cd648dcfb571eadcd9d3fadfca0361d0a7c6feeb5a30474e92faaa49f067a6e1c05e49b6a09812879992028ff3ef3446229ff132d6e1de7eb6 - languageName: node - linkType: hard - "babel-plugin-polyfill-regenerator@npm:^0.6.1": version: 0.6.1 resolution: "babel-plugin-polyfill-regenerator@npm:0.6.1" @@ -12409,6 +12076,15 @@ __metadata: languageName: node linkType: hard +"babylon@npm:^6.9.1": + version: 6.18.0 + resolution: "babylon@npm:6.18.0" + bin: + babylon: ./bin/babylon.js + checksum: 10/b35e415886a012545305eede2fd3cbd6ec7c54ed0b19e74f9c3478831fef9bbc24f1c3917e29b338d76d8e58ad1c895a296e27c8f76cef4f3be1ccaad3bfaecb + languageName: node + linkType: hard + "balanced-match@npm:^1.0.0": version: 1.0.2 resolution: "balanced-match@npm:1.0.2" @@ -12528,19 +12204,6 @@ __metadata: languageName: node linkType: hard -"blink-diff@npm:1.0.13": - version: 1.0.13 - resolution: "blink-diff@npm:1.0.13" - dependencies: - pngjs-image: "npm:~0.11.5" - preceptor-core: "npm:~0.10.0" - promise: "npm:6.0.0" - bin: - blink-diff: bin/blink-diff - checksum: 10/fec6001ba4942217545b476031472e18bb2916139be1ed210d285ce04c79408f16223c5efccb37a54e9ef22f52d0c4e6e313ea846dfc00139e2a19124f18f281 - languageName: node - linkType: hard - "blob-polyfill@npm:7.0.20220408": version: 7.0.20220408 resolution: "blob-polyfill@npm:7.0.20220408" @@ -12569,21 +12232,23 @@ __metadata: languageName: node linkType: hard -"body-parser@npm:1.19.2": - version: 1.19.2 - resolution: "body-parser@npm:1.19.2" +"body-parser@npm:1.20.2": + version: 1.20.2 + resolution: "body-parser@npm:1.20.2" dependencies: bytes: "npm:3.1.2" - content-type: "npm:~1.0.4" + content-type: "npm:~1.0.5" debug: "npm:2.6.9" - depd: "npm:~1.1.2" - http-errors: "npm:1.8.1" + depd: "npm:2.0.0" + destroy: "npm:1.2.0" + http-errors: "npm:2.0.0" iconv-lite: "npm:0.4.24" - on-finished: "npm:~2.3.0" - qs: "npm:6.9.7" - raw-body: "npm:2.4.3" + on-finished: "npm:2.4.1" + qs: "npm:6.11.0" + raw-body: "npm:2.5.2" type-is: "npm:~1.6.18" - checksum: 10/8a5f59d7e51b4000082a5e8bccf548cdbe77140520a55e380fecf5becf87a950b39e157a5c2a13334aa2ad45cc850b98e6c8652a4f033581c552001176dc9fc0 + unpipe: "npm:1.0.0" + checksum: 10/3cf171b82190cf91495c262b073e425fc0d9e25cc2bf4540d43f7e7bbca27d6a9eae65ca367b6ef3993eea261159d9d2ab37ce444e8979323952e12eb3df319a languageName: node linkType: hard @@ -12695,13 +12360,6 @@ __metadata: languageName: node linkType: hard -"browser-stdout@npm:1.3.1": - version: 1.3.1 - resolution: "browser-stdout@npm:1.3.1" - checksum: 10/ac70a84e346bb7afc5045ec6f22f6a681b15a4057447d4cc1c48a25c6dedb302a49a46dd4ddfb5cdd9c96e0c905a8539be1b98ae7bc440512152967009ec7015 - languageName: node - linkType: hard - "browserify-zlib@npm:^0.1.4": version: 0.1.4 resolution: "browserify-zlib@npm:0.1.4" @@ -12711,7 +12369,7 @@ __metadata: languageName: node linkType: hard -"browserslist@npm:^4.0.0, browserslist@npm:^4.14.5, browserslist@npm:^4.21.10, browserslist@npm:^4.21.4, browserslist@npm:^4.22.2, browserslist@npm:^4.23.0": +"browserslist@npm:^4.0.0, browserslist@npm:^4.21.10, browserslist@npm:^4.21.4, browserslist@npm:^4.22.2, browserslist@npm:^4.23.0": version: 4.23.0 resolution: "browserslist@npm:4.23.0" dependencies: @@ -13003,7 +12661,7 @@ __metadata: languageName: node linkType: hard -"camelcase@npm:^6.0.0, camelcase@npm:^6.2.0": +"camelcase@npm:^6.2.0": version: 6.3.0 resolution: "camelcase@npm:6.3.0" checksum: 10/8c96818a9076434998511251dcb2761a94817ea17dbdc37f47ac080bd088fc62c7369429a19e2178b993497132c8cbcf5cc1f44ba963e76782ba469c0474938d @@ -13216,25 +12874,6 @@ __metadata: languageName: node linkType: hard -"chokidar@npm:3.5.3": - version: 3.5.3 - resolution: "chokidar@npm:3.5.3" - dependencies: - anymatch: "npm:~3.1.2" - braces: "npm:~3.0.2" - fsevents: "npm:~2.3.2" - glob-parent: "npm:~5.1.2" - is-binary-path: "npm:~2.1.0" - is-glob: "npm:~4.0.1" - normalize-path: "npm:~3.0.0" - readdirp: "npm:~3.6.0" - dependenciesMeta: - fsevents: - optional: true - checksum: 10/863e3ff78ee7a4a24513d2a416856e84c8e4f5e60efbe03e8ab791af1a183f569b62fc6f6b8044e2804966cb81277ddbbc1dc374fba3265bd609ea8efd62f5b3 - languageName: node - linkType: hard - "chokidar@npm:>=3.0.0 <4.0.0, chokidar@npm:^3.3.1, chokidar@npm:^3.5.3, chokidar@npm:^3.6.0": version: 3.6.0 resolution: "chokidar@npm:3.6.0" @@ -13629,13 +13268,6 @@ __metadata: languageName: node linkType: hard -"commander@npm:8.3.0, commander@npm:^8.1.0, commander@npm:^8.3.0": - version: 8.3.0 - resolution: "commander@npm:8.3.0" - checksum: 10/6b7b5d334483ce24bd73c5dac2eab901a7dbb25fd983ea24a1eeac6e7166bb1967f641546e8abf1920afbde86a45fbfe5812fbc69d0dc451bb45ca416a12a3a3 - languageName: node - linkType: hard - "commander@npm:^10.0.0, commander@npm:^10.0.1": version: 10.0.1 resolution: "commander@npm:10.0.1" @@ -13650,10 +13282,10 @@ __metadata: languageName: node linkType: hard -"commander@npm:^5.1.0": - version: 5.1.0 - resolution: "commander@npm:5.1.0" - checksum: 10/3e2ef5c003c5179250161e42ce6d48e0e69a54af970c65b7f985c70095240c260fd647453efd4c2c5a31b30ce468f373dc70f769c2f54a2c014abc4792aaca28 +"commander@npm:^4.1.1": + version: 4.1.1 + resolution: "commander@npm:4.1.1" + checksum: 10/3b2dc4125f387dab73b3294dbcb0ab2a862f9c0ad748ee2b27e3544d25325b7a8cdfbcc228d103a98a716960b14478114a5206b5415bd48cdafa38797891562c languageName: node linkType: hard @@ -13664,6 +13296,13 @@ __metadata: languageName: node linkType: hard +"commander@npm:^8.1.0, commander@npm:^8.3.0": + version: 8.3.0 + resolution: "commander@npm:8.3.0" + checksum: 10/6b7b5d334483ce24bd73c5dac2eab901a7dbb25fd983ea24a1eeac6e7166bb1967f641546e8abf1920afbde86a45fbfe5812fbc69d0dc451bb45ca416a12a3a3 + languageName: node + linkType: hard + "commander@npm:^9.4.1": version: 9.5.0 resolution: "commander@npm:9.5.0" @@ -13753,6 +13392,15 @@ __metadata: languageName: node linkType: hard +"compose-function@npm:^3.0.3": + version: 3.0.3 + resolution: "compose-function@npm:3.0.3" + dependencies: + arity-n: "npm:^1.0.4" + checksum: 10/9f17d431e3ee4797c844f2870e13494079882ac3dbc54c143b7d99967b371908e0ce7ceb71c6aed61e2ecddbcd7bb437d91428a3d0e6569aee17a87fcbc7918f + languageName: node + linkType: hard + "compressible@npm:~2.0.16": version: 2.0.18 resolution: "compressible@npm:2.0.18" @@ -13845,10 +13493,10 @@ __metadata: languageName: node linkType: hard -"content-type@npm:~1.0.4": - version: 1.0.4 - resolution: "content-type@npm:1.0.4" - checksum: 10/5ea85c5293475c0cdf2f84e2c71f0519ced565840fb8cbda35997cb67cc45b879d5b9dbd37760c4041ca7415a3687f8a5f2f87b556b2aaefa49c0f3436a346d4 +"content-type@npm:~1.0.4, content-type@npm:~1.0.5": + version: 1.0.5 + resolution: "content-type@npm:1.0.5" + checksum: 10/585847d98dc7fb8035c02ae2cb76c7a9bd7b25f84c447e5ed55c45c2175e83617c8813871b4ee22f368126af6b2b167df655829007b21aa10302873ea9c62662 languageName: node linkType: hard @@ -13968,10 +13616,10 @@ __metadata: languageName: node linkType: hard -"cookie@npm:0.4.2": - version: 0.4.2 - resolution: "cookie@npm:0.4.2" - checksum: 10/2e1de9fdedca54881eab3c0477aeb067f281f3155d9cfee9d28dfb252210d09e85e9d175c0a60689661feb9e35e588515352f2456bc1f8e8db4267e05fd70137 +"cookie@npm:0.6.0": + version: 0.6.0 + resolution: "cookie@npm:0.6.0" + checksum: 10/c1f8f2ea7d443b9331680598b0ae4e6af18a618c37606d1bbdc75bec8361cce09fe93e727059a673f2ba24467131a9fb5a4eec76bb1b149c1b3e1ccb268dc583 languageName: node linkType: hard @@ -14007,7 +13655,7 @@ __metadata: languageName: node linkType: hard -"core-js-compat@npm:^3.31.0, core-js-compat@npm:^3.33.1, core-js-compat@npm:^3.36.1": +"core-js-compat@npm:^3.31.0, core-js-compat@npm:^3.36.1": version: 3.36.1 resolution: "core-js-compat@npm:3.36.1" dependencies: @@ -14023,10 +13671,10 @@ __metadata: languageName: node linkType: hard -"core-js@npm:3.36.1, core-js@npm:^3.6.0, core-js@npm:^3.8.3": - version: 3.36.1 - resolution: "core-js@npm:3.36.1" - checksum: 10/ce1e1bfc1034b6f2ff7c91077319e8abdd650ee606ffe6e80073e64ab9d8aad2d6a6d953461b01f331a6f796ad2fd766a3386b88aa371b45d44fa7c0b9913ce6 +"core-js@npm:3.37.0, core-js@npm:^3.6.0, core-js@npm:^3.8.3": + version: 3.37.0 + resolution: "core-js@npm:3.37.0" + checksum: 10/97feac0b54b95d928bda6a6e611cf34963a265a5fe8ab46ed35bbc9d32a14221bf6bede5d6cd4b0c0f30e8440cf1eff0c4f0c242d719c561e5dd73d3b005d63c languageName: node linkType: hard @@ -14270,7 +13918,31 @@ __metadata: languageName: node linkType: hard -"css-loader@npm:6.10.0, css-loader@npm:^6.7.1": +"css-loader@npm:7.1.1": + version: 7.1.1 + resolution: "css-loader@npm:7.1.1" + dependencies: + icss-utils: "npm:^5.1.0" + postcss: "npm:^8.4.33" + postcss-modules-extract-imports: "npm:^3.1.0" + postcss-modules-local-by-default: "npm:^4.0.5" + postcss-modules-scope: "npm:^3.2.0" + postcss-modules-values: "npm:^4.0.0" + postcss-value-parser: "npm:^4.2.0" + semver: "npm:^7.5.4" + peerDependencies: + "@rspack/core": 0.x || 1.x + webpack: ^5.27.0 + peerDependenciesMeta: + "@rspack/core": + optional: true + webpack: + optional: true + checksum: 10/435a21f19594f89e4d5da51f4d6d2de4d25d6f882117890875f6529e99fbe931ea258662fb680b70e7ccab2fd723084f2c3fff022c76d45c38893ae50ab6f08e + languageName: node + linkType: hard + +"css-loader@npm:^6.7.1": version: 6.10.0 resolution: "css-loader@npm:6.10.0" dependencies: @@ -14617,58 +14289,6 @@ __metadata: languageName: node linkType: hard -"cypress@npm:9.5.1": - version: 9.5.1 - resolution: "cypress@npm:9.5.1" - dependencies: - "@cypress/request": "npm:^2.88.10" - "@cypress/xvfb": "npm:^1.2.4" - "@types/node": "npm:^14.14.31" - "@types/sinonjs__fake-timers": "npm:8.1.1" - "@types/sizzle": "npm:^2.3.2" - arch: "npm:^2.2.0" - blob-util: "npm:^2.0.2" - bluebird: "npm:^3.7.2" - buffer: "npm:^5.6.0" - cachedir: "npm:^2.3.0" - chalk: "npm:^4.1.0" - check-more-types: "npm:^2.24.0" - cli-cursor: "npm:^3.1.0" - cli-table3: "npm:~0.6.1" - commander: "npm:^5.1.0" - common-tags: "npm:^1.8.0" - dayjs: "npm:^1.10.4" - debug: "npm:^4.3.2" - enquirer: "npm:^2.3.6" - eventemitter2: "npm:^6.4.3" - execa: "npm:4.1.0" - executable: "npm:^4.1.1" - extract-zip: "npm:2.0.1" - figures: "npm:^3.2.0" - fs-extra: "npm:^9.1.0" - getos: "npm:^3.2.1" - is-ci: "npm:^3.0.0" - is-installed-globally: "npm:~0.4.0" - lazy-ass: "npm:^1.6.0" - listr2: "npm:^3.8.3" - lodash: "npm:^4.17.21" - log-symbols: "npm:^4.0.0" - minimist: "npm:^1.2.5" - ospath: "npm:^1.2.2" - pretty-bytes: "npm:^5.6.0" - proxy-from-env: "npm:1.0.0" - request-progress: "npm:^3.0.0" - semver: "npm:^7.3.2" - supports-color: "npm:^8.1.1" - tmp: "npm:~0.2.1" - untildify: "npm:^4.0.0" - yauzl: "npm:^2.10.0" - bin: - cypress: bin/cypress - checksum: 10/a5c84a1bc40ffa3e540f00b61e03caf977e7119318bebc5ac6fa0689eda4591a54f17d1b5883e016eabd3c9d704faafdf8c5aa8eee50369815606f52aa250bea - languageName: node - linkType: hard - "d3-array@npm:2 - 3, d3-array@npm:2.10.0 - 3, d3-array@npm:2.5.0 - 3, d3-array@npm:3, d3-array@npm:^3.2.0": version: 3.2.2 resolution: "d3-array@npm:3.2.2" @@ -15102,13 +14722,6 @@ __metadata: languageName: node linkType: hard -"date-format@npm:^0.0.0": - version: 0.0.0 - resolution: "date-format@npm:0.0.0" - checksum: 10/5521db0145ab6fc16c43f409be0cc08bfc204a9204a442165eda0f45afc89c4c51d8aea3848d7eb68b3c998b208f67e5482ee57e7f3ec394accf21a87e2c7882 - languageName: node - linkType: hard - "dateformat@npm:^3.0.3": version: 3.0.3 resolution: "dateformat@npm:3.0.3" @@ -15153,7 +14766,7 @@ __metadata: languageName: node linkType: hard -"debug@npm:4, debug@npm:4.3.4, debug@npm:^4.1.0, debug@npm:^4.1.1, debug@npm:^4.3.1, debug@npm:^4.3.2, debug@npm:^4.3.3, debug@npm:^4.3.4": +"debug@npm:4, debug@npm:^4.1.0, debug@npm:^4.1.1, debug@npm:^4.3.1, debug@npm:^4.3.2, debug@npm:^4.3.3, debug@npm:^4.3.4": version: 4.3.4 resolution: "debug@npm:4.3.4" dependencies: @@ -15191,13 +14804,6 @@ __metadata: languageName: node linkType: hard -"decamelize@npm:^4.0.0": - version: 4.0.0 - resolution: "decamelize@npm:4.0.0" - checksum: 10/b7d09b82652c39eead4d6678bb578e3bebd848add894b76d0f6b395bc45b2d692fb88d977e7cfb93c4ed6c119b05a1347cef261174916c2e75c0a8ca57da1809 - languageName: node - linkType: hard - "decimal.js@npm:^10.4.1": version: 10.4.2 resolution: "decimal.js@npm:10.4.2" @@ -15231,6 +14837,13 @@ __metadata: languageName: node linkType: hard +"deep-freeze@npm:0.0.1": + version: 0.0.1 + resolution: "deep-freeze@npm:0.0.1" + checksum: 10/40a3cc1be9db9c09e23097ff51d7f842a950d4d55e9561cb77ded051de4381fe7d077be939cb9545013e0215e577dc3dbd21cba9972f7dad179a2d3c635d1a9b + languageName: node + linkType: hard + "deep-is@npm:^0.1.3": version: 0.1.4 resolution: "deep-is@npm:0.1.4" @@ -15379,6 +14992,13 @@ __metadata: languageName: node linkType: hard +"depd@npm:2.0.0": + version: 2.0.0 + resolution: "depd@npm:2.0.0" + checksum: 10/c0c8ff36079ce5ada64f46cc9d6fd47ebcf38241105b6e0c98f412e8ad91f084bcf906ff644cc3a4bd876ca27a62accb8b0fff72ea6ed1a414b89d8506f4a5ca + languageName: node + linkType: hard + "depd@npm:^1.1.2, depd@npm:~1.1.2": version: 1.1.2 resolution: "depd@npm:1.1.2" @@ -15400,10 +15020,10 @@ __metadata: languageName: node linkType: hard -"destroy@npm:~1.0.4": - version: 1.0.4 - resolution: "destroy@npm:1.0.4" - checksum: 10/da9ab4961dc61677c709da0c25ef01733042614453924d65636a7db37308fef8a24cd1e07172e61173d471ca175371295fbc984b0af5b2b4ff47cd57bd784c03 +"destroy@npm:1.2.0": + version: 1.2.0 + resolution: "destroy@npm:1.2.0" + checksum: 10/0acb300b7478a08b92d810ab229d5afe0d2f4399272045ab22affa0d99dbaf12637659411530a6fcd597a9bdac718fc94373a61a95b4651bbc7b83684a565e38 languageName: node linkType: hard @@ -15471,20 +15091,6 @@ __metadata: languageName: node linkType: hard -"devtools-protocol@npm:0.0.1170333": - version: 0.0.1170333 - resolution: "devtools-protocol@npm:0.0.1170333" - checksum: 10/ef33b830d27ab7e4c98931559f095987f7cb07295a12b0a7be2ac06d40388f276c0e2de51e86b9e83ba365ad0ea6d683919432d3dea7b3947f8592b369d4b489 - languageName: node - linkType: hard - -"devtools-protocol@npm:0.0.927104": - version: 0.0.927104 - resolution: "devtools-protocol@npm:0.0.927104" - checksum: 10/2f645d5d8f8df8d520c7162264ad37caed40de6c57456febb42e22cc54d527a349d05ab16f4d9f9bbd715ce425b00232b6baa66ad339bb3caa077528684ce8ca - languageName: node - linkType: hard - "diff-sequences@npm:^27.5.1": version: 27.5.1 resolution: "diff-sequences@npm:27.5.1" @@ -15499,13 +15105,6 @@ __metadata: languageName: node linkType: hard -"diff@npm:5.0.0": - version: 5.0.0 - resolution: "diff@npm:5.0.0" - checksum: 10/4a179a75b17cbb420eb9145be913f9ddb34b47cb2ba4301e80ae745122826a468f02ca8f5e56945958de26ace594899c8381acb6659c88e7803ef078b53d690c - languageName: node - linkType: hard - "diff@npm:^4.0.1": version: 4.0.2 resolution: "diff@npm:4.0.2" @@ -15788,6 +15387,19 @@ __metadata: languageName: node linkType: hard +"easy-table@npm:1.2.0": + version: 1.2.0 + resolution: "easy-table@npm:1.2.0" + dependencies: + ansi-regex: "npm:^5.0.1" + wcwidth: "npm:^1.0.1" + dependenciesMeta: + wcwidth: + optional: true + checksum: 10/0d1be7cd9419cd1b56ca5a978646b3cff241ccd8cf95bdb2742f36854084b3aef2e9af6ec14142855aa80e4cab1f4baad0f610a99c77509f23676b8330730177 + languageName: node + linkType: hard + "ecc-jsbn@npm:~0.1.1": version: 0.1.2 resolution: "ecc-jsbn@npm:0.1.2" @@ -15858,13 +15470,6 @@ __metadata: languageName: node linkType: hard -"emotion@npm:11.0.0": - version: 11.0.0 - resolution: "emotion@npm:11.0.0" - checksum: 10/ab160e73785c49216652ba85e33db6d4426fd7cb28bd5341fea42542e4e24dd671e508226bf4f8dbd99b946fff2e1f1441c58169356659e60f89dc2276f97993 - languageName: node - linkType: hard - "emotion@npm:^10.0.14": version: 10.0.27 resolution: "emotion@npm:10.0.27" @@ -15911,18 +15516,7 @@ __metadata: languageName: node linkType: hard -"enhanced-resolve@npm:^4.0.0": - version: 4.5.0 - resolution: "enhanced-resolve@npm:4.5.0" - dependencies: - graceful-fs: "npm:^4.1.2" - memory-fs: "npm:^0.5.0" - tapable: "npm:^1.0.0" - checksum: 10/ae19d36c0faf6b3f66033f9e639a4358ff28c0dbb28438b1c5ab2ba04b7158fba27f4adbc4ae4116a2683b3f7063586ba8d9af2e7625fd5184ecdc83a2a0723a - languageName: node - linkType: hard - -"enhanced-resolve@npm:^5.15.0, enhanced-resolve@npm:^5.16.0": +"enhanced-resolve@npm:^5.16.0": version: 5.16.0 resolution: "enhanced-resolve@npm:5.16.0" dependencies: @@ -15992,17 +15586,6 @@ __metadata: languageName: node linkType: hard -"errno@npm:^0.1.3": - version: 0.1.8 - resolution: "errno@npm:0.1.8" - dependencies: - prr: "npm:~1.0.1" - bin: - errno: cli.js - checksum: 10/93076ed11bedb8f0389cbefcbdd3445f66443159439dccbaac89a053428ad92147676736235d275612dc0296d3f9a7e6b7177ed78a566b6cd15dacd4fa0d5888 - languageName: node - linkType: hard - "error-ex@npm:^1.3.1": version: 1.3.2 resolution: "error-ex@npm:1.3.2" @@ -16224,110 +15807,33 @@ __metadata: languageName: node linkType: hard -"esbuild@npm:0.18.12": - version: 0.18.12 - resolution: "esbuild@npm:0.18.12" +"esbuild@npm:0.20.2, esbuild@npm:^0.20.0, esbuild@npm:^0.20.1": + version: 0.20.2 + resolution: "esbuild@npm:0.20.2" dependencies: - "@esbuild/android-arm": "npm:0.18.12" - "@esbuild/android-arm64": "npm:0.18.12" - "@esbuild/android-x64": "npm:0.18.12" - "@esbuild/darwin-arm64": "npm:0.18.12" - "@esbuild/darwin-x64": "npm:0.18.12" - "@esbuild/freebsd-arm64": "npm:0.18.12" - "@esbuild/freebsd-x64": "npm:0.18.12" - "@esbuild/linux-arm": "npm:0.18.12" - "@esbuild/linux-arm64": "npm:0.18.12" - "@esbuild/linux-ia32": "npm:0.18.12" - "@esbuild/linux-loong64": "npm:0.18.12" - "@esbuild/linux-mips64el": "npm:0.18.12" - "@esbuild/linux-ppc64": "npm:0.18.12" - "@esbuild/linux-riscv64": "npm:0.18.12" - "@esbuild/linux-s390x": "npm:0.18.12" - "@esbuild/linux-x64": "npm:0.18.12" - "@esbuild/netbsd-x64": "npm:0.18.12" - "@esbuild/openbsd-x64": "npm:0.18.12" - "@esbuild/sunos-x64": "npm:0.18.12" - "@esbuild/win32-arm64": "npm:0.18.12" - "@esbuild/win32-ia32": "npm:0.18.12" - "@esbuild/win32-x64": "npm:0.18.12" - dependenciesMeta: - "@esbuild/android-arm": - optional: true - "@esbuild/android-arm64": - optional: true - "@esbuild/android-x64": - optional: true - "@esbuild/darwin-arm64": - optional: true - "@esbuild/darwin-x64": - optional: true - "@esbuild/freebsd-arm64": - optional: true - "@esbuild/freebsd-x64": - optional: true - "@esbuild/linux-arm": - optional: true - "@esbuild/linux-arm64": - optional: true - "@esbuild/linux-ia32": - optional: true - "@esbuild/linux-loong64": - optional: true - "@esbuild/linux-mips64el": - optional: true - "@esbuild/linux-ppc64": - optional: true - "@esbuild/linux-riscv64": - optional: true - "@esbuild/linux-s390x": - optional: true - "@esbuild/linux-x64": - optional: true - "@esbuild/netbsd-x64": - optional: true - "@esbuild/openbsd-x64": - optional: true - "@esbuild/sunos-x64": - optional: true - "@esbuild/win32-arm64": - optional: true - "@esbuild/win32-ia32": - optional: true - "@esbuild/win32-x64": - optional: true - bin: - esbuild: bin/esbuild - checksum: 10/724411d6dc01ac4ee9a6a7f9f735765f81383c9f2a1cbc35dcccf558cd5bc13f0913ff9a3a3eab0dfe3db20f85991e9f60f59a5d709443ed2179d525722d17e9 - languageName: node - linkType: hard - -"esbuild@npm:0.20.1, esbuild@npm:^0.20.0, esbuild@npm:^0.20.1": - version: 0.20.1 - resolution: "esbuild@npm:0.20.1" - dependencies: - "@esbuild/aix-ppc64": "npm:0.20.1" - "@esbuild/android-arm": "npm:0.20.1" - "@esbuild/android-arm64": "npm:0.20.1" - "@esbuild/android-x64": "npm:0.20.1" - "@esbuild/darwin-arm64": "npm:0.20.1" - "@esbuild/darwin-x64": "npm:0.20.1" - "@esbuild/freebsd-arm64": "npm:0.20.1" - "@esbuild/freebsd-x64": "npm:0.20.1" - "@esbuild/linux-arm": "npm:0.20.1" - "@esbuild/linux-arm64": "npm:0.20.1" - "@esbuild/linux-ia32": "npm:0.20.1" - "@esbuild/linux-loong64": "npm:0.20.1" - "@esbuild/linux-mips64el": "npm:0.20.1" - "@esbuild/linux-ppc64": "npm:0.20.1" - "@esbuild/linux-riscv64": "npm:0.20.1" - "@esbuild/linux-s390x": "npm:0.20.1" - "@esbuild/linux-x64": "npm:0.20.1" - "@esbuild/netbsd-x64": "npm:0.20.1" - "@esbuild/openbsd-x64": "npm:0.20.1" - "@esbuild/sunos-x64": "npm:0.20.1" - "@esbuild/win32-arm64": "npm:0.20.1" - "@esbuild/win32-ia32": "npm:0.20.1" - "@esbuild/win32-x64": "npm:0.20.1" + "@esbuild/aix-ppc64": "npm:0.20.2" + "@esbuild/android-arm": "npm:0.20.2" + "@esbuild/android-arm64": "npm:0.20.2" + "@esbuild/android-x64": "npm:0.20.2" + "@esbuild/darwin-arm64": "npm:0.20.2" + "@esbuild/darwin-x64": "npm:0.20.2" + "@esbuild/freebsd-arm64": "npm:0.20.2" + "@esbuild/freebsd-x64": "npm:0.20.2" + "@esbuild/linux-arm": "npm:0.20.2" + "@esbuild/linux-arm64": "npm:0.20.2" + "@esbuild/linux-ia32": "npm:0.20.2" + "@esbuild/linux-loong64": "npm:0.20.2" + "@esbuild/linux-mips64el": "npm:0.20.2" + "@esbuild/linux-ppc64": "npm:0.20.2" + "@esbuild/linux-riscv64": "npm:0.20.2" + "@esbuild/linux-s390x": "npm:0.20.2" + "@esbuild/linux-x64": "npm:0.20.2" + "@esbuild/netbsd-x64": "npm:0.20.2" + "@esbuild/openbsd-x64": "npm:0.20.2" + "@esbuild/sunos-x64": "npm:0.20.2" + "@esbuild/win32-arm64": "npm:0.20.2" + "@esbuild/win32-ia32": "npm:0.20.2" + "@esbuild/win32-x64": "npm:0.20.2" dependenciesMeta: "@esbuild/aix-ppc64": optional: true @@ -16377,7 +15883,7 @@ __metadata: optional: true bin: esbuild: bin/esbuild - checksum: 10/b672fd5df28ae917e2b16e77edbbf6b3099c390ab0a9d4cd331f78b4a4567cf33f506a055e1aa272ac90f7f522835b2173abea9bac6c38906acfda68e60a7ab7 + checksum: 10/663215ab7e599651e00d61b528a63136e1f1d397db8b9c3712540af928c9476d61da95aefa81b7a8dfc7a9fdd7616fcf08395c27be68be8c99953fb461863ce4 languageName: node linkType: hard @@ -16549,13 +16055,6 @@ __metadata: languageName: node linkType: hard -"escape-string-regexp@npm:4.0.0, escape-string-regexp@npm:^4.0.0": - version: 4.0.0 - resolution: "escape-string-regexp@npm:4.0.0" - checksum: 10/98b48897d93060f2322108bf29db0feba7dd774be96cd069458d1453347b25ce8682ecc39859d4bca2203cc0ab19c237bcc71755eff49a0f8d90beadeeba5cc5 - languageName: node - linkType: hard - "escape-string-regexp@npm:^1.0.5": version: 1.0.5 resolution: "escape-string-regexp@npm:1.0.5" @@ -16570,6 +16069,13 @@ __metadata: languageName: node linkType: hard +"escape-string-regexp@npm:^4.0.0": + version: 4.0.0 + resolution: "escape-string-regexp@npm:4.0.0" + checksum: 10/98b48897d93060f2322108bf29db0feba7dd774be96cd069458d1453347b25ce8682ecc39859d4bca2203cc0ab19c237bcc71755eff49a0f8d90beadeeba5cc5 + languageName: node + linkType: hard + "escodegen@npm:^2.0.0, escodegen@npm:^2.1.0": version: 2.1.0 resolution: "escodegen@npm:2.1.0" @@ -16660,21 +16166,21 @@ __metadata: languageName: node linkType: hard -"eslint-plugin-jest@npm:27.9.0": - version: 27.9.0 - resolution: "eslint-plugin-jest@npm:27.9.0" +"eslint-plugin-jest@npm:28.2.0": + version: 28.2.0 + resolution: "eslint-plugin-jest@npm:28.2.0" dependencies: - "@typescript-eslint/utils": "npm:^5.10.0" + "@typescript-eslint/utils": "npm:^6.0.0" peerDependencies: - "@typescript-eslint/eslint-plugin": ^5.0.0 || ^6.0.0 || ^7.0.0 - eslint: ^7.0.0 || ^8.0.0 + "@typescript-eslint/eslint-plugin": ^6.0.0 || ^7.0.0 + eslint: ^7.0.0 || ^8.0.0 || ^9.0.0 jest: "*" peerDependenciesMeta: "@typescript-eslint/eslint-plugin": optional: true jest: optional: true - checksum: 10/bca54347280c06c56516faea76042134dd74355c2de6c23361ba0e8736ecc01c62b144eea7eda7570ea4f4ee511c583bb8dab00d7153a1bd1740eb77b0038fd4 + checksum: 10/029a3d140a561d941580cbfee15ccacf4584971975f61111f07b87f01bf64c9739607cbe8e6fd3888429179ea8fd733e655ccd87b3b83b3b5cee2187e2355a4e languageName: node linkType: hard @@ -16825,7 +16331,7 @@ __metadata: languageName: node linkType: hard -"eslint-scope@npm:5.1.1, eslint-scope@npm:^5.1.1": +"eslint-scope@npm:5.1.1": version: 5.1.1 resolution: "eslint-scope@npm:5.1.1" dependencies: @@ -17082,13 +16588,6 @@ __metadata: languageName: node linkType: hard -"eventemitter2@npm:^6.4.3": - version: 6.4.9 - resolution: "eventemitter2@npm:6.4.9" - checksum: 10/b829b1c6b11e15926b635092b5ad62b4463d1c928859831dcae606e988cf41893059e3541f5a8209d21d2f15314422ddd4d84d20830b4bf44978608d15b06b08 - languageName: node - linkType: hard - "eventemitter3@npm:5.0.1": version: 5.0.1 resolution: "eventemitter3@npm:5.0.1" @@ -17144,7 +16643,7 @@ __metadata: languageName: node linkType: hard -"execa@npm:5.1.1, execa@npm:^5.0.0, execa@npm:^5.1.1": +"execa@npm:^5.0.0, execa@npm:^5.1.1": version: 5.1.1 resolution: "execa@npm:5.1.1" dependencies: @@ -17214,40 +16713,41 @@ __metadata: linkType: hard "express@npm:^4.17.3": - version: 4.17.3 - resolution: "express@npm:4.17.3" + version: 4.19.2 + resolution: "express@npm:4.19.2" dependencies: accepts: "npm:~1.3.8" array-flatten: "npm:1.1.1" - body-parser: "npm:1.19.2" + body-parser: "npm:1.20.2" content-disposition: "npm:0.5.4" content-type: "npm:~1.0.4" - cookie: "npm:0.4.2" + cookie: "npm:0.6.0" cookie-signature: "npm:1.0.6" debug: "npm:2.6.9" - depd: "npm:~1.1.2" + depd: "npm:2.0.0" encodeurl: "npm:~1.0.2" escape-html: "npm:~1.0.3" etag: "npm:~1.8.1" - finalhandler: "npm:~1.1.2" + finalhandler: "npm:1.2.0" fresh: "npm:0.5.2" + http-errors: "npm:2.0.0" merge-descriptors: "npm:1.0.1" methods: "npm:~1.1.2" - on-finished: "npm:~2.3.0" + on-finished: "npm:2.4.1" parseurl: "npm:~1.3.3" path-to-regexp: "npm:0.1.7" proxy-addr: "npm:~2.0.7" - qs: "npm:6.9.7" + qs: "npm:6.11.0" range-parser: "npm:~1.2.1" safe-buffer: "npm:5.2.1" - send: "npm:0.17.2" - serve-static: "npm:1.14.2" + send: "npm:0.18.0" + serve-static: "npm:1.15.0" setprototypeof: "npm:1.2.0" - statuses: "npm:~1.5.0" + statuses: "npm:2.0.1" type-is: "npm:~1.6.18" utils-merge: "npm:1.0.1" vary: "npm:~1.1.2" - checksum: 10/e3970a6cccdeec918d249c9dd8b0a83bd64eb5730f33c69f449763d16155c3ff9631a3f5aa6191c7d56a6bb9d8b40fcce69c12563bf593208ec2b4902d7b8475 + checksum: 10/3fcd792536f802c059789ef48db3851b87e78fba103423e524144d79af37da7952a2b8d4e1a007f423329c7377d686d9476ac42e7d9ea413b80345d495e30a3a languageName: node linkType: hard @@ -17342,7 +16842,7 @@ __metadata: languageName: node linkType: hard -"fast-glob@npm:^3.0.3, fast-glob@npm:^3.2.9, fast-glob@npm:^3.3.2": +"fast-glob@npm:3.3.2, fast-glob@npm:^3.0.3, fast-glob@npm:^3.2.9, fast-glob@npm:^3.3.2": version: 3.3.2 resolution: "fast-glob@npm:3.3.2" dependencies: @@ -17432,7 +16932,7 @@ __metadata: languageName: node linkType: hard -"fastq@npm:^1.13.0, fastq@npm:^1.6.0": +"fastq@npm:^1.13.0, fastq@npm:^1.15.0, fastq@npm:^1.6.0": version: 1.17.1 resolution: "fastq@npm:1.17.1" dependencies: @@ -17500,6 +17000,15 @@ __metadata: languageName: node linkType: hard +"file-entry-cache@npm:8.0.0, file-entry-cache@npm:^8.0.0": + version: 8.0.0 + resolution: "file-entry-cache@npm:8.0.0" + dependencies: + flat-cache: "npm:^4.0.0" + checksum: 10/afe55c4de4e0d226a23c1eae62a7219aafb390859122608a89fa4df6addf55c7fd3f1a2da6f5b41e7cdff496e4cf28bbd215d53eab5c817afa96d2b40c81bfb0 + languageName: node + linkType: hard + "file-entry-cache@npm:^6.0.1": version: 6.0.1 resolution: "file-entry-cache@npm:6.0.1" @@ -17509,15 +17018,6 @@ __metadata: languageName: node linkType: hard -"file-entry-cache@npm:^8.0.0": - version: 8.0.0 - resolution: "file-entry-cache@npm:8.0.0" - dependencies: - flat-cache: "npm:^4.0.0" - checksum: 10/afe55c4de4e0d226a23c1eae62a7219aafb390859122608a89fa4df6addf55c7fd3f1a2da6f5b41e7cdff496e4cf28bbd215d53eab5c817afa96d2b40c81bfb0 - languageName: node - linkType: hard - "file-saver@npm:2.0.5": version: 2.0.5 resolution: "file-saver@npm:2.0.5" @@ -17562,18 +17062,32 @@ __metadata: languageName: node linkType: hard -"finalhandler@npm:~1.1.2": - version: 1.1.2 - resolution: "finalhandler@npm:1.1.2" +"filter-iterator@npm:0.0.1": + version: 0.0.1 + resolution: "filter-iterator@npm:0.0.1" + checksum: 10/1dd34e0728183b3cc69d45c57ec58cbdd21c5160d53e70592e2db049ed4da81ba551b7bfaccb11731011ce5b07ee09961b09fa4573fe906145ce63d9a1813db0 + languageName: node + linkType: hard + +"filter-obj@npm:^1.1.0": + version: 1.1.0 + resolution: "filter-obj@npm:1.1.0" + checksum: 10/9d681939eec2b4b129cb4f307b7e93d954a0657421d4e5357d86093b26d3f4f570909ed43717dcfd62428b3cf8cddd9841b35f9d40d12ac62cfabaa677942593 + languageName: node + linkType: hard + +"finalhandler@npm:1.2.0": + version: 1.2.0 + resolution: "finalhandler@npm:1.2.0" dependencies: debug: "npm:2.6.9" encodeurl: "npm:~1.0.2" escape-html: "npm:~1.0.3" - on-finished: "npm:~2.3.0" + on-finished: "npm:2.4.1" parseurl: "npm:~1.3.3" - statuses: "npm:~1.5.0" + statuses: "npm:2.0.1" unpipe: "npm:~1.0.0" - checksum: 10/351e99a889abf149eb3edb24568586469feeb3019f5eafb9b31e632a5ad886f12a5595a221508245e6a37da69ae866c9fb411eb541a844238e2c900f63ac1576 + checksum: 10/635718cb203c6d18e6b48dfbb6c54ccb08ea470e4f474ddcef38c47edcf3227feec316f886dd701235997d8af35240cae49856721ce18f539ad038665ebbf163 languageName: node linkType: hard @@ -17609,13 +17123,6 @@ __metadata: languageName: node linkType: hard -"find-parent-dir@npm:~0.3.0": - version: 0.3.1 - resolution: "find-parent-dir@npm:0.3.1" - checksum: 10/da726722ca86463711589820b5dd530eb1023df550bc5e55fb27103d96a54bfced2cae6e1959bd77065384da8716676952848ace76d65323c5afc32d0c3b8363 - languageName: node - linkType: hard - "find-root@npm:^1.1.0": version: 1.1.0 resolution: "find-root@npm:1.1.0" @@ -17623,16 +17130,6 @@ __metadata: languageName: node linkType: hard -"find-up@npm:5.0.0, find-up@npm:^5.0.0": - version: 5.0.0 - resolution: "find-up@npm:5.0.0" - dependencies: - locate-path: "npm:^6.0.0" - path-exists: "npm:^4.0.0" - checksum: 10/07955e357348f34660bde7920783204ff5a26ac2cafcaa28bace494027158a97b9f56faaf2d89a6106211a8174db650dd9f503f9c0d526b1202d5554a00b9095 - languageName: node - linkType: hard - "find-up@npm:^2.0.0, find-up@npm:^2.1.0": version: 2.1.0 resolution: "find-up@npm:2.1.0" @@ -17661,6 +17158,16 @@ __metadata: languageName: node linkType: hard +"find-up@npm:^5.0.0": + version: 5.0.0 + resolution: "find-up@npm:5.0.0" + dependencies: + locate-path: "npm:^6.0.0" + path-exists: "npm:^4.0.0" + checksum: 10/07955e357348f34660bde7920783204ff5a26ac2cafcaa28bace494027158a97b9f56faaf2d89a6106211a8174db650dd9f503f9c0d526b1202d5554a00b9095 + languageName: node + linkType: hard + "find-up@npm:^6.3.0": version: 6.3.0 resolution: "find-up@npm:6.3.0" @@ -18406,7 +17913,7 @@ __metadata: languageName: node linkType: hard -"glob@npm:10.3.12, glob@npm:^10.0.0, glob@npm:^10.2.2, glob@npm:^10.2.5, glob@npm:^10.2.7, glob@npm:^10.3.10, glob@npm:^10.3.7": +"glob@npm:10.3.12, glob@npm:^10.0.0, glob@npm:^10.2.2, glob@npm:^10.2.7, glob@npm:^10.3.10, glob@npm:^10.3.7": version: 10.3.12 resolution: "glob@npm:10.3.12" dependencies: @@ -18421,20 +17928,6 @@ __metadata: languageName: node linkType: hard -"glob@npm:7.2.0": - version: 7.2.0 - resolution: "glob@npm:7.2.0" - dependencies: - fs.realpath: "npm:^1.0.0" - inflight: "npm:^1.0.4" - inherits: "npm:2" - minimatch: "npm:^3.0.4" - once: "npm:^1.3.0" - path-is-absolute: "npm:^1.0.0" - checksum: 10/bc78b6ea0735b6e23d20678aba4ae6a4760e8c9527e3c4683ac25b14e70f55f9531245dcf25959b70cbc4aa3dcce1fc37ab65fd026a4cbd70aa3a44880bd396b - languageName: node - linkType: hard - "glob@npm:^7.1.2, glob@npm:^7.1.3, glob@npm:^7.1.4": version: 7.2.3 resolution: "glob@npm:7.2.3" @@ -18599,12 +18092,13 @@ __metadata: version: 0.0.0-use.local resolution: "grafana@workspace:." dependencies: + "@babel/core": "npm:7.24.4" + "@babel/preset-env": "npm:7.24.4" "@babel/runtime": "npm:7.24.4" "@betterer/betterer": "npm:5.4.0" "@betterer/cli": "npm:5.4.0" "@betterer/eslint": "npm:5.4.0" "@cypress/webpack-preprocessor": "npm:6.0.1" - "@daybrush/utils": "npm:1.13.0" "@emotion/css": "npm:11.11.2" "@emotion/eslint-plugin": "npm:11.11.0" "@emotion/react": "npm:11.11.4" @@ -18624,11 +18118,11 @@ __metadata: "@grafana/lezer-logql": "npm:0.2.3" "@grafana/monaco-logql": "npm:^0.0.7" "@grafana/o11y-ds-frontend": "workspace:*" - "@grafana/plugin-e2e": "npm:1.1.1" + "@grafana/plugin-e2e": "npm:1.2.0" "@grafana/prometheus": "workspace:*" "@grafana/runtime": "workspace:*" "@grafana/saga-icons": "workspace:*" - "@grafana/scenes": "npm:^4.10.0" + "@grafana/scenes": "npm:^4.13.0" "@grafana/schema": "workspace:*" "@grafana/sql": "workspace:*" "@grafana/tsconfig": "npm:^1.3.0-rc1" @@ -18655,14 +18149,12 @@ __metadata: "@react-aria/focus": "npm:3.16.2" "@react-aria/overlays": "npm:3.21.1" "@react-aria/utils": "npm:3.23.2" - "@react-awesome-query-builder/core": "npm:6.4.2" "@react-awesome-query-builder/ui": "npm:6.4.2" "@react-types/button": "npm:3.9.2" "@react-types/menu": "npm:3.9.7" "@react-types/overlays": "npm:3.8.5" "@react-types/shared": "npm:3.22.1" "@reduxjs/toolkit": "npm:1.9.5" - "@remix-run/router": "npm:^1.5.0" "@rtk-query/codegen-openapi": "npm:^1.2.0" "@rtsao/plugin-proposal-class-properties": "npm:7.0.1-patch.1" "@swc/core": "npm:1.4.2" @@ -18674,6 +18166,8 @@ __metadata: "@testing-library/user-event": "npm:14.5.2" "@types/angular": "npm:1.8.9" "@types/angular-route": "npm:1.7.6" + "@types/babel__core": "npm:^7" + "@types/babel__preset-env": "npm:^7" "@types/chance": "npm:^1.1.3" "@types/common-tags": "npm:^1.8.0" "@types/d3": "npm:7.4.3" @@ -18688,7 +18182,6 @@ __metadata: "@types/google.analytics": "npm:^0.0.46" "@types/gtag.js": "npm:^0.0.19" "@types/history": "npm:4.7.11" - "@types/hoist-non-react-statics": "npm:3.3.5" "@types/jest": "npm:29.5.12" "@types/jquery": "npm:3.5.29" "@types/js-yaml": "npm:^4.0.5" @@ -18698,9 +18191,7 @@ __metadata: "@types/lucene": "npm:^2" "@types/mousetrap": "npm:1.6.15" "@types/node": "npm:20.12.7" - "@types/node-forge": "npm:^1" "@types/ol-ext": "npm:@siedlerchr/types-ol-ext@3.2.4" - "@types/papaparse": "npm:5.3.14" "@types/pluralize": "npm:^0.0.33" "@types/prismjs": "npm:1.26.3" "@types/react": "npm:18.2.79" @@ -18725,17 +18216,15 @@ __metadata: "@types/systemjs": "npm:6.13.5" "@types/testing-library__jest-dom": "npm:5.14.9" "@types/tinycolor2": "npm:1.4.6" - "@types/trusted-types": "npm:2.0.7" "@types/uuid": "npm:9.0.8" "@types/webpack-assets-manifest": "npm:^5" - "@types/webpack-env": "npm:1.18.4" + "@types/webpack-env": "npm:^1.18.4" "@types/yargs": "npm:17.0.32" "@typescript-eslint/eslint-plugin": "npm:6.21.0" "@typescript-eslint/parser": "npm:6.21.0" "@visx/event": "npm:3.3.0" "@visx/gradient": "npm:3.3.0" "@visx/group": "npm:3.3.0" - "@visx/scale": "npm:3.5.0" "@visx/shape": "npm:3.5.0" "@visx/tooltip": "npm:3.3.0" "@welldone-software/why-did-you-render": "npm:8.0.1" @@ -18745,11 +18234,11 @@ __metadata: angular-sanitize: "npm:1.8.3" ansicolor: "npm:1.1.100" autoprefixer: "npm:10.4.19" + babel-loader: "npm:9.1.3" baron: "npm:3.0.3" blob-polyfill: "npm:7.0.20220408" brace: "npm:0.11.1" browserslist: "npm:^4.21.4" - calculate-size: "npm:1.1.1" centrifuge: "npm:5.1.1" chance: "npm:^1.0.10" chrome-remote-interface: "npm:0.33.0" @@ -18759,8 +18248,8 @@ __metadata: comlink: "npm:4.4.1" common-tags: "npm:1.8.2" copy-webpack-plugin: "npm:12.0.2" - core-js: "npm:3.36.1" - css-loader: "npm:6.10.0" + core-js: "npm:3.37.0" + css-loader: "npm:7.1.1" css-minimizer-webpack-plugin: "npm:6.0.0" cypress: "npm:13.1.0" cypress-file-upload: "npm:5.0.8" @@ -18771,14 +18260,13 @@ __metadata: date-fns: "npm:3.6.0" debounce-promise: "npm:3.1.2" diff: "npm:^5.1.0" - emotion: "npm:11.0.0" - esbuild: "npm:0.20.1" + esbuild: "npm:0.20.2" esbuild-loader: "npm:4.1.0" esbuild-plugin-browserslist: "npm:^0.11.0" eslint: "npm:8.57.0" eslint-config-prettier: "npm:9.1.0" eslint-plugin-import: "npm:^2.26.0" - eslint-plugin-jest: "npm:27.9.0" + eslint-plugin-jest: "npm:28.2.0" eslint-plugin-jsdoc: "npm:48.2.3" eslint-plugin-jsx-a11y: "npm:6.8.0" eslint-plugin-lodash: "npm:7.4.0" @@ -18787,16 +18275,13 @@ __metadata: eslint-plugin-react-hooks: "npm:4.6.0" eslint-scope: "npm:^8.0.0" eslint-webpack-plugin: "npm:4.1.0" - eventemitter3: "npm:5.0.1" expose-loader: "npm:5.0.0" fast-deep-equal: "npm:^3.1.3" fast-json-patch: "npm:3.1.1" file-saver: "npm:2.0.5" fork-ts-checker-webpack-plugin: "npm:9.0.2" - framework-utils: "npm:^1.1.0" glob: "npm:10.3.12" history: "npm:4.10.1" - hoist-non-react-statics: "npm:3.3.2" html-loader: "npm:5.0.0" html-webpack-plugin: "npm:5.6.0" http-server: "npm:14.1.1" @@ -18819,6 +18304,7 @@ __metadata: json-source-map: "npm:0.6.1" jsurl: "npm:^0.1.5" kbar: "npm:0.1.0-beta.45" + knip: "npm:^5.10.0" lerna: "npm:8.1.2" leven: "npm:^4.0.0" lodash: "npm:4.17.21" @@ -18827,15 +18313,13 @@ __metadata: lru-memoize: "npm:^1.1.0" lucene: "npm:^2.1.1" marked: "npm:12.0.2" - marked-mangle: "npm:1.1.7" memoize-one: "npm:6.0.0" - mini-css-extract-plugin: "npm:2.8.1" + mini-css-extract-plugin: "npm:2.9.0" ml-regression-polynomial: "npm:^3.0.0" ml-regression-simple-linear: "npm:^3.0.0" moment: "npm:2.30.1" moment-timezone: "npm:0.5.45" monaco-editor: "npm:0.34.1" - monaco-promql: "npm:1.7.4" mousetrap: "npm:1.6.5" mousetrap-global-bind: "npm:1.1.0" moveable: "npm:0.53.0" @@ -18843,12 +18327,10 @@ __metadata: mutationobserver-shim: "npm:0.3.7" nanoid: "npm:^5.0.4" ngtemplate-loader: "npm:2.1.0" - node-forge: "npm:^1.3.1" node-notifier: "npm:10.0.1" nx: "npm:18.1.3" ol: "npm:7.4.0" ol-ext: "npm:4.0.18" - papaparse: "npm:5.4.1" pluralize: "npm:^8.0.0" postcss: "npm:8.4.38" postcss-loader: "npm:8.1.1" @@ -18856,11 +18338,7 @@ __metadata: postcss-scss: "npm:4.0.9" prettier: "npm:3.2.5" prismjs: "npm:1.29.0" - prop-types: "npm:15.8.1" - pseudoizer: "npm:^0.1.0" - rc-cascader: "npm:3.24.1" - rc-drawer: "npm:7.1.0" - rc-slider: "npm:10.5.0" + rc-slider: "npm:10.6.2" rc-time-picker: "npm:3.7.3" rc-tree: "npm:5.8.5" re-resizable: "npm:6.9.14" @@ -18873,7 +18351,7 @@ __metadata: react-grid-layout: "npm:1.4.4" react-highlight-words: "npm:0.20.0" react-hook-form: "npm:^7.49.2" - react-i18next: "npm:^12.0.0" + react-i18next: "npm:^14.0.0" react-inlinesvg: "npm:3.0.2" react-loading-skeleton: "npm:3.4.0" react-moveable: "npm:0.56.0" @@ -18886,7 +18364,6 @@ __metadata: react-router-dom-v5-compat: "npm:^6.10.0" react-select: "npm:5.8.0" react-select-event: "npm:5.5.1" - react-simple-compat: "npm:1.2.3" react-split-pane: "npm:0.1.92" react-table: "npm:7.8.0" react-test-renderer: "npm:18.2.0" @@ -18912,7 +18389,7 @@ __metadata: slate: "npm:0.47.9" slate-plain-serializer: "npm:0.7.13" slate-react: "npm:0.22.10" - style-loader: "npm:3.3.4" + style-loader: "npm:4.0.0" stylelint: "npm:16.3.1" stylelint-config-sass-guidelines: "npm:11.1.0" symbol-observable: "npm:4.0.0" @@ -18940,7 +18417,7 @@ __metadata: webpack-merge: "npm:5.10.0" webpackbar: "npm:^6.0.0" whatwg-fetch: "npm:3.6.20" - xlsx: "https://cdn.sheetjs.com/xlsx-0.19.1/xlsx-0.19.1.tgz" + xlsx: "https://cdn.sheetjs.com/xlsx-0.20.2/xlsx-0.20.2.tgz" yaml: "npm:^2.0.0" yargs: "npm:^17.5.1" dependenciesMeta: @@ -19036,23 +18513,6 @@ __metadata: languageName: node linkType: hard -"har-schema@npm:^2.0.0": - version: 2.0.0 - resolution: "har-schema@npm:2.0.0" - checksum: 10/d8946348f333fb09e2bf24cc4c67eabb47c8e1d1aa1c14184c7ffec1140a49ec8aa78aa93677ae452d71d5fc0fdeec20f0c8c1237291fc2bcb3f502a5d204f9b - languageName: node - linkType: hard - -"har-validator@npm:~5.1.3": - version: 5.1.5 - resolution: "har-validator@npm:5.1.5" - dependencies: - ajv: "npm:^6.12.3" - har-schema: "npm:^2.0.0" - checksum: 10/b998a7269ca560d7f219eedc53e2c664cd87d487e428ae854a6af4573fc94f182fe9d2e3b92ab968249baec7ebaf9ead69cf975c931dc2ab282ec182ee988280 - languageName: node - linkType: hard - "hard-rejection@npm:^2.1.0": version: 2.1.0 resolution: "hard-rejection@npm:2.1.0" @@ -19081,6 +18541,13 @@ __metadata: languageName: node linkType: hard +"has-own-property@npm:^0.1.0": + version: 0.1.0 + resolution: "has-own-property@npm:0.1.0" + checksum: 10/4754f42e8a54860ea1a397c231843937ba890f3aa556698c9a2160df5f9b1a02ddb321ef0528294aec3aaa3139d17744da048027aa7129a631cb6554b6faed6f + languageName: node + linkType: hard + "has-property-descriptors@npm:^1.0.0, has-property-descriptors@npm:^1.0.2": version: 1.0.2 resolution: "has-property-descriptors@npm:1.0.2" @@ -19149,7 +18616,7 @@ __metadata: languageName: node linkType: hard -"he@npm:1.2.0, he@npm:^1.2.0": +"he@npm:^1.2.0": version: 1.2.0 resolution: "he@npm:1.2.0" bin: @@ -19460,16 +18927,16 @@ __metadata: languageName: node linkType: hard -"http-errors@npm:1.8.1": - version: 1.8.1 - resolution: "http-errors@npm:1.8.1" +"http-errors@npm:2.0.0": + version: 2.0.0 + resolution: "http-errors@npm:2.0.0" dependencies: - depd: "npm:~1.1.2" + depd: "npm:2.0.0" inherits: "npm:2.0.4" setprototypeof: "npm:1.2.0" - statuses: "npm:>= 1.5.0 < 2" + statuses: "npm:2.0.1" toidentifier: "npm:1.0.1" - checksum: 10/76fc491bd8df2251e21978e080d5dae20d9736cfb29bb72b5b76ec1bcebb1c14f0f58a3a128dd89288934379d2173cfb0421c571d54103e93dd65ef6243d64d8 + checksum: 10/0e7f76ee8ff8a33e58a3281a469815b893c41357378f408be8f6d4aa7d1efafb0da064625518e7078381b6a92325949b119dc38fcb30bdbc4e3a35f78c44c439 languageName: node linkType: hard @@ -19576,17 +19043,6 @@ __metadata: languageName: node linkType: hard -"http-signature@npm:~1.2.0": - version: 1.2.0 - resolution: "http-signature@npm:1.2.0" - dependencies: - assert-plus: "npm:^1.0.0" - jsprim: "npm:^1.2.2" - sshpk: "npm:^1.7.0" - checksum: 10/2ff7112e6b0d8f08b382dfe705078c655501f2ddd76cf589d108445a9dd388a0a9be928c37108261519a7f53e6bbd1651048d74057b804807cce1ec49e87a95b - languageName: node - linkType: hard - "http-signature@npm:~1.3.6": version: 1.3.6 resolution: "http-signature@npm:1.3.6" @@ -19711,7 +19167,7 @@ __metadata: languageName: node linkType: hard -"iconv-lite@npm:0.4.24, iconv-lite@npm:^0.4.24, iconv-lite@npm:^0.4.8": +"iconv-lite@npm:0.4.24, iconv-lite@npm:^0.4.24": version: 0.4.24 resolution: "iconv-lite@npm:0.4.24" dependencies: @@ -19738,6 +19194,13 @@ __metadata: languageName: node linkType: hard +"identity-function@npm:^1.0.0": + version: 1.0.0 + resolution: "identity-function@npm:1.0.0" + checksum: 10/0ec311050c69679334b12479f53226f67ca41bc3ba7283ede3dacfb80802ec07643868d16b274ab823ceb2e81291697f01b07e32c7796f92255bd81452b7ea15 + languageName: node + linkType: hard + "ieee754@npm:^1.1.12, ieee754@npm:^1.1.13, ieee754@npm:^1.2.1": version: 1.2.1 resolution: "ieee754@npm:1.2.1" @@ -19770,7 +19233,7 @@ __metadata: languageName: node linkType: hard -"ignore@npm:^5.0.4, ignore@npm:^5.1.1, ignore@npm:^5.2.0, ignore@npm:^5.2.4, ignore@npm:^5.3.1": +"ignore@npm:^5.0.4, ignore@npm:^5.1.1, ignore@npm:^5.1.8, ignore@npm:^5.2.0, ignore@npm:^5.2.4, ignore@npm:^5.3.1": version: 5.3.1 resolution: "ignore@npm:5.3.1" checksum: 10/0a884c2fbc8c316f0b9f92beaf84464253b73230a4d4d286697be45fca081199191ca33e1c2e82d9e5f851f5e9a48a78e25a35c951e7eb41e59f150db3530065 @@ -19858,7 +19321,7 @@ __metadata: languageName: node linkType: hard -"inherits@npm:2, inherits@npm:2.0.4, inherits@npm:^2.0.1, inherits@npm:^2.0.3, inherits@npm:^2.0.4, inherits@npm:~2.0.1, inherits@npm:~2.0.3, inherits@npm:~2.0.4": +"inherits@npm:2, inherits@npm:2.0.4, inherits@npm:^2.0.1, inherits@npm:^2.0.3, inherits@npm:^2.0.4, inherits@npm:~2.0.3, inherits@npm:~2.0.4": version: 2.0.4 resolution: "inherits@npm:2.0.4" checksum: 10/cd45e923bee15186c07fa4c89db0aace24824c482fb887b528304694b2aa6ff8a898da8657046a5dcf3e46cd6db6c61629551f9215f208d7c3f157cf9b290521 @@ -19991,9 +19454,9 @@ __metadata: linkType: hard "ip@npm:^2.0.0": - version: 2.0.0 - resolution: "ip@npm:2.0.0" - checksum: 10/1270b11e534a466fb4cf4426cbcc3a907c429389f7f4e4e3b288b42823562e88d6a509ceda8141a507de147ca506141f745005c0aa144569d94cf24a54eb52bc + version: 2.0.1 + resolution: "ip@npm:2.0.1" + checksum: 10/d6dd154e1bc5e8725adfdd6fb92218635b9cbe6d873d051bd63b178f009777f751a5eea4c67021723a7056325fc3052f8b6599af0a2d56f042c93e684b4a0349 languageName: node linkType: hard @@ -20328,6 +19791,13 @@ __metadata: languageName: node linkType: hard +"is-iterable@npm:^1.1.0": + version: 1.1.1 + resolution: "is-iterable@npm:1.1.1" + checksum: 10/d059aaf00899cf351cdf4d71ea6b4e8912107c47b31b554d28205199b306420f3b5d30a419efa6b807f466f675fd945822f1651fd6d1fd45469a578573da671e + languageName: node + linkType: hard + "is-lambda@npm:^1.0.1": version: 1.0.1 resolution: "is-lambda@npm:1.0.1" @@ -20396,6 +19866,13 @@ __metadata: languageName: node linkType: hard +"is-number@npm:^4.0.0": + version: 4.0.0 + resolution: "is-number@npm:4.0.0" + checksum: 10/7f25967eb4fd92c4c5c282f6510c86d257de5894501b831ae6ab44a964c690ba721eef931eb2aaad2aacf35fd0cf57b9d2135b0bd25147dc5bc64f99fbca1785 + languageName: node + linkType: hard + "is-number@npm:^7.0.0": version: 7.0.0 resolution: "is-number@npm:7.0.0" @@ -20431,13 +19908,6 @@ __metadata: languageName: node linkType: hard -"is-plain-obj@npm:^2.1.0": - version: 2.1.0 - resolution: "is-plain-obj@npm:2.1.0" - checksum: 10/cec9100678b0a9fe0248a81743041ed990c2d4c99f893d935545cfbc42876cbe86d207f3b895700c690ad2fa520e568c44afc1605044b535a7820c1d40e38daa - languageName: node - linkType: hard - "is-plain-obj@npm:^3.0.0": version: 3.0.0 resolution: "is-plain-obj@npm:3.0.0" @@ -20760,6 +20230,13 @@ __metadata: languageName: node linkType: hard +"iterable-lookahead@npm:^1.0.0": + version: 1.0.0 + resolution: "iterable-lookahead@npm:1.0.0" + checksum: 10/1fd0a7fd6362e77683b2ab48fc042251d600d780cbf870f8211ef7793fe4ae31cfddc1b3248584ea888a23c17fe41bdb21b09544db599b4dc90a6d68b95cb0ed + languageName: node + linkType: hard + "iterator.prototype@npm:^1.1.2": version: 1.1.2 resolution: "iterator.prototype@npm:1.1.2" @@ -21343,7 +20820,7 @@ __metadata: languageName: node linkType: hard -"jiti@npm:^1.20.0": +"jiti@npm:1.21.0, jiti@npm:^1.20.0": version: 1.21.0 resolution: "jiti@npm:1.21.0" bin: @@ -21694,18 +21171,6 @@ __metadata: languageName: node linkType: hard -"jsprim@npm:^1.2.2": - version: 1.4.2 - resolution: "jsprim@npm:1.4.2" - dependencies: - assert-plus: "npm:1.0.0" - extsprintf: "npm:1.3.0" - json-schema: "npm:0.4.0" - verror: "npm:1.10.0" - checksum: 10/df2bf234eab1b5078d01bcbff3553d50a243f7b5c10a169745efeda6344d62798bd1d85bcca6a8446f3b5d0495e989db45f9de8dae219f0f9796e70e0c776089 - languageName: node - linkType: hard - "jsprim@npm:^2.0.2": version: 2.0.2 resolution: "jsprim@npm:2.0.2" @@ -21801,6 +21266,38 @@ __metadata: languageName: node linkType: hard +"knip@npm:^5.10.0": + version: 5.10.0 + resolution: "knip@npm:5.10.0" + dependencies: + "@ericcornelissen/bash-parser": "npm:0.5.2" + "@nodelib/fs.walk": "npm:2.0.0" + "@snyk/github-codeowners": "npm:1.1.0" + easy-table: "npm:1.2.0" + fast-glob: "npm:3.3.2" + file-entry-cache: "npm:8.0.0" + jiti: "npm:1.21.0" + js-yaml: "npm:4.1.0" + minimist: "npm:1.2.8" + picocolors: "npm:1.0.0" + picomatch: "npm:^4.0.1" + pretty-ms: "npm:9.0.0" + resolve: "npm:1.22.8" + smol-toml: "npm:1.1.4" + strip-json-comments: "npm:5.0.1" + summary: "npm:2.1.0" + zod: "npm:^3.22.4" + zod-validation-error: "npm:^3.0.3" + peerDependencies: + "@types/node": ">=18" + typescript: ">=5.0.4" + bin: + knip: bin/knip.js + knip-bun: bin/knip-bun.js + checksum: 10/eafc84cae08ddb249623c13814ebd1932abb1fb2e3d9c8061454cf2cb672ac478f6e1d63c4c80cb8af8d7ef05f0d03b40504894e4ed0d560c883c584d98d96ca + languageName: node + linkType: hard + "known-css-properties@npm:^0.29.0": version: 0.29.0 resolution: "known-css-properties@npm:0.29.0" @@ -22161,6 +21658,13 @@ __metadata: languageName: node linkType: hard +"lodash.curry@npm:^4.1.1": + version: 4.1.1 + resolution: "lodash.curry@npm:4.1.1" + checksum: 10/ce6c2bc42eacc25c5697b90a6fc42a121fec2b3c944fd324b61f93a6e1b4c8bb4875dc8c32b89ca4ce5f7be7346f485ed8410d3f4728eceebcbca9760bcac3d1 + languageName: node + linkType: hard + "lodash.debounce@npm:^4.0.8": version: 4.0.8 resolution: "lodash.debounce@npm:4.0.8" @@ -22266,7 +21770,7 @@ __metadata: languageName: node linkType: hard -"log-symbols@npm:4.1.0, log-symbols@npm:^4.0.0, log-symbols@npm:^4.1.0": +"log-symbols@npm:^4.0.0, log-symbols@npm:^4.1.0": version: 4.1.0 resolution: "log-symbols@npm:4.1.0" dependencies: @@ -22288,17 +21792,6 @@ __metadata: languageName: node linkType: hard -"log4js@npm:1.1.1": - version: 1.1.1 - resolution: "log4js@npm:1.1.1" - dependencies: - debug: "npm:^2.2.0" - semver: "npm:^5.3.0" - streamroller: "npm:^0.4.0" - checksum: 10/d7912f2a9718882a94297741b9770aabd1a2c12936191244312e211a94cbf44d46ffcc63ac874c096d9e3e69756077e4a0a55f7f3740e22056eddeceb0b6b1a4 - languageName: node - linkType: hard - "logfmt@npm:^1.3.2": version: 1.4.0 resolution: "logfmt@npm:1.4.0" @@ -22403,6 +21896,15 @@ __metadata: languageName: node linkType: hard +"magic-string@npm:^0.16.0": + version: 0.16.0 + resolution: "magic-string@npm:0.16.0" + dependencies: + vlq: "npm:^0.2.1" + checksum: 10/ca23685676fe3f4f1152256750ba2612c53f5d4107ed019e9eddbc309e908e55bb29fd6e334d0fd4be5739f57aa54f87baceda4455b6f71d5117789e36cc805a + languageName: node + linkType: hard + "magic-string@npm:^0.30.0, magic-string@npm:^0.30.4": version: 0.30.8 resolution: "magic-string@npm:0.30.8" @@ -22529,6 +22031,13 @@ __metadata: languageName: node linkType: hard +"map-obj@npm:^2.0.0": + version: 2.0.0 + resolution: "map-obj@npm:2.0.0" + checksum: 10/77d2b7b03398a71c84bd7df8ab7be2139e5459fc1e18dbb5f15055fe7284bec0fc37fe410185b5f8ca2e3c3e01fd0fd1f946c579607878adb26cad1cd75314aa + languageName: node + linkType: hard + "map-obj@npm:^4.0.0": version: 4.3.0 resolution: "map-obj@npm:4.3.0" @@ -22695,16 +22204,6 @@ __metadata: languageName: node linkType: hard -"memory-fs@npm:^0.5.0": - version: 0.5.0 - resolution: "memory-fs@npm:0.5.0" - dependencies: - errno: "npm:^0.1.3" - readable-stream: "npm:^2.0.1" - checksum: 10/5f146821d02406d031785b23e0ce4e76e751b039dbd8875e38496498dfb8bb6c0cb4b210e8d67a97af14da4c8b275c5b1a3fffdf930ec4ea35a01622e0301ecc - languageName: node - linkType: hard - "meow@npm:^13.2.0": version: 13.2.0 resolution: "meow@npm:13.2.0" @@ -22766,7 +22265,7 @@ __metadata: languageName: node linkType: hard -"micromatch@npm:^4.0.0, micromatch@npm:^4.0.2, micromatch@npm:^4.0.4, micromatch@npm:^4.0.5": +"micromatch@npm:^4.0.2, micromatch@npm:^4.0.4, micromatch@npm:^4.0.5": version: 4.0.5 resolution: "micromatch@npm:4.0.5" dependencies: @@ -22837,15 +22336,15 @@ __metadata: languageName: node linkType: hard -"mini-css-extract-plugin@npm:2.8.1": - version: 2.8.1 - resolution: "mini-css-extract-plugin@npm:2.8.1" +"mini-css-extract-plugin@npm:2.9.0": + version: 2.9.0 + resolution: "mini-css-extract-plugin@npm:2.9.0" dependencies: schema-utils: "npm:^4.0.0" tapable: "npm:^2.2.1" peerDependencies: webpack: ^5.0.0 - checksum: 10/e00f6d19ad1be94701db8e5f126bdf8a9f4739cd8e8eb68690254aac4699c49c872a1ca761461d7d0c37a933f823df5f87674688fe0d568e00e7c0e9d6e5c798 + checksum: 10/4c9ee9c0c6160a64a4884d5a92a1a5c0b68d556cd00f975cf6c8a79b51ac90e6130a37b3832b17d377d0cb1b31c0313c8c023458d4f69e95fe3424a8b43d834f languageName: node linkType: hard @@ -22874,15 +22373,6 @@ __metadata: languageName: node linkType: hard -"minimatch@npm:5.0.1, minimatch@npm:^5.0.1": - version: 5.0.1 - resolution: "minimatch@npm:5.0.1" - dependencies: - brace-expansion: "npm:^2.0.1" - checksum: 10/2656580f18d9f38ada186196fcc72dc9076d70f7227adc664e72614d464e075dc4ae3936e6742519e09e336996ef33c6035e606888b12f65ca7fda792ddd2085 - languageName: node - linkType: hard - "minimatch@npm:9.0.3, minimatch@npm:^9.0.0, minimatch@npm:^9.0.1, minimatch@npm:^9.0.3": version: 9.0.3 resolution: "minimatch@npm:9.0.3" @@ -22901,6 +22391,15 @@ __metadata: languageName: node linkType: hard +"minimatch@npm:^5.0.1": + version: 5.0.1 + resolution: "minimatch@npm:5.0.1" + dependencies: + brace-expansion: "npm:^2.0.1" + checksum: 10/2656580f18d9f38ada186196fcc72dc9076d70f7227adc664e72614d464e075dc4ae3936e6742519e09e336996ef33c6035e606888b12f65ca7fda792ddd2085 + languageName: node + linkType: hard + "minimatch@npm:^8.0.2": version: 8.0.4 resolution: "minimatch@npm:8.0.4" @@ -22921,7 +22420,7 @@ __metadata: languageName: node linkType: hard -"minimist@npm:^1.2.0, minimist@npm:^1.2.5, minimist@npm:^1.2.6, minimist@npm:^1.2.8": +"minimist@npm:1.2.8, minimist@npm:^1.2.0, minimist@npm:^1.2.5, minimist@npm:^1.2.6, minimist@npm:^1.2.8": version: 1.2.8 resolution: "minimist@npm:1.2.8" checksum: 10/908491b6cc15a6c440ba5b22780a0ba89b9810e1aea684e253e43c4e3b8d56ec1dcdd7ea96dde119c29df59c936cde16062159eae4225c691e19c70b432b6e6f @@ -23060,7 +22559,7 @@ __metadata: languageName: node linkType: hard -"mkdirp@npm:^0.5.1, mkdirp@npm:^0.5.4, mkdirp@npm:^0.5.6": +"mkdirp@npm:^0.5.4, mkdirp@npm:^0.5.6": version: 0.5.6 resolution: "mkdirp@npm:0.5.6" dependencies: @@ -23157,38 +22656,6 @@ __metadata: languageName: node linkType: hard -"mocha@npm:10.2.0": - version: 10.2.0 - resolution: "mocha@npm:10.2.0" - dependencies: - ansi-colors: "npm:4.1.1" - browser-stdout: "npm:1.3.1" - chokidar: "npm:3.5.3" - debug: "npm:4.3.4" - diff: "npm:5.0.0" - escape-string-regexp: "npm:4.0.0" - find-up: "npm:5.0.0" - glob: "npm:7.2.0" - he: "npm:1.2.0" - js-yaml: "npm:4.1.0" - log-symbols: "npm:4.1.0" - minimatch: "npm:5.0.1" - ms: "npm:2.1.3" - nanoid: "npm:3.3.3" - serialize-javascript: "npm:6.0.0" - strip-json-comments: "npm:3.1.1" - supports-color: "npm:8.1.1" - workerpool: "npm:6.2.1" - yargs: "npm:16.2.0" - yargs-parser: "npm:20.2.4" - yargs-unparser: "npm:2.0.0" - bin: - _mocha: bin/_mocha - mocha: bin/mocha.js - checksum: 10/f7362898ae65e8fe716cfe62fd014b432d100c9611aaf5abe85ed14efcbfdd82f3bdf32c44bccf00c9059a264c7e8d93a69dd5b830652109052a92beffb7ea35 - languageName: node - linkType: hard - "mock-raf@npm:1.0.1": version: 1.0.1 resolution: "mock-raf@npm:1.0.1" @@ -23412,15 +22879,6 @@ __metadata: languageName: node linkType: hard -"nanoid@npm:3.3.3": - version: 3.3.3 - resolution: "nanoid@npm:3.3.3" - bin: - nanoid: bin/nanoid.cjs - checksum: 10/c703ed58a234b68245a8a4826dd25c1453a9017d34fa28bc58e7aa8247de87d854582fa2209d7aee04084cff9ce150be8fd30300abe567dc615d4e8e735f2d99 - languageName: node - linkType: hard - "nanoid@npm:^3.3.7": version: 3.3.7 resolution: "nanoid@npm:3.3.7" @@ -23540,7 +22998,7 @@ __metadata: languageName: node linkType: hard -"node-forge@npm:^1, node-forge@npm:^1.3.1": +"node-forge@npm:^1": version: 1.3.1 resolution: "node-forge@npm:1.3.1" checksum: 10/05bab6868633bf9ad4c3b1dd50ec501c22ffd69f556cdf169a00998ca1d03e8107a6032ba013852f202035372021b845603aeccd7dfcb58cdb7430013b3daa8d @@ -24075,13 +23533,6 @@ __metadata: languageName: node linkType: hard -"oauth-sign@npm:~0.9.0": - version: 0.9.0 - resolution: "oauth-sign@npm:0.9.0" - checksum: 10/1809a366d258f41fdf4ab5310cff3d1e15f96b187503bc7333cef4351de7bd0f52cb269bc95800f1fae5fb04dd886287df1471985fd67e8484729fdbcf857119 - languageName: node - linkType: hard - "oazapfts@npm:^4.8.0": version: 4.12.0 resolution: "oazapfts@npm:4.12.0" @@ -24135,6 +23586,20 @@ __metadata: languageName: node linkType: hard +"object-pairs@npm:^0.1.0": + version: 0.1.0 + resolution: "object-pairs@npm:0.1.0" + checksum: 10/8bde82dda701c84a27ba5bcf5e014283c6defbdab6df189af0b6582541711ed86ba2f0cce0a300a2220ba27b54ee11128c508982a191fa04f81770a7695b23d9 + languageName: node + linkType: hard + +"object-values@npm:^1.0.0": + version: 1.0.0 + resolution: "object-values@npm:1.0.0" + checksum: 10/b86e7ef56349de1444e45b00f4aac7dcb76f2973f3e2cd5836cf86815b1ea4b2b3827bb2320cd5d1a50c78dd3068ce23cbcb5a1f024abe12296af8cf50d17a22 + languageName: node + linkType: hard + "object.assign@npm:^4.1.4, object.assign@npm:^4.1.5": version: 4.1.5 resolution: "object.assign@npm:4.1.5" @@ -24258,7 +23723,7 @@ __metadata: languageName: node linkType: hard -"on-finished@npm:^2.4.1": +"on-finished@npm:2.4.1, on-finished@npm:^2.4.1": version: 2.4.1 resolution: "on-finished@npm:2.4.1" dependencies: @@ -24267,15 +23732,6 @@ __metadata: languageName: node linkType: hard -"on-finished@npm:~2.3.0": - version: 2.3.0 - resolution: "on-finished@npm:2.3.0" - dependencies: - ee-first: "npm:1.1.1" - checksum: 10/1db595bd963b0124d6fa261d18320422407b8f01dc65863840f3ddaaf7bcad5b28ff6847286703ca53f4ec19595bd67a2f1253db79fc4094911ec6aa8df1671b - languageName: node - linkType: hard - "on-headers@npm:~1.0.2": version: 1.0.2 resolution: "on-headers@npm:1.0.2" @@ -24609,13 +24065,6 @@ __metadata: languageName: node linkType: hard -"pako@npm:^0.2.6, pako@npm:~0.2.0": - version: 0.2.9 - resolution: "pako@npm:0.2.9" - checksum: 10/627c6842e90af0b3a9ee47345bd66485a589aff9514266f4fa9318557ad819c46fedf97510f2cef9b6224c57913777966a05cb46caf6a9b31177a5401a06fe15 - languageName: node - linkType: hard - "pako@npm:^2.0.4": version: 2.0.4 resolution: "pako@npm:2.0.4" @@ -24623,6 +24072,13 @@ __metadata: languageName: node linkType: hard +"pako@npm:~0.2.0": + version: 0.2.9 + resolution: "pako@npm:0.2.9" + checksum: 10/627c6842e90af0b3a9ee47345bd66485a589aff9514266f4fa9318557ad819c46fedf97510f2cef9b6224c57913777966a05cb46caf6a9b31177a5401a06fe15 + languageName: node + linkType: hard + "papaparse@npm:5.4.1": version: 5.4.1 resolution: "papaparse@npm:5.4.1" @@ -24701,6 +24157,13 @@ __metadata: languageName: node linkType: hard +"parse-ms@npm:^4.0.0": + version: 4.0.0 + resolution: "parse-ms@npm:4.0.0" + checksum: 10/673c801d9f957ff79962d71ed5a24850163f4181a90dd30c4e3666b3a804f53b77f1f0556792e8b2adbb5d58757907d1aa51d7d7dc75997c2a56d72937cbc8b7 + languageName: node + linkType: hard + "parse-path@npm:^7.0.0": version: 7.0.0 resolution: "parse-path@npm:7.0.0" @@ -24911,7 +24374,7 @@ __metadata: languageName: node linkType: hard -"picocolors@npm:^1.0.0": +"picocolors@npm:1.0.0, picocolors@npm:^1.0.0": version: 1.0.0 resolution: "picocolors@npm:1.0.0" checksum: 10/a2e8092dd86c8396bdba9f2b5481032848525b3dc295ce9b57896f931e63fc16f79805144321f72976383fc249584672a75cc18d6777c6b757603f372f745981 @@ -24925,6 +24388,13 @@ __metadata: languageName: node linkType: hard +"picomatch@npm:^4.0.1": + version: 4.0.2 + resolution: "picomatch@npm:4.0.2" + checksum: 10/ce617b8da36797d09c0baacb96ca8a44460452c89362d7cb8f70ca46b4158ba8bc3606912de7c818eb4a939f7f9015cef3c766ec8a0c6bfc725fdc078e39c717 + languageName: node + linkType: hard + "pify@npm:5.0.0": version: 5.0.0 resolution: "pify@npm:5.0.0" @@ -25027,27 +24497,6 @@ __metadata: languageName: node linkType: hard -"pngjs-image@npm:~0.11.5": - version: 0.11.7 - resolution: "pngjs-image@npm:0.11.7" - dependencies: - iconv-lite: "npm:^0.4.8" - pako: "npm:^0.2.6" - pngjs: "npm:2.3.1" - request: "npm:^2.55.0" - stream-buffers: "npm:1.0.1" - underscore: "npm:1.7.0" - checksum: 10/314554f989187e6fbb376c0a5217088a91686d972a0f1932ca31b8dd3817a5d2aff2a9cd5f73135fe282bbc190032fc52fa027638b34b21e96cbbf7cbd641a90 - languageName: node - linkType: hard - -"pngjs@npm:2.3.1": - version: 2.3.1 - resolution: "pngjs@npm:2.3.1" - checksum: 10/f53b492c9c5a0586de34f8df501381013c175be0f2c04a659aca30dce08105b59d20927f2209c3fb563908180dee73082be3f5bbbfa760eee98c064baa51fe7c - languageName: node - linkType: hard - "polished@npm:^4.2.2": version: 4.2.2 resolution: "polished@npm:4.2.2" @@ -25250,36 +24699,36 @@ __metadata: languageName: node linkType: hard -"postcss-modules-extract-imports@npm:^3.0.0": - version: 3.0.0 - resolution: "postcss-modules-extract-imports@npm:3.0.0" +"postcss-modules-extract-imports@npm:^3.0.0, postcss-modules-extract-imports@npm:^3.1.0": + version: 3.1.0 + resolution: "postcss-modules-extract-imports@npm:3.1.0" peerDependencies: postcss: ^8.1.0 - checksum: 10/8d68bb735cef4d43f9cdc1053581e6c1c864860b77fcfb670372b39c5feeee018dc5ddb2be4b07fef9bcd601edded4262418bbaeaf1bd4af744446300cebe358 + checksum: 10/00bfd3aff045fc13ded8e3bbfd8dfc73eff9a9708db1b2a132266aef6544c8d2aee7a5d7e021885f6f9bbd5565a9a9ab52990316e21ad9468a2534f87df8e849 languageName: node linkType: hard -"postcss-modules-local-by-default@npm:^4.0.4": - version: 4.0.4 - resolution: "postcss-modules-local-by-default@npm:4.0.4" +"postcss-modules-local-by-default@npm:^4.0.4, postcss-modules-local-by-default@npm:^4.0.5": + version: 4.0.5 + resolution: "postcss-modules-local-by-default@npm:4.0.5" dependencies: icss-utils: "npm:^5.0.0" postcss-selector-parser: "npm:^6.0.2" postcss-value-parser: "npm:^4.1.0" peerDependencies: postcss: ^8.1.0 - checksum: 10/45790af417b2ed6ed26e9922724cf3502569995833a2489abcfc2bb44166096762825cc02f6132cc6a2fb235165e76b859f9d90e8a057bc188a1b2c17f2d7af0 + checksum: 10/b08b01aa7f3d1a80bb1a5508ba3a208578fdd2fb6e54e5613fac244a4e014aa7ca639a614859fec93b399e5a6f86938f7690ca60f7e57c4e35b75621d3c07734 languageName: node linkType: hard -"postcss-modules-scope@npm:^3.1.1": - version: 3.1.1 - resolution: "postcss-modules-scope@npm:3.1.1" +"postcss-modules-scope@npm:^3.1.1, postcss-modules-scope@npm:^3.2.0": + version: 3.2.0 + resolution: "postcss-modules-scope@npm:3.2.0" dependencies: postcss-selector-parser: "npm:^6.0.4" peerDependencies: postcss: ^8.1.0 - checksum: 10/ca035969eba62cf126864b10d7722e49c0d4f050cbd4618b6e9714d81b879cf4c53a5682501e00f9622e8f4ea6d7d7d53af295ae935fa833e0cc0bda416a287b + checksum: 10/17c293ad13355ba456498aa5815ddb7a4a736f7b781d89b294e1602a53b8d0e336131175f82460e290a0d672642f9039540042edc361d9000b682c44e766925b languageName: node linkType: hard @@ -25515,16 +24964,6 @@ __metadata: languageName: node linkType: hard -"preceptor-core@npm:~0.10.0": - version: 0.10.1 - resolution: "preceptor-core@npm:0.10.1" - dependencies: - log4js: "npm:1.1.1" - underscore: "npm:1.7.0" - checksum: 10/0ae2ab8b79104cf78f798d151aba995be2ccab77a0ab1b4df9a9c653fb782166ab62c4069f4a9f639a31c395ca2c8356aaee9f78725b160093e11cf8c757d806 - languageName: node - linkType: hard - "prefix-style@npm:2.0.1": version: 2.0.1 resolution: "prefix-style@npm:2.0.1" @@ -25603,6 +25042,15 @@ __metadata: languageName: node linkType: hard +"pretty-ms@npm:9.0.0": + version: 9.0.0 + resolution: "pretty-ms@npm:9.0.0" + dependencies: + parse-ms: "npm:^4.0.0" + checksum: 10/b11e1eda41a2efcc16aab218392c8e457a8ae5c8edf63aafba0477123426b1268136b9b532cbfd84625bcb826739120ec8490286dab66102b9f09e717bdb4e45 + languageName: node + linkType: hard + "pretty-time@npm:^1.1.0": version: 1.1.0 resolution: "pretty-time@npm:1.1.0" @@ -25676,15 +25124,6 @@ __metadata: languageName: node linkType: hard -"promise@npm:6.0.0": - version: 6.0.0 - resolution: "promise@npm:6.0.0" - dependencies: - asap: "npm:~1.0.0" - checksum: 10/0801d3d23ddc6f98936fc52a220379b9c27b9c0c56361cb10f1de8ce9927a28b71581f137228ddbf9b6fbaeebefc267a1b9166ac99318a33cc660e9cabc37a7a - languageName: node - linkType: hard - "prompts@npm:^2.0.1, prompts@npm:^2.4.0": version: 2.4.2 resolution: "prompts@npm:2.4.2" @@ -25704,7 +25143,7 @@ __metadata: languageName: node linkType: hard -"prop-types@npm:15.8.1, prop-types@npm:15.x, prop-types@npm:^15.5.10, prop-types@npm:^15.5.4, prop-types@npm:^15.5.7, prop-types@npm:^15.5.8, prop-types@npm:^15.6.0, prop-types@npm:^15.6.2, prop-types@npm:^15.7.2, prop-types@npm:^15.8.1": +"prop-types@npm:15.x, prop-types@npm:^15.5.10, prop-types@npm:^15.5.4, prop-types@npm:^15.5.7, prop-types@npm:^15.5.8, prop-types@npm:^15.6.0, prop-types@npm:^15.6.2, prop-types@npm:^15.7.2, prop-types@npm:^15.8.1": version: 15.8.1 resolution: "prop-types@npm:15.8.1" dependencies: @@ -25791,21 +25230,7 @@ __metadata: languageName: node linkType: hard -"prr@npm:~1.0.1": - version: 1.0.1 - resolution: "prr@npm:1.0.1" - checksum: 10/3bca2db0479fd38f8c4c9439139b0c42dcaadcc2fbb7bb8e0e6afaa1383457f1d19aea9e5f961d5b080f1cfc05bfa1fe9e45c97a1d3fd6d421950a73d3108381 - languageName: node - linkType: hard - -"pseudoizer@npm:^0.1.0": - version: 0.1.0 - resolution: "pseudoizer@npm:0.1.0" - checksum: 10/fd3fa95ea4f330f268cad2388f3829e4c29249e7ffea970467b9123f1ef2a81334e92425c5cad0c72ab04c320203501b65ae86aa8d88320b63e0e75e944c6e76 - languageName: node - linkType: hard - -"psl@npm:^1.1.28, psl@npm:^1.1.33": +"psl@npm:^1.1.33": version: 1.9.0 resolution: "psl@npm:1.9.0" checksum: 10/d07879d4bfd0ac74796306a8e5a36a93cfb9c4f4e8ee8e63fbb909066c192fe1008cd8f12abd8ba2f62ca28247949a20c8fb32e1d18831d9e71285a1569720f9 @@ -25882,7 +25307,7 @@ __metadata: languageName: node linkType: hard -"qs@npm:6.10.4, qs@npm:~6.10.3": +"qs@npm:6.10.4": version: 6.10.4 resolution: "qs@npm:6.10.4" dependencies: @@ -25891,10 +25316,12 @@ __metadata: languageName: node linkType: hard -"qs@npm:6.9.7": - version: 6.9.7 - resolution: "qs@npm:6.9.7" - checksum: 10/fb364b54bf4f092a095554968f5abf06036cfe359c9aba258a81b0c0366f625a46098fe1224b2a71ee2f88642470af391c7a8a1496508eca29c37093293f91a9 +"qs@npm:6.11.0": + version: 6.11.0 + resolution: "qs@npm:6.11.0" + dependencies: + side-channel: "npm:^1.0.4" + checksum: 10/5a3bfea3e2f359ede1bfa5d2f0dbe54001aa55e40e27dc3e60fab814362d83a9b30758db057c2011b6f53a2d4e4e5150194b5bac45372652aecb3e3c0d4b256e languageName: node linkType: hard @@ -25907,13 +25334,6 @@ __metadata: languageName: node linkType: hard -"qs@npm:~6.5.2": - version: 6.5.3 - resolution: "qs@npm:6.5.3" - checksum: 10/485c990fba7ad17671e16c92715fb064c1600337738f5d140024eb33a49fbc1ed31890d3db850117c760caeb9c9cc9f4ba22a15c20dd119968e41e3d3fe60b28 - languageName: node - linkType: hard - "querystringify@npm:^2.1.1": version: 2.2.0 resolution: "querystringify@npm:2.2.0" @@ -26015,15 +25435,15 @@ __metadata: languageName: node linkType: hard -"raw-body@npm:2.4.3": - version: 2.4.3 - resolution: "raw-body@npm:2.4.3" +"raw-body@npm:2.5.2": + version: 2.5.2 + resolution: "raw-body@npm:2.5.2" dependencies: bytes: "npm:3.1.2" - http-errors: "npm:1.8.1" + http-errors: "npm:2.0.0" iconv-lite: "npm:0.4.24" unpipe: "npm:1.0.0" - checksum: 10/b3a7cfacfa00778abce59fe1c698bd44edfdbea9ddd39e22c553dcd50a0376f4c3ad0e650f1ba9d20495bab81251844e14448292071487bd372e506faf0a1a2e + checksum: 10/863b5171e140546a4d99f349b720abac4410338e23df5e409cfcc3752538c9caf947ce382c89129ba976f71894bd38b5806c774edac35ebf168d02aa1ac11a95 languageName: node linkType: hard @@ -26158,17 +25578,17 @@ __metadata: languageName: node linkType: hard -"rc-slider@npm:10.5.0": - version: 10.5.0 - resolution: "rc-slider@npm:10.5.0" +"rc-slider@npm:10.6.2": + version: 10.6.2 + resolution: "rc-slider@npm:10.6.2" dependencies: "@babel/runtime": "npm:^7.10.1" classnames: "npm:^2.2.5" - rc-util: "npm:^5.27.0" + rc-util: "npm:^5.36.0" peerDependencies: react: ">=16.9.0" react-dom: ">=16.9.0" - checksum: 10/9fe5b45a0e199311d665312e9940a59e007b81bfdd868cfecb70f4f95299b5447ee2a7c5a4c90a0ee7bbffad9dfa73cc01c779946adc4ca4fe22d2cf02bac046 + checksum: 10/ee5ec34fe940487a44cb03e89abf4199cbae990a334756fa9536a158d3a790cb7c1ebf321fc8048050a0cefa86138e097eb11bcc135bdb10db287b0738370790 languageName: node linkType: hard @@ -26257,6 +25677,19 @@ __metadata: languageName: node linkType: hard +"rc-util@npm:^5.36.0": + version: 5.39.1 + resolution: "rc-util@npm:5.39.1" + dependencies: + "@babel/runtime": "npm:^7.18.3" + react-is: "npm:^18.2.0" + peerDependencies: + react: ">=16.9.0" + react-dom: ">=16.9.0" + checksum: 10/475e7755f8a8aaf8428c535e14ad1475d3d764108852a011cb5186fb7d905e064c0d56b77818ddaea0c5fbde439d3606b52f459cef7725fde8abf15e3e2ece2b + languageName: node + linkType: hard + "rc-virtual-list@npm:^3.5.1, rc-virtual-list@npm:^3.5.2": version: 3.5.3 resolution: "rc-virtual-list@npm:3.5.3" @@ -26552,21 +25985,21 @@ __metadata: languageName: node linkType: hard -"react-i18next@npm:^12.0.0": - version: 12.0.0 - resolution: "react-i18next@npm:12.0.0" +"react-i18next@npm:^14.0.0": + version: 14.1.1 + resolution: "react-i18next@npm:14.1.1" dependencies: - "@babel/runtime": "npm:^7.14.5" + "@babel/runtime": "npm:^7.23.9" html-parse-stringify: "npm:^3.0.1" peerDependencies: - i18next: ">= 19.0.0" + i18next: ">= 23.2.3" react: ">= 16.8.0" peerDependenciesMeta: react-dom: optional: true react-native: optional: true - checksum: 10/91da29572d0059783277b468999092d1d2a96efacdc29b0ab143e9b7ac22e293925c972b713ac08b57e65618b75f2848416e64751e210148f94bc87d86589e0f + checksum: 10/6ad103e0fb5eaedda8a87bf9f6ac0071ef73e7b1ec0d5a5d7ccd2c326183038b7d4722c87a253209f08878c51e5943be327cd48b0c6f3a87a8dc0fc2fca8cbfe languageName: node linkType: hard @@ -26956,16 +26389,6 @@ __metadata: languageName: node linkType: hard -"react-simple-compat@npm:1.2.3": - version: 1.2.3 - resolution: "react-simple-compat@npm:1.2.3" - dependencies: - "@daybrush/utils": "npm:^1.0.0" - "@egjs/list-differ": "npm:^1.0.0" - checksum: 10/779aaffe2fa90dec58b93b2787726ef9f8604cb0085121293ef6143a33790af3c6941b0f73344764ceb9b3415a27e29f18998732829df953e3692c385ffc388e - languageName: node - linkType: hard - "react-split-pane@npm:0.1.92": version: 0.1.92 resolution: "react-split-pane@npm:0.1.92" @@ -27323,18 +26746,6 @@ __metadata: languageName: node linkType: hard -"readable-stream@npm:^1.1.7": - version: 1.1.14 - resolution: "readable-stream@npm:1.1.14" - dependencies: - core-util-is: "npm:~1.0.0" - inherits: "npm:~2.0.1" - isarray: "npm:0.0.1" - string_decoder: "npm:~0.10.x" - checksum: 10/1aa2cf4bd02f9ab3e1d57842a43a413b52be5300aa089ad1f2e3cea00684532d73edc6a2ba52b0c3210d8b57eb20a695a6d2b96d1c6085ee979c6021ad48ad20 - languageName: node - linkType: hard - "readable-stream@npm:^2.0.0, readable-stream@npm:^2.0.1, readable-stream@npm:^2.0.6, readable-stream@npm:^2.2.2, readable-stream@npm:~2.3.6": version: 2.3.8 resolution: "readable-stream@npm:2.3.8" @@ -27644,34 +27055,6 @@ __metadata: languageName: node linkType: hard -"request@npm:^2.55.0": - version: 2.88.2 - resolution: "request@npm:2.88.2" - dependencies: - aws-sign2: "npm:~0.7.0" - aws4: "npm:^1.8.0" - caseless: "npm:~0.12.0" - combined-stream: "npm:~1.0.6" - extend: "npm:~3.0.2" - forever-agent: "npm:~0.6.1" - form-data: "npm:~2.3.2" - har-validator: "npm:~5.1.3" - http-signature: "npm:~1.2.0" - is-typedarray: "npm:~1.0.0" - isstream: "npm:~0.1.2" - json-stringify-safe: "npm:~5.0.1" - mime-types: "npm:~2.1.19" - oauth-sign: "npm:~0.9.0" - performance-now: "npm:^2.1.0" - qs: "npm:~6.5.2" - safe-buffer: "npm:^5.1.2" - tough-cookie: "npm:~2.5.0" - tunnel-agent: "npm:^0.6.0" - uuid: "npm:^3.3.2" - checksum: 10/005b8b237b56f1571cfd4ecc09772adaa2e82dcb884fc14ea2bb25e23dbf7c2009f9929e0b6d3fd5802e33ed8ee705a3b594c8f9467c1458cd973872bf89db8e - languageName: node - linkType: hard - "require-directory@npm:^2.1.1": version: 2.1.1 resolution: "require-directory@npm:2.1.1" @@ -27707,15 +27090,6 @@ __metadata: languageName: node linkType: hard -"resolve-bin@npm:1.0.1": - version: 1.0.1 - resolution: "resolve-bin@npm:1.0.1" - dependencies: - find-parent-dir: "npm:~0.3.0" - checksum: 10/439678433138cbea49b848be68ae24aa2afa1b3bb82c30fa763ebbc5615e8c340debfe635ee902cdbb4be72528e4139e2adb0503370ee755ad3702d03976af4c - languageName: node - linkType: hard - "resolve-cwd@npm:^3.0.0": version: 3.0.0 resolution: "resolve-cwd@npm:3.0.0" @@ -27778,7 +27152,7 @@ __metadata: languageName: node linkType: hard -"resolve@npm:^1.10.0, resolve@npm:^1.12.0, resolve@npm:^1.14.2, resolve@npm:^1.19.0, resolve@npm:^1.20.0, resolve@npm:^1.22.1, resolve@npm:^1.22.4": +"resolve@npm:1.22.8, resolve@npm:^1.10.0, resolve@npm:^1.12.0, resolve@npm:^1.14.2, resolve@npm:^1.19.0, resolve@npm:^1.20.0, resolve@npm:^1.22.1, resolve@npm:^1.22.4": version: 1.22.8 resolution: "resolve@npm:1.22.8" dependencies: @@ -27804,7 +27178,7 @@ __metadata: languageName: node linkType: hard -"resolve@patch:resolve@npm%3A^1.10.0#optional!builtin, resolve@patch:resolve@npm%3A^1.12.0#optional!builtin, resolve@patch:resolve@npm%3A^1.14.2#optional!builtin, resolve@patch:resolve@npm%3A^1.19.0#optional!builtin, resolve@patch:resolve@npm%3A^1.20.0#optional!builtin, resolve@patch:resolve@npm%3A^1.22.1#optional!builtin, resolve@patch:resolve@npm%3A^1.22.4#optional!builtin": +"resolve@patch:resolve@npm%3A1.22.8#optional!builtin, resolve@patch:resolve@npm%3A^1.10.0#optional!builtin, resolve@patch:resolve@npm%3A^1.12.0#optional!builtin, resolve@patch:resolve@npm%3A^1.14.2#optional!builtin, resolve@patch:resolve@npm%3A^1.19.0#optional!builtin, resolve@patch:resolve@npm%3A^1.20.0#optional!builtin, resolve@patch:resolve@npm%3A^1.22.1#optional!builtin, resolve@patch:resolve@npm%3A^1.22.4#optional!builtin": version: 1.22.8 resolution: "resolve@patch:resolve@npm%3A1.22.8#optional!builtin::version=1.22.8&hash=c3c19d" dependencies: @@ -27861,6 +27235,13 @@ __metadata: languageName: node linkType: hard +"reverse-arguments@npm:^1.0.0": + version: 1.0.0 + resolution: "reverse-arguments@npm:1.0.0" + checksum: 10/d891be1c02c2af317eff154267f82487f07bf2796909f86d017b0a45f6ecf8223315f491acb1846727330052a82038a486115fdc1a4dee0faf97a24ecb30af53 + languageName: node + linkType: hard + "rfdc@npm:^1.3.0": version: 1.3.0 resolution: "rfdc@npm:1.3.0" @@ -27875,17 +27256,6 @@ __metadata: languageName: node linkType: hard -"rimraf@npm:5.0.1": - version: 5.0.1 - resolution: "rimraf@npm:5.0.1" - dependencies: - glob: "npm:^10.2.5" - bin: - rimraf: dist/cjs/src/bin.js - checksum: 10/0691e4d7482f2de2af8628976413e146cd6b204f52ab88be91a3bb69cb9c8669ee795ef7c1e964e8ec6bfeaec0212326287d53ec3eef26dac406c8f19c97f0c4 - languageName: node - linkType: hard - "rimraf@npm:5.0.5, rimraf@npm:^5.0.5": version: 5.0.5 resolution: "rimraf@npm:5.0.5" @@ -28122,7 +27492,7 @@ __metadata: languageName: node linkType: hard -"run-parallel@npm:^1.1.9": +"run-parallel@npm:^1.1.9, run-parallel@npm:^1.2.0": version: 1.2.0 resolution: "run-parallel@npm:1.2.0" dependencies: @@ -28334,7 +27704,7 @@ __metadata: languageName: node linkType: hard -"semver@npm:2 || 3 || 4 || 5, semver@npm:^5.3.0, semver@npm:^5.6.0": +"semver@npm:2 || 3 || 4 || 5, semver@npm:^5.6.0": version: 5.7.2 resolution: "semver@npm:5.7.2" bin: @@ -28343,7 +27713,7 @@ __metadata: languageName: node linkType: hard -"semver@npm:7.6.0, semver@npm:^7.0.0, semver@npm:^7.1.1, semver@npm:^7.3.2, semver@npm:^7.3.4, semver@npm:^7.3.5, semver@npm:^7.3.7, semver@npm:^7.3.8, semver@npm:^7.5.3, semver@npm:^7.5.4, semver@npm:^7.6.0": +"semver@npm:7.6.0, semver@npm:^7.0.0, semver@npm:^7.1.1, semver@npm:^7.3.4, semver@npm:^7.3.5, semver@npm:^7.3.7, semver@npm:^7.3.8, semver@npm:^7.5.3, semver@npm:^7.5.4, semver@npm:^7.6.0": version: 7.6.0 resolution: "semver@npm:7.6.0" dependencies: @@ -28363,33 +27733,24 @@ __metadata: languageName: node linkType: hard -"send@npm:0.17.2": - version: 0.17.2 - resolution: "send@npm:0.17.2" +"send@npm:0.18.0": + version: 0.18.0 + resolution: "send@npm:0.18.0" dependencies: debug: "npm:2.6.9" - depd: "npm:~1.1.2" - destroy: "npm:~1.0.4" + depd: "npm:2.0.0" + destroy: "npm:1.2.0" encodeurl: "npm:~1.0.2" escape-html: "npm:~1.0.3" etag: "npm:~1.8.1" fresh: "npm:0.5.2" - http-errors: "npm:1.8.1" + http-errors: "npm:2.0.0" mime: "npm:1.6.0" ms: "npm:2.1.3" - on-finished: "npm:~2.3.0" + on-finished: "npm:2.4.1" range-parser: "npm:~1.2.1" - statuses: "npm:~1.5.0" - checksum: 10/b1e4f9b99a5571626dad1d4401363f6107e245c84e91ccae221ab55fea4d1f12be1b9ef657235381e2b9d3a5840b8ceb4d20c9c322e85535b587b12676bc340c - languageName: node - linkType: hard - -"serialize-javascript@npm:6.0.0": - version: 6.0.0 - resolution: "serialize-javascript@npm:6.0.0" - dependencies: - randombytes: "npm:^2.1.0" - checksum: 10/ed3dabfbb565c48c9eb1ca8fe58f0d256902ab70a8a605be634ddd68388d5f728bb0bd1268e94fab628748ba8ad8392f01b05f3cbe1e4878b5c58c669fd3d1b4 + statuses: "npm:2.0.1" + checksum: 10/ec66c0ad109680ad8141d507677cfd8b4e40b9559de23191871803ed241718e99026faa46c398dcfb9250676076573bd6bfe5d0ec347f88f4b7b8533d1d391cb languageName: node linkType: hard @@ -28430,15 +27791,15 @@ __metadata: languageName: node linkType: hard -"serve-static@npm:1.14.2": - version: 1.14.2 - resolution: "serve-static@npm:1.14.2" +"serve-static@npm:1.15.0": + version: 1.15.0 + resolution: "serve-static@npm:1.15.0" dependencies: encodeurl: "npm:~1.0.2" escape-html: "npm:~1.0.3" parseurl: "npm:~1.3.3" - send: "npm:0.17.2" - checksum: 10/b84f9d58b07db2e4d6b1f60e60a27839f5247331b9bbfa1a90eda3523d8bc58585c914e069e785bb2645668e0b9cdd1a305bc1aa81c4284f5ead103741bb705c + send: "npm:0.18.0" + checksum: 10/699b2d4c29807a51d9b5e0f24955346911437aebb0178b3c4833ad30d3eca93385ff9927254f5c16da345903cad39d9cd4a532198c95a5129cc4ed43911b15a4 languageName: node linkType: hard @@ -28527,6 +27888,13 @@ __metadata: languageName: node linkType: hard +"shell-quote-word@npm:^1.0.1": + version: 1.0.1 + resolution: "shell-quote-word@npm:1.0.1" + checksum: 10/05c5df92acba3e7920dbd987b235276871d38cf360d339074cbfabea49bbca8406a6f06a822cd1e84912c3150277d73de5194ca21c37ef95e108dfe45372526f + languageName: node + linkType: hard + "shell-quote@npm:^1.8.1": version: 1.8.1 resolution: "shell-quote@npm:1.8.1" @@ -28856,6 +28224,13 @@ __metadata: languageName: node linkType: hard +"smol-toml@npm:1.1.4": + version: 1.1.4 + resolution: "smol-toml@npm:1.1.4" + checksum: 10/62365de0d1624aaef2839b0f6bf01d70c780234cc7ea896a15e48535b89122a1ca8c20870a9f201b08946634158fdb8cc83f3d60de796134e6ea4a3c661aac78 + languageName: node + linkType: hard + "snake-case@npm:^3.0.4": version: 3.0.4 resolution: "snake-case@npm:3.0.4" @@ -29177,7 +28552,7 @@ __metadata: languageName: node linkType: hard -"sshpk@npm:^1.14.1, sshpk@npm:^1.7.0": +"sshpk@npm:^1.14.1": version: 1.17.0 resolution: "sshpk@npm:1.17.0" dependencies: @@ -29294,20 +28669,20 @@ __metadata: languageName: node linkType: hard -"statuses@npm:>= 1.4.0 < 2, statuses@npm:>= 1.5.0 < 2, statuses@npm:~1.5.0": - version: 1.5.0 - resolution: "statuses@npm:1.5.0" - checksum: 10/c469b9519de16a4bb19600205cffb39ee471a5f17b82589757ca7bd40a8d92ebb6ed9f98b5a540c5d302ccbc78f15dc03cc0280dd6e00df1335568a5d5758a5c - languageName: node - linkType: hard - -"statuses@npm:^2.0.1": +"statuses@npm:2.0.1, statuses@npm:^2.0.1": version: 2.0.1 resolution: "statuses@npm:2.0.1" checksum: 10/18c7623fdb8f646fb213ca4051be4df7efb3484d4ab662937ca6fbef7ced9b9e12842709872eb3020cc3504b93bde88935c9f6417489627a7786f24f8031cbcb languageName: node linkType: hard +"statuses@npm:>= 1.4.0 < 2": + version: 1.5.0 + resolution: "statuses@npm:1.5.0" + checksum: 10/c469b9519de16a4bb19600205cffb39ee471a5f17b82589757ca7bd40a8d92ebb6ed9f98b5a540c5d302ccbc78f15dc03cc0280dd6e00df1335568a5d5758a5c + languageName: node + linkType: hard + "std-env@npm:^3.7.0": version: 3.7.0 resolution: "std-env@npm:3.7.0" @@ -29377,13 +28752,6 @@ __metadata: languageName: node linkType: hard -"stream-buffers@npm:1.0.1": - version: 1.0.1 - resolution: "stream-buffers@npm:1.0.1" - checksum: 10/df6e4f5004ccae2ec52bec81ec910204508e5cc4830d3aaac2979e481ee0bba2fda13636d9b63ce594a35f3d9b66d97eeb84214a25f217bb31d651664835d130 - languageName: node - linkType: hard - "stream-composer@npm:^1.0.2": version: 1.0.2 resolution: "stream-composer@npm:1.0.2" @@ -29400,18 +28768,6 @@ __metadata: languageName: node linkType: hard -"streamroller@npm:^0.4.0": - version: 0.4.1 - resolution: "streamroller@npm:0.4.1" - dependencies: - date-format: "npm:^0.0.0" - debug: "npm:^0.7.2" - mkdirp: "npm:^0.5.1" - readable-stream: "npm:^1.1.7" - checksum: 10/a546d4905478c6aa32edffa970a485b641358e1a4b26b99a3caee05ca8054dd90eefdc0eac19f3a35a1984f9747dc35cd26f346cba084d2ee80c8844f9d7090b - languageName: node - linkType: hard - "streamx@npm:^2.12.0, streamx@npm:^2.12.5, streamx@npm:^2.13.2, streamx@npm:^2.14.0": version: 2.15.7 resolution: "streamx@npm:2.15.7" @@ -29489,6 +28845,13 @@ __metadata: languageName: node linkType: hard +"string.fromcodepoint@npm:^0.2.1": + version: 0.2.1 + resolution: "string.fromcodepoint@npm:0.2.1" + checksum: 10/6ba80f70c3e2a36dab87f5d68168936403295a73838564e701f5c861d397d77d9e97b0e2aa0f3c163a25a96c785dcc2145452b220753fb7b3e6c6fe431c9c411 + languageName: node + linkType: hard + "string.prototype.matchall@npm:^4.0.10, string.prototype.matchall@npm:^4.0.8": version: 4.0.10 resolution: "string.prototype.matchall@npm:4.0.10" @@ -29548,13 +28911,6 @@ __metadata: languageName: node linkType: hard -"string_decoder@npm:~0.10.x": - version: 0.10.31 - resolution: "string_decoder@npm:0.10.31" - checksum: 10/cc43e6b1340d4c7843da0e37d4c87a4084c2342fc99dcf6563c3ec273bb082f0cbd4ebf25d5da19b04fb16400d393885fda830be5128e1c416c73b5a6165f175 - languageName: node - linkType: hard - "string_decoder@npm:~1.1.1": version: 1.1.1 resolution: "string_decoder@npm:1.1.1" @@ -29621,7 +28977,14 @@ __metadata: languageName: node linkType: hard -"strip-json-comments@npm:3.1.1, strip-json-comments@npm:^3.0.1, strip-json-comments@npm:^3.1.1": +"strip-json-comments@npm:5.0.1": + version: 5.0.1 + resolution: "strip-json-comments@npm:5.0.1" + checksum: 10/b314af70c6666a71133e309a571bdb87687fc878d9fd8b38ebed393a77b89835b92f191aa6b0bc10dfd028ba99eed6b6365985001d64c5aef32a4a82456a156b + languageName: node + linkType: hard + +"strip-json-comments@npm:^3.0.1, strip-json-comments@npm:^3.1.1": version: 3.1.1 resolution: "strip-json-comments@npm:3.1.1" checksum: 10/492f73e27268f9b1c122733f28ecb0e7e8d8a531a6662efbd08e22cccb3f9475e90a1b82cab06a392f6afae6d2de636f977e231296400d0ec5304ba70f166443 @@ -29641,7 +29004,16 @@ __metadata: languageName: node linkType: hard -"style-loader@npm:3.3.4, style-loader@npm:^3.3.1": +"style-loader@npm:4.0.0": + version: 4.0.0 + resolution: "style-loader@npm:4.0.0" + peerDependencies: + webpack: ^5.27.0 + checksum: 10/93f25b7e70cfca9d1d8427170384262b59a5b0e84e7191a5a26636a77799caeed46d9a3e45ee7b9afa0f69176e3b98d5a6c5e81593ff1fd0946f1c5682fd2a68 + languageName: node + linkType: hard + +"style-loader@npm:^3.3.1": version: 3.3.4 resolution: "style-loader@npm:3.3.4" peerDependencies: @@ -29753,12 +29125,10 @@ __metadata: languageName: node linkType: hard -"supports-color@npm:8.1.1, supports-color@npm:^8.0.0, supports-color@npm:^8.1.1": - version: 8.1.1 - resolution: "supports-color@npm:8.1.1" - dependencies: - has-flag: "npm:^4.0.0" - checksum: 10/157b534df88e39c5518c5e78c35580c1eca848d7dbaf31bbe06cdfc048e22c7ff1a9d046ae17b25691128f631a51d9ec373c1b740c12ae4f0de6e292037e4282 +"summary@npm:2.1.0": + version: 2.1.0 + resolution: "summary@npm:2.1.0" + checksum: 10/10ac12ce12c013b56ad44c37cfac206961f0993d98867b33b1b03a27b38a1cf8dd2db0b788883356c5335bbbb37d953772ef4a381d6fc8f408faf99f2bc54af5 languageName: node linkType: hard @@ -29780,6 +29150,15 @@ __metadata: languageName: node linkType: hard +"supports-color@npm:^8.0.0, supports-color@npm:^8.1.1": + version: 8.1.1 + resolution: "supports-color@npm:8.1.1" + dependencies: + has-flag: "npm:^4.0.0" + checksum: 10/157b534df88e39c5518c5e78c35580c1eca848d7dbaf31bbe06cdfc048e22c7ff1a9d046ae17b25691128f631a51d9ec373c1b740c12ae4f0de6e292037e4282 + languageName: node + linkType: hard + "supports-hyperlinks@npm:^3.0.0": version: 3.0.0 resolution: "supports-hyperlinks@npm:3.0.0" @@ -29932,13 +29311,6 @@ __metadata: languageName: node linkType: hard -"tapable@npm:^1.0.0": - version: 1.1.3 - resolution: "tapable@npm:1.1.3" - checksum: 10/1cec71f00f9a6cb1d88961b5d4f2dead4e185508b18b1bf1e688c8135039a391dd3e12b0887232b682ef28f1ef6f0c5e9a48794f6f5ef68f35d05de7e7a0a578 - languageName: node - linkType: hard - "tapable@npm:^2.0.0, tapable@npm:^2.1.1, tapable@npm:^2.2.0, tapable@npm:^2.2.1": version: 2.2.1 resolution: "tapable@npm:2.2.1" @@ -30053,7 +29425,7 @@ __metadata: languageName: node linkType: hard -"terser-webpack-plugin@npm:5.3.10, terser-webpack-plugin@npm:^5.3.1, terser-webpack-plugin@npm:^5.3.10, terser-webpack-plugin@npm:^5.3.7": +"terser-webpack-plugin@npm:5.3.10, terser-webpack-plugin@npm:^5.3.1, terser-webpack-plugin@npm:^5.3.10": version: 5.3.10 resolution: "terser-webpack-plugin@npm:5.3.10" dependencies: @@ -30276,6 +29648,15 @@ __metadata: languageName: node linkType: hard +"to-pascal-case@npm:^1.0.0": + version: 1.0.0 + resolution: "to-pascal-case@npm:1.0.0" + dependencies: + to-space-case: "npm:^1.0.0" + checksum: 10/3956e209defc6df9de98c5db24a16fb2a1a11f711350ea3bdd9466240a04ab889fa09f1bd005e26fc31343c1cca341981daf2d80d4ec3f2d0706a557978f8b91 + languageName: node + linkType: hard + "to-regex-range@npm:^5.0.1": version: 5.0.1 resolution: "to-regex-range@npm:5.0.1" @@ -30343,16 +29724,6 @@ __metadata: languageName: node linkType: hard -"tough-cookie@npm:~2.5.0": - version: 2.5.0 - resolution: "tough-cookie@npm:2.5.0" - dependencies: - psl: "npm:^1.1.28" - punycode: "npm:^2.1.1" - checksum: 10/024cb13a4d1fe9af57f4323dff765dd9b217cc2a69be77e3b8a1ca45600aa33a097b6ad949f225d885e904f4bd3ceccef104741ef202d8378e6ca78e850ff82f - languageName: node - linkType: hard - "tr46@npm:^3.0.0": version: 3.0.0 resolution: "tr46@npm:3.0.0" @@ -30457,22 +29828,6 @@ __metadata: languageName: node linkType: hard -"ts-loader@npm:8.4.0": - version: 8.4.0 - resolution: "ts-loader@npm:8.4.0" - dependencies: - chalk: "npm:^4.1.0" - enhanced-resolve: "npm:^4.0.0" - loader-utils: "npm:^2.0.0" - micromatch: "npm:^4.0.0" - semver: "npm:^7.3.4" - peerDependencies: - typescript: "*" - webpack: "*" - checksum: 10/d5cd87c1070840e0502ca8f348c385980a159efbe2d2d24cc259a4063b562c9e1c3e38c7b150f8aabeee3097132aca20a60eced335037631cb25e3f3f906ea0c - languageName: node - linkType: hard - "ts-node@npm:10.9.2, ts-node@npm:^10.2.1": version: 10.9.2 resolution: "ts-node@npm:10.9.2" @@ -30555,24 +29910,13 @@ __metadata: languageName: node linkType: hard -"tslib@npm:^1.10.0, tslib@npm:^1.13.0, tslib@npm:^1.8.1": +"tslib@npm:^1.10.0, tslib@npm:^1.13.0": version: 1.14.1 resolution: "tslib@npm:1.14.1" checksum: 10/7dbf34e6f55c6492637adb81b555af5e3b4f9cc6b998fb440dac82d3b42bdc91560a35a5fb75e20e24a076c651438234da6743d139e4feabf0783f3cdfe1dddb languageName: node linkType: hard -"tsutils@npm:^3.21.0": - version: 3.21.0 - resolution: "tsutils@npm:3.21.0" - dependencies: - tslib: "npm:^1.8.1" - peerDependencies: - typescript: ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta" - checksum: 10/ea036bec1dd024e309939ffd49fda7a351c0e87a1b8eb049570dd119d447250e2c56e0e6c00554e8205760e7417793fdebff752a46e573fbe07d4f375502a5b2 - languageName: node - linkType: hard - "tuf-js@npm:^1.1.7": version: 1.1.7 resolution: "tuf-js@npm:1.1.7" @@ -30865,13 +30209,6 @@ __metadata: languageName: node linkType: hard -"underscore@npm:1.13.6": - version: 1.13.6 - resolution: "underscore@npm:1.13.6" - checksum: 10/58cf5dc42cb0ac99c146ae4064792c0a2cc84f3a3c4ad88f5082e79057dfdff3371d896d1ec20379e9ece2450d94fa78f2ef5bfefc199ba320653e32c009bd66 - languageName: node - linkType: hard - "undici-types@npm:~5.26.4": version: 5.26.5 resolution: "undici-types@npm:5.26.5" @@ -30879,6 +30216,15 @@ __metadata: languageName: node linkType: hard +"unescape-js@npm:^1.0.5": + version: 1.1.4 + resolution: "unescape-js@npm:1.1.4" + dependencies: + string.fromcodepoint: "npm:^0.2.1" + checksum: 10/97acf60a8f6c170f8a66b48b71f5c56bda728c2ff6b08c3443c5f21635bf5fa38a4265bcfcf46d17cb6ac9bbb8b913a34b1abc5cfe8db5d7cc5c8eecb1817472 + languageName: node + linkType: hard + "unicode-canonical-property-names-ecmascript@npm:^2.0.0": version: 2.0.0 resolution: "unicode-canonical-property-names-ecmascript@npm:2.0.0" @@ -31233,15 +30579,6 @@ __metadata: languageName: node linkType: hard -"uuid@npm:9.0.0": - version: 9.0.0 - resolution: "uuid@npm:9.0.0" - bin: - uuid: dist/bin/uuid - checksum: 10/23857699a616d1b48224bc2b8440eae6e57d25463c3a0200e514ba8279dfa3bde7e92ea056122237839cfa32045e57d8f8f4a30e581d720fd72935572853ae2e - languageName: node - linkType: hard - "uuid@npm:9.0.1, uuid@npm:^9.0.0, uuid@npm:^9.0.1": version: 9.0.1 resolution: "uuid@npm:9.0.1" @@ -31251,15 +30588,6 @@ __metadata: languageName: node linkType: hard -"uuid@npm:^3.3.2": - version: 3.4.0 - resolution: "uuid@npm:3.4.0" - bin: - uuid: ./bin/uuid - checksum: 10/4f2b86432b04cc7c73a0dd1bcf11f1fc18349d65d2e4e32dd0fc658909329a1e0cc9244aa93f34c0cccfdd5ae1af60a149251a5f420ec3ac4223a3dab198fb2e - languageName: node - linkType: hard - "uuid@npm:^8.3.2": version: 8.3.2 resolution: "uuid@npm:8.3.2" @@ -31420,6 +30748,13 @@ __metadata: languageName: node linkType: hard +"vlq@npm:^0.2.1": + version: 0.2.3 + resolution: "vlq@npm:0.2.3" + checksum: 10/2231d8caeb5b2c1a438677ab029e9a94aa6fb61ab05819c72691b792aea0456dab29576aff5ae29309ee45bad0a309e832dc45173119bca1393f3b87709d8f8d + languageName: node + linkType: hard + "void-elements@npm:3.1.0": version: 3.1.0 resolution: "void-elements@npm:3.1.0" @@ -31483,7 +30818,7 @@ __metadata: languageName: node linkType: hard -"watchpack@npm:^2.2.0, watchpack@npm:^2.4.0, watchpack@npm:^2.4.1": +"watchpack@npm:^2.2.0, watchpack@npm:^2.4.1": version: 2.4.1 resolution: "watchpack@npm:2.4.1" dependencies: @@ -31611,8 +30946,8 @@ __metadata: linkType: hard "webpack-dev-middleware@npm:^6.1.1": - version: 6.1.1 - resolution: "webpack-dev-middleware@npm:6.1.1" + version: 6.1.3 + resolution: "webpack-dev-middleware@npm:6.1.3" dependencies: colorette: "npm:^2.0.10" memfs: "npm:^3.4.12" @@ -31624,7 +30959,7 @@ __metadata: peerDependenciesMeta: webpack: optional: true - checksum: 10/b0637584f18b02174fd7fc2e6278efb8e2fb5308abe4ffe73658e59ff53a62c05686f161b06bd5c41d42611aa395b8c8f087d7ff8cf2304232c097a694a5b94e + checksum: 10/ee699430c33c4dfa2a016becc85e32a9b04aa0b6edbce0bb173c4dfd29c80c77d192d14fd2f2ec500dbdede4e0f1c5557993aa20a04a44190750a1e8e13f6d67 languageName: node linkType: hard @@ -31799,43 +31134,6 @@ __metadata: languageName: node linkType: hard -"webpack@npm:5.89.0": - version: 5.89.0 - resolution: "webpack@npm:5.89.0" - dependencies: - "@types/eslint-scope": "npm:^3.7.3" - "@types/estree": "npm:^1.0.0" - "@webassemblyjs/ast": "npm:^1.11.5" - "@webassemblyjs/wasm-edit": "npm:^1.11.5" - "@webassemblyjs/wasm-parser": "npm:^1.11.5" - acorn: "npm:^8.7.1" - acorn-import-assertions: "npm:^1.9.0" - browserslist: "npm:^4.14.5" - chrome-trace-event: "npm:^1.0.2" - enhanced-resolve: "npm:^5.15.0" - es-module-lexer: "npm:^1.2.1" - eslint-scope: "npm:5.1.1" - events: "npm:^3.2.0" - glob-to-regexp: "npm:^0.4.1" - graceful-fs: "npm:^4.2.9" - json-parse-even-better-errors: "npm:^2.3.1" - loader-runner: "npm:^4.2.0" - mime-types: "npm:^2.1.27" - neo-async: "npm:^2.6.2" - schema-utils: "npm:^3.2.0" - tapable: "npm:^2.1.1" - terser-webpack-plugin: "npm:^5.3.7" - watchpack: "npm:^2.4.0" - webpack-sources: "npm:^3.2.3" - peerDependenciesMeta: - webpack-cli: - optional: true - bin: - webpack: bin/webpack.js - checksum: 10/ee19b070279c9bc3bf21eeaac3ea08e6583c1b8da334e595b3c9badedbd7f9fad071b9f785076081af661ef247bb72441e86e8b903bf253ae9300007a048ea6e - languageName: node - linkType: hard - "webpackbar@npm:^6.0.0": version: 6.0.1 resolution: "webpackbar@npm:6.0.1" @@ -32029,13 +31327,6 @@ __metadata: languageName: node linkType: hard -"workerpool@npm:6.2.1": - version: 6.2.1 - resolution: "workerpool@npm:6.2.1" - checksum: 10/3e637f76320cab92eaeffa4fbefb351db02e20aa29245d8ee05fa7c088780ef7b4446bfafff2668a22fd94b7d9d97c7020117036ac77a76370ecea278b9a9b91 - languageName: node - linkType: hard - "wrap-ansi-cjs@npm:wrap-ansi@^7.0.0, wrap-ansi@npm:^7.0.0": version: 7.0.0 resolution: "wrap-ansi@npm:7.0.0" @@ -32171,12 +31462,12 @@ __metadata: languageName: node linkType: hard -"xlsx@https://cdn.sheetjs.com/xlsx-0.19.1/xlsx-0.19.1.tgz": - version: 0.19.1 - resolution: "xlsx@https://cdn.sheetjs.com/xlsx-0.19.1/xlsx-0.19.1.tgz" +"xlsx@https://cdn.sheetjs.com/xlsx-0.20.2/xlsx-0.20.2.tgz": + version: 0.20.2 + resolution: "xlsx@https://cdn.sheetjs.com/xlsx-0.20.2/xlsx-0.20.2.tgz" bin: xlsx: ./bin/xlsx.njs - checksum: 10/d84f81bec6501f26728764c207a88a3676c8979e801d5be4d60500859bf4d799256dc07823e6fb3fa404f583aa11c55071483c1f76372416588a643a92ab06cf + checksum: 10/2d8e0644888f90fa9145ea74ed90b844154ce89c4f0e4f92fcce3f224fa71654da99aa48d99d65ba86eb0632a4858ba2dea7eef8b54fd8bd23954a09d1884aa1 languageName: node linkType: hard @@ -32273,13 +31564,6 @@ __metadata: languageName: node linkType: hard -"yargs-parser@npm:20.2.4": - version: 20.2.4 - resolution: "yargs-parser@npm:20.2.4" - checksum: 10/db8f251ae40e24782d5c089ed86883ba3c0ce7f3c174002a67ec500802f928df9d505fea5d04829769221ce20b0f69f6fb1138fbb2e2fb102e3e9d426d20edab - languageName: node - linkType: hard - "yargs-parser@npm:21.1.1, yargs-parser@npm:^21.0.1, yargs-parser@npm:^21.1.1": version: 21.1.1 resolution: "yargs-parser@npm:21.1.1" @@ -32294,33 +31578,6 @@ __metadata: languageName: node linkType: hard -"yargs-unparser@npm:2.0.0": - version: 2.0.0 - resolution: "yargs-unparser@npm:2.0.0" - dependencies: - camelcase: "npm:^6.0.0" - decamelize: "npm:^4.0.0" - flat: "npm:^5.0.2" - is-plain-obj: "npm:^2.1.0" - checksum: 10/68f9a542c6927c3768c2f16c28f71b19008710abd6b8f8efbac6dcce26bbb68ab6503bed1d5994bdbc2df9a5c87c161110c1dfe04c6a3fe5c6ad1b0e15d9a8a3 - languageName: node - linkType: hard - -"yargs@npm:16.2.0, yargs@npm:^16.2.0": - version: 16.2.0 - resolution: "yargs@npm:16.2.0" - dependencies: - cliui: "npm:^7.0.2" - escalade: "npm:^3.1.1" - get-caller-file: "npm:^2.0.5" - require-directory: "npm:^2.1.1" - string-width: "npm:^4.2.0" - y18n: "npm:^5.0.5" - yargs-parser: "npm:^20.2.2" - checksum: 10/807fa21211d2117135d557f95fcd3c3d390530cda2eca0c840f1d95f0f40209dcfeb5ec18c785a1f3425896e623e3b2681e8bb7b6600060eda1c3f4804e7957e - languageName: node - linkType: hard - "yargs@npm:17.7.2, yargs@npm:^17.0.1, yargs@npm:^17.3.1, yargs@npm:^17.5.1, yargs@npm:^17.6.2, yargs@npm:^17.7.2": version: 17.7.2 resolution: "yargs@npm:17.7.2" @@ -32336,6 +31593,21 @@ __metadata: languageName: node linkType: hard +"yargs@npm:^16.2.0": + version: 16.2.0 + resolution: "yargs@npm:16.2.0" + dependencies: + cliui: "npm:^7.0.2" + escalade: "npm:^3.1.1" + get-caller-file: "npm:^2.0.5" + require-directory: "npm:^2.1.1" + string-width: "npm:^4.2.0" + y18n: "npm:^5.0.5" + yargs-parser: "npm:^20.2.2" + checksum: 10/807fa21211d2117135d557f95fcd3c3d390530cda2eca0c840f1d95f0f40209dcfeb5ec18c785a1f3425896e623e3b2681e8bb7b6600060eda1c3f4804e7957e + languageName: node + linkType: hard + "yauzl@npm:^2.10.0": version: 2.10.0 resolution: "yauzl@npm:2.10.0" @@ -32367,9 +31639,25 @@ __metadata: languageName: node linkType: hard +"zod-validation-error@npm:^3.0.3": + version: 3.2.0 + resolution: "zod-validation-error@npm:3.2.0" + peerDependencies: + zod: ^3.18.0 + checksum: 10/2e2aec95e43cc34b741faf2863019be2558da5eb902b7eeb8bbf4c11c24c1b1f91e92e6f97077584b8301bc927061b9b2f1c0ede562f0bc350726c26870f75c1 + languageName: node + linkType: hard + "zod@npm:^3.21.4": version: 3.22.3 resolution: "zod@npm:3.22.3" checksum: 10/3aad6e6b61ddceaeb887dccc5f747903e619b09dfd208f6dc30eef15edf3942b8e6cd97a08e080c9c8723575446941edb823a8881c512e00e8dd3085f20659cc languageName: node linkType: hard + +"zod@npm:^3.22.4": + version: 3.23.4 + resolution: "zod@npm:3.23.4" + checksum: 10/29117ce59f16af155d9a3aab024d18e858e55b4b51c1f848772516ec6f019e617a0cdaf4c6e20f15154360a9f4c160056aba34502ec68072c8f0af31dfd653d6 + languageName: node + linkType: hard